Rev 2431: Implement digest authentication. Test suite passes. Tested against apache-2.x. in http://bazaar.launchpad.net/~bzr/bzr/bzr.http.auth
Vincent Ladeuil
v.ladeuil+lp at free.fr
Sat Apr 21 21:39:09 BST 2007
At http://bazaar.launchpad.net/~bzr/bzr/bzr.http.auth
------------------------------------------------------------
revno: 2431
revision-id: v.ladeuil+lp at free.fr-20070421203906-hta5jt0nmauyl9qy
parent: v.ladeuil+lp at free.fr-20070421113113-b0br4norqbtlx1o2
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: bzr.http.auth
timestamp: Sat 2007-04-21 22:39:06 +0200
message:
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.
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/tests/HTTPTestUtil.py HTTPTestUtil.py-20050914180604-247d3aafb7a43343
bzrlib/tests/test_http.py testhttp.py-20051018020158-b2eef6e867c514d9
bzrlib/transport/http/_urllib.py _urlgrabber.py-20060113083826-0bbf7d992fbf090c
bzrlib/transport/http/_urllib2_wrappers.py _urllib2_wrappers.py-20060913231729-ha9ugi48ktx481ao-1
-------------- next part --------------
=== modified file 'NEWS'
--- a/NEWS 2007-04-21 11:31:13 +0000
+++ b/NEWS 2007-04-21 20:39:06 +0000
@@ -5,6 +5,9 @@
* Merge directives can now be supplied as input to `merge` and `pull`,
like bundles can. (Aaron Bentley)
+ * digest authentication is now supported for proxy and http.
+ (Vincent Ladeuil).
+
INTERNALS:
* bzrlib API compatability with 0.8 has been dropped, cleaning up some
@@ -92,6 +95,9 @@
basic authentication should be used, but plug the security
hole. (Vincent Ladeuil)
+ * Handle http and proxy digest authentication.
+ (Vincent Ladeuil, #94034).
+
TESTING:
* Added ``bzrlib.strace.strace`` which will strace a single callable and
=== modified file 'bzrlib/tests/HTTPTestUtil.py'
--- a/bzrlib/tests/HTTPTestUtil.py 2007-04-21 11:31:13 +0000
+++ b/bzrlib/tests/HTTPTestUtil.py 2007-04-21 20:39:06 +0000
@@ -16,9 +16,12 @@
from cStringIO import StringIO
import errno
+import md5
from SimpleHTTPServer import SimpleHTTPRequestHandler
import re
+import sha
import socket
+import time
import urllib2
import urlparse
@@ -332,38 +335,54 @@
if tcs.auth_scheme != 'basic':
return False
- auth_header = self.headers.get(tcs.auth_header_recv)
- if auth_header and auth_header.lower().startswith('basic '):
- raw_auth = auth_header[len('Basic '):]
- user, password = raw_auth.decode('base64').split(':')
- return tcs.authorized(user, password)
+ auth_header = self.headers.get(tcs.auth_header_recv, None)
+ if auth_header:
+ scheme, raw_auth = auth_header.split(' ', 1)
+ if scheme.lower() == tcs.auth_scheme:
+ user, password = raw_auth.decode('base64').split(':')
+ return tcs.authorized(user, password)
return False
def send_header_auth_reqed(self):
- self.send_header(self.server.test_case_server.auth_header_sent,
- 'Basic realm="Thou should not pass"')
-
+ tcs = self.server.test_case_server
+ self.send_header(tcs.auth_header_sent,
+ 'Basic realm="%s"' % tcs.auth_realm)
+
+
+# FIXME: We should send an Authentication-Info header too when
+# the autheticaion is succesful
class DigestAuthRequestHandler(AuthRequestHandler):
- """Implements the digest authentication of a request"""
+ """Implements the digest authentication of a request.
+
+ We need persistence for some attributes and that can't be
+ achieved here since we get instantiated for each request. We
+ rely on the DigestAuthServer to take care of them.
+ """
def authorized(self):
tcs = self.server.test_case_server
if tcs.auth_scheme != 'digest':
return False
- auth_header = self.headers.get(tcs.auth_header_recv)
- if auth_header and auth_header.lower().startswith('digest '):
- raw_auth = auth_header[len('Basic '):]
- user, password = raw_auth.decode('base64').split(':')
- return tcs.authorized(user, password)
+ auth_header = self.headers.get(tcs.auth_header_recv, None)
+ if auth_header is None:
+ return False
+ scheme, auth = auth_header.split(None, 1)
+ if scheme.lower() == tcs.auth_scheme:
+ auth_dict = urllib2.parse_keqv_list(urllib2.parse_http_list(auth))
+
+ return tcs.digest_authorized(auth_dict, self.command)
return False
def send_header_auth_reqed(self):
- self.send_header(self.server.test_case_server.auth_header_sent,
- 'Basic realm="Thou should not pass"')
+ tcs = self.server.test_case_server
+ header = 'Digest realm="%s", ' % tcs.auth_realm
+ header += 'nonce="%s", algorithm=%s, qop=auth' % (tcs.auth_nonce, 'MD5')
+ self.send_header(tcs.auth_header_sent,header)
+
class AuthServer(HttpServer):
"""Extends HttpServer with a dictionary of passwords.
@@ -373,13 +392,14 @@
Note that no users are defined by default, so add_user should
be called before issuing the first request.
+ """
- """
# The following attributes should be set dy daughter classes
# and are used by AuthRequestHandler.
auth_header_sent = None
auth_header_recv = None
auth_error_code = None
+ auth_realm = "Thou should not pass"
def __init__(self, request_handler, auth_scheme):
HttpServer.__init__(self, request_handler)
@@ -396,25 +416,71 @@
self.password_of[user] = password
def authorized(self, user, password):
+ """Check that the given user provided the right password"""
expected_password = self.password_of.get(user, None)
return expected_password is not None and password == expected_password
+class DigestAuthServer(AuthServer):
+ """A digest authentication server"""
+
+ auth_nonce = 'rRQ+Lp4uBAA=301b77beb156b6158b73dee026b8be23302292b4'
+
+ def __init__(self, request_handler, auth_scheme):
+ AuthServer.__init__(self, request_handler, auth_scheme)
+
+ def digest_authorized(self, auth, command):
+ realm = auth['realm']
+ if realm != self.auth_realm:
+ return False
+ user = auth['username']
+ if not self.password_of.has_key(user):
+ return False
+ algorithm= auth['algorithm']
+ if algorithm != 'MD5':
+ return False
+ qop = auth['qop']
+ if qop != 'auth':
+ return False
+
+ password = self.password_of[user]
+
+ # Recalculate the response_digest to compare with the one
+ # sent by the client
+ A1 = '%s:%s:%s' % (user, realm, password)
+ A2 = '%s:%s' % (command, auth['uri'])
+
+ H = lambda x: md5.new(x).hexdigest()
+ KD = lambda secret, data: H("%s:%s" % (secret, data))
+
+ nonce = auth['nonce']
+ nonce_count = int(auth['nc'], 16)
+
+ ncvalue = '%08x' % nonce_count
+
+ cnonce = auth['cnonce']
+ noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
+ response_digest = KD(H(A1), noncebit)
+
+ return response_digest == auth['response']
+
class HTTPAuthServer(AuthServer):
"""An HTTP server requiring authentication"""
- auth_header_sent = 'WWW-Authenticate'
- auth_header_recv = 'Authorization'
- auth_error_code = 401
+ def init_http_auth(self):
+ self.auth_header_sent = 'WWW-Authenticate'
+ self.auth_header_recv = 'Authorization'
+ self.auth_error_code = 401
class ProxyAuthServer(AuthServer):
"""A proxy server requiring authentication"""
- proxy_requests = True
- auth_header_sent = 'Proxy-Authenticate'
- auth_header_recv = 'Proxy-Authorization'
- auth_error_code = 407
+ def init_proxy_auth(self):
+ self.proxy_requests = True
+ self.auth_header_sent = 'Proxy-Authenticate'
+ self.auth_header_recv = 'Proxy-Authorization'
+ self.auth_error_code = 407
class HTTPBasicAuthServer(HTTPAuthServer):
@@ -422,26 +488,30 @@
def __init__(self):
HTTPAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
-
-
-class HTTPDigestAuthServer(HTTPAuthServer):
+ self.init_http_auth()
+
+
+class HTTPDigestAuthServer(DigestAuthServer, HTTPAuthServer):
"""An HTTP server requiring digest authentication"""
def __init__(self):
- HTTPAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
+ DigestAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
+ self.init_http_auth()
class ProxyBasicAuthServer(ProxyAuthServer):
- """An proxy server requiring basic authentication"""
+ """A proxy server requiring basic authentication"""
def __init__(self):
ProxyAuthServer.__init__(self, BasicAuthRequestHandler, 'basic')
-
-
-class ProxyDigestAuthServer(ProxyAuthServer):
- """An proxy server requiring basic authentication"""
+ self.init_proxy_auth()
+
+
+class ProxyDigestAuthServer(DigestAuthServer, ProxyAuthServer):
+ """A proxy server requiring basic authentication"""
def __init__(self):
ProxyAuthServer.__init__(self, DigestAuthRequestHandler, 'digest')
+ self.init_proxy_auth()
=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py 2007-04-21 09:26:30 +0000
+++ b/bzrlib/tests/test_http.py 2007-04-21 20:39:06 +0000
@@ -1170,6 +1170,13 @@
url += '%s:%s/' % (self.server.host, self.server.port)
return url
+ def test_no_user(self):
+ self.server.add_user('joe', 'foo')
+ t = self.get_user_transport()
+ self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
+ # Only one 'Authentication Required' error should occur
+ self.assertEqual(1, self.server.auth_required_errors)
+
def test_empty_pass(self):
self.server.add_user('joe', '')
t = self.get_user_transport('joe', '')
@@ -1231,7 +1238,7 @@
self.server = self.get_readonly_server()
TestAuth.setUp(self)
- def get_user_transport(self, user, password=None):
+ def get_user_transport(self, user=None, password=None):
return self._transport(self.get_user_url(user, password))
@@ -1255,7 +1262,7 @@
('b-proxied', 'contents of b\n'),
])
- def get_user_transport(self, user, password=None):
+ def get_user_transport(self, user=None, password=None):
self._install_env({'all_proxy': self.get_user_url(user, password)})
return self._transport(self.server.get_url())
=== modified file 'bzrlib/transport/http/_urllib.py'
--- a/bzrlib/transport/http/_urllib.py 2007-04-20 06:50:59 +0000
+++ b/bzrlib/transport/http/_urllib.py 2007-04-21 20:39:06 +0000
@@ -64,7 +64,7 @@
self._connection = None
self._opener = self._opener_class()
- authuri = extract_authentication_uri(self.base)
+ authuri = extract_authentication_uri(self._real_abspath(self._path))
self._auth = {'user': user, 'password': password,
'authuri': authuri}
if user and password is not None: # '' is a valid password
=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
--- a/bzrlib/transport/http/_urllib2_wrappers.py 2007-04-21 09:26:30 +0000
+++ b/bzrlib/transport/http/_urllib2_wrappers.py 2007-04-21 20:39:06 +0000
@@ -51,12 +51,15 @@
# ensure that.
import httplib
+import md5
+import sha
import socket
import urllib
import urllib2
import urlparse
import re
import sys
+import time
from bzrlib import __version__ as bzrlib_version
from bzrlib import (
@@ -760,7 +763,7 @@
# The following attributes should be defined by daughter
# classes:
- # - auth_reqed_header: the header received from the server
+ # - auth_required_header: the header received from the server
# - auth_header: the header sent in the request
def __init__(self, password_manager):
@@ -775,21 +778,32 @@
:param headers: The headers for the authentication error response.
:return: None or the response for the authenticated request.
"""
- server_header = headers.get(self.auth_reqed_header, None)
+ server_header = headers.get(self.auth_required_header, None)
if server_header is None:
# The http error MUST have the associated
# header. This must never happen in production code.
- raise KeyError('%s not found' % self.auth_reqed_header)
-
- auth = self.get_auth(request)
+ raise KeyError('%s not found' % self.auth_required_header)
+
+ auth = self.get_auth(request).copy()
+ if auth.get('user', None) is None:
+ # Without a known user, we can't authenticate
+ return None
+
if self.auth_match(server_header, auth):
- client_header = self.build_auth_header(auth)
- if client_header == request.get_header(self.auth_header, None):
+ # auth_match may have modified auth (by adding the
+ # password or changing the realm, for example)
+ old = self.get_auth(request)
+ if request.get_header(self.auth_header, None) is not None \
+ and old.get('user') == auth.get('user') \
+ and old.get('realm') == auth.get('realm') \
+ and old.get('password') == auth.get('password'):
# We already tried that, give up
return None
- self.add_auth_header(request, client_header)
- request.add_unredirected_header(self.auth_header, client_header)
+ # We will try to authenticate, save the auth so that
+ # the build_auth_header that will be called during
+ # parent.open use the right values
+ self.set_auth(request, auth)
response = self.parent.open(request)
if response:
self.auth_successful(request, response, auth)
@@ -803,15 +817,8 @@
# 'Proxy Authentication Required' error.
return None
- def get_auth(self, request):
- """Get the auth params from the request"""
- raise NotImplementedError(self.get_auth)
-
- def set_auth(self, request, auth):
- """Set the auth params for the request"""
- raise NotImplementedError(self.set_auth)
-
def add_auth_header(self, request, header):
+ """Add the authentication header to the request"""
request.add_unredirected_header(self.auth_header, header)
def auth_match(self, header, auth):
@@ -827,10 +834,11 @@
"""
raise NotImplementedError(self.auth_match)
- def build_auth_header(self, auth):
+ def build_auth_header(self, auth, request):
"""Build the value of the header used to authenticate.
:param auth: The auth parameters needed to build the header.
+ :param request: The request needing authentication.
:return: None or header.
"""
@@ -875,89 +883,213 @@
"""Insert an authentication header if information is available"""
auth = self.get_auth(request)
if self.auth_params_reusable(auth):
- self.add_auth_header(request, self.build_auth_header(auth))
+ self.add_auth_header(request, self.build_auth_header(auth, request))
return request
https_request = http_request # FIXME: Need test
-class AbstractBasicAuthHandler(AbstractAuthHandler):
- """A custom basic auth handler."""
-
- auth_regexp = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', re.I)
-
- def build_auth_header(self, auth):
+class BasicAuthHandler(AbstractAuthHandler):
+ """A custom basic authentication handler."""
+
+ auth_regexp = re.compile('realm="([^"]*)"', re.I)
+
+ def build_auth_header(self, auth, request):
raw = '%s:%s' % (auth['user'], auth['password'])
auth_header = 'Basic ' + raw.encode('base64').strip()
return auth_header
def auth_match(self, header, auth):
- match = self.auth_regexp.search(header)
+ scheme, raw_auth = header.split(None, 1)
+ scheme = scheme.lower()
+ if scheme != 'basic':
+ return False
+
+ match = self.auth_regexp.search(raw_auth)
if match:
- scheme, auth['realm'] = match.groups()
- auth['scheme'] = scheme.lower()
- if auth['scheme'] != 'basic':
- match = None
- else:
- if auth.get('password',None) is None:
- auth['password'] = self.get_password(auth['user'],
- auth['authuri'],
- auth['realm'])
+ realm = match.groups()
+ if scheme != 'basic':
+ return False
+ # Put useful info into auth
+ auth['scheme'] = scheme
+ auth['realm'] = realm
+ if auth.get('password',None) is None:
+ auth['password'] = self.get_password(auth['user'],
+ auth['authuri'],
+ auth['realm'])
return match is not None
def auth_params_reusable(self, auth):
# If the auth scheme is known, it means a previous
# authentication was succesful, all information is
# available, no further checks are needed.
- return auth.get('scheme',None) == 'basic'
-
-
-class HTTPBasicAuthHandler(AbstractBasicAuthHandler):
- """Custom basic authentication handler.
+ return auth.get('scheme', None) == 'basic'
+
+
+def get_digest_algorithm_impls(algorithm):
+ H = None
+ if algorithm == 'MD5':
+ H = lambda x: md5.new(x).hexdigest()
+ elif algorithm == 'SHA':
+ H = lambda x: sha.new(x).hexdigest()
+ if H is not None:
+ KD = lambda secret, data: H("%s:%s" % (secret, data))
+ return H, KD
+
+
+class DigestAuthHandler(AbstractAuthHandler):
+ """A custom digest authentication handler."""
+
+ def auth_params_reusable(self, auth):
+ # If the auth scheme is known, it means a previous
+ # authentication was succesful, all information is
+ # available, no further checks are needed.
+ return auth.get('scheme', None) == 'digest'
+
+ def auth_match(self, header, auth):
+ scheme, raw_auth = header.split(None, 1)
+ scheme = scheme.lower()
+ if scheme != 'digest':
+ return False
+
+ # Put the requested authentication info into a dict
+ req_auth = urllib2.parse_keqv_list(urllib2.parse_http_list(raw_auth))
+
+ # Check that we can handle that authentication
+ qop = req_auth.get('qop', None)
+ if qop != 'auth': # No auth-int so far
+ return False
+
+ nonce = req_auth.get('nonce', None)
+ old_nonce = auth.get('nonce', None)
+ if nonce and old_nonce and nonce == old_nonce:
+ # We already tried that
+ return False
+
+ algorithm = req_auth.get('algorithm', 'MD5')
+ H, KD = get_digest_algorithm_impls(algorithm)
+ if H is None:
+ return False
+
+ realm = req_auth.get('realm', None)
+ if auth.get('password',None) is None:
+ auth['password'] = self.get_password(auth['user'],
+ auth['authuri'],
+ realm)
+ # Put useful info into auth
+ try:
+ auth['scheme'] = scheme
+ auth['algorithm'] = algorithm
+ auth['realm'] = req_auth['realm']
+ auth['nonce'] = req_auth['nonce']
+ auth['qop'] = qop
+ auth['opaque'] = req_auth.get('opaque', None)
+ except KeyError:
+ return False
+
+ return True
+
+ def build_auth_header(self, auth, request):
+ uri = request.get_selector()
+ A1 = '%s:%s:%s' % (auth['user'], auth['realm'], auth['password'])
+ A2 = '%s:%s' % (request.get_method(), uri)
+ nonce = auth['nonce']
+ qop = auth['qop']
+
+ H, KD = get_digest_algorithm_impls(auth['algorithm'])
+ nonce_count = auth.get('nonce_count',0)
+ nonce_count += 1
+ ncvalue = '%08x' % nonce_count
+ cnonce = sha.new("%s:%s:%s:%s" % (nonce_count, nonce,
+ time.ctime(), urllib2.randombytes(8))
+ ).hexdigest()[:16]
+ noncebit = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
+ response_digest = KD(H(A1), noncebit)
+
+ header = 'Digest '
+ header += 'username="%s", realm="%s", nonce="%s",' % (auth['user'],
+ auth['realm'],
+ nonce)
+ header += ' uri="%s", response="%s"' % (uri, response_digest)
+ opaque = auth.get('opaque', None)
+ if opaque:
+ header += ', opaque="%s"' % opaque
+ header += ', algorithm="%s"' % auth['algorithm']
+ header += ', qop="%s", nc="%s", cnonce="%s"' % (qop, ncvalue, cnonce)
+
+ # We have used the nonce once more, update the count
+ auth['nonce_count'] = nonce_count
+
+ return header
+
+
+class HTTPAuthHandler(AbstractAuthHandler):
+ """Custom http authentication handler.
Send the authentication preventively to avoid the roundtrip
- associated with the 401 error.
+ associated with the 401 error and keep the revelant info in
+ the auth request attribute.
"""
password_prompt = 'HTTP %(user)s@%(host)s%(realm)s password'
- auth_reqed_header = 'www-authenticate'
+ auth_required_header = 'www-authenticate'
auth_header = 'Authorization'
def get_auth(self, request):
+ """Get the auth params from the request"""
return request.auth
def set_auth(self, request, auth):
+ """Set the auth params for the request"""
request.auth = auth
def http_error_401(self, req, fp, code, msg, headers):
return self.auth_required(req, headers)
-class ProxyBasicAuthHandler(AbstractBasicAuthHandler):
- """Custom proxy basic authentication handler.
+class ProxyAuthHandler(AbstractAuthHandler):
+ """Custom proxy authentication handler.
Send the authentication preventively to avoid the roundtrip
- associated with the 407 error.
+ associated with the 407 error and keep the revelant info in
+ the proxy_auth request attribute..
"""
password_prompt = 'Proxy %(user)s@%(host)s%(realm)s password'
- auth_reqed_header = 'proxy-authenticate'
+ auth_required_header = 'proxy-authenticate'
# FIXME: the correct capitalization is Proxy-Authorization,
# but python-2.4 urllib2.Request insist on using capitalize()
# instead of title().
auth_header = 'Proxy-authorization'
def get_auth(self, request):
+ """Get the auth params from the request"""
return request.proxy_auth
def set_auth(self, request, auth):
+ """Set the auth params for the request"""
request.proxy_auth = auth
def http_error_407(self, req, fp, code, msg, headers):
return self.auth_required(req, headers)
+class HTTPBasicAuthHandler(BasicAuthHandler, HTTPAuthHandler):
+ """Custom http basic authentication handler"""
+
+
+class ProxyBasicAuthHandler(BasicAuthHandler, ProxyAuthHandler):
+ """Custom proxy basic authentication handler"""
+
+
+class HTTPDigestAuthHandler(DigestAuthHandler, HTTPAuthHandler):
+ """Custom http basic authentication handler"""
+
+
+class ProxyDigestAuthHandler(DigestAuthHandler, ProxyAuthHandler):
+ """Custom proxy basic authentication handler"""
+
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
"""Process HTTP error responses.
@@ -1013,15 +1145,13 @@
redirect=HTTPRedirectHandler,
error=HTTPErrorProcessor,):
self.password_manager = PasswordManager()
- # TODO: Implements the necessary wrappers for the handlers
- # commented out below
self._opener = urllib2.build_opener( \
connection, redirect, error,
ProxyHandler(self.password_manager),
HTTPBasicAuthHandler(self.password_manager),
- #urllib2.HTTPDigestAuthHandler(self.password_manager),
+ HTTPDigestAuthHandler(self.password_manager),
ProxyBasicAuthHandler(self.password_manager),
- #urllib2.ProxyDigestAuthHandler,
+ ProxyDigestAuthHandler(self.password_manager),
HTTPHandler,
HTTPSHandler,
HTTPDefaultErrorHandler,
More information about the bazaar-commits
mailing list