Rev 2372: Catch first succesful authentification to avoid further 401 in file:///v/home/vila/src/bugs/72792/
Vincent Ladeuil
v.ladeuil+lp at free.fr
Fri Apr 13 13:17:47 BST 2007
At file:///v/home/vila/src/bugs/72792/
------------------------------------------------------------
revno: 2372
revision-id: v.ladeuil+lp at free.fr-20070413121740-mnwzf1656e31aenj
parent: v.ladeuil+lp at free.fr-20070413081101-j0keov4vgf493m2d
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: 72792
timestamp: Fri 2007-04-13 14:17:40 +0200
message:
Catch first succesful authentification to avoid further 401
roudtrips in hhtp urllib implementation.
* bzrlib/transport/http/_urllib2_wrappers.py:
(Request.__init__): Initialize auth parameters.
(Request.extract_auth): Moved to
HttpTransport_urllib._extract_auth.
(Request.set_auth): New method.
(PasswordManager): Now that the transport handles the auth
parameters, we can use transport.base as the auth uri and work
around the python-2.4 bug.
(HTTPBasicAuthHandler.http_error_401): Capture the auth scheme
when the authentication succeeds.
* bzrlib/transport/http/_urllib.py:
(HttpTransport_urllib.__init__): Extract authentication at
construction time so that we don't have to do it at request build
time. urllib2 will be happier without it.
(HttpTransport_urllib._extract_auth): Moved from
_urllib2_wrappers.Request.extract_auth.
(HttpTransport_urllib._ask_password): Made private and do not
require a 'request' parameter anymore.
(HttpTransport_urllib._perform): The transport is now responsible
for handling the auth parameters and provide them to the
requests. And from there we can avoid the 401 roundtrips
yeaaaaah! (Except the first one of course to determine the auth
scheme).
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/transport/http/__init__.py http_transport.py-20050711212304-506c5fd1059ace96
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-10 05:54:15 +0000
+++ b/NEWS 2007-04-13 12:17:40 +0000
@@ -40,6 +40,9 @@
* Move most SmartServer code into a new package, bzrlib/smart.
bzrlib/transport/remote.py contains just the Transport classes that used
to be in bzrlib/transport/smart.py. (Andrew Bennetts)
+
+ * urllib http implementation avoid roundtrips associated with 401 errors
+ once the the authentication succeeds. (Vincent Ladeuil).
BUGFIXES:
=== modified file 'bzrlib/transport/http/__init__.py'
--- a/bzrlib/transport/http/__init__.py 2007-04-09 04:49:55 +0000
+++ b/bzrlib/transport/http/__init__.py 2007-04-13 12:17:40 +0000
@@ -143,11 +143,12 @@
self._query, self._fragment) = urlparse.urlparse(self.base)
self._qualified_proto = apparent_proto
# range hint is handled dynamically throughout the life
- # of the object. We start by trying multi-range requests
- # and if the server returns bougs results, we retry with
- # single range requests and, finally, we forget about
- # range if the server really can't understand. Once
- # aquired, this piece of info is propogated to clones.
+ # of the transport object. We start by trying multi-range
+ # requests and if the server returns bogus results, we
+ # retry with single range requests and, finally, we
+ # forget about range if the server really can't
+ # understand. Once acquired, this piece of info is
+ # propagated to clones.
if from_transport is not None:
self._range_hint = from_transport._range_hint
else:
=== modified file 'bzrlib/transport/http/_urllib.py'
--- a/bzrlib/transport/http/_urllib.py 2007-04-10 21:22:35 +0000
+++ b/bzrlib/transport/http/_urllib.py 2007-04-13 12:17:40 +0000
@@ -15,6 +15,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from cStringIO import StringIO
+import urllib
+import urlparse
from bzrlib import (
ui,
@@ -45,40 +47,78 @@
def __init__(self, base, from_transport=None):
"""Set the base path where files will be stored."""
- super(HttpTransport_urllib, self).__init__(base, from_transport)
if from_transport is not None:
+ super(HttpTransport_urllib, self).__init__(base, from_transport)
self._connection = from_transport._connection
+ self._auth = from_transport._auth
self._user = from_transport._user
self._password = from_transport._password
self._opener = from_transport._opener
else:
+ # urllib2 will be confused if it find
+ # authentification infos in the urls. So we handle
+ # them separatly. Note that we don't need to do that
+ # when cloning (as above) since the cloned base is
+ # already clean.
+ clean_base, user, password = self._extract_auth(base)
+ super(HttpTransport_urllib, self).__init__(clean_base,
+ from_transport)
self._connection = None
- self._user = None
- self._password = None
+ self._auth = None # We have to wait the 401 to know
+ self._user = user
+ self._password = password
self._opener = self._opener_class()
+ if password is not None: # '' is a valid password
+ # Make the (user, password) available to urllib2
+ pm = self._opener.password_manager
+ pm.add_password(None, self.base, self._user, self._password)
- def ask_password(self, request):
- """Ask for a password if none is already provided in the request"""
+ def _ask_password(self):
+ """Ask for a password if none is already available"""
# TODO: jam 20060915 There should be a test that asserts we ask
# for a password at the right time.
- host = request.get_host()
- password_manager = self._opener.password_manager
- if request.password is None:
+ if self._password is None:
# We can't predict realm, let's try None, we'll get a
# 401 if we are wrong anyway
realm = None
# Query the password manager first
- user, password = password_manager.find_user_password(None, host)
- if user == request.user and password is not None:
- request.password = password
+ authuri = self.base
+ pm = self._opener.password_manager
+ user, password = pm.find_user_password(None, authuri)
+ if user == self._user and password is not None:
+ self._password = password
else:
# Ask the user if we MUST
http_pass = 'HTTP %(user)s@%(host)s password'
- request.password = ui.ui_factory.get_password(prompt=http_pass,
- user=request.user,
- host=host)
- password_manager.add_password(None, host,
- request.user, request.password)
+ self._password = ui.ui_factory.get_password(prompt=http_pass,
+ user=self._user,
+ host=self._host)
+ pm.add_password(None, authuri, self._user, self._password)
+
+ def _extract_auth(self, url):
+ """Extracts authentification information from url.
+
+ Get user and password from url of the form: http://user:pass@host/path
+ :returns: (clean_url, user, password)
+ """
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
+
+ if '@' in netloc:
+ auth, netloc = netloc.split('@', 1)
+ if ':' in auth:
+ user, password = auth.split(':', 1)
+ else:
+ user, password = auth, None
+ user = urllib.unquote(user)
+ if password is not None:
+ password = urllib.unquote(password)
+ else:
+ user = None
+ password = None
+
+ url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+ return url, user, password
def _perform(self, request):
"""Send the request to the server and handles common errors.
@@ -88,13 +128,12 @@
if self._connection is not None:
# Give back shared info
request.connection = self._connection
- if self._user is not None:
- request.user = self._user
- request.password = self._password
- elif request.user is not None:
+ elif self._user is not None:
# We will issue our first request, time to ask for a
# password if needed
- self.ask_password(request)
+ self._ask_password()
+ # Ensure authentification info is provided
+ request.set_auth(self._auth, self._user, self._password)
mutter('%s: [%s]' % (request.method, request.get_full_url()))
if self._debuglevel > 0:
@@ -106,6 +145,8 @@
# Acquire connection when the first request is able
# to connect to the server
self._connection = request.connection
+ # And get auth parameters too
+ self._auth = request.auth
self._user = request.user
self._password = request.password
=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
--- a/bzrlib/transport/http/_urllib2_wrappers.py 2007-04-13 08:11:01 +0000
+++ b/bzrlib/transport/http/_urllib2_wrappers.py 2007-04-13 12:17:40 +0000
@@ -146,10 +146,6 @@
def __init__(self, method, url, data=None, headers={},
origin_req_host=None, unverifiable=False,
connection=None, parent=None,):
- # urllib2.Request will be confused if we don't extract
- # authentification info before building the request
- url, self.user, self.password = self.extract_auth(url)
- self.auth = None # Until the first 401
urllib2.Request.__init__(self, url, data, headers,
origin_req_host, unverifiable)
self.method = method
@@ -159,30 +155,12 @@
self.redirected_to = None
# Unless told otherwise, redirections are not followed
self.follow_redirections = False
-
- def extract_auth(self, url):
- """Extracts authentification information from url.
-
- Get user and password from url of the form: http://user:pass@host/path
- """
- scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
-
- if '@' in netloc:
- auth, netloc = netloc.split('@', 1)
- if ':' in auth:
- user, password = auth.split(':', 1)
- else:
- user, password = auth, None
- user = urllib.unquote(user)
- if password is not None:
- password = urllib.unquote(password)
- else:
- user = None
- password = None
-
- url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
-
- return url, user, password
+ self.set_auth(None, None, None) # Until the first 401
+
+ def set_auth(self, auth_scheme, user, password=None):
+ self.auth = auth_scheme
+ self.user = user
+ self.password = password
def get_method(self):
return self.method
@@ -197,52 +175,6 @@
def __init__(self):
urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
- def add_password(self, realm, uri, user, passwd):
- # uri could be a single URI or a sequence
- if isinstance(uri, basestring):
- uri = [uri]
- if not realm in self.passwd:
- self.passwd[realm] = {}
- for default_port in True, False:
- reduced_uri = tuple(
- [self.reduce_uri(u, default_port) for u in uri])
- self.passwd[realm][reduced_uri] = (user, passwd)
-
- def find_user_password(self, realm, authuri):
- domains = self.passwd.get(realm, {})
- for default_port in True, False:
- reduced_authuri = self.reduce_uri(authuri, default_port)
- for uris, authinfo in domains.iteritems():
- for uri in uris:
- if self.is_suburi(uri, reduced_authuri):
- return authinfo
- if realm is not None:
- return self.find_user_password(None, authuri)
- return None, None
-
- def reduce_uri(self, uri, default_port=True):
- """Accept authority or URI and extract only the authority and path."""
- # note HTTP URLs do not have a userinfo component
- parts = urlparse.urlsplit(uri)
- if parts[1]:
- # URI
- scheme = parts[0]
- authority = parts[1]
- path = parts[2] or '/'
- else:
- # host or host:port
- scheme = None
- authority = uri
- path = '/'
- host, port = urllib.splitport(authority)
- if default_port and port is None and scheme is not None:
- dport = {"http": 80,
- "https": 443,
- }.get(scheme)
- if dport is not None:
- authority = "%s:%d" % (host, dport)
- return authority, path
-
class ConnectionHandler(urllib2.BaseHandler):
"""Provides connection-sharing by pre-processing requests.
@@ -788,6 +720,16 @@
https_request = http_request # FIXME: Need test
+ def http_error_401(self, req, fp, code, msg, headers):
+ """Trap the 401 to gather the auth type."""
+ response = urllib2.HTTPBasicAuthHandler.http_error_401(self, req, fp,
+ code, msg,
+ headers)
+ if response is not None:
+ req.auth = 'basic'
+
+ return response
+
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
"""Process HTTP error responses.
More information about the bazaar-commits
mailing list