/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_utils.py

  • Committer: Martin Pool
  • Date: 2008-04-24 07:38:09 UTC
  • mto: This revision was merged to the branch mainline in revision 3415.
  • Revision ID: mbp@sourcefrog.net-20080424073809-ueh0p57961v1q5cs
Treat assert statements in our code as a hard error

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from cStringIO import StringIO
 
18
import errno
 
19
import md5
 
20
import re
 
21
import sha
 
22
import socket
 
23
import threading
 
24
import time
 
25
import urllib2
 
26
import urlparse
 
27
 
 
28
from bzrlib import (
 
29
    errors,
 
30
    tests,
 
31
    transport,
 
32
    )
 
33
from bzrlib.smart import protocol
 
34
from bzrlib.tests import http_server
 
35
 
 
36
 
 
37
class HTTPServerWithSmarts(http_server.HttpServer):
 
38
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
 
39
    trigger a smart server to execute with a transport rooted at the rootdir of
 
40
    the HTTP server.
 
41
    """
 
42
 
 
43
    def __init__(self, protocol_version=None):
 
44
        http_server.HttpServer.__init__(self, SmartRequestHandler,
 
45
                                        protocol_version=protocol_version)
 
46
 
 
47
 
 
48
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
 
49
    """Extend TestingHTTPRequestHandler to support smart client POSTs."""
 
50
 
 
51
    def do_POST(self):
 
52
        """Hand the request off to a smart server instance."""
 
53
        self.send_response(200)
 
54
        self.send_header("Content-type", "application/octet-stream")
 
55
        t = transport.get_transport(self.server.test_case_server._home_dir)
 
56
        # TODO: We might like to support streaming responses.  1.0 allows no
 
57
        # Content-length in this case, so for integrity we should perform our
 
58
        # own chunking within the stream.
 
59
        # 1.1 allows chunked responses, and in this case we could chunk using
 
60
        # the HTTP chunking as this will allow HTTP persistence safely, even if
 
61
        # we have to stop early due to error, but we would also have to use the
 
62
        # HTTP trailer facility which may not be widely available.
 
63
        out_buffer = StringIO()
 
64
        smart_protocol_request = protocol.SmartServerRequestProtocolOne(
 
65
                t, out_buffer.write)
 
66
        # if this fails, we should return 400 bad request, but failure is
 
67
        # failure for now - RBC 20060919
 
68
        data_length = int(self.headers['Content-Length'])
 
69
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
 
70
        # feeding the bytes in the http request to the smart_protocol_request,
 
71
        # but for now it's simpler to just feed the bytes directly.
 
72
        smart_protocol_request.accept_bytes(self.rfile.read(data_length))
 
73
        if not (smart_protocol_request.next_read_size() == 0):
 
74
            raise errors.SmartProtocolError(
 
75
                "not finished reading, but all data sent to protocol.")
 
76
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
 
77
        self.end_headers()
 
78
        self.wfile.write(out_buffer.getvalue())
 
79
 
 
80
 
 
81
class TestCaseWithWebserver(tests.TestCaseWithTransport):
 
82
    """A support class that provides readonly urls that are http://.
 
83
 
 
84
    This is done by forcing the readonly server to be an http
 
85
    one. This will currently fail if the primary transport is not
 
86
    backed by regular disk files.
 
87
    """
 
88
    def setUp(self):
 
89
        super(TestCaseWithWebserver, self).setUp()
 
90
        self.transport_readonly_server = http_server.HttpServer
 
91
 
 
92
 
 
93
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
 
94
    """A support class providing readonly urls on two servers that are http://.
 
95
 
 
96
    We set up two webservers to allows various tests involving
 
97
    proxies or redirections from one server to the other.
 
98
    """
 
99
    def setUp(self):
 
100
        super(TestCaseWithTwoWebservers, self).setUp()
 
101
        self.transport_secondary_server = http_server.HttpServer
 
102
        self.__secondary_server = None
 
103
 
 
104
    def create_transport_secondary_server(self):
 
105
        """Create a transport server from class defined at init.
 
106
 
 
107
        This is mostly a hook for daughter classes.
 
108
        """
 
109
        return self.transport_secondary_server()
 
110
 
 
111
    def get_secondary_server(self):
 
112
        """Get the server instance for the secondary transport."""
 
113
        if self.__secondary_server is None:
 
114
            self.__secondary_server = self.create_transport_secondary_server()
 
115
            self.__secondary_server.setUp()
 
116
            self.addCleanup(self.__secondary_server.tearDown)
 
117
        return self.__secondary_server
 
118
 
 
119
 
 
120
class ProxyServer(http_server.HttpServer):
 
121
    """A proxy test server for http transports."""
 
122
 
 
123
    proxy_requests = True
 
124
 
 
125
 
 
126
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
127
    """Redirect all request to the specified server"""
 
128
 
 
129
    def parse_request(self):
 
130
        """Redirect a single HTTP request to another host"""
 
131
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
132
        if valid:
 
133
            tcs = self.server.test_case_server
 
134
            code, target = tcs.is_redirected(self.path)
 
135
            if code is not None and target is not None:
 
136
                # Redirect as instructed
 
137
                self.send_response(code)
 
138
                self.send_header('Location', target)
 
139
                # We do not send a body
 
140
                self.send_header('Content-Length', '0')
 
141
                self.end_headers()
 
142
                return False # The job is done
 
143
            else:
 
144
                # We leave the parent class serve the request
 
145
                pass
 
146
        return valid
 
147
 
 
148
 
 
149
class HTTPServerRedirecting(http_server.HttpServer):
 
150
    """An HttpServer redirecting to another server """
 
151
 
 
152
    def __init__(self, request_handler=RedirectRequestHandler,
 
153
                 protocol_version=None):
 
154
        http_server.HttpServer.__init__(self, request_handler,
 
155
                                        protocol_version=protocol_version)
 
156
        # redirections is a list of tuples (source, target, code)
 
157
        # - source is a regexp for the paths requested
 
158
        # - target is a replacement for re.sub describing where
 
159
        #   the request will be redirected
 
160
        # - code is the http error code associated to the
 
161
        #   redirection (301 permanent, 302 temporarry, etc
 
162
        self.redirections = []
 
163
 
 
164
    def redirect_to(self, host, port):
 
165
        """Redirect all requests to a specific host:port"""
 
166
        self.redirections = [('(.*)',
 
167
                              r'http://%s:%s\1' % (host, port) ,
 
168
                              301)]
 
169
 
 
170
    def is_redirected(self, path):
 
171
        """Is the path redirected by this server.
 
172
 
 
173
        :param path: the requested relative path
 
174
 
 
175
        :returns: a tuple (code, target) if a matching
 
176
             redirection is found, (None, None) otherwise.
 
177
        """
 
178
        code = None
 
179
        target = None
 
180
        for (rsource, rtarget, rcode) in self.redirections:
 
181
            target, match = re.subn(rsource, rtarget, path)
 
182
            if match:
 
183
                code = rcode
 
184
                break # The first match wins
 
185
            else:
 
186
                target = None
 
187
        return code, target
 
188
 
 
189
 
 
190
class TestCaseWithRedirectedWebserver(TestCaseWithTwoWebservers):
 
191
   """A support class providing redirections from one server to another.
 
192
 
 
193
   We set up two webservers to allows various tests involving
 
194
   redirections.
 
195
   The 'old' server is redirected to the 'new' server.
 
196
   """
 
197
 
 
198
   def create_transport_secondary_server(self):
 
199
       """Create the secondary server redirecting to the primary server"""
 
200
       new = self.get_readonly_server()
 
201
       redirecting = HTTPServerRedirecting()
 
202
       redirecting.redirect_to(new.host, new.port)
 
203
       return redirecting
 
204
 
 
205
   def setUp(self):
 
206
       super(TestCaseWithRedirectedWebserver, self).setUp()
 
207
       # The redirections will point to the new server
 
208
       self.new_server = self.get_readonly_server()
 
209
       # The requests to the old server will be redirected
 
210
       self.old_server = self.get_secondary_server()
 
211
 
 
212
 
 
213
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
214
    """Requires an authentication to process requests.
 
215
 
 
216
    This is intended to be used with a server that always and
 
217
    only use one authentication scheme (implemented by daughter
 
218
    classes).
 
219
    """
 
220
 
 
221
    # The following attributes should be defined in the server
 
222
    # - auth_header_sent: the header name sent to require auth
 
223
    # - auth_header_recv: the header received containing auth
 
224
    # - auth_error_code: the error code to indicate auth required
 
225
 
 
226
    def do_GET(self):
 
227
        if self.authorized():
 
228
            return http_server.TestingHTTPRequestHandler.do_GET(self)
 
229
        else:
 
230
            # Note that we must update test_case_server *before*
 
231
            # sending the error or the client may try to read it
 
232
            # before we have sent the whole error back.
 
233
            tcs = self.server.test_case_server
 
234
            tcs.auth_required_errors += 1
 
235
            self.send_response(tcs.auth_error_code)
 
236
            self.send_header_auth_reqed()
 
237
            # We do not send a body
 
238
            self.send_header('Content-Length', '0')
 
239
            self.end_headers()
 
240
            return
 
241
 
 
242
 
 
243
class BasicAuthRequestHandler(AuthRequestHandler):
 
244
    """Implements the basic authentication of a request"""
 
245
 
 
246
    def authorized(self):
 
247
        tcs = self.server.test_case_server
 
248
        if tcs.auth_scheme != 'basic':
 
249
            return False
 
250
 
 
251
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
252
        if auth_header:
 
253
            scheme, raw_auth = auth_header.split(' ', 1)
 
254
            if scheme.lower() == tcs.auth_scheme:
 
255
                user, password = raw_auth.decode('base64').split(':')
 
256
                return tcs.authorized(user, password)
 
257
 
 
258
        return False
 
259
 
 
260
    def send_header_auth_reqed(self):
 
261
        tcs = self.server.test_case_server
 
262
        self.send_header(tcs.auth_header_sent,
 
263
                         'Basic realm="%s"' % tcs.auth_realm)
 
264
 
 
265
 
 
266
# FIXME: We could send an Authentication-Info header too when
 
267
# the authentication is succesful
 
268
 
 
269
class DigestAuthRequestHandler(AuthRequestHandler):
 
270
    """Implements the digest authentication of a request.
 
271
 
 
272
    We need persistence for some attributes and that can't be
 
273
    achieved here since we get instantiated for each request. We
 
274
    rely on the DigestAuthServer to take care of them.
 
275
    """
 
276
 
 
277
    def authorized(self):
 
278
        tcs = self.server.test_case_server
 
279
        if tcs.auth_scheme != 'digest':
 
280
            return False
 
281
 
 
282
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
283
        if auth_header is None:
 
284
            return False
 
285
        scheme, auth = auth_header.split(None, 1)
 
286
        if scheme.lower() == tcs.auth_scheme:
 
287
            auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
 
288
 
 
289
            return tcs.digest_authorized(auth_dict, self.command)
 
290
 
 
291
        return False
 
292
 
 
293
    def send_header_auth_reqed(self):
 
294
        tcs = self.server.test_case_server
 
295
        header = 'Digest realm="%s", ' % tcs.auth_realm
 
296
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
 
297
                                                              'MD5')
 
298
        self.send_header(tcs.auth_header_sent,header)
 
299
 
 
300
 
 
301
class AuthServer(http_server.HttpServer):
 
302
    """Extends HttpServer with a dictionary of passwords.
 
303
 
 
304
    This is used as a base class for various schemes which should
 
305
    all use or redefined the associated AuthRequestHandler.
 
306
 
 
307
    Note that no users are defined by default, so add_user should
 
308
    be called before issuing the first request.
 
309
    """
 
310
 
 
311
    # The following attributes should be set dy daughter classes
 
312
    # and are used by AuthRequestHandler.
 
313
    auth_header_sent = None
 
314
    auth_header_recv = None
 
315
    auth_error_code = None
 
316
    auth_realm = "Thou should not pass"
 
317
 
 
318
    def __init__(self, request_handler, auth_scheme,
 
319
                 protocol_version=None):
 
320
        http_server.HttpServer.__init__(self, request_handler,
 
321
                                        protocol_version=protocol_version)
 
322
        self.auth_scheme = auth_scheme
 
323
        self.password_of = {}
 
324
        self.auth_required_errors = 0
 
325
 
 
326
    def add_user(self, user, password):
 
327
        """Declare a user with an associated password.
 
328
 
 
329
        password can be empty, use an empty string ('') in that
 
330
        case, not None.
 
331
        """
 
332
        self.password_of[user] = password
 
333
 
 
334
    def authorized(self, user, password):
 
335
        """Check that the given user provided the right password"""
 
336
        expected_password = self.password_of.get(user, None)
 
337
        return expected_password is not None and password == expected_password
 
338
 
 
339
 
 
340
# FIXME: There is some code duplication with
 
341
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
 
342
# grows, it may require a refactoring. Also, we don't implement
 
343
# SHA algorithm nor MD5-sess here, but that does not seem worth
 
344
# it.
 
345
class DigestAuthServer(AuthServer):
 
346
    """A digest authentication server"""
 
347
 
 
348
    auth_nonce = 'now!'
 
349
 
 
350
    def __init__(self, request_handler, auth_scheme,
 
351
                 protocol_version=None):
 
352
        AuthServer.__init__(self, request_handler, auth_scheme,
 
353
                            protocol_version=protocol_version)
 
354
 
 
355
    def digest_authorized(self, auth, command):
 
356
        nonce = auth['nonce']
 
357
        if nonce != self.auth_nonce:
 
358
            return False
 
359
        realm = auth['realm']
 
360
        if realm != self.auth_realm:
 
361
            return False
 
362
        user = auth['username']
 
363
        if not self.password_of.has_key(user):
 
364
            return False
 
365
        algorithm= auth['algorithm']
 
366
        if algorithm != 'MD5':
 
367
            return False
 
368
        qop = auth['qop']
 
369
        if qop != 'auth':
 
370
            return False
 
371
 
 
372
        password = self.password_of[user]
 
373
 
 
374
        # Recalculate the response_digest to compare with the one
 
375
        # sent by the client
 
376
        A1 = '%s:%s:%s' % (user, realm, password)
 
377
        A2 = '%s:%s' % (command, auth['uri'])
 
378
 
 
379
        H = lambda x: md5.new(x).hexdigest()
 
380
        KD = lambda secret, data: H("%s:%s" % (secret, data))
 
381
 
 
382
        nonce_count = int(auth['nc'], 16)
 
383
 
 
384
        ncvalue = '%08x' % nonce_count
 
385
 
 
386
        cnonce = auth['cnonce']
 
387
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
388
        response_digest = KD(H(A1), noncebit)
 
389
 
 
390
        return response_digest == auth['response']
 
391
 
 
392
class HTTPAuthServer(AuthServer):
 
393
    """An HTTP server requiring authentication"""
 
394
 
 
395
    def init_http_auth(self):
 
396
        self.auth_header_sent = 'WWW-Authenticate'
 
397
        self.auth_header_recv = 'Authorization'
 
398
        self.auth_error_code = 401
 
399
 
 
400
 
 
401
class ProxyAuthServer(AuthServer):
 
402
    """A proxy server requiring authentication"""
 
403
 
 
404
    def init_proxy_auth(self):
 
405
        self.proxy_requests = True
 
406
        self.auth_header_sent = 'Proxy-Authenticate'
 
407
        self.auth_header_recv = 'Proxy-Authorization'
 
408
        self.auth_error_code = 407
 
409
 
 
410
 
 
411
class HTTPBasicAuthServer(HTTPAuthServer):
 
412
    """An HTTP server requiring basic authentication"""
 
413
 
 
414
    def __init__(self, protocol_version=None):
 
415
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
416
                                protocol_version=protocol_version)
 
417
        self.init_http_auth()
 
418
 
 
419
 
 
420
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
 
421
    """An HTTP server requiring digest authentication"""
 
422
 
 
423
    def __init__(self, protocol_version=None):
 
424
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
425
                                  protocol_version=protocol_version)
 
426
        self.init_http_auth()
 
427
 
 
428
 
 
429
class ProxyBasicAuthServer(ProxyAuthServer):
 
430
    """A proxy server requiring basic authentication"""
 
431
 
 
432
    def __init__(self, protocol_version=None):
 
433
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
 
434
                                 protocol_version=protocol_version)
 
435
        self.init_proxy_auth()
 
436
 
 
437
 
 
438
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
439
    """A proxy server requiring basic authentication"""
 
440
 
 
441
    def __init__(self, protocol_version=None):
 
442
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
 
443
                                 protocol_version=protocol_version)
 
444
        self.init_proxy_auth()
 
445
 
 
446