~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/HTTPTestUtil.py

  • Committer: Vincent Ladeuil
  • Date: 2007-04-21 20:39:06 UTC
  • mto: (2420.1.21 bzr.http.auth)
  • mto: This revision was merged to the branch mainline in revision 2463.
  • Revision ID: v.ladeuil+lp@free.fr-20070421203906-hta5jt0nmauyl9qy
Implement digest authentication. Test suite passes. Tested against apache-2.x.

* bzrlib/transport/http/_urllib2_wrappers.py:
(AbstractAuthHandler.auth_required): Do not attempt to
authenticate if we don't have a user. Rework the detection of
already tried authentications. Avoid building the auth header two
times, save the auth info at the right places.
(AbstractAuthHandler.build_auth_header): Add a request parameter
for digest needs.
(BasicAuthHandler.auth_match): Simplify.
(get_digest_algorithm_impls, DigestAuthHandler): Implements client
digest authentication. MD5 and SHA algorithms are supported. Only
'auth' qop is suppoted.
(HTTPBasicAuthHandler, ProxyBasicAuthHandler): Renamed HTTPHandler
and ProxyAuthHandler respectively.
(HTTPBasicAuthHandler, ProxyBasicAuthHandler,
HTTPDigestAuthHandler, ProxyDigestAuthHandler): New classes
implementing the combinations between (http, proxy) and (basic,
digest).
(Opener.__init__): No more handlers in comment ! One TODO less !

* bzrlib/transport/http/_urllib.py:
(HttpTransport_urllib.__init__): self.base is not suitable for an
auth uri, it can contain decorators.

* bzrlib/tests/test_http.py:
(TestAuth.test_no_user): New test to check the behavior with no
user when authentication is required.

* bzrlib/tests/HTTPTestUtil.py:
(DigestAuthRequestHandler.authorized): Delegate most of the work
to the server that control the needed persistent infos.
(AuthServer): Define an auth_relam attribute.
(DigestAuthServer): Implement a first version of digest
authentication. Only the MD5 algorithm and the 'auth' qop are
supported so far.
(HTTPAuthServer.init_http_auth): New method to simplify
the [http|proxy], [basic|digest] server combinations writing.

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
22
25
import urllib2
23
26
import urlparse
24
27
 
332
335
        if tcs.auth_scheme != 'basic':
333
336
            return False
334
337
 
335
 
        auth_header = self.headers.get(tcs.auth_header_recv)
336
 
        if auth_header and auth_header.lower().startswith('basic '):
337
 
            raw_auth = auth_header[len('Basic '):]
338
 
            user, password = raw_auth.decode('base64').split(':')
339
 
            return tcs.authorized(user, password)
 
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)
340
344
 
341
345
        return False
342
346
 
343
347
    def send_header_auth_reqed(self):
344
 
        self.send_header(self.server.test_case_server.auth_header_sent,
345
 
                         'Basic realm="Thou should not pass"')
346
 
 
 
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 should send an Authentication-Info header too when
 
354
# the autheticaion is succesful
347
355
 
348
356
class DigestAuthRequestHandler(AuthRequestHandler):
349
 
    """Implements the digest authentication of a request"""
 
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
    """
350
363
 
351
364
    def authorized(self):
352
365
        tcs = self.server.test_case_server
353
366
        if tcs.auth_scheme != 'digest':
354
367
            return False
355
368
 
356
 
        auth_header = self.headers.get(tcs.auth_header_recv)
357
 
        if auth_header and auth_header.lower().startswith('digest '):
358
 
            raw_auth = auth_header[len('Basic '):]
359
 
            user, password = raw_auth.decode('base64').split(':')
360
 
            return tcs.authorized(user, password)
 
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)
361
377
 
362
378
        return False
363
379
 
364
380
    def send_header_auth_reqed(self):
365
 
        self.send_header(self.server.test_case_server.auth_header_sent,
366
 
                         'Basic realm="Thou should not pass"')
 
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
 
367
386
 
368
387
class AuthServer(HttpServer):
369
388
    """Extends HttpServer with a dictionary of passwords.
373
392
 
374
393
    Note that no users are defined by default, so add_user should
375
394
    be called before issuing the first request.
 
395
    """
376
396
 
377
 
    """
378
397
    # The following attributes should be set dy daughter classes
379
398
    # and are used by AuthRequestHandler.
380
399
    auth_header_sent = None
381
400
    auth_header_recv = None
382
401
    auth_error_code = None
 
402
    auth_realm = "Thou should not pass"
383
403
 
384
404
    def __init__(self, request_handler, auth_scheme):
385
405
        HttpServer.__init__(self, request_handler)
396
416
        self.password_of[user] = password
397
417
 
398
418
    def authorized(self, user, password):
 
419
        """Check that the given user provided the right password"""
399
420
        expected_password = self.password_of.get(user, None)
400
421
        return expected_password is not None and password == expected_password
401
422
 
402
423
 
 
424
class DigestAuthServer(AuthServer):
 
425
    """A digest authentication server"""
 
426
 
 
427
    auth_nonce = 'rRQ+Lp4uBAA=301b77beb156b6158b73dee026b8be23302292b4'
 
428
 
 
429
    def __init__(self, request_handler, auth_scheme):
 
430
        AuthServer.__init__(self, request_handler, auth_scheme)
 
431
 
 
432
    def digest_authorized(self, auth, command):
 
433
        realm = auth['realm']
 
434
        if realm != self.auth_realm:
 
435
            return False
 
436
        user = auth['username']
 
437
        if not self.password_of.has_key(user):
 
438
            return False
 
439
        algorithm= auth['algorithm']
 
440
        if algorithm != 'MD5':
 
441
            return False
 
442
        qop = auth['qop']
 
443
        if qop != 'auth':
 
444
            return False
 
445
 
 
446
        password = self.password_of[user]
 
447
 
 
448
        # Recalculate the response_digest to compare with the one
 
449
        # sent by the client
 
450
        A1 = '%s:%s:%s' % (user, realm, password)
 
451
        A2 = '%s:%s' % (command, auth['uri'])
 
452
 
 
453
        H = lambda x: md5.new(x).hexdigest()
 
454
        KD = lambda secret, data: H("%s:%s" % (secret, data))
 
455
 
 
456
        nonce = auth['nonce']
 
457
        nonce_count = int(auth['nc'], 16)
 
458
 
 
459
        ncvalue = '%08x' % nonce_count
 
460
 
 
461
        cnonce = auth['cnonce']
 
462
        noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
463
        response_digest = KD(H(A1), noncebit)
 
464
 
 
465
        return response_digest == auth['response']
 
466
 
403
467
class HTTPAuthServer(AuthServer):
404
468
    """An HTTP server requiring authentication"""
405
469
 
406
 
    auth_header_sent = 'WWW-Authenticate'
407
 
    auth_header_recv = 'Authorization'
408
 
    auth_error_code = 401
 
470
    def init_http_auth(self):
 
471
        self.auth_header_sent = 'WWW-Authenticate'
 
472
        self.auth_header_recv = 'Authorization'
 
473
        self.auth_error_code = 401
409
474
 
410
475
 
411
476
class ProxyAuthServer(AuthServer):
412
477
    """A proxy server requiring authentication"""
413
478
 
414
 
    proxy_requests = True
415
 
    auth_header_sent = 'Proxy-Authenticate'
416
 
    auth_header_recv = 'Proxy-Authorization'
417
 
    auth_error_code = 407
 
479
    def init_proxy_auth(self):
 
480
        self.proxy_requests = True
 
481
        self.auth_header_sent = 'Proxy-Authenticate'
 
482
        self.auth_header_recv = 'Proxy-Authorization'
 
483
        self.auth_error_code = 407
418
484
 
419
485
 
420
486
class HTTPBasicAuthServer(HTTPAuthServer):
422
488
 
423
489
    def __init__(self):
424
490
        HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
425
 
 
426
 
 
427
 
class HTTPDigestAuthServer(HTTPAuthServer):
 
491
        self.init_http_auth()
 
492
 
 
493
 
 
494
class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
428
495
    """An HTTP server requiring digest authentication"""
429
496
 
430
497
    def __init__(self):
431
 
        HTTPAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
498
        DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
499
        self.init_http_auth()
432
500
 
433
501
 
434
502
class ProxyBasicAuthServer(ProxyAuthServer):
435
 
    """An proxy server requiring basic authentication"""
 
503
    """A proxy server requiring basic authentication"""
436
504
 
437
505
    def __init__(self):
438
506
        ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
439
 
 
440
 
 
441
 
class ProxyDigestAuthServer(ProxyAuthServer):
442
 
    """An proxy server requiring basic authentication"""
 
507
        self.init_proxy_auth()
 
508
 
 
509
 
 
510
class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
 
511
    """A proxy server requiring basic authentication"""
443
512
 
444
513
    def __init__(self):
445
514
        ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
 
515
        self.init_proxy_auth()
446
516
 
447
517