~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: Martin Pool
  • Date: 2007-06-25 05:12:57 UTC
  • mto: This revision was merged to the branch mainline in revision 2546.
  • Revision ID: mbp@sourcefrog.net-20070625051257-fpzcv067ye6a341c
Fix typo and remove version number from README

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
from cStringIO import StringIO
18
18
import errno
 
19
import md5
 
20
from SimpleHTTPServer import SimpleHTTPRequestHandler
19
21
import re
 
22
import sha
20
23
import socket
21
 
import threading
22
24
import time
23
25
import urllib2
24
26
import urlparse
25
27
 
26
 
 
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    tests,
 
28
from bzrlib.smart import protocol
 
29
from bzrlib.tests import TestCaseWithTransport
 
30
from bzrlib.tests.HttpServer import (
 
31
    HttpServer,
 
32
    TestingHTTPRequestHandler,
31
33
    )
32
 
from bzrlib.smart import medium, protocol
33
 
from bzrlib.tests import http_server
34
34
from bzrlib.transport import (
35
 
    chroot,
36
35
    get_transport,
37
36
    )
38
37
 
39
38
 
40
 
class HTTPServerWithSmarts(http_server.HttpServer):
 
39
class WallRequestHandler(TestingHTTPRequestHandler):
 
40
    """Whatever request comes in, close the connection"""
 
41
 
 
42
    def handle_one_request(self):
 
43
        """Handle a single HTTP request, by abruptly closing the connection"""
 
44
        self.close_connection = 1
 
45
 
 
46
 
 
47
class BadStatusRequestHandler(TestingHTTPRequestHandler):
 
48
    """Whatever request comes in, returns a bad status"""
 
49
 
 
50
    def parse_request(self):
 
51
        """Fakes handling a single HTTP request, returns a bad status"""
 
52
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
53
        try:
 
54
            self.send_response(0, "Bad status")
 
55
            self.end_headers()
 
56
        except socket.error, e:
 
57
            # We don't want to pollute the test results with
 
58
            # spurious server errors while test succeed. In our
 
59
            # case, it may occur that the test has already read
 
60
            # the 'Bad Status' and closed the socket while we are
 
61
            # still trying to send some headers... So the test is
 
62
            # ok, but if we raise the exception, the output is
 
63
            # dirty. So we don't raise, but we close the
 
64
            # connection, just to be safe :)
 
65
            spurious = [errno.EPIPE,
 
66
                        errno.ECONNRESET,
 
67
                        errno.ECONNABORTED,
 
68
                        ]
 
69
            if (len(e.args) > 0) and (e.args[0] in spurious):
 
70
                self.close_connection = 1
 
71
                pass
 
72
            else:
 
73
                raise
 
74
        return False
 
75
 
 
76
 
 
77
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
 
78
    """Whatever request comes in, returns am invalid status"""
 
79
 
 
80
    def parse_request(self):
 
81
        """Fakes handling a single HTTP request, returns a bad status"""
 
82
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
83
        self.wfile.write("Invalid status line\r\n")
 
84
        return False
 
85
 
 
86
 
 
87
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
 
88
    """Whatever request comes in, returns a bad protocol version"""
 
89
 
 
90
    def parse_request(self):
 
91
        """Fakes handling a single HTTP request, returns a bad status"""
 
92
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
93
        # Returns an invalid protocol version, but curl just
 
94
        # ignores it and those cannot be tested.
 
95
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
 
96
                                           404,
 
97
                                           'Look at my protocol version'))
 
98
        return False
 
99
 
 
100
 
 
101
class ForbiddenRequestHandler(TestingHTTPRequestHandler):
 
102
    """Whatever request comes in, returns a 403 code"""
 
103
 
 
104
    def parse_request(self):
 
105
        """Handle a single HTTP request, by replying we cannot handle it"""
 
106
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
107
        self.send_error(403)
 
108
        return False
 
109
 
 
110
 
 
111
class HTTPServerWithSmarts(HttpServer):
41
112
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
42
113
    trigger a smart server to execute with a transport rooted at the rootdir of
43
114
    the HTTP server.
44
115
    """
45
116
 
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
 
    """
 
117
    def __init__(self):
 
118
        HttpServer.__init__(self, SmartRequestHandler)
 
119
 
 
120
 
 
121
class SmartRequestHandler(TestingHTTPRequestHandler):
 
122
    """Extend TestingHTTPRequestHandler to support smart client POSTs."""
56
123
 
57
124
    def do_POST(self):
58
125
        """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.start_server()
62
 
        try:
63
 
            t = get_transport(chroot_server.get_url())
64
 
            self.do_POST_inner(t)
65
 
        finally:
66
 
            chroot_server.stop_server()
67
 
 
68
 
    def do_POST_inner(self, chrooted_transport):
69
126
        self.send_response(200)
70
127
        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'])
 
128
        transport = get_transport(self.server.test_case_server._home_dir)
78
129
        # TODO: We might like to support streaming responses.  1.0 allows no
79
130
        # Content-length in this case, so for integrity we should perform our
80
131
        # own chunking within the stream.
82
133
        # the HTTP chunking as this will allow HTTP persistence safely, even if
83
134
        # we have to stop early due to error, but we would also have to use the
84
135
        # 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
136
        out_buffer = StringIO()
89
 
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
 
137
        smart_protocol_request = protocol.SmartServerRequestProtocolOne(
 
138
                transport, out_buffer.write)
 
139
        # if this fails, we should return 400 bad request, but failure is
 
140
        # failure for now - RBC 20060919
 
141
        data_length = int(self.headers['Content-Length'])
90
142
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
91
143
        # feeding the bytes in the http request to the smart_protocol_request,
92
144
        # 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.")
 
145
        smart_protocol_request.accept_bytes(self.rfile.read(data_length))
 
146
        assert smart_protocol_request.next_read_size() == 0, (
 
147
            "not finished reading, but all data sent to protocol.")
97
148
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
98
149
        self.end_headers()
99
150
        self.wfile.write(out_buffer.getvalue())
100
151
 
101
152
 
102
 
class TestCaseWithWebserver(tests.TestCaseWithTransport):
 
153
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
 
154
    """Always reply to range request as if they were single.
 
155
 
 
156
    Don't be explicit about it, just to annoy the clients.
 
157
    """
 
158
 
 
159
    def get_multiple_ranges(self, file, file_size, ranges):
 
160
        """Answer as if it was a single range request and ignores the rest"""
 
161
        (start, end) = ranges[0]
 
162
        return self.get_single_range(file, file_size, start, end)
 
163
 
 
164
 
 
165
class SingleOnlyRangeRequestHandler(TestingHTTPRequestHandler):
 
166
    """Only reply to simple range requests, errors out on multiple"""
 
167
 
 
168
    def get_multiple_ranges(self, file, file_size, ranges):
 
169
        """Refuses the multiple ranges request"""
 
170
        if len(ranges) > 1:
 
171
            file.close()
 
172
            self.send_error(416, "Requested range not satisfiable")
 
173
            return
 
174
        (start, end) = ranges[0]
 
175
        return self.get_single_range(file, file_size, start, end)
 
176
 
 
177
 
 
178
class NoRangeRequestHandler(TestingHTTPRequestHandler):
 
179
    """Ignore range requests without notice"""
 
180
 
 
181
    # Just bypass the range handling done by TestingHTTPRequestHandler
 
182
    do_GET = SimpleHTTPRequestHandler.do_GET
 
183
 
 
184
 
 
185
class TestCaseWithWebserver(TestCaseWithTransport):
103
186
    """A support class that provides readonly urls that are http://.
104
187
 
105
188
    This is done by forcing the readonly server to be an http
106
189
    one. This will currently fail if the primary transport is not
107
190
    backed by regular disk files.
108
191
    """
109
 
 
110
 
    # This can be overriden or parametrized by daughter clasess if needed, but
111
 
    # it must exist so that the create_transport_readonly_server() method can
112
 
    # propagate it.
113
 
    _protocol_version = None
114
 
 
115
192
    def setUp(self):
116
193
        super(TestCaseWithWebserver, self).setUp()
117
 
        self.transport_readonly_server = http_server.HttpServer
118
 
 
119
 
    def create_transport_readonly_server(self):
120
 
        return self.transport_readonly_server(
121
 
            protocol_version=self._protocol_version)
 
194
        self.transport_readonly_server = HttpServer
122
195
 
123
196
 
124
197
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
129
202
    """
130
203
    def setUp(self):
131
204
        super(TestCaseWithTwoWebservers, self).setUp()
132
 
        self.transport_secondary_server = http_server.HttpServer
 
205
        self.transport_secondary_server = HttpServer
133
206
        self.__secondary_server = None
134
207
 
135
208
    def create_transport_secondary_server(self):
137
210
 
138
211
        This is mostly a hook for daughter classes.
139
212
        """
140
 
        return self.transport_secondary_server(
141
 
            protocol_version=self._protocol_version)
 
213
        return self.transport_secondary_server()
142
214
 
143
215
    def get_secondary_server(self):
144
216
        """Get the server instance for the secondary transport."""
145
217
        if self.__secondary_server is None:
146
218
            self.__secondary_server = self.create_transport_secondary_server()
147
 
            self.start_server(self.__secondary_server)
 
219
            self.__secondary_server.setUp()
 
220
            self.addCleanup(self.__secondary_server.tearDown)
148
221
        return self.__secondary_server
149
222
 
150
223
 
151
 
class ProxyServer(http_server.HttpServer):
 
224
class ProxyServer(HttpServer):
152
225
    """A proxy test server for http transports."""
153
226
 
154
227
    proxy_requests = True
155
228
 
156
229
 
157
 
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
230
class RedirectRequestHandler(TestingHTTPRequestHandler):
158
231
    """Redirect all request to the specified server"""
159
232
 
160
233
    def parse_request(self):
161
234
        """Redirect a single HTTP request to another host"""
162
 
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
235
        valid = TestingHTTPRequestHandler.parse_request(self)
163
236
        if valid:
164
237
            tcs = self.server.test_case_server
165
238
            code, target = tcs.is_redirected(self.path)
167
240
                # Redirect as instructed
168
241
                self.send_response(code)
169
242
                self.send_header('Location', target)
170
 
                # We do not send a body
171
 
                self.send_header('Content-Length', '0')
172
243
                self.end_headers()
173
244
                return False # The job is done
174
245
            else:
177
248
        return valid
178
249
 
179
250
 
180
 
class HTTPServerRedirecting(http_server.HttpServer):
 
251
class HTTPServerRedirecting(HttpServer):
181
252
    """An HttpServer redirecting to another server """
182
253
 
183
 
    def __init__(self, request_handler=RedirectRequestHandler,
184
 
                 protocol_version=None):
185
 
        http_server.HttpServer.__init__(self, request_handler,
186
 
                                        protocol_version=protocol_version)
 
254
    def __init__(self, request_handler=RedirectRequestHandler):
 
255
        HttpServer.__init__(self, request_handler)
187
256
        # redirections is a list of tuples (source, target, code)
188
257
        # - source is a regexp for the paths requested
189
258
        # - target is a replacement for re.sub describing where
229
298
   def create_transport_secondary_server(self):
230
299
       """Create the secondary server redirecting to the primary server"""
231
300
       new = self.get_readonly_server()
232
 
       redirecting = HTTPServerRedirecting(
233
 
           protocol_version=self._protocol_version)
 
301
       redirecting = HTTPServerRedirecting()
234
302
       redirecting.redirect_to(new.host, new.port)
235
303
       return redirecting
236
304
 
242
310
       self.old_server = self.get_secondary_server()
243
311
 
244
312
 
245
 
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
313
class AuthRequestHandler(TestingHTTPRequestHandler):
246
314
    """Requires an authentication to process requests.
247
315
 
248
316
    This is intended to be used with a server that always and
257
325
 
258
326
    def do_GET(self):
259
327
        if self.authorized():
260
 
            return http_server.TestingHTTPRequestHandler.do_GET(self)
 
328
            return TestingHTTPRequestHandler.do_GET(self)
261
329
        else:
262
330
            # Note that we must update test_case_server *before*
263
331
            # sending the error or the client may try to read it
266
334
            tcs.auth_required_errors += 1
267
335
            self.send_response(tcs.auth_error_code)
268
336
            self.send_header_auth_reqed()
269
 
            # We do not send a body
270
 
            self.send_header('Content-Length', '0')
271
337
            self.end_headers()
272
338
            return
273
339
 
 
340
        TestingHTTPRequestHandler.do_GET(self)
 
341
 
274
342
 
275
343
class BasicAuthRequestHandler(AuthRequestHandler):
276
344
    """Implements the basic authentication of a request"""
308
376
 
309
377
    def authorized(self):
310
378
        tcs = self.server.test_case_server
 
379
        if tcs.auth_scheme != 'digest':
 
380
            return False
311
381
 
312
382
        auth_header = self.headers.get(tcs.auth_header_recv, None)
313
383
        if auth_header is None:
323
393
    def send_header_auth_reqed(self):
324
394
        tcs = self.server.test_case_server
325
395
        header = 'Digest realm="%s", ' % tcs.auth_realm
326
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
327
 
                                                              'MD5')
328
 
        self.send_header(tcs.auth_header_sent,header)
329
 
 
330
 
 
331
 
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
332
 
    """Implements a digest and basic authentication of a request.
333
 
 
334
 
    I.e. the server proposes both schemes and the client should choose the best
335
 
    one it can handle, which, in that case, should be digest, the only scheme
336
 
    accepted here.
337
 
    """
338
 
 
339
 
    def send_header_auth_reqed(self):
340
 
        tcs = self.server.test_case_server
341
 
        self.send_header(tcs.auth_header_sent,
342
 
                         'Basic realm="%s"' % tcs.auth_realm)
343
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
344
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
345
 
                                                              'MD5')
346
 
        self.send_header(tcs.auth_header_sent,header)
347
 
 
348
 
 
349
 
class AuthServer(http_server.HttpServer):
 
396
        header += 'nonce="%s", algorithm=%s, qop=auth' % (tcs.auth_nonce, 'MD5')
 
397
        self.send_header(tcs.auth_header_sent,header)
 
398
 
 
399
 
 
400
class AuthServer(HttpServer):
350
401
    """Extends HttpServer with a dictionary of passwords.
351
402
 
352
403
    This is used as a base class for various schemes which should
363
414
    auth_error_code = None
364
415
    auth_realm = "Thou should not pass"
365
416
 
366
 
    def __init__(self, request_handler, auth_scheme,
367
 
                 protocol_version=None):
368
 
        http_server.HttpServer.__init__(self, request_handler,
369
 
                                        protocol_version=protocol_version)
 
417
    def __init__(self, request_handler, auth_scheme):
 
418
        HttpServer.__init__(self, request_handler)
370
419
        self.auth_scheme = auth_scheme
371
420
        self.password_of = {}
372
421
        self.auth_required_errors = 0
386
435
 
387
436
 
388
437
# FIXME: There is some code duplication with
389
 
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
 
438
# _urllib2_wrappers.py.DigestAuthHandler. If that duplciation
390
439
# grows, it may require a refactoring. Also, we don't implement
391
440
# SHA algorithm nor MD5-sess here, but that does not seem worth
392
441
# it.
395
444
 
396
445
    auth_nonce = 'now!'
397
446
 
398
 
    def __init__(self, request_handler, auth_scheme,
399
 
                 protocol_version=None):
400
 
        AuthServer.__init__(self, request_handler, auth_scheme,
401
 
                            protocol_version=protocol_version)
 
447
    def __init__(self, request_handler, auth_scheme):
 
448
        AuthServer.__init__(self, request_handler, auth_scheme)
402
449
 
403
450
    def digest_authorized(self, auth, command):
404
451
        nonce = auth['nonce']
424
471
        A1 = '%s:%s:%s' % (user, realm, password)
425
472
        A2 = '%s:%s' % (command, auth['uri'])
426
473
 
427
 
        H = lambda x: osutils.md5(x).hexdigest()
 
474
        H = lambda x: md5.new(x).hexdigest()
428
475
        KD = lambda secret, data: H("%s:%s" % (secret, data))
429
476
 
430
477
        nonce_count = int(auth['nc'], 16)
437
484
 
438
485
        return response_digest == auth['response']
439
486
 
440
 
 
441
487
class HTTPAuthServer(AuthServer):
442
488
    """An HTTP server requiring authentication"""
443
489
 
460
506
class HTTPBasicAuthServer(HTTPAuthServer):
461
507
    """An HTTP server requiring basic authentication"""
462
508
 
463
 
    def __init__(self, protocol_version=None):
464
 
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
465
 
                                protocol_version=protocol_version)
 
509
    def __init__(self):
 
510
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
466
511
        self.init_http_auth()
467
512
 
468
513
 
469
514
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
470
515
    """An HTTP server requiring digest authentication"""
471
516
 
472
 
    def __init__(self, protocol_version=None):
473
 
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
474
 
                                  protocol_version=protocol_version)
475
 
        self.init_http_auth()
476
 
 
477
 
 
478
 
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
479
 
    """An HTTP server requiring basic or digest authentication"""
480
 
 
481
 
    def __init__(self, protocol_version=None):
482
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
483
 
                                  'basicdigest',
484
 
                                  protocol_version=protocol_version)
485
 
        self.init_http_auth()
486
 
        # We really accept Digest only
487
 
        self.auth_scheme = 'digest'
 
517
    def __init__(self):
 
518
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
519
        self.init_http_auth()
488
520
 
489
521
 
490
522
class ProxyBasicAuthServer(ProxyAuthServer):
491
523
    """A proxy server requiring basic authentication"""
492
524
 
493
 
    def __init__(self, protocol_version=None):
494
 
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
495
 
                                 protocol_version=protocol_version)
 
525
    def __init__(self):
 
526
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
496
527
        self.init_proxy_auth()
497
528
 
498
529
 
499
530
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
500
531
    """A proxy server requiring basic authentication"""
501
532
 
502
 
    def __init__(self, protocol_version=None):
503
 
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
504
 
                                 protocol_version=protocol_version)
505
 
        self.init_proxy_auth()
506
 
 
507
 
 
508
 
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
509
 
    """An proxy server requiring basic or digest authentication"""
510
 
 
511
 
    def __init__(self, protocol_version=None):
512
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
513
 
                                  'basicdigest',
514
 
                                  protocol_version=protocol_version)
515
 
        self.init_proxy_auth()
516
 
        # We really accept Digest only
517
 
        self.auth_scheme = 'digest'
 
533
    def __init__(self):
 
534
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
535
        self.init_proxy_auth()
518
536
 
519
537