~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: John Arbash Meinel
  • Date: 2007-04-28 15:04:17 UTC
  • mfrom: (2466 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2566.
  • Revision ID: john@arbash-meinel.com-20070428150417-trp3pi0pzd411pu4
[merge] bzr.dev 2466

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
from cStringIO import StringIO
18
18
import errno
 
19
import md5
19
20
from SimpleHTTPServer import SimpleHTTPRequestHandler
20
21
import re
 
22
import sha
21
23
import socket
 
24
import time
 
25
import urllib2
22
26
import urlparse
23
27
 
 
28
from bzrlib.smart import protocol
24
29
from bzrlib.tests import TestCaseWithTransport
25
30
from bzrlib.tests.HttpServer import (
26
31
    HttpServer,
29
34
from bzrlib.transport import (
30
35
    get_transport,
31
36
    )
32
 
from bzrlib.smart import protocol
33
37
 
34
38
 
35
39
class WallRequestHandler(TestingHTTPRequestHandler):
204
208
        return self.__secondary_server
205
209
 
206
210
 
207
 
class FakeProxyRequestHandler(TestingHTTPRequestHandler):
208
 
    """Append a '-proxied' suffix to file served"""
209
 
 
210
 
    def translate_path(self, path):
211
 
        # We need to act as a proxy and accept absolute urls,
212
 
        # which SimpleHTTPRequestHandler (grand parent) is not
213
 
        # ready for. So we just drop the protocol://host:port
214
 
        # part in front of the request-url (because we know we
215
 
        # would not forward the request to *another* proxy).
216
 
 
217
 
        # So we do what SimpleHTTPRequestHandler.translate_path
218
 
        # do beginning with python 2.4.3: abandon query
219
 
        # parameters, scheme, host port, etc (which ensure we
220
 
        # provide the right behaviour on all python versions).
221
 
        path = urlparse.urlparse(path)[2]
222
 
        # And now, we can apply *our* trick to proxy files
223
 
        self.path += '-proxied'
224
 
        # An finally we leave our mother class do whatever it
225
 
        # wants with the path
226
 
        return TestingHTTPRequestHandler.translate_path(self, path)
 
211
class ProxyServer(HttpServer):
 
212
    """A proxy test server for http transports."""
 
213
 
 
214
    proxy_requests = True
227
215
 
228
216
 
229
217
class RedirectRequestHandler(TestingHTTPRequestHandler):
309
297
       self.old_server = self.get_secondary_server()
310
298
 
311
299
 
 
300
class AuthRequestHandler(TestingHTTPRequestHandler):
 
301
    """Requires an authentication to process requests.
 
302
 
 
303
    This is intended to be used with a server that always and
 
304
    only use one authentication scheme (implemented by daughter
 
305
    classes).
 
306
    """
 
307
 
 
308
    # The following attributes should be defined in the server
 
309
    # - auth_header_sent: the header name sent to require auth
 
310
    # - auth_header_recv: the header received containing auth
 
311
    # - auth_error_code: the error code to indicate auth required
 
312
 
 
313
    def do_GET(self):
 
314
        if self.authorized():
 
315
            return TestingHTTPRequestHandler.do_GET(self)
 
316
        else:
 
317
            # Note that we must update test_case_server *before*
 
318
            # sending the error or the client may try to read it
 
319
            # before we have sent the whole error back.
 
320
            tcs = self.server.test_case_server
 
321
            tcs.auth_required_errors += 1
 
322
            self.send_response(tcs.auth_error_code)
 
323
            self.send_header_auth_reqed()
 
324
            self.end_headers()
 
325
            return
 
326
 
 
327
        TestingHTTPRequestHandler.do_GET(self)
 
328
 
 
329
 
 
330
class BasicAuthRequestHandler(AuthRequestHandler):
 
331
    """Implements the basic authentication of a request"""
 
332
 
 
333
    def authorized(self):
 
334
        tcs = self.server.test_case_server
 
335
        if tcs.auth_scheme != 'basic':
 
336
            return False
 
337
 
 
338
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
339
        if auth_header:
 
340
            scheme, raw_auth = auth_header.split(' ', 1)
 
341
            if scheme.lower() == tcs.auth_scheme:
 
342
                user, password = raw_auth.decode('base64').split(':')
 
343
                return tcs.authorized(user, password)
 
344
 
 
345
        return False
 
346
 
 
347
    def send_header_auth_reqed(self):
 
348
        tcs = self.server.test_case_server
 
349
        self.send_header(tcs.auth_header_sent,
 
350
                         'Basic realm="%s"' % tcs.auth_realm)
 
351
 
 
352
 
 
353
# FIXME: We could send an Authentication-Info header too when
 
354
# the authentication is succesful
 
355
 
 
356
class DigestAuthRequestHandler(AuthRequestHandler):
 
357
    """Implements the digest authentication of a request.
 
358
 
 
359
    We need persistence for some attributes and that can't be
 
360
    achieved here since we get instantiated for each request. We
 
361
    rely on the DigestAuthServer to take care of them.
 
362
    """
 
363
 
 
364
    def authorized(self):
 
365
        tcs = self.server.test_case_server
 
366
        if tcs.auth_scheme != 'digest':
 
367
            return False
 
368
 
 
369
        auth_header = self.headers.get(tcs.auth_header_recv, None)
 
370
        if auth_header is None:
 
371
            return False
 
372
        scheme, auth = auth_header.split(None, 1)
 
373
        if scheme.lower() == tcs.auth_scheme:
 
374
            auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
 
375
 
 
376
            return tcs.digest_authorized(auth_dict, self.command)
 
377
 
 
378
        return False
 
379
 
 
380
    def send_header_auth_reqed(self):
 
381
        tcs = self.server.test_case_server
 
382
        header = 'Digest realm="%s", ' % tcs.auth_realm
 
383
        header += 'nonce="%s", algorithm=%s, qop=auth' % (tcs.auth_nonce, 'MD5')
 
384
        self.send_header(tcs.auth_header_sent,header)
 
385
 
 
386
 
 
387
class AuthServer(HttpServer):
 
388
    """Extends HttpServer with a dictionary of passwords.
 
389
 
 
390
    This is used as a base class for various schemes which should
 
391
    all use or redefined the associated AuthRequestHandler.
 
392
 
 
393
    Note that no users are defined by default, so add_user should
 
394
    be called before issuing the first request.
 
395
    """
 
396
 
 
397
    # The following attributes should be set dy daughter classes
 
398
    # and are used by AuthRequestHandler.
 
399
    auth_header_sent = None
 
400
    auth_header_recv = None
 
401
    auth_error_code = None
 
402
    auth_realm = "Thou should not pass"
 
403
 
 
404
    def __init__(self, request_handler, auth_scheme):
 
405
        HttpServer.__init__(self, request_handler)
 
406
        self.auth_scheme = auth_scheme
 
407
        self.password_of = {}
 
408
        self.auth_required_errors = 0
 
409
 
 
410
    def add_user(self, user, password):
 
411
        """Declare a user with an associated password.
 
412
 
 
413
        password can be empty, use an empty string ('') in that
 
414
        case, not None.
 
415
        """
 
416
        self.password_of[user] = password
 
417
 
 
418
    def authorized(self, user, password):
 
419
        """Check that the given user provided the right password"""
 
420
        expected_password = self.password_of.get(user, None)
 
421
        return expected_password is not None and password == expected_password
 
422
 
 
423
 
 
424
# FIXME: There is some code duplication with
 
425
# _urllib2_wrappers.py.DigestAuthHandler. If that duplciation
 
426
# grows, it may require a refactoring. Also, we don't implement
 
427
# SHA algorithm nor MD5-sess here, but that does not seem worth
 
428
# it.
 
429
class DigestAuthServer(AuthServer):
 
430
    """A digest authentication server"""
 
431
 
 
432
    auth_nonce = 'now!'
 
433
 
 
434
    def __init__(self, request_handler, auth_scheme):
 
435
        AuthServer.__init__(self, request_handler, auth_scheme)
 
436
 
 
437
    def digest_authorized(self, auth, command):
 
438
        nonce = auth['nonce']
 
439
        if nonce != self.auth_nonce:
 
440
            return False
 
441
        realm = auth['realm']
 
442
        if realm != self.auth_realm:
 
443
            return False
 
444
        user = auth['username']
 
445
        if not self.password_of.has_key(user):
 
446
            return False
 
447
        algorithm= auth['algorithm']
 
448
        if algorithm != 'MD5':
 
449
            return False
 
450
        qop = auth['qop']
 
451
        if qop != 'auth':
 
452
            return False
 
453
 
 
454
        password = self.password_of[user]
 
455
 
 
456
        # Recalculate the response_digest to compare with the one
 
457
        # sent by the client
 
458
        A1 = '%s:%s:%s' % (user, realm, password)
 
459
        A2 = '%s:%s' % (command, auth['uri'])
 
460
 
 
461
        H = lambda x: md5.new(x).hexdigest()
 
462
        KD = lambda secret, data: H("%s:%s" % (secret, data))
 
463
 
 
464
        nonce_count = int(auth['nc'], 16)
 
465
 
 
466
        ncvalue = '%08x' % nonce_count
 
467
 
 
468
        cnonce = auth['cnonce']
 
469
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
470
        response_digest = KD(H(A1), noncebit)
 
471
 
 
472
        return response_digest == auth['response']
 
473
 
 
474
class HTTPAuthServer(AuthServer):
 
475
    """An HTTP server requiring authentication"""
 
476
 
 
477
    def init_http_auth(self):
 
478
        self.auth_header_sent = 'WWW-Authenticate'
 
479
        self.auth_header_recv = 'Authorization'
 
480
        self.auth_error_code = 401
 
481
 
 
482
 
 
483
class ProxyAuthServer(AuthServer):
 
484
    """A proxy server requiring authentication"""
 
485
 
 
486
    def init_proxy_auth(self):
 
487
        self.proxy_requests = True
 
488
        self.auth_header_sent = 'Proxy-Authenticate'
 
489
        self.auth_header_recv = 'Proxy-Authorization'
 
490
        self.auth_error_code = 407
 
491
 
 
492
 
 
493
class HTTPBasicAuthServer(HTTPAuthServer):
 
494
    """An HTTP server requiring basic authentication"""
 
495
 
 
496
    def __init__(self):
 
497
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
 
498
        self.init_http_auth()
 
499
 
 
500
 
 
501
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
 
502
    """An HTTP server requiring digest authentication"""
 
503
 
 
504
    def __init__(self):
 
505
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
506
        self.init_http_auth()
 
507
 
 
508
 
 
509
class ProxyBasicAuthServer(ProxyAuthServer):
 
510
    """A proxy server requiring basic authentication"""
 
511
 
 
512
    def __init__(self):
 
513
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
 
514
        self.init_proxy_auth()
 
515
 
 
516
 
 
517
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
518
    """A proxy server requiring basic authentication"""
 
519
 
 
520
    def __init__(self):
 
521
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
522
        self.init_proxy_auth()
 
523
 
 
524