~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: Martin Pool
  • Date: 2007-04-04 06:17:31 UTC
  • mto: This revision was merged to the branch mainline in revision 2397.
  • Revision ID: mbp@sourcefrog.net-20070404061731-tt2xrzllqhbodn83
Contents of TODO file moved into bug tracker

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
from cStringIO import StringIO
18
18
import errno
 
19
from SimpleHTTPServer import SimpleHTTPRequestHandler
19
20
import re
20
21
import socket
21
 
import threading
22
 
import time
23
 
import urllib2
24
22
import urlparse
25
23
 
26
 
 
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    tests,
 
24
from bzrlib.tests import TestCaseWithTransport
 
25
from bzrlib.tests.HttpServer import (
 
26
    HttpServer,
 
27
    TestingHTTPRequestHandler,
31
28
    )
32
 
from bzrlib.smart import medium, protocol
33
 
from bzrlib.tests import http_server
34
29
from bzrlib.transport import (
35
 
    chroot,
36
30
    get_transport,
 
31
    smart,
37
32
    )
38
33
 
39
34
 
40
 
class HTTPServerWithSmarts(http_server.HttpServer):
 
35
class WallRequestHandler(TestingHTTPRequestHandler):
 
36
    """Whatever request comes in, close the connection"""
 
37
 
 
38
    def handle_one_request(self):
 
39
        """Handle a single HTTP request, by abruptly closing the connection"""
 
40
        self.close_connection = 1
 
41
 
 
42
 
 
43
class BadStatusRequestHandler(TestingHTTPRequestHandler):
 
44
    """Whatever request comes in, returns a bad status"""
 
45
 
 
46
    def parse_request(self):
 
47
        """Fakes handling a single HTTP request, returns a bad status"""
 
48
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
49
        try:
 
50
            self.send_response(0, "Bad status")
 
51
            self.end_headers()
 
52
        except socket.error, e:
 
53
            # We don't want to pollute the test results with
 
54
            # spurious server errors while test succeed. In our
 
55
            # case, it may occur that the test has already read
 
56
            # the 'Bad Status' and closed the socket while we are
 
57
            # still trying to send some headers... So the test is
 
58
            # ok, but if we raise the exception, the output is
 
59
            # dirty. So we don't raise, but we close the
 
60
            # connection, just to be safe :)
 
61
            spurious = [errno.EPIPE,
 
62
                        errno.ECONNRESET,
 
63
                        errno.ECONNABORTED,
 
64
                        ]
 
65
            if (len(e.args) > 0) and (e.args[0] in spurious):
 
66
                self.close_connection = 1
 
67
                pass
 
68
            else:
 
69
                raise
 
70
        return False
 
71
 
 
72
 
 
73
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
 
74
    """Whatever request comes in, returns am invalid status"""
 
75
 
 
76
    def parse_request(self):
 
77
        """Fakes handling a single HTTP request, returns a bad status"""
 
78
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
79
        self.wfile.write("Invalid status line\r\n")
 
80
        return False
 
81
 
 
82
 
 
83
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
 
84
    """Whatever request comes in, returns a bad protocol version"""
 
85
 
 
86
    def parse_request(self):
 
87
        """Fakes handling a single HTTP request, returns a bad status"""
 
88
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
89
        # Returns an invalid protocol version, but curl just
 
90
        # ignores it and those cannot be tested.
 
91
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
 
92
                                           404,
 
93
                                           'Look at my protocol version'))
 
94
        return False
 
95
 
 
96
 
 
97
class ForbiddenRequestHandler(TestingHTTPRequestHandler):
 
98
    """Whatever request comes in, returns a 403 code"""
 
99
 
 
100
    def parse_request(self):
 
101
        """Handle a single HTTP request, by replying we cannot handle it"""
 
102
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
103
        self.send_error(403)
 
104
        return False
 
105
 
 
106
 
 
107
class HTTPServerWithSmarts(HttpServer):
41
108
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
42
109
    trigger a smart server to execute with a transport rooted at the rootdir of
43
110
    the HTTP server.
44
111
    """
45
112
 
46
 
    def __init__(self, protocol_version=None):
47
 
        http_server.HttpServer.__init__(self, SmartRequestHandler,
48
 
                                        protocol_version=protocol_version)
49
 
 
50
 
 
51
 
class SmartRequestHandler(http_server.TestingHTTPRequestHandler):
52
 
    """Extend TestingHTTPRequestHandler to support smart client POSTs.
53
 
    
54
 
    XXX: This duplicates a fair bit of the logic in bzrlib.transport.http.wsgi.
55
 
    """
 
113
    def __init__(self):
 
114
        HttpServer.__init__(self, SmartRequestHandler)
 
115
 
 
116
 
 
117
class SmartRequestHandler(TestingHTTPRequestHandler):
 
118
    """Extend TestingHTTPRequestHandler to support smart client POSTs."""
56
119
 
57
120
    def do_POST(self):
58
121
        """Hand the request off to a smart server instance."""
59
 
        backing = get_transport(self.server.test_case_server._home_dir)
60
 
        chroot_server = chroot.ChrootServer(backing)
61
 
        chroot_server.setUp()
62
 
        try:
63
 
            t = get_transport(chroot_server.get_url())
64
 
            self.do_POST_inner(t)
65
 
        finally:
66
 
            chroot_server.tearDown()
67
 
 
68
 
    def do_POST_inner(self, chrooted_transport):
69
122
        self.send_response(200)
70
123
        self.send_header("Content-type", "application/octet-stream")
71
 
        if not self.path.endswith('.bzr/smart'):
72
 
            raise AssertionError(
73
 
                'POST to path not ending in .bzr/smart: %r' % (self.path,))
74
 
        t = chrooted_transport.clone(self.path[:-len('.bzr/smart')])
75
 
        # if this fails, we should return 400 bad request, but failure is
76
 
        # failure for now - RBC 20060919
77
 
        data_length = int(self.headers['Content-Length'])
 
124
        transport = get_transport(self.server.test_case_server._home_dir)
78
125
        # TODO: We might like to support streaming responses.  1.0 allows no
79
126
        # Content-length in this case, so for integrity we should perform our
80
127
        # own chunking within the stream.
82
129
        # the HTTP chunking as this will allow HTTP persistence safely, even if
83
130
        # we have to stop early due to error, but we would also have to use the
84
131
        # HTTP trailer facility which may not be widely available.
85
 
        request_bytes = self.rfile.read(data_length)
86
 
        protocol_factory, unused_bytes = medium._get_protocol_factory_for_bytes(
87
 
            request_bytes)
88
132
        out_buffer = StringIO()
89
 
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
 
133
        smart_protocol_request = smart.SmartServerRequestProtocolOne(
 
134
                transport, out_buffer.write)
 
135
        # if this fails, we should return 400 bad request, but failure is
 
136
        # failure for now - RBC 20060919
 
137
        data_length = int(self.headers['Content-Length'])
90
138
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
91
139
        # feeding the bytes in the http request to the smart_protocol_request,
92
140
        # but for now it's simpler to just feed the bytes directly.
93
 
        smart_protocol_request.accept_bytes(unused_bytes)
94
 
        if not (smart_protocol_request.next_read_size() == 0):
95
 
            raise errors.SmartProtocolError(
96
 
                "not finished reading, but all data sent to protocol.")
 
141
        smart_protocol_request.accept_bytes(self.rfile.read(data_length))
 
142
        assert smart_protocol_request.next_read_size() == 0, (
 
143
            "not finished reading, but all data sent to protocol.")
97
144
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
98
145
        self.end_headers()
99
146
        self.wfile.write(out_buffer.getvalue())
100
147
 
101
148
 
102
 
class TestCaseWithWebserver(tests.TestCaseWithTransport):
 
149
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
 
150
    """Always reply to range request as if they were single.
 
151
 
 
152
    Don't be explicit about it, just to annoy the clients.
 
153
    """
 
154
 
 
155
    def get_multiple_ranges(self, file, file_size, ranges):
 
156
        """Answer as if it was a single range request and ignores the rest"""
 
157
        (start, end) = ranges[0]
 
158
        return self.get_single_range(file, file_size, start, end)
 
159
 
 
160
 
 
161
class NoRangeRequestHandler(TestingHTTPRequestHandler):
 
162
    """Ignore range requests without notice"""
 
163
 
 
164
    # Just bypass the range handling done by TestingHTTPRequestHandler
 
165
    do_GET = SimpleHTTPRequestHandler.do_GET
 
166
 
 
167
 
 
168
class TestCaseWithWebserver(TestCaseWithTransport):
103
169
    """A support class that provides readonly urls that are http://.
104
170
 
105
171
    This is done by forcing the readonly server to be an http
108
174
    """
109
175
    def setUp(self):
110
176
        super(TestCaseWithWebserver, self).setUp()
111
 
        self.transport_readonly_server = http_server.HttpServer
 
177
        self.transport_readonly_server = HttpServer
112
178
 
113
179
 
114
180
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
119
185
    """
120
186
    def setUp(self):
121
187
        super(TestCaseWithTwoWebservers, self).setUp()
122
 
        self.transport_secondary_server = http_server.HttpServer
 
188
        self.transport_secondary_server = HttpServer
123
189
        self.__secondary_server = None
124
190
 
125
191
    def create_transport_secondary_server(self):
138
204
        return self.__secondary_server
139
205
 
140
206
 
141
 
class ProxyServer(http_server.HttpServer):
142
 
    """A proxy test server for http transports."""
143
 
 
144
 
    proxy_requests = True
145
 
 
146
 
 
147
 
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
207
class FakeProxyRequestHandler(TestingHTTPRequestHandler):
 
208
    """Append a '-proxied' suffix to file served"""
 
209
 
 
210
    def translate_path(self, path):
 
211
        # We need to act as a proxy and accept absolute urls,
 
212
        # which SimpleHTTPRequestHandler (grand parent) is not
 
213
        # ready for. So we just drop the protocol://host:port
 
214
        # part in front of the request-url (because we know we
 
215
        # would not forward the request to *another* proxy).
 
216
 
 
217
        # So we do what SimpleHTTPRequestHandler.translate_path
 
218
        # do beginning with python 2.4.3: abandon query
 
219
        # parameters, scheme, host port, etc (which ensure we
 
220
        # provide the right behaviour on all python versions).
 
221
        path = urlparse.urlparse(path)[2]
 
222
        # And now, we can apply *our* trick to proxy files
 
223
        self.path += '-proxied'
 
224
        # An finally we leave our mother class do whatever it
 
225
        # wants with the path
 
226
        return TestingHTTPRequestHandler.translate_path(self, path)
 
227
 
 
228
 
 
229
class RedirectRequestHandler(TestingHTTPRequestHandler):
148
230
    """Redirect all request to the specified server"""
149
231
 
150
232
    def parse_request(self):
151
233
        """Redirect a single HTTP request to another host"""
152
 
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
234
        valid = TestingHTTPRequestHandler.parse_request(self)
153
235
        if valid:
154
236
            tcs = self.server.test_case_server
155
237
            code, target = tcs.is_redirected(self.path)
157
239
                # Redirect as instructed
158
240
                self.send_response(code)
159
241
                self.send_header('Location', target)
160
 
                # We do not send a body
161
 
                self.send_header('Content-Length', '0')
162
242
                self.end_headers()
163
243
                return False # The job is done
164
244
            else:
167
247
        return valid
168
248
 
169
249
 
170
 
class HTTPServerRedirecting(http_server.HttpServer):
 
250
class HTTPServerRedirecting(HttpServer):
171
251
    """An HttpServer redirecting to another server """
172
252
 
173
 
    def __init__(self, request_handler=RedirectRequestHandler,
174
 
                 protocol_version=None):
175
 
        http_server.HttpServer.__init__(self, request_handler,
176
 
                                        protocol_version=protocol_version)
 
253
    def __init__(self, request_handler=RedirectRequestHandler):
 
254
        HttpServer.__init__(self, request_handler)
177
255
        # redirections is a list of tuples (source, target, code)
178
256
        # - source is a regexp for the paths requested
179
257
        # - target is a replacement for re.sub describing where
231
309
       self.old_server = self.get_secondary_server()
232
310
 
233
311
 
234
 
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
235
 
    """Requires an authentication to process requests.
236
 
 
237
 
    This is intended to be used with a server that always and
238
 
    only use one authentication scheme (implemented by daughter
239
 
    classes).
240
 
    """
241
 
 
242
 
    # The following attributes should be defined in the server
243
 
    # - auth_header_sent: the header name sent to require auth
244
 
    # - auth_header_recv: the header received containing auth
245
 
    # - auth_error_code: the error code to indicate auth required
246
 
 
247
 
    def do_GET(self):
248
 
        if self.authorized():
249
 
            return http_server.TestingHTTPRequestHandler.do_GET(self)
250
 
        else:
251
 
            # Note that we must update test_case_server *before*
252
 
            # sending the error or the client may try to read it
253
 
            # before we have sent the whole error back.
254
 
            tcs = self.server.test_case_server
255
 
            tcs.auth_required_errors += 1
256
 
            self.send_response(tcs.auth_error_code)
257
 
            self.send_header_auth_reqed()
258
 
            # We do not send a body
259
 
            self.send_header('Content-Length', '0')
260
 
            self.end_headers()
261
 
            return
262
 
 
263
 
 
264
 
class BasicAuthRequestHandler(AuthRequestHandler):
265
 
    """Implements the basic authentication of a request"""
266
 
 
267
 
    def authorized(self):
268
 
        tcs = self.server.test_case_server
269
 
        if tcs.auth_scheme != 'basic':
270
 
            return False
271
 
 
272
 
        auth_header = self.headers.get(tcs.auth_header_recv, None)
273
 
        if auth_header:
274
 
            scheme, raw_auth = auth_header.split(' ', 1)
275
 
            if scheme.lower() == tcs.auth_scheme:
276
 
                user, password = raw_auth.decode('base64').split(':')
277
 
                return tcs.authorized(user, password)
278
 
 
279
 
        return False
280
 
 
281
 
    def send_header_auth_reqed(self):
282
 
        tcs = self.server.test_case_server
283
 
        self.send_header(tcs.auth_header_sent,
284
 
                         'Basic realm="%s"' % tcs.auth_realm)
285
 
 
286
 
 
287
 
# FIXME: We could send an Authentication-Info header too when
288
 
# the authentication is succesful
289
 
 
290
 
class DigestAuthRequestHandler(AuthRequestHandler):
291
 
    """Implements the digest authentication of a request.
292
 
 
293
 
    We need persistence for some attributes and that can't be
294
 
    achieved here since we get instantiated for each request. We
295
 
    rely on the DigestAuthServer to take care of them.
296
 
    """
297
 
 
298
 
    def authorized(self):
299
 
        tcs = self.server.test_case_server
300
 
        if tcs.auth_scheme != 'digest':
301
 
            return False
302
 
 
303
 
        auth_header = self.headers.get(tcs.auth_header_recv, None)
304
 
        if auth_header is None:
305
 
            return False
306
 
        scheme, auth = auth_header.split(None, 1)
307
 
        if scheme.lower() == tcs.auth_scheme:
308
 
            auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
309
 
 
310
 
            return tcs.digest_authorized(auth_dict, self.command)
311
 
 
312
 
        return False
313
 
 
314
 
    def send_header_auth_reqed(self):
315
 
        tcs = self.server.test_case_server
316
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
317
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
318
 
                                                              'MD5')
319
 
        self.send_header(tcs.auth_header_sent,header)
320
 
 
321
 
 
322
 
class AuthServer(http_server.HttpServer):
323
 
    """Extends HttpServer with a dictionary of passwords.
324
 
 
325
 
    This is used as a base class for various schemes which should
326
 
    all use or redefined the associated AuthRequestHandler.
327
 
 
328
 
    Note that no users are defined by default, so add_user should
329
 
    be called before issuing the first request.
330
 
    """
331
 
 
332
 
    # The following attributes should be set dy daughter classes
333
 
    # and are used by AuthRequestHandler.
334
 
    auth_header_sent = None
335
 
    auth_header_recv = None
336
 
    auth_error_code = None
337
 
    auth_realm = "Thou should not pass"
338
 
 
339
 
    def __init__(self, request_handler, auth_scheme,
340
 
                 protocol_version=None):
341
 
        http_server.HttpServer.__init__(self, request_handler,
342
 
                                        protocol_version=protocol_version)
343
 
        self.auth_scheme = auth_scheme
344
 
        self.password_of = {}
345
 
        self.auth_required_errors = 0
346
 
 
347
 
    def add_user(self, user, password):
348
 
        """Declare a user with an associated password.
349
 
 
350
 
        password can be empty, use an empty string ('') in that
351
 
        case, not None.
352
 
        """
353
 
        self.password_of[user] = password
354
 
 
355
 
    def authorized(self, user, password):
356
 
        """Check that the given user provided the right password"""
357
 
        expected_password = self.password_of.get(user, None)
358
 
        return expected_password is not None and password == expected_password
359
 
 
360
 
 
361
 
# FIXME: There is some code duplication with
362
 
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
363
 
# grows, it may require a refactoring. Also, we don't implement
364
 
# SHA algorithm nor MD5-sess here, but that does not seem worth
365
 
# it.
366
 
class DigestAuthServer(AuthServer):
367
 
    """A digest authentication server"""
368
 
 
369
 
    auth_nonce = 'now!'
370
 
 
371
 
    def __init__(self, request_handler, auth_scheme,
372
 
                 protocol_version=None):
373
 
        AuthServer.__init__(self, request_handler, auth_scheme,
374
 
                            protocol_version=protocol_version)
375
 
 
376
 
    def digest_authorized(self, auth, command):
377
 
        nonce = auth['nonce']
378
 
        if nonce != self.auth_nonce:
379
 
            return False
380
 
        realm = auth['realm']
381
 
        if realm != self.auth_realm:
382
 
            return False
383
 
        user = auth['username']
384
 
        if not self.password_of.has_key(user):
385
 
            return False
386
 
        algorithm= auth['algorithm']
387
 
        if algorithm != 'MD5':
388
 
            return False
389
 
        qop = auth['qop']
390
 
        if qop != 'auth':
391
 
            return False
392
 
 
393
 
        password = self.password_of[user]
394
 
 
395
 
        # Recalculate the response_digest to compare with the one
396
 
        # sent by the client
397
 
        A1 = '%s:%s:%s' % (user, realm, password)
398
 
        A2 = '%s:%s' % (command, auth['uri'])
399
 
 
400
 
        H = lambda x: osutils.md5(x).hexdigest()
401
 
        KD = lambda secret, data: H("%s:%s" % (secret, data))
402
 
 
403
 
        nonce_count = int(auth['nc'], 16)
404
 
 
405
 
        ncvalue = '%08x' % nonce_count
406
 
 
407
 
        cnonce = auth['cnonce']
408
 
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
409
 
        response_digest = KD(H(A1), noncebit)
410
 
 
411
 
        return response_digest == auth['response']
412
 
 
413
 
class HTTPAuthServer(AuthServer):
414
 
    """An HTTP server requiring authentication"""
415
 
 
416
 
    def init_http_auth(self):
417
 
        self.auth_header_sent = 'WWW-Authenticate'
418
 
        self.auth_header_recv = 'Authorization'
419
 
        self.auth_error_code = 401
420
 
 
421
 
 
422
 
class ProxyAuthServer(AuthServer):
423
 
    """A proxy server requiring authentication"""
424
 
 
425
 
    def init_proxy_auth(self):
426
 
        self.proxy_requests = True
427
 
        self.auth_header_sent = 'Proxy-Authenticate'
428
 
        self.auth_header_recv = 'Proxy-Authorization'
429
 
        self.auth_error_code = 407
430
 
 
431
 
 
432
 
class HTTPBasicAuthServer(HTTPAuthServer):
433
 
    """An HTTP server requiring basic authentication"""
434
 
 
435
 
    def __init__(self, protocol_version=None):
436
 
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
437
 
                                protocol_version=protocol_version)
438
 
        self.init_http_auth()
439
 
 
440
 
 
441
 
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
442
 
    """An HTTP server requiring digest authentication"""
443
 
 
444
 
    def __init__(self, protocol_version=None):
445
 
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
446
 
                                  protocol_version=protocol_version)
447
 
        self.init_http_auth()
448
 
 
449
 
 
450
 
class ProxyBasicAuthServer(ProxyAuthServer):
451
 
    """A proxy server requiring basic authentication"""
452
 
 
453
 
    def __init__(self, protocol_version=None):
454
 
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
455
 
                                 protocol_version=protocol_version)
456
 
        self.init_proxy_auth()
457
 
 
458
 
 
459
 
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
460
 
    """A proxy server requiring basic authentication"""
461
 
 
462
 
    def __init__(self, protocol_version=None):
463
 
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
464
 
                                 protocol_version=protocol_version)
465
 
        self.init_proxy_auth()
466
 
 
467