~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_utils.py

  • Committer: Vincent Ladeuil
  • Date: 2007-11-24 14:20:59 UTC
  • mto: (3928.1.1 bzr.integration)
  • mto: This revision was merged to the branch mainline in revision 3929.
  • Revision ID: v.ladeuil+lp@free.fr-20071124142059-2114qtsgfdv8g9p1
Ssl files needed for the test https server.

* bzrlib/tests/ssl_certs/create_ssls.py: 
Script to create the ssl keys and certificates.

* bzrlib/tests/ssl_certs/server.crt: 
Server certificate signed by the certificate authority.

* bzrlib/tests/ssl_certs/server.csr: 
Server certificate signing request.

* bzrlib/tests/ssl_certs/server_without_pass.key: 
Server key usable without password.

* bzrlib/tests/ssl_certs/server_with_pass.key: 
Server key.

* bzrlib/tests/ssl_certs/ca.key: 
Certificate authority private key.

* bzrlib/tests/ssl_certs/ca.crt: 
Certificate authority certificate.

* bzrlib/tests/ssl_certs/__init__.py: 
Provide access to ssl files (keys and certificates). 

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
from SimpleHTTPServer import SimpleHTTPRequestHandler
19
20
import re
20
21
import socket
21
 
import threading
22
22
import time
23
23
import urllib2
24
24
import urlparse
25
25
 
26
 
 
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    tests,
 
26
from bzrlib.osutils import md5
 
27
from bzrlib.smart import protocol
 
28
from bzrlib.tests import TestCaseWithTransport
 
29
from bzrlib.tests.http_server import (
 
30
    HttpServer,
 
31
    TestingHTTPRequestHandler,
31
32
    )
32
 
from bzrlib.smart import medium, protocol
33
 
from bzrlib.tests import http_server
34
33
from bzrlib.transport import (
35
 
    chroot,
36
34
    get_transport,
37
35
    )
38
36
 
39
37
 
40
 
class HTTPServerWithSmarts(http_server.HttpServer):
 
38
class WallRequestHandler(TestingHTTPRequestHandler):
 
39
    """Whatever request comes in, close the connection"""
 
40
 
 
41
    def handle_one_request(self):
 
42
        """Handle a single HTTP request, by abruptly closing the connection"""
 
43
        self.close_connection = 1
 
44
 
 
45
 
 
46
class BadStatusRequestHandler(TestingHTTPRequestHandler):
 
47
    """Whatever request comes in, returns a bad status"""
 
48
 
 
49
    def parse_request(self):
 
50
        """Fakes handling a single HTTP request, returns a bad status"""
 
51
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
52
        try:
 
53
            self.send_response(0, "Bad status")
 
54
            self.end_headers()
 
55
        except socket.error, e:
 
56
            # We don't want to pollute the test results with
 
57
            # spurious server errors while test succeed. In our
 
58
            # case, it may occur that the test has already read
 
59
            # the 'Bad Status' and closed the socket while we are
 
60
            # still trying to send some headers... So the test is
 
61
            # ok, but if we raise the exception, the output is
 
62
            # dirty. So we don't raise, but we close the
 
63
            # connection, just to be safe :)
 
64
            spurious = [errno.EPIPE,
 
65
                        errno.ECONNRESET,
 
66
                        errno.ECONNABORTED,
 
67
                        ]
 
68
            if (len(e.args) > 0) and (e.args[0] in spurious):
 
69
                self.close_connection = 1
 
70
                pass
 
71
            else:
 
72
                raise
 
73
        return False
 
74
 
 
75
 
 
76
class InvalidStatusRequestHandler(TestingHTTPRequestHandler):
 
77
    """Whatever request comes in, returns am invalid status"""
 
78
 
 
79
    def parse_request(self):
 
80
        """Fakes handling a single HTTP request, returns a bad status"""
 
81
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
82
        self.wfile.write("Invalid status line\r\n")
 
83
        return False
 
84
 
 
85
 
 
86
class BadProtocolRequestHandler(TestingHTTPRequestHandler):
 
87
    """Whatever request comes in, returns a bad protocol version"""
 
88
 
 
89
    def parse_request(self):
 
90
        """Fakes handling a single HTTP request, returns a bad status"""
 
91
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
92
        # Returns an invalid protocol version, but curl just
 
93
        # ignores it and those cannot be tested.
 
94
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
 
95
                                           404,
 
96
                                           'Look at my protocol version'))
 
97
        return False
 
98
 
 
99
 
 
100
class ForbiddenRequestHandler(TestingHTTPRequestHandler):
 
101
    """Whatever request comes in, returns a 403 code"""
 
102
 
 
103
    def parse_request(self):
 
104
        """Handle a single HTTP request, by replying we cannot handle it"""
 
105
        ignored = TestingHTTPRequestHandler.parse_request(self)
 
106
        self.send_error(403)
 
107
        return False
 
108
 
 
109
 
 
110
class HTTPServerWithSmarts(HttpServer):
41
111
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
42
112
    trigger a smart server to execute with a transport rooted at the rootdir of
43
113
    the HTTP server.
44
114
    """
45
115
 
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
 
    """
 
116
    def __init__(self):
 
117
        HttpServer.__init__(self, SmartRequestHandler)
 
118
 
 
119
 
 
120
class SmartRequestHandler(TestingHTTPRequestHandler):
 
121
    """Extend TestingHTTPRequestHandler to support smart client POSTs."""
56
122
 
57
123
    def do_POST(self):
58
124
        """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
125
        self.send_response(200)
70
126
        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'])
 
127
        transport = get_transport(self.server.test_case_server._home_dir)
78
128
        # TODO: We might like to support streaming responses.  1.0 allows no
79
129
        # Content-length in this case, so for integrity we should perform our
80
130
        # own chunking within the stream.
82
132
        # the HTTP chunking as this will allow HTTP persistence safely, even if
83
133
        # we have to stop early due to error, but we would also have to use the
84
134
        # 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
135
        out_buffer = StringIO()
89
 
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
 
136
        smart_protocol_request = protocol.SmartServerRequestProtocolOne(
 
137
                transport, out_buffer.write)
 
138
        # if this fails, we should return 400 bad request, but failure is
 
139
        # failure for now - RBC 20060919
 
140
        data_length = int(self.headers['Content-Length'])
90
141
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
91
142
        # feeding the bytes in the http request to the smart_protocol_request,
92
143
        # 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.")
 
144
        smart_protocol_request.accept_bytes(self.rfile.read(data_length))
 
145
        assert smart_protocol_request.next_read_size() == 0, (
 
146
            "not finished reading, but all data sent to protocol.")
97
147
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
98
148
        self.end_headers()
99
149
        self.wfile.write(out_buffer.getvalue())
100
150
 
101
151
 
102
 
class TestCaseWithWebserver(tests.TestCaseWithTransport):
 
152
class LimitedRangeRequestHandler(TestingHTTPRequestHandler):
 
153
    """Errors out when range specifiers exceed the limit"""
 
154
 
 
155
    def get_multiple_ranges(self, file, file_size, ranges):
 
156
        """Refuses the multiple ranges request"""
 
157
        tcs = self.server.test_case_server
 
158
        if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
 
159
            file.close()
 
160
            # Emulate apache behavior
 
161
            self.send_error(400, "Bad Request")
 
162
            return
 
163
        return TestingHTTPRequestHandler.get_multiple_ranges(self, file,
 
164
                                                             file_size, ranges)
 
165
 
 
166
    def do_GET(self):
 
167
        tcs = self.server.test_case_server
 
168
        tcs.GET_request_nb += 1
 
169
        return TestingHTTPRequestHandler.do_GET(self)
 
170
 
 
171
 
 
172
class LimitedRangeHTTPServer(HttpServer):
 
173
    """An HttpServer erroring out on requests with too much range specifiers"""
 
174
 
 
175
    def __init__(self, request_handler=LimitedRangeRequestHandler,
 
176
                 range_limit=None):
 
177
        HttpServer.__init__(self, request_handler)
 
178
        self.range_limit = range_limit
 
179
        self.GET_request_nb = 0
 
180
 
 
181
 
 
182
class SingleRangeRequestHandler(TestingHTTPRequestHandler):
 
183
    """Always reply to range request as if they were single.
 
184
 
 
185
    Don't be explicit about it, just to annoy the clients.
 
186
    """
 
187
 
 
188
    def get_multiple_ranges(self, file, file_size, ranges):
 
189
        """Answer as if it was a single range request and ignores the rest"""
 
190
        (start, end) = ranges[0]
 
191
        return self.get_single_range(file, file_size, start, end)
 
192
 
 
193
 
 
194
class SingleOnlyRangeRequestHandler(TestingHTTPRequestHandler):
 
195
    """Only reply to simple range requests, errors out on multiple"""
 
196
 
 
197
    def get_multiple_ranges(self, file, file_size, ranges):
 
198
        """Refuses the multiple ranges request"""
 
199
        if len(ranges) > 1:
 
200
            file.close()
 
201
            self.send_error(416, "Requested range not satisfiable")
 
202
            return
 
203
        (start, end) = ranges[0]
 
204
        return self.get_single_range(file, file_size, start, end)
 
205
 
 
206
 
 
207
class NoRangeRequestHandler(TestingHTTPRequestHandler):
 
208
    """Ignore range requests without notice"""
 
209
 
 
210
    # Just bypass the range handling done by TestingHTTPRequestHandler
 
211
    do_GET = SimpleHTTPRequestHandler.do_GET
 
212
 
 
213
 
 
214
class TestCaseWithWebserver(TestCaseWithTransport):
103
215
    """A support class that provides readonly urls that are http://.
104
216
 
105
217
    This is done by forcing the readonly server to be an http
108
220
    """
109
221
    def setUp(self):
110
222
        super(TestCaseWithWebserver, self).setUp()
111
 
        self.transport_readonly_server = http_server.HttpServer
 
223
        self.transport_readonly_server = HttpServer
112
224
 
113
225
 
114
226
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
119
231
    """
120
232
    def setUp(self):
121
233
        super(TestCaseWithTwoWebservers, self).setUp()
122
 
        self.transport_secondary_server = http_server.HttpServer
 
234
        self.transport_secondary_server = HttpServer
123
235
        self.__secondary_server = None
124
236
 
125
237
    def create_transport_secondary_server(self):
133
245
        """Get the server instance for the secondary transport."""
134
246
        if self.__secondary_server is None:
135
247
            self.__secondary_server = self.create_transport_secondary_server()
136
 
            self.start_server(self.__secondary_server)
 
248
            self.__secondary_server.setUp()
 
249
            self.addCleanup(self.__secondary_server.tearDown)
137
250
        return self.__secondary_server
138
251
 
139
252
 
140
 
class ProxyServer(http_server.HttpServer):
 
253
class ProxyServer(HttpServer):
141
254
    """A proxy test server for http transports."""
142
255
 
143
256
    proxy_requests = True
144
257
 
145
258
 
146
 
class RedirectRequestHandler(http_server.TestingHTTPRequestHandler):
 
259
class RedirectRequestHandler(TestingHTTPRequestHandler):
147
260
    """Redirect all request to the specified server"""
148
261
 
149
262
    def parse_request(self):
150
263
        """Redirect a single HTTP request to another host"""
151
 
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
 
264
        valid = TestingHTTPRequestHandler.parse_request(self)
152
265
        if valid:
153
266
            tcs = self.server.test_case_server
154
267
            code, target = tcs.is_redirected(self.path)
156
269
                # Redirect as instructed
157
270
                self.send_response(code)
158
271
                self.send_header('Location', target)
159
 
                # We do not send a body
160
 
                self.send_header('Content-Length', '0')
161
272
                self.end_headers()
162
273
                return False # The job is done
163
274
            else:
166
277
        return valid
167
278
 
168
279
 
169
 
class HTTPServerRedirecting(http_server.HttpServer):
 
280
class HTTPServerRedirecting(HttpServer):
170
281
    """An HttpServer redirecting to another server """
171
282
 
172
 
    def __init__(self, request_handler=RedirectRequestHandler,
173
 
                 protocol_version=None):
174
 
        http_server.HttpServer.__init__(self, request_handler,
175
 
                                        protocol_version=protocol_version)
 
283
    def __init__(self, request_handler=RedirectRequestHandler):
 
284
        HttpServer.__init__(self, request_handler)
176
285
        # redirections is a list of tuples (source, target, code)
177
286
        # - source is a regexp for the paths requested
178
287
        # - target is a replacement for re.sub describing where
230
339
       self.old_server = self.get_secondary_server()
231
340
 
232
341
 
233
 
class AuthRequestHandler(http_server.TestingHTTPRequestHandler):
 
342
class AuthRequestHandler(TestingHTTPRequestHandler):
234
343
    """Requires an authentication to process requests.
235
344
 
236
345
    This is intended to be used with a server that always and
245
354
 
246
355
    def do_GET(self):
247
356
        if self.authorized():
248
 
            return http_server.TestingHTTPRequestHandler.do_GET(self)
 
357
            return TestingHTTPRequestHandler.do_GET(self)
249
358
        else:
250
359
            # Note that we must update test_case_server *before*
251
360
            # sending the error or the client may try to read it
254
363
            tcs.auth_required_errors += 1
255
364
            self.send_response(tcs.auth_error_code)
256
365
            self.send_header_auth_reqed()
257
 
            # We do not send a body
258
 
            self.send_header('Content-Length', '0')
259
366
            self.end_headers()
260
367
            return
261
368
 
296
403
 
297
404
    def authorized(self):
298
405
        tcs = self.server.test_case_server
 
406
        if tcs.auth_scheme != 'digest':
 
407
            return False
299
408
 
300
409
        auth_header = self.headers.get(tcs.auth_header_recv, None)
301
410
        if auth_header is None:
316
425
        self.send_header(tcs.auth_header_sent,header)
317
426
 
318
427
 
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):
 
428
class AuthServer(HttpServer):
338
429
    """Extends HttpServer with a dictionary of passwords.
339
430
 
340
431
    This is used as a base class for various schemes which should
351
442
    auth_error_code = None
352
443
    auth_realm = "Thou should not pass"
353
444
 
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)
 
445
    def __init__(self, request_handler, auth_scheme):
 
446
        HttpServer.__init__(self, request_handler)
358
447
        self.auth_scheme = auth_scheme
359
448
        self.password_of = {}
360
449
        self.auth_required_errors = 0
383
472
 
384
473
    auth_nonce = 'now!'
385
474
 
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)
 
475
    def __init__(self, request_handler, auth_scheme):
 
476
        AuthServer.__init__(self, request_handler, auth_scheme)
390
477
 
391
478
    def digest_authorized(self, auth, command):
392
479
        nonce = auth['nonce']
412
499
        A1 = '%s:%s:%s' % (user, realm, password)
413
500
        A2 = '%s:%s' % (command, auth['uri'])
414
501
 
415
 
        H = lambda x: osutils.md5(x).hexdigest()
 
502
        H = lambda x: md5(x).hexdigest()
416
503
        KD = lambda secret, data: H("%s:%s" % (secret, data))
417
504
 
418
505
        nonce_count = int(auth['nc'], 16)
425
512
 
426
513
        return response_digest == auth['response']
427
514
 
428
 
 
429
515
class HTTPAuthServer(AuthServer):
430
516
    """An HTTP server requiring authentication"""
431
517
 
448
534
class HTTPBasicAuthServer(HTTPAuthServer):
449
535
    """An HTTP server requiring basic authentication"""
450
536
 
451
 
    def __init__(self, protocol_version=None):
452
 
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
453
 
                                protocol_version=protocol_version)
 
537
    def __init__(self):
 
538
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
454
539
        self.init_http_auth()
455
540
 
456
541
 
457
542
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
458
543
    """An HTTP server requiring digest authentication"""
459
544
 
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'
 
545
    def __init__(self):
 
546
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
547
        self.init_http_auth()
476
548
 
477
549
 
478
550
class ProxyBasicAuthServer(ProxyAuthServer):
479
551
    """A proxy server requiring basic authentication"""
480
552
 
481
 
    def __init__(self, protocol_version=None):
482
 
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
483
 
                                 protocol_version=protocol_version)
 
553
    def __init__(self):
 
554
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
484
555
        self.init_proxy_auth()
485
556
 
486
557
 
487
558
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
488
559
    """A proxy server requiring basic authentication"""
489
560
 
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'
 
561
    def __init__(self):
 
562
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
563
        self.init_proxy_auth()
506
564
 
507
565