~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: Ian Clatworthy
  • Date: 2007-12-11 02:07:30 UTC
  • mto: (3119.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3120.
  • Revision ID: ian.clatworthy@internode.on.net-20071211020730-sdj4kj794dw0628e
make help topics more discoverable

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 LimitedRangeRequestHandler(TestingHTTPRequestHandler):
 
154
    """Errors out when range specifiers exceed the limit"""
 
155
 
 
156
    def get_multiple_ranges(self, file, file_size, ranges):
 
157
        """Refuses the multiple ranges request"""
 
158
        tcs = self.server.test_case_server
 
159
        if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
 
160
            file.close()
 
161
            # Emulate apache behavior
 
162
            self.send_error(400, "Bad Request")
 
163
            return
 
164
        return TestingHTTPRequestHandler.get_multiple_ranges(self, file,
 
165
                                                             file_size, ranges)
 
166
 
 
167
 
 
168
class LimitedRangeHTTPServer(HttpServer):
 
169
    """An HttpServer erroring out on requests with too much range specifiers"""
 
170
 
 
171
    def __init__(self, request_handler=LimitedRangeRequestHandler,
 
172
                 range_limit=None):
 
173
        HttpServer.__init__(self, request_handler)
 
174
        self.range_limit = range_limit
 
175
 
 
176
 
 
177
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
 
178
    """Always reply to range request as if they were single.
 
179
 
 
180
    Don't be explicit about it, just to annoy the clients.
 
181
    """
 
182
 
 
183
    def get_multiple_ranges(self, file, file_size, ranges):
 
184
        """Answer as if it was a single range request and ignores the rest"""
 
185
        (start, end) = ranges[0]
 
186
        return self.get_single_range(file, file_size, start, end)
 
187
 
 
188
 
 
189
class SingleOnlyRangeRequestHandler(TestingHTTPRequestHandler):
 
190
    """Only reply to simple range requests, errors out on multiple"""
 
191
 
 
192
    def get_multiple_ranges(self, file, file_size, ranges):
 
193
        """Refuses the multiple ranges request"""
 
194
        if len(ranges) > 1:
 
195
            file.close()
 
196
            self.send_error(416, "Requested range not satisfiable")
 
197
            return
 
198
        (start, end) = ranges[0]
 
199
        return self.get_single_range(file, file_size, start, end)
 
200
 
 
201
 
 
202
class NoRangeRequestHandler(TestingHTTPRequestHandler):
 
203
    """Ignore range requests without notice"""
 
204
 
 
205
    def do_GET(self):
 
206
        # Update the statistics
 
207
        self.server.test_case_server.GET_request_nb += 1
 
208
        # Just bypass the range handling done by TestingHTTPRequestHandler
 
209
        return SimpleHTTPRequestHandler.do_GET(self)
 
210
 
 
211
 
 
212
class TestCaseWithWebserver(TestCaseWithTransport):
103
213
    """A support class that provides readonly urls that are http://.
104
214
 
105
215
    This is done by forcing the readonly server to be an http
108
218
    """
109
219
    def setUp(self):
110
220
        super(TestCaseWithWebserver, self).setUp()
111
 
        self.transport_readonly_server = http_server.HttpServer
 
221
        self.transport_readonly_server = HttpServer
112
222
 
113
223
 
114
224
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
119
229
    """
120
230
    def setUp(self):
121
231
        super(TestCaseWithTwoWebservers, self).setUp()
122
 
        self.transport_secondary_server = http_server.HttpServer
 
232
        self.transport_secondary_server = HttpServer
123
233
        self.__secondary_server = None
124
234
 
125
235
    def create_transport_secondary_server(self):
133
243
        """Get the server instance for the secondary transport."""
134
244
        if self.__secondary_server is None:
135
245
            self.__secondary_server = self.create_transport_secondary_server()
136
 
            self.start_server(self.__secondary_server)
 
246
            self.__secondary_server.setUp()
 
247
            self.addCleanup(self.__secondary_server.tearDown)
137
248
        return self.__secondary_server
138
249
 
139
250
 
140
 
class ProxyServer(http_server.HttpServer):
 
251
class ProxyServer(HttpServer):
141
252
    """A proxy test server for http transports."""
142
253
 
143
254
    proxy_requests = True
144
255
 
145
256
 
146
 
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
257
class RedirectRequestHandler(TestingHTTPRequestHandler):
147
258
    """Redirect all request to the specified server"""
148
259
 
149
260
    def parse_request(self):
150
261
        """Redirect a single HTTP request to another host"""
151
 
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
262
        valid = TestingHTTPRequestHandler.parse_request(self)
152
263
        if valid:
153
264
            tcs = self.server.test_case_server
154
265
            code, target = tcs.is_redirected(self.path)
156
267
                # Redirect as instructed
157
268
                self.send_response(code)
158
269
                self.send_header('Location', target)
159
 
                # We do not send a body
160
 
                self.send_header('Content-Length', '0')
161
270
                self.end_headers()
162
271
                return False # The job is done
163
272
            else:
166
275
        return valid
167
276
 
168
277
 
169
 
class HTTPServerRedirecting(http_server.HttpServer):
 
278
class HTTPServerRedirecting(HttpServer):
170
279
    """An HttpServer redirecting to another server """
171
280
 
172
 
    def __init__(self, request_handler=RedirectRequestHandler,
173
 
                 protocol_version=None):
174
 
        http_server.HttpServer.__init__(self, request_handler,
175
 
                                        protocol_version=protocol_version)
 
281
    def __init__(self, request_handler=RedirectRequestHandler):
 
282
        HttpServer.__init__(self, request_handler)
176
283
        # redirections is a list of tuples (source, target, code)
177
284
        # - source is a regexp for the paths requested
178
285
        # - target is a replacement for re.sub describing where
230
337
       self.old_server = self.get_secondary_server()
231
338
 
232
339
 
233
 
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
340
class AuthRequestHandler(TestingHTTPRequestHandler):
234
341
    """Requires an authentication to process requests.
235
342
 
236
343
    This is intended to be used with a server that always and
245
352
 
246
353
    def do_GET(self):
247
354
        if self.authorized():
248
 
            return http_server.TestingHTTPRequestHandler.do_GET(self)
 
355
            return TestingHTTPRequestHandler.do_GET(self)
249
356
        else:
250
357
            # Note that we must update test_case_server *before*
251
358
            # sending the error or the client may try to read it
254
361
            tcs.auth_required_errors += 1
255
362
            self.send_response(tcs.auth_error_code)
256
363
            self.send_header_auth_reqed()
257
 
            # We do not send a body
258
 
            self.send_header('Content-Length', '0')
259
364
            self.end_headers()
260
365
            return
261
366
 
296
401
 
297
402
    def authorized(self):
298
403
        tcs = self.server.test_case_server
 
404
        if tcs.auth_scheme != 'digest':
 
405
            return False
299
406
 
300
407
        auth_header = self.headers.get(tcs.auth_header_recv, None)
301
408
        if auth_header is None:
316
423
        self.send_header(tcs.auth_header_sent,header)
317
424
 
318
425
 
319
 
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
320
 
    """Implements a digest and basic authentication of a request.
321
 
 
322
 
    I.e. the server proposes both schemes and the client should choose the best
323
 
    one it can handle, which, in that case, should be digest, the only scheme
324
 
    accepted here.
325
 
    """
326
 
 
327
 
    def send_header_auth_reqed(self):
328
 
        tcs = self.server.test_case_server
329
 
        self.send_header(tcs.auth_header_sent,
330
 
                         'Basic realm="%s"' % tcs.auth_realm)
331
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
332
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
333
 
                                                              'MD5')
334
 
        self.send_header(tcs.auth_header_sent,header)
335
 
 
336
 
 
337
 
class AuthServer(http_server.HttpServer):
 
426
class AuthServer(HttpServer):
338
427
    """Extends HttpServer with a dictionary of passwords.
339
428
 
340
429
    This is used as a base class for various schemes which should
351
440
    auth_error_code = None
352
441
    auth_realm = "Thou should not pass"
353
442
 
354
 
    def __init__(self, request_handler, auth_scheme,
355
 
                 protocol_version=None):
356
 
        http_server.HttpServer.__init__(self, request_handler,
357
 
                                        protocol_version=protocol_version)
 
443
    def __init__(self, request_handler, auth_scheme):
 
444
        HttpServer.__init__(self, request_handler)
358
445
        self.auth_scheme = auth_scheme
359
446
        self.password_of = {}
360
447
        self.auth_required_errors = 0
383
470
 
384
471
    auth_nonce = 'now!'
385
472
 
386
 
    def __init__(self, request_handler, auth_scheme,
387
 
                 protocol_version=None):
388
 
        AuthServer.__init__(self, request_handler, auth_scheme,
389
 
                            protocol_version=protocol_version)
 
473
    def __init__(self, request_handler, auth_scheme):
 
474
        AuthServer.__init__(self, request_handler, auth_scheme)
390
475
 
391
476
    def digest_authorized(self, auth, command):
392
477
        nonce = auth['nonce']
412
497
        A1 = '%s:%s:%s' % (user, realm, password)
413
498
        A2 = '%s:%s' % (command, auth['uri'])
414
499
 
415
 
        H = lambda x: osutils.md5(x).hexdigest()
 
500
        H = lambda x: md5.new(x).hexdigest()
416
501
        KD = lambda secret, data: H("%s:%s" % (secret, data))
417
502
 
418
503
        nonce_count = int(auth['nc'], 16)
425
510
 
426
511
        return response_digest == auth['response']
427
512
 
428
 
 
429
513
class HTTPAuthServer(AuthServer):
430
514
    """An HTTP server requiring authentication"""
431
515
 
448
532
class HTTPBasicAuthServer(HTTPAuthServer):
449
533
    """An HTTP server requiring basic authentication"""
450
534
 
451
 
    def __init__(self, protocol_version=None):
452
 
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
453
 
                                protocol_version=protocol_version)
 
535
    def __init__(self):
 
536
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
454
537
        self.init_http_auth()
455
538
 
456
539
 
457
540
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
458
541
    """An HTTP server requiring digest authentication"""
459
542
 
460
 
    def __init__(self, protocol_version=None):
461
 
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
462
 
                                  protocol_version=protocol_version)
463
 
        self.init_http_auth()
464
 
 
465
 
 
466
 
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
467
 
    """An HTTP server requiring basic or digest authentication"""
468
 
 
469
 
    def __init__(self, protocol_version=None):
470
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
471
 
                                  'basicdigest',
472
 
                                  protocol_version=protocol_version)
473
 
        self.init_http_auth()
474
 
        # We really accept Digest only
475
 
        self.auth_scheme = 'digest'
 
543
    def __init__(self):
 
544
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
545
        self.init_http_auth()
476
546
 
477
547
 
478
548
class ProxyBasicAuthServer(ProxyAuthServer):
479
549
    """A proxy server requiring basic authentication"""
480
550
 
481
 
    def __init__(self, protocol_version=None):
482
 
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
483
 
                                 protocol_version=protocol_version)
 
551
    def __init__(self):
 
552
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
484
553
        self.init_proxy_auth()
485
554
 
486
555
 
487
556
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
488
557
    """A proxy server requiring basic authentication"""
489
558
 
490
 
    def __init__(self, protocol_version=None):
491
 
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
492
 
                                 protocol_version=protocol_version)
493
 
        self.init_proxy_auth()
494
 
 
495
 
 
496
 
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
497
 
    """An proxy server requiring basic or digest authentication"""
498
 
 
499
 
    def __init__(self, protocol_version=None):
500
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
501
 
                                  'basicdigest',
502
 
                                  protocol_version=protocol_version)
503
 
        self.init_proxy_auth()
504
 
        # We really accept Digest only
505
 
        self.auth_scheme = 'digest'
 
559
    def __init__(self):
 
560
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
561
        self.init_proxy_auth()
506
562
 
507
563