Rev 2423: Implement http proxy basic authentication. in http://bazaar.launchpad.net/~bzr/bzr/bzr.http.auth

Vincent Ladeuil v.ladeuil+lp at free.fr
Tue Apr 17 23:07:20 BST 2007


At http://bazaar.launchpad.net/~bzr/bzr/bzr.http.auth

------------------------------------------------------------
revno: 2423
revision-id: v.ladeuil+lp at free.fr-20070417220718-kce3mj0wn8hi8m02
parent: v.ladeuil+lp at free.fr-20070417103339-3kywr38d0p50czrw
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: bzr.http.auth
timestamp: Wed 2007-04-18 00:07:18 +0200
message:
  Implement http proxy basic authentication.
  
  * bzrlib/transport/http/_urllib2_wrappers.py:
  (Request.set_proxy_auth): New method.
  (extract_credentials): Moved from
  HttpTransport_urllib._extract_auth.
  (ProxyHandler.__init__): We need a password_manager for
  authentication.
  (ProxyHandler.set_proxy): Don't add the auth header, the
  ProxyHandlers will do it later.
  (ProxyBasicAuthHandler): New class. Handle the http basic
  authentication for proxy.
  (Opener.__init__): Enable ProxyBasicAuthHandler.
  
  * bzrlib/transport/http/_urllib.py:
  (HttpTransport_urllib._extract_auth): Moved to
  _urllib2_wrappers.extract_credentials.
  
  * bzrlib/tests/test_http.py:
  (TestHttpProxyWhiteBox.test_empty_pass,
  TestHttpProxyWhiteBox.test_user_pass): Deleted. Euivalent tests
  exists in TestHTTPProxyBasicAuth now.
  
  * bzrlib/tests/HTTPTestUtil.py:
  (BasicAuthRequestHandler): Fix inheritance.
  (ProxyBasicAuthRequestHandler): Force the use of
  FakeProxyRequestHandler.translate_path. This is a bit ugly :-/
modified:
  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 'bzrlib/tests/HTTPTestUtil.py'
--- a/bzrlib/tests/HTTPTestUtil.py	2007-04-17 10:33:39 +0000
+++ b/bzrlib/tests/HTTPTestUtil.py	2007-04-17 22:07:18 +0000
@@ -367,14 +367,8 @@
         return expected_password is not None and password == expected_password
 
 
-class BasicAuthRequestHandler(AbstractBasicAuthRequestHandler,
-                              FakeProxyRequestHandler):
-    """Requires a basic authentication to process requests.
-
-    Note: Each of the inherited request handler overrides
-    different parts of processing in a compatible way, so it is
-    okay to inherit from both.
-    """
+class BasicAuthRequestHandler(AbstractBasicAuthRequestHandler):
+    """Requires a basic authentication to process requests"""
 
     _auth_header_sent = 'WWW-Authenticate'
     _auth_header_recv = 'Authorization'
@@ -392,15 +386,17 @@
                                    FakeProxyRequestHandler):
     """Requires a basic authentication to proxy requests.
 
-    Note: Each of the inherited request handler overrides
-    different parts of processing in a compatible way, so it is
-    okay to inherit from both.
+    Note: We have to override translate_path because otherwise
+    AbstractBasicAuthRequestHandler.translate_path take
+    precedence.  
     """
 
     _auth_header_sent = 'Proxy-Authenticate'
     _auth_header_recv = 'Proxy-Authorization'
     _auth_error_code = 407
 
+    translate_path = FakeProxyRequestHandler.translate_path
+
 
 class ProxyBasicAuthHTTPServer(AuthHTTPServer):
     """An HTTP server requiring basic authentication"""

=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py	2007-04-17 10:33:39 +0000
+++ b/bzrlib/tests/test_http.py	2007-04-17 22:07:18 +0000
@@ -71,6 +71,11 @@
     _urllib2_wrappers,
     )
 from bzrlib.transport.http._urllib import HttpTransport_urllib
+from bzrlib.transport.http._urllib2_wrappers import (
+    PasswordManager,
+    ProxyHandler,
+    Request,
+    )
 
 
 class FakeManager(object):
@@ -708,7 +713,7 @@
 class TestHttpProxyWhiteBox(TestCase):
     """Whitebox test proxy http authorization.
 
-    These tests concern urllib implementation only.
+    Only the urllib implementation is tested here.
     """
 
     def setUp(self):
@@ -727,8 +732,8 @@
             osutils.set_or_unset_env(name, value)
 
     def _proxied_request(self):
-        handler = _urllib2_wrappers.ProxyHandler()
-        request = _urllib2_wrappers.Request('GET','http://baz/buzzle')
+        handler = ProxyHandler(PasswordManager())
+        request = Request('GET','http://baz/buzzle')
         handler.set_proxy(request, 'http')
         return request
 
@@ -737,17 +742,6 @@
         request = self._proxied_request()
         self.assertFalse(request.headers.has_key('Proxy-authorization'))
 
-    def test_empty_pass(self):
-        self._install_env({'http_proxy': 'http://joe@bar.com'})
-        request = self._proxied_request()
-        self.assertEqual('Basic ' + 'joe:'.encode('base64').strip(),
-                         request.headers['Proxy-authorization'])
-    def test_user_pass(self):
-        self._install_env({'http_proxy': 'http://joe:foo@bar.com'})
-        request = self._proxied_request()
-        self.assertEqual('Basic ' + 'joe:foo'.encode('base64').strip(),
-                         request.headers['Proxy-authorization'])
-
     def test_invalid_proxy(self):
         """A proxy env variable without scheme"""
         self._install_env({'http_proxy': 'host:1234'})
@@ -1002,10 +996,10 @@
     """Tests redirections for pycurl implementation"""
 
 
-class RedirectedRequest(_urllib2_wrappers.Request):
+class RedirectedRequest(Request):
     """Request following redirections"""
 
-    init_orig = _urllib2_wrappers.Request.__init__
+    init_orig = Request.__init__
 
     def __init__(self, method, url, *args, **kwargs):
         RedirectedRequest.init_orig(self, method, url, args, kwargs)

=== modified file 'bzrlib/transport/http/_urllib.py'
--- a/bzrlib/transport/http/_urllib.py	2007-04-15 15:57:08 +0000
+++ b/bzrlib/transport/http/_urllib.py	2007-04-17 22:07:18 +0000
@@ -30,6 +30,7 @@
 from bzrlib.transport.http._urllib2_wrappers import (
     Opener,
     Request,
+    extract_credentials,
     )
 
 
@@ -59,7 +60,7 @@
             # info in the urls. So we handle them separatly.
             # Note: we don't need to when cloning because it was
             # already done.
-            clean_base, user, password = self._extract_auth(base)
+            clean_base, user, password = extract_credentials(base)
             super(HttpTransport_urllib, self).__init__(clean_base,
                                                        from_transport)
             self._connection = None
@@ -94,31 +95,6 @@
                                                             host=self._host)
                 pm.add_password(None, authuri, self._user, self._password)
 
-    def _extract_auth(self, url):
-        """Extracts authentication 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.
 

=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
--- a/bzrlib/transport/http/_urllib2_wrappers.py	2007-04-15 15:57:08 +0000
+++ b/bzrlib/transport/http/_urllib2_wrappers.py	2007-04-17 22:07:18 +0000
@@ -157,16 +157,51 @@
         # Unless told otherwise, redirections are not followed
         self.follow_redirections = False
         self.set_auth(None, None, None) # Until the first 401
+        self.set_proxy_auth(None, None, None) # Until the first 407
 
     def set_auth(self, auth_scheme, user, password=None):
         self.auth_scheme = auth_scheme
         self.user = user
         self.password = password
 
+    # FIXME: this is not called, we incur a rountrip for each
+    # request.
+    def set_proxy_auth(self, auth_scheme, user, password=None):
+        self.proxy_auth_scheme = auth_scheme
+        self.proxy_user = user
+        self.proxy_password = password
+
     def get_method(self):
         return self.method
 
 
+def extract_credentials(url):
+    """Extracts credentials 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
+
+    # Build the clean url
+    clean_url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+    return clean_url, user, password
+
+
 # The urlib2.xxxAuthHandler handle the authentication of the
 # requests, to do that, they need an urllib2 PasswordManager *at
 # build time*. We also need one to reuse the passwords already
@@ -594,8 +629,9 @@
     handler_order = 100
     _debuglevel = DEBUG
 
-    def __init__(self, proxies=None):
+    def __init__(self, password_manager, proxies=None):
         urllib2.ProxyHandler.__init__(self, proxies)
+        self.password_manager = password_manager
         # First, let's get rid of urllib2 implementation
         for type, proxy in self.proxies.items():
             if self._debuglevel > 0:
@@ -675,16 +711,13 @@
             raise errors.InvalidURL(proxy,
                                     'Invalid syntax in proxy env variable')
         elif '@' in host:
-            user_pass, host = host.split('@', 1)
-            if ':' in user_pass:
-                user, password = user_pass.split(':', 1)
-            else:
-                user = user_pass
-                password = ''
-            user_pass = '%s:%s' % (urllib.unquote(user),
-                                   urllib.unquote(password))
-            user_pass = user_pass.encode('base64').strip()
-            request.add_header('Proxy-authorization', 'Basic ' + user_pass)
+            # Extract credentials from the url and store them in
+            # the password manager so that the
+            # ProxyxxxAuthHandler can use them later.
+            clean_url, user, password = extract_credentials(proxy)
+            if user and password is not None: # '' is a valid password
+                pm = self.password_manager
+                pm.add_password(None, self.base, self._user, self._password)
         host = urllib.unquote(host)
         request.set_proxy(host, type)
         if self._debuglevel > 0:
@@ -739,6 +772,55 @@
         return response
 
 
+class ProxyBasicAuthHandler(urllib2.ProxyBasicAuthHandler):
+    """Custom proxy basic authentication handler.
+
+    Send the authentication preventively to avoid the roundtrip
+    associated with the 407 error.
+    """
+
+    def get_auth(self, user, password):
+        raw = '%s:%s' % (user, password)
+        auth = 'Basic ' + raw.encode('base64').strip()
+        return auth
+
+    def set_auth(self, request):
+        """Add the authentication header if needed.
+
+        All required informations should be part of the request.
+        """
+        if request.proxy_password is not None:
+            request.add_header(self.auth_header,
+                               self.get_auth(request.proxy_user,
+                                             request.proxy_password))
+
+    def http_request(self, request):
+        """Insert an authentication header if information is available"""
+        if request.proxy_auth_scheme == 'basic' \
+                and request.proxy_password is not None:
+            self.set_auth(request)
+        return request
+
+    https_request = http_request # FIXME: Need test
+
+    def http_error_407(self, req, fp, code, msg, headers):
+        """Trap the 401 to gather the auth type."""
+        response = urllib2.ProxyBasicAuthHandler.http_error_407(self, req, fp,
+                                                                code, msg,
+                                                                headers)
+        if response is not None:
+            # We capture the auth_scheme to be able to send the
+            # authentication header with the next requests
+            # without waiting for a 407 error.
+            # The urllib2.ProxyBasicAuthHandler will return a
+            # response *only* if the basic authentication
+            # succeeds. If another scheme is used or the
+            # authentication fails, the response will be None.
+            req.proxy_auth_scheme = 'basic'
+
+        return response
+
+
 class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
     """Process HTTP error responses.
 
@@ -797,10 +879,10 @@
         # commented out below
         self._opener = urllib2.build_opener( \
             connection, redirect, error,
-            ProxyHandler,
+            ProxyHandler(self.password_manager),
             HTTPBasicAuthHandler(self.password_manager),
             #urllib2.HTTPDigestAuthHandler(self.password_manager),
-            #urllib2.ProxyBasicAuthHandler,
+            ProxyBasicAuthHandler(self.password_manager),
             #urllib2.ProxyDigestAuthHandler,
             HTTPHandler,
             HTTPSHandler,



More information about the bazaar-commits mailing list