~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_utils.py

  • Committer: Robert Collins
  • Date: 2005-08-23 06:52:09 UTC
  • mto: (974.1.50) (1185.1.10) (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1139.
  • Revision ID: robertc@robertcollins.net-20050823065209-81cd5962c401751b
move io redirection into each test case from the global runner

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
from cStringIO import StringIO
18
 
import errno
19
 
import re
20
 
import socket
21
 
import threading
22
 
import time
23
 
import urllib2
24
 
import urlparse
25
 
 
26
 
 
27
 
from bzrlib import (
28
 
    errors,
29
 
    osutils,
30
 
    tests,
31
 
    )
32
 
from bzrlib.smart import medium, protocol
33
 
from bzrlib.tests import http_server
34
 
from bzrlib.transport import (
35
 
    chroot,
36
 
    get_transport,
37
 
    )
38
 
 
39
 
 
40
 
class HTTPServerWithSmarts(http_server.HttpServer):
41
 
    """HTTPServerWithSmarts extends the HttpServer with POST methods that will
42
 
    trigger a smart server to execute with a transport rooted at the rootdir of
43
 
    the HTTP server.
44
 
    """
45
 
 
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
 
    """
56
 
 
57
 
    def do_POST(self):
58
 
        """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
 
        self.send_response(200)
70
 
        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'])
78
 
        # TODO: We might like to support streaming responses.  1.0 allows no
79
 
        # Content-length in this case, so for integrity we should perform our
80
 
        # own chunking within the stream.
81
 
        # 1.1 allows chunked responses, and in this case we could chunk using
82
 
        # the HTTP chunking as this will allow HTTP persistence safely, even if
83
 
        # we have to stop early due to error, but we would also have to use the
84
 
        # 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
 
        out_buffer = StringIO()
89
 
        smart_protocol_request = protocol_factory(t, out_buffer.write, '/')
90
 
        # Perhaps there should be a SmartServerHTTPMedium that takes care of
91
 
        # feeding the bytes in the http request to the smart_protocol_request,
92
 
        # 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.")
97
 
        self.send_header("Content-Length", str(len(out_buffer.getvalue())))
98
 
        self.end_headers()
99
 
        self.wfile.write(out_buffer.getvalue())
100
 
 
101
 
 
102
 
class TestCaseWithWebserver(tests.TestCaseWithTransport):
103
 
    """A support class that provides readonly urls that are http://.
104
 
 
105
 
    This is done by forcing the readonly server to be an http
106
 
    one. This will currently fail if the primary transport is not
107
 
    backed by regular disk files.
108
 
    """
109
 
    def setUp(self):
110
 
        super(TestCaseWithWebserver, self).setUp()
111
 
        self.transport_readonly_server = http_server.HttpServer
112
 
 
113
 
 
114
 
class TestCaseWithTwoWebservers(TestCaseWithWebserver):
115
 
    """A support class providing readonly urls on two servers that are http://.
116
 
 
117
 
    We set up two webservers to allows various tests involving
118
 
    proxies or redirections from one server to the other.
119
 
    """
120
 
    def setUp(self):
121
 
        super(TestCaseWithTwoWebservers, self).setUp()
122
 
        self.transport_secondary_server = http_server.HttpServer
123
 
        self.__secondary_server = None
124
 
 
125
 
    def create_transport_secondary_server(self):
126
 
        """Create a transport server from class defined at init.
127
 
 
128
 
        This is mostly a hook for daughter classes.
129
 
        """
130
 
        return self.transport_secondary_server()
131
 
 
132
 
    def get_secondary_server(self):
133
 
        """Get the server instance for the secondary transport."""
134
 
        if self.__secondary_server is None:
135
 
            self.__secondary_server = self.create_transport_secondary_server()
136
 
            self.__secondary_server.setUp()
137
 
            self.addCleanup(self.__secondary_server.tearDown)
138
 
        return self.__secondary_server
139
 
 
140
 
 
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):
148
 
    """Redirect all request to the specified server"""
149
 
 
150
 
    def parse_request(self):
151
 
        """Redirect a single HTTP request to another host"""
152
 
        valid = http_server.TestingHTTPRequestHandler.parse_request(self)
153
 
        if valid:
154
 
            tcs = self.server.test_case_server
155
 
            code, target = tcs.is_redirected(self.path)
156
 
            if code is not None and target is not None:
157
 
                # Redirect as instructed
158
 
                self.send_response(code)
159
 
                self.send_header('Location', target)
160
 
                # We do not send a body
161
 
                self.send_header('Content-Length', '0')
162
 
                self.end_headers()
163
 
                return False # The job is done
164
 
            else:
165
 
                # We leave the parent class serve the request
166
 
                pass
167
 
        return valid
168
 
 
169
 
 
170
 
class HTTPServerRedirecting(http_server.HttpServer):
171
 
    """An HttpServer redirecting to another server """
172
 
 
173
 
    def __init__(self, request_handler=RedirectRequestHandler,
174
 
                 protocol_version=None):
175
 
        http_server.HttpServer.__init__(self, request_handler,
176
 
                                        protocol_version=protocol_version)
177
 
        # redirections is a list of tuples (source, target, code)
178
 
        # - source is a regexp for the paths requested
179
 
        # - target is a replacement for re.sub describing where
180
 
        #   the request will be redirected
181
 
        # - code is the http error code associated to the
182
 
        #   redirection (301 permanent, 302 temporarry, etc
183
 
        self.redirections = []
184
 
 
185
 
    def redirect_to(self, host, port):
186
 
        """Redirect all requests to a specific host:port"""
187
 
        self.redirections = [('(.*)',
188
 
                              r'http://%s:%s\1' % (host, port) ,
189
 
                              301)]
190
 
 
191
 
    def is_redirected(self, path):
192
 
        """Is the path redirected by this server.
193
 
 
194
 
        :param path: the requested relative path
195
 
 
196
 
        :returns: a tuple (code, target) if a matching
197
 
             redirection is found, (None, None) otherwise.
198
 
        """
199
 
        code = None
200
 
        target = None
201
 
        for (rsource, rtarget, rcode) in self.redirections:
202
 
            target, match = re.subn(rsource, rtarget, path)
203
 
            if match:
204
 
                code = rcode
205
 
                break # The first match wins
206
 
            else:
207
 
                target = None
208
 
        return code, target
209
 
 
210
 
 
211
 
class TestCaseWithRedirectedWebserver(TestCaseWithTwoWebservers):
212
 
   """A support class providing redirections from one server to another.
213
 
 
214
 
   We set up two webservers to allows various tests involving
215
 
   redirections.
216
 
   The 'old' server is redirected to the 'new' server.
217
 
   """
218
 
 
219
 
   def create_transport_secondary_server(self):
220
 
       """Create the secondary server redirecting to the primary server"""
221
 
       new = self.get_readonly_server()
222
 
       redirecting = HTTPServerRedirecting()
223
 
       redirecting.redirect_to(new.host, new.port)
224
 
       return redirecting
225
 
 
226
 
   def setUp(self):
227
 
       super(TestCaseWithRedirectedWebserver, self).setUp()
228
 
       # The redirections will point to the new server
229
 
       self.new_server = self.get_readonly_server()
230
 
       # The requests to the old server will be redirected
231
 
       self.old_server = self.get_secondary_server()
232
 
 
233
 
 
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
 
 
301
 
        auth_header = self.headers.get(tcs.auth_header_recv, None)
302
 
        if auth_header is None:
303
 
            return False
304
 
        scheme, auth = auth_header.split(None, 1)
305
 
        if scheme.lower() == tcs.auth_scheme:
306
 
            auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
307
 
 
308
 
            return tcs.digest_authorized(auth_dict, self.command)
309
 
 
310
 
        return False
311
 
 
312
 
    def send_header_auth_reqed(self):
313
 
        tcs = self.server.test_case_server
314
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
315
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
316
 
                                                              'MD5')
317
 
        self.send_header(tcs.auth_header_sent,header)
318
 
 
319
 
 
320
 
class DigestAndBasicAuthRequestHandler(DigestAuthRequestHandler):
321
 
    """Implements a digest and basic authentication of a request.
322
 
 
323
 
    I.e. the server proposes both schemes and the client should choose the best
324
 
    one it can handle, which, in that case, should be digest, the only scheme
325
 
    accepted here.
326
 
    """
327
 
 
328
 
    def send_header_auth_reqed(self):
329
 
        tcs = self.server.test_case_server
330
 
        self.send_header(tcs.auth_header_sent,
331
 
                         'Basic realm="%s"' % tcs.auth_realm)
332
 
        header = 'Digest realm="%s", ' % tcs.auth_realm
333
 
        header += 'nonce="%s", algorithm="%s", qop="auth"' % (tcs.auth_nonce,
334
 
                                                              'MD5')
335
 
        self.send_header(tcs.auth_header_sent,header)
336
 
 
337
 
 
338
 
class AuthServer(http_server.HttpServer):
339
 
    """Extends HttpServer with a dictionary of passwords.
340
 
 
341
 
    This is used as a base class for various schemes which should
342
 
    all use or redefined the associated AuthRequestHandler.
343
 
 
344
 
    Note that no users are defined by default, so add_user should
345
 
    be called before issuing the first request.
346
 
    """
347
 
 
348
 
    # The following attributes should be set dy daughter classes
349
 
    # and are used by AuthRequestHandler.
350
 
    auth_header_sent = None
351
 
    auth_header_recv = None
352
 
    auth_error_code = None
353
 
    auth_realm = "Thou should not pass"
354
 
 
355
 
    def __init__(self, request_handler, auth_scheme,
356
 
                 protocol_version=None):
357
 
        http_server.HttpServer.__init__(self, request_handler,
358
 
                                        protocol_version=protocol_version)
359
 
        self.auth_scheme = auth_scheme
360
 
        self.password_of = {}
361
 
        self.auth_required_errors = 0
362
 
 
363
 
    def add_user(self, user, password):
364
 
        """Declare a user with an associated password.
365
 
 
366
 
        password can be empty, use an empty string ('') in that
367
 
        case, not None.
368
 
        """
369
 
        self.password_of[user] = password
370
 
 
371
 
    def authorized(self, user, password):
372
 
        """Check that the given user provided the right password"""
373
 
        expected_password = self.password_of.get(user, None)
374
 
        return expected_password is not None and password == expected_password
375
 
 
376
 
 
377
 
# FIXME: There is some code duplication with
378
 
# _urllib2_wrappers.py.DigestAuthHandler. If that duplication
379
 
# grows, it may require a refactoring. Also, we don't implement
380
 
# SHA algorithm nor MD5-sess here, but that does not seem worth
381
 
# it.
382
 
class DigestAuthServer(AuthServer):
383
 
    """A digest authentication server"""
384
 
 
385
 
    auth_nonce = 'now!'
386
 
 
387
 
    def __init__(self, request_handler, auth_scheme,
388
 
                 protocol_version=None):
389
 
        AuthServer.__init__(self, request_handler, auth_scheme,
390
 
                            protocol_version=protocol_version)
391
 
 
392
 
    def digest_authorized(self, auth, command):
393
 
        nonce = auth['nonce']
394
 
        if nonce != self.auth_nonce:
395
 
            return False
396
 
        realm = auth['realm']
397
 
        if realm != self.auth_realm:
398
 
            return False
399
 
        user = auth['username']
400
 
        if not self.password_of.has_key(user):
401
 
            return False
402
 
        algorithm= auth['algorithm']
403
 
        if algorithm != 'MD5':
404
 
            return False
405
 
        qop = auth['qop']
406
 
        if qop != 'auth':
407
 
            return False
408
 
 
409
 
        password = self.password_of[user]
410
 
 
411
 
        # Recalculate the response_digest to compare with the one
412
 
        # sent by the client
413
 
        A1 = '%s:%s:%s' % (user, realm, password)
414
 
        A2 = '%s:%s' % (command, auth['uri'])
415
 
 
416
 
        H = lambda x: osutils.md5(x).hexdigest()
417
 
        KD = lambda secret, data: H("%s:%s" % (secret, data))
418
 
 
419
 
        nonce_count = int(auth['nc'], 16)
420
 
 
421
 
        ncvalue = '%08x' % nonce_count
422
 
 
423
 
        cnonce = auth['cnonce']
424
 
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
425
 
        response_digest = KD(H(A1), noncebit)
426
 
 
427
 
        return response_digest == auth['response']
428
 
 
429
 
 
430
 
class HTTPAuthServer(AuthServer):
431
 
    """An HTTP server requiring authentication"""
432
 
 
433
 
    def init_http_auth(self):
434
 
        self.auth_header_sent = 'WWW-Authenticate'
435
 
        self.auth_header_recv = 'Authorization'
436
 
        self.auth_error_code = 401
437
 
 
438
 
 
439
 
class ProxyAuthServer(AuthServer):
440
 
    """A proxy server requiring authentication"""
441
 
 
442
 
    def init_proxy_auth(self):
443
 
        self.proxy_requests = True
444
 
        self.auth_header_sent = 'Proxy-Authenticate'
445
 
        self.auth_header_recv = 'Proxy-Authorization'
446
 
        self.auth_error_code = 407
447
 
 
448
 
 
449
 
class HTTPBasicAuthServer(HTTPAuthServer):
450
 
    """An HTTP server requiring basic authentication"""
451
 
 
452
 
    def __init__(self, protocol_version=None):
453
 
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
454
 
                                protocol_version=protocol_version)
455
 
        self.init_http_auth()
456
 
 
457
 
 
458
 
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
459
 
    """An HTTP server requiring digest authentication"""
460
 
 
461
 
    def __init__(self, protocol_version=None):
462
 
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
463
 
                                  protocol_version=protocol_version)
464
 
        self.init_http_auth()
465
 
 
466
 
 
467
 
class HTTPBasicAndDigestAuthServer(DigestAuthServer, HTTPAuthServer):
468
 
    """An HTTP server requiring basic or digest authentication"""
469
 
 
470
 
    def __init__(self, protocol_version=None):
471
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
472
 
                                  'basicdigest',
473
 
                                  protocol_version=protocol_version)
474
 
        self.init_http_auth()
475
 
        # We really accept Digest only
476
 
        self.auth_scheme = 'digest'
477
 
 
478
 
 
479
 
class ProxyBasicAuthServer(ProxyAuthServer):
480
 
    """A proxy server requiring basic authentication"""
481
 
 
482
 
    def __init__(self, protocol_version=None):
483
 
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic',
484
 
                                 protocol_version=protocol_version)
485
 
        self.init_proxy_auth()
486
 
 
487
 
 
488
 
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
489
 
    """A proxy server requiring basic authentication"""
490
 
 
491
 
    def __init__(self, protocol_version=None):
492
 
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest',
493
 
                                 protocol_version=protocol_version)
494
 
        self.init_proxy_auth()
495
 
 
496
 
 
497
 
class ProxyBasicAndDigestAuthServer(DigestAuthServer, ProxyAuthServer):
498
 
    """An proxy server requiring basic or digest authentication"""
499
 
 
500
 
    def __init__(self, protocol_version=None):
501
 
        DigestAuthServer.__init__(self, DigestAndBasicAuthRequestHandler,
502
 
                                  'basicdigest',
503
 
                                  protocol_version=protocol_version)
504
 
        self.init_proxy_auth()
505
 
        # We really accept Digest only
506
 
        self.auth_scheme = 'digest'
507
 
 
508