Rev 2421: merge basic auth in file:///v/home/vila/src/experimental/bzr.http.auth/

Vincent Ladeuil v.ladeuil+lp at free.fr
Mon Apr 16 14:41:23 BST 2007


At file:///v/home/vila/src/experimental/bzr.http.auth/

------------------------------------------------------------
revno: 2421
revision-id: v.ladeuil+lp at free.fr-20070416134120-i8y220zv30spaq0a
parent: pqm at pqm.ubuntu.com-20070416080254-bf3rfk77k5bgfdl7
parent: v.ladeuil+lp at free.fr-20070415155708-frrm29cd9vvvd8do
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: bzr.http.auth
timestamp: Mon 2007-04-16 15:41:20 +0200
message:
  merge basic auth
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/tests/HTTPTestUtil.py   HTTPTestUtil.py-20050914180604-247d3aafb7a43343
  bzrlib/tests/test_http.py      testhttp.py-20051018020158-b2eef6e867c514d9
  bzrlib/tests/test_ui.py        test_ui.py-20051130162854-458e667a7414af09
  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
    ------------------------------------------------------------
    revno: 2363.4.12
    merged: v.ladeuil+lp at free.fr-20070415155708-frrm29cd9vvvd8do
    parent: v.ladeuil+lp at free.fr-20070413163858-3v6pox4wls7j9ljb
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Sun 2007-04-15 17:57:08 +0200
    message:
      Take jam's review comments into account. Fix typos, give better
      explanations, add a test, complete another.
      
      * bzrlib/transport/http/_urllib2_wrappers.py:
      (HTTPBasicAuthHandler.http_error_401): Better explanation.
      
      * bzrlib/transport/http/_urllib.py:
      (HttpTransport_urllib.__init__): _auth renamed to _auth_scheme.
      
      * bzrlib/tests/test_http.py:
      (TestHTTPBasicAuth.test_unknown_user): New test.
      
      * bzrlib/tests/HTTPTestUtil.py:
      (BasicAuthHTTPServer): New class. Be explicit about use
      requirements: basic authentication is mandatory.
    ------------------------------------------------------------
    revno: 2363.4.11
    merged: v.ladeuil+lp at free.fr-20070413163858-3v6pox4wls7j9ljb
    parent: v.ladeuil+lp at free.fr-20070413161654-kp4kajrrr23jge2f
    parent: pqm at pqm.ubuntu.com-20070413160237-0weampli2rrmzjht
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Fri 2007-04-13 18:38:58 +0200
    message:
      merge bzr.dev
    ------------------------------------------------------------
    revno: 2363.4.10
    merged: v.ladeuil+lp at free.fr-20070413161654-kp4kajrrr23jge2f
    parent: v.ladeuil+lp at free.fr-20070413121740-mnwzf1656e31aenj
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Fri 2007-04-13 18:16:54 +0200
    message:
      Complete tests.
      
      * bzrlib/transport/http/_urllib.py:
      (HttpTransport_urllib.__init__): Be more strict on valid users.
      (HttpTransport_urllib._ask_password): Delete John's TODO. See ? I
      didn't forget ! :-)
      
      * bzrlib/tests/test_ui.py: 
      Fix some inconsistencies.
      
      * bzrlib/tests/test_http.py:
      Add more tests.
      (TestHTTPBasicAuth.setUp): Setup a private ui_factory.
    ------------------------------------------------------------
    revno: 2363.4.9
    merged: 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).
    ------------------------------------------------------------
    revno: 2363.4.8
    merged: v.ladeuil+lp at free.fr-20070413081101-j0keov4vgf493m2d
    parent: v.ladeuil+lp at free.fr-20070412160004-zrffqcemqyjb8gvq
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Fri 2007-04-13 10:11:01 +0200
    message:
      Implement a basic auth HTTP server, rewrite tests accordingly.
      
      * bzrlib/transport/http/_urllib2_wrappers.py:
      (PasswordManager.add_password, PasswordManager.find_user_password,
      PasswordManager.reduce_uri): Copied from python-2.5 urllib2.py as
      a stop gap. A python-2.4 compatible work must be found.
      (Opener.preprocess_request): Deleted.
      
      * bzrlib/tests/test_http.py:
      (TestHTTPBasicAuth.create_transport_readonly_server): Use the
      right auth HTTP server.
      (TestHTTPBasicAuth.setUp): Plug the the server.
      (TestHTTPBasicAuth.process_request): Deleted.
      
      * bzrlib/tests/HTTPTestUtil.py:
      (BasicAuthRequestHandler, AuthHTTPServer): New classes. Implement
      basic authentication on HTTP server.
    ------------------------------------------------------------
    revno: 2363.4.7
    merged: v.ladeuil+lp at free.fr-20070412160004-zrffqcemqyjb8gvq
    parent: v.ladeuil+lp at free.fr-20070412142800-6e1pc8aksxlp7pg4
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Thu 2007-04-12 18:00:04 +0200
    message:
      Deeper tests, prepare the auth setting that will avoid the
      roundtrip with each 401.
      
      * bzrlib/transport/http/_urllib2_wrappers.py:
      (Request.__init__): Create the auth field.
      (ConnectionHandler.get_key): Deleted.
      (HTTPBasicAuthHandler.http_request): Add the authentication
      parameter if the request requires it.
      
      * bzrlib/tests/test_http.py:
      (TestHTTPBasicAuth.setUp): Create the transport to access the
      underlying _urllib2_wrappers opener.
      (TestHTTPBasicAuth.process_request): Leave the opener process the
      request based on the request.auth field.
    ------------------------------------------------------------
    revno: 2363.4.6
    merged: v.ladeuil+lp at free.fr-20070412142800-6e1pc8aksxlp7pg4
    parent: v.ladeuil+lp at free.fr-20070412130953-pwgok8w72gxazlel
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Thu 2007-04-12 16:28:00 +0200
    message:
      Fix tests around stdin emptyness.
      
      * bzrlib/tests/test_ui.py:
      Use readline not getline and test '' not None.
    ------------------------------------------------------------
    revno: 2363.4.5
    merged: v.ladeuil+lp at free.fr-20070412130953-pwgok8w72gxazlel
    parent: v.ladeuil+lp at free.fr-20070411142442-a8eilux70zs8ddg6
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Thu 2007-04-12 15:09:53 +0200
    message:
      Add white box tests for basic HTTP auth.
      
      * bzrlib/transport/http/_urllib2_wrappers.py:
      (HTTPBasicAuthHandler.get_auth, HTTPBasicAuthHandler.set_auth):
      New methods.
      
      * bzrlib/tests/test_http.py:
      (TestHttpProxyWhiteBox._proxied_request): Get rid of local imports.
      (TestHTTPBasicAuth): New class. Tests basic HTTP auth.
    ------------------------------------------------------------
    revno: 2363.4.4
    merged: v.ladeuil+lp at free.fr-20070411142442-a8eilux70zs8ddg6
    parent: v.ladeuil+lp at free.fr-20070411141729-cdaswhptaqmz4jrw
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Wed 2007-04-11 16:24:42 +0200
    message:
      More tidying-up.
      
      * bzrlib/tests/test_ui.py:
      Fix some spaceing issues and check more stdin emptiness.
    ------------------------------------------------------------
    revno: 2363.4.3
    merged: v.ladeuil+lp at free.fr-20070411141729-cdaswhptaqmz4jrw
    parent: v.ladeuil+lp at free.fr-20070410212235-vlcb8g9h421qpnwf
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Wed 2007-04-11 16:17:29 +0200
    message:
      Tidy-up tests.
      
      * bzrlib/tests/test_ui.py:
      Assert that stdin is empty after passwords have been queried.
    ------------------------------------------------------------
    revno: 2363.4.2
    merged: v.ladeuil+lp at free.fr-20070410212235-vlcb8g9h421qpnwf
    parent: v.ladeuil+lp at free.fr-20070403130039-5yuvjob1hwqjkvsd
    parent: pqm at pqm.ubuntu.com-20070410074302-cf6b95587a1058cd
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Tue 2007-04-10 23:22:35 +0200
    message:
      Merge bzr.dev
    ------------------------------------------------------------
    revno: 2363.4.1
    merged: v.ladeuil+lp at free.fr-20070403130039-5yuvjob1hwqjkvsd
    parent: pqm at pqm.ubuntu.com-20070317015305-7b7562331da9f786
    committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
    branch nick: 72792
    timestamp: Tue 2007-04-03 15:00:39 +0200
    message:
      Partial fix for bug #72292.
      
      * bzrlib/transport/http/_urllib.py: 
      (HttpTransport_urllib.ask_password): If a password is already
      supplied, pass it to the password manager.
-------------- next part --------------
=== modified file 'NEWS'
--- a/NEWS	2007-04-13 15:37:58 +0000
+++ b/NEWS	2007-04-13 16:38:58 +0000
@@ -41,6 +41,9 @@
       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).
+
     * Renamed SmartTransport (and subclasses like SmartTCPTransport) to
       RemoteTransport (and subclasses to RemoteTCPTransport, etc).  This is more
       consistent with its new home in bzrlib/transport/remote.py, and because

=== modified file 'bzrlib/tests/HTTPTestUtil.py'
--- a/bzrlib/tests/HTTPTestUtil.py	2007-04-09 04:49:55 +0000
+++ b/bzrlib/tests/HTTPTestUtil.py	2007-04-15 15:57:08 +0000
@@ -309,3 +309,61 @@
        self.old_server = self.get_secondary_server()
 
 
+class BasicAuthRequestHandler(TestingHTTPRequestHandler):
+    """Requires a basic authentication to process requests.
+
+    This is intended to be used with a server that always and
+    only use basic authentication.
+    """
+
+    def do_GET(self):
+        tcs = self.server.test_case_server
+        if tcs.auth_scheme == 'basic':
+            auth_header = self.headers.get('Authorization')
+            authorized = False
+            if auth_header and auth_header.lower().startswith('basic '):
+                coded_auth = auth_header[len('Basic '):]
+                user, password = coded_auth.decode('base64').split(':')
+                authorized = tcs.authorized(user, password)
+            if not authorized:
+                self.send_response(401)
+                self.send_header('www-authenticate',
+                                 'Basic realm="Thou should not pass"')
+                self.end_headers()
+                return
+
+        TestingHTTPRequestHandler.do_GET(self)
+
+
+class AuthHTTPServer(HttpServer):
+    """AuthHTTPServer extends HttpServer with a dictionary of passwords.
+
+    This is used as a base class for various schemes.
+
+    Note that no users are defined by default, so add_user should
+    be called before issuing the first request.
+    """
+
+    def __init__(self, request_handler, auth_scheme):
+        HttpServer.__init__(self, request_handler)
+        self.auth_scheme = auth_scheme
+        self.password_of = {}
+
+    def add_user(self, user, password):
+        """Declare a user with an associated password.
+
+        password can be empty, use an empty string ('') in that
+        case, not None.
+        """
+        self.password_of[user] = password
+
+    def authorized(self, user, password):
+        expected_password = self.password_of.get(user, None)
+        return expected_password is not None and password == expected_password
+
+
+class BasicAuthHTTPServer(AuthHTTPServer):
+    """An HTTP server requiring basic authentication"""
+
+    def __init__(self):
+        AuthHTTPServer.__init__(self, BasicAuthRequestHandler, 'basic')

=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py	2007-03-14 16:39:24 +0000
+++ b/bzrlib/tests/test_http.py	2007-04-15 15:57:08 +0000
@@ -20,6 +20,7 @@
 # TODO: Should be renamed to bzrlib.transport.http.tests?
 # TODO: What about renaming to bzrlib.tests.transport.http ?
 
+from cStringIO import StringIO
 import os
 import select
 import socket
@@ -29,11 +30,14 @@
 from bzrlib import (
     errors,
     osutils,
+    ui,
     urlutils,
     )
 from bzrlib.tests import (
     TestCase,
+    TestUIFactory,
     TestSkipped,
+    StringIOWrapper,
     )
 from bzrlib.tests.HttpServer import (
     HttpServer,
@@ -43,6 +47,7 @@
 from bzrlib.tests.HTTPTestUtil import (
     BadProtocolRequestHandler,
     BadStatusRequestHandler,
+    BasicAuthHTTPServer,
     FakeProxyRequestHandler,
     ForbiddenRequestHandler,
     HTTPServerRedirecting,
@@ -725,13 +730,8 @@
             osutils.set_or_unset_env(name, value)
 
     def _proxied_request(self):
-        from bzrlib.transport.http._urllib2_wrappers import (
-            ProxyHandler,
-            Request,
-            )
-
-        handler = ProxyHandler()
-        request = Request('GET','http://baz/buzzle')
+        handler = _urllib2_wrappers.ProxyHandler()
+        request = _urllib2_wrappers.Request('GET','http://baz/buzzle')
         handler.set_proxy(request, 'http')
         return request
 
@@ -1145,3 +1145,70 @@
 
         self.assertRaises(errors.TooManyRedirections, do_catching_redirections,
                           self.get_a, self.old_transport, redirected)
+
+
+class TestHTTPBasicAuth(TestCaseWithWebserver):
+    """Test basic authentication scheme"""
+
+    _transport = HttpTransport_urllib
+    _auth_header = 'Authorization'
+
+    def create_transport_readonly_server(self):
+        return BasicAuthHTTPServer()
+
+    def setUp(self):
+        super(TestHTTPBasicAuth, self).setUp()
+        self.build_tree_contents([('a', 'contents of a\n'),
+                                  ('b', 'contents of b\n'),])
+        self.server = self.get_readonly_server()
+
+        self.old_factory = ui.ui_factory
+        self.addCleanup(self.restoreUIFactory)
+
+    def restoreUIFactory(self):
+        ui.ui_factory = self.old_factory
+
+    def get_user_url(self, user=None, password=None):
+        """Build an url embedding user and password"""
+        url = '%s://' % self.server._url_protocol
+        if user is not None:
+            url += user
+            if password is not None:
+                url += ':' + password
+            url += '@'
+        url += '%s:%s/' % (self.server.host, self.server.port)
+        return url
+
+    def test_empty_pass(self):
+        self.server.add_user('joe', '')
+        t = self._transport(self.get_user_url('joe', ''))
+        self.assertEqual('contents of a\n', t.get('a').read())
+
+    def test_user_pass(self):
+        self.server.add_user('joe', 'foo')
+        t = self._transport(self.get_user_url('joe', 'foo'))
+        self.assertEqual('contents of a\n', t.get('a').read())
+
+    def test_unknown_user(self):
+        self.server.add_user('joe', 'foo')
+        t = self._transport(self.get_user_url('bill', 'foo'))
+        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
+
+    def test_wrong_pass(self):
+        self.server.add_user('joe', 'foo')
+        t = self._transport(self.get_user_url('joe', 'bar'))
+        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
+
+    def test_prompt_for_password(self):
+        self.server.add_user('joe', 'foo')
+        t = self._transport(self.get_user_url('joe'))
+        ui.ui_factory = TestUIFactory(stdin='foo\n', stdout=StringIOWrapper())
+        self.assertEqual('contents of a\n',t.get('a').read())
+        # stdin should be empty
+        self.assertEqual('', ui.ui_factory.stdin.readline())
+        # And we shouldn't prompt again for a different request
+        # against the same transport.
+        self.assertEqual('contents of b\n',t.get('b').read())
+        t2 = t.clone()
+        # And neither against a clone
+        self.assertEqual('contents of b\n',t2.get('b').read())

=== modified file 'bzrlib/tests/test_ui.py'
--- a/bzrlib/tests/test_ui.py	2007-02-21 14:46:06 +0000
+++ b/bzrlib/tests/test_ui.py	2007-04-13 16:16:54 +0000
@@ -24,14 +24,21 @@
 
 import bzrlib
 import bzrlib.errors as errors
-from bzrlib.progress import DotsProgressBar, TTYProgressBar, ProgressBarStack
+from bzrlib.progress import (
+    DotsProgressBar,
+    ProgressBarStack,
+    TTYProgressBar,
+    )
 from bzrlib.tests import (
+    TestCase,
     TestUIFactory,
     StringIOWrapper,
-    TestCase,
     )
 from bzrlib.tests.test_progress import _TTYStringIO
-from bzrlib.ui import SilentUIFactory
+from bzrlib.ui import (
+    CLIUIFactory,
+    SilentUIFactory,
+    )
 from bzrlib.ui.text import TextUIFactory
 
 
@@ -61,6 +68,8 @@
                                                    ui.get_password))
             # ': ' is appended to prompt
             self.assertEqual(': ', ui.stdout.getvalue())
+            # stdin should be empty
+            self.assertEqual('', ui.stdin.readline())
         finally:
             pb.finished()
 
@@ -84,6 +93,8 @@
             self.assertEqual(u'baz\u1234', password.decode('utf8'))
             self.assertEqual(u'Hello \u1234 some\u1234: ',
                              ui.stdout.getvalue().decode('utf8'))
+            # stdin should be empty
+            self.assertEqual('', ui.stdin.readline())
         finally:
             pb.finished()
 
@@ -99,7 +110,8 @@
             self.assertEqual(None, result)
             self.assertEqual("t\n", stdout.getvalue())
             # Since there was no update() call, there should be no clear() call
-            self.failIf(re.search(r'^\r {10,}\r$', stderr.getvalue()) is not None,
+            self.failIf(re.search(r'^\r {10,}\r$',
+                                  stderr.getvalue()) is not None,
                         'We cleared the stderr without anything to put there')
         finally:
             pb.finished()
@@ -161,18 +173,19 @@
 
     def test_text_factory_setting_progress_bar(self):
         # we should be able to choose the progress bar type used.
-        factory = bzrlib.ui.text.TextUIFactory(
-            bar_type=DotsProgressBar)
+        factory = TextUIFactory(bar_type=DotsProgressBar)
         bar = factory.nested_progress_bar()
         bar.finished()
         self.assertIsInstance(bar, DotsProgressBar)
 
     def test_cli_stdin_is_default_stdin(self):
-        factory = bzrlib.ui.CLIUIFactory()
+        factory = CLIUIFactory()
         self.assertEqual(sys.stdin, factory.stdin)
 
     def assert_get_bool_acceptance_of_user_input(self, factory):
-        factory.stdin = StringIO("y\nyes with garbage\nyes\nn\nnot an answer\nno\nfoo\n")
+        factory.stdin = StringIO("y\nyes with garbage\n"
+                                 "yes\nn\nnot an answer\n"
+                                 "no\nfoo\n")
         factory.stdout = StringIO()
         # there is no output from the base factory
         self.assertEqual(True, factory.get_boolean(""))
@@ -180,42 +193,47 @@
         self.assertEqual(False, factory.get_boolean(""))
         self.assertEqual(False, factory.get_boolean(""))
         self.assertEqual("foo\n", factory.stdin.read())
+        # stdin should be empty
+        self.assertEqual('', factory.stdin.readline())
 
     def test_silent_ui_getbool(self):
-        factory = bzrlib.ui.SilentUIFactory()
+        factory = SilentUIFactory()
         self.assert_get_bool_acceptance_of_user_input(factory)
 
     def test_silent_factory_prompts_silently(self):
-        factory = bzrlib.ui.SilentUIFactory()
+        factory = SilentUIFactory()
         stdout = StringIO()
         factory.stdin = StringIO("y\n")
-        self.assertEqual(
-            True,
-            self.apply_redirected(
-                None, stdout, stdout, factory.get_boolean, "foo")
-            )
+        self.assertEqual(True,
+                         self.apply_redirected(None, stdout, stdout,
+                                               factory.get_boolean, "foo"))
         self.assertEqual("", stdout.getvalue())
-        
+        # stdin should be empty
+        self.assertEqual('', factory.stdin.readline())
+
     def test_text_ui_getbool(self):
-        factory = bzrlib.ui.text.TextUIFactory()
+        factory = TextUIFactory()
         self.assert_get_bool_acceptance_of_user_input(factory)
 
     def test_text_factory_prompts_and_clears(self):
         # a get_boolean call should clear the pb before prompting
-        factory = bzrlib.ui.text.TextUIFactory(bar_type=DotsProgressBar)
+        factory = TextUIFactory(bar_type=DotsProgressBar)
         factory.stdout = _TTYStringIO()
         factory.stdin = StringIO("yada\ny\n")
-        pb = self.apply_redirected(
-            factory.stdin, factory.stdout, factory.stdout, factory.nested_progress_bar)
+        pb = self.apply_redirected(factory.stdin, factory.stdout,
+                                   factory.stdout, factory.nested_progress_bar)
         pb.start_time = None
-        self.apply_redirected(
-            factory.stdin, factory.stdout, factory.stdout, pb.update, "foo", 0, 1)
-        self.assertEqual(
-            True,
-            self.apply_redirected(
-                None, factory.stdout, factory.stdout, factory.get_boolean, "what do you want")
-            )
+        self.apply_redirected(factory.stdin, factory.stdout,
+                              factory.stdout, pb.update, "foo", 0, 1)
+        self.assertEqual(True,
+                         self.apply_redirected(None, factory.stdout,
+                                               factory.stdout,
+                                               factory.get_boolean,
+                                               "what do you want"))
         output = factory.stdout.getvalue()
         self.assertEqual("foo: .\n"
                          "what do you want? [y/n]: what do you want? [y/n]: ",
                          factory.stdout.getvalue())
+        # stdin should be empty
+        self.assertEqual('', factory.stdin.readline())
+

=== modified file 'bzrlib/transport/http/__init__.py'
--- a/bzrlib/transport/http/__init__.py	2007-04-13 07:59:17 +0000
+++ b/bzrlib/transport/http/__init__.py	2007-04-16 13:41:20 +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-02-12 14:02:01 +0000
+++ b/bzrlib/transport/http/_urllib.py	2007-04-15 15:57:08 +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,77 @@
 
     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_scheme = from_transport._auth_scheme
             self._user = from_transport._user
             self._password = from_transport._password
             self._opener = from_transport._opener
         else:
+            # urllib2 will be confused if it find authentication
+            # 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)
+            super(HttpTransport_urllib, self).__init__(clean_base,
+                                                       from_transport)
             self._connection = None
-            self._user = None
-            self._password = None
+            # auth_scheme will be set once we authenticate
+            # successfully after a 401 error.
+            self._auth_scheme = None
+            self._user = user
+            self._password = password
             self._opener = self._opener_class()
+            if user and 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"""
-        # TODO: jam 20060915 There should be a test that asserts we ask 
-        #       for a password at the right time.
-        if request.password is None:
+    def _ask_password(self):
+        """Ask for a password if none is already available"""
+        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
-            host = request.get_host()
-            password_manager = self._opener.password_manager
             # 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 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.
@@ -88,13 +127,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:
             # We will issue our first request, time to ask for a
             # password if needed
-            self.ask_password(request)
+            self._ask_password()
+        # Ensure authentication info is provided
+        request.set_auth(self._auth_scheme, self._user, self._password)
 
         mutter('%s: [%s]' % (request.method, request.get_full_url()))
         if self._debuglevel > 0:
@@ -106,6 +144,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_scheme = request.auth_scheme
             self._user = request.user
             self._password = request.password
 

=== modified file 'bzrlib/transport/http/_urllib2_wrappers.py'
--- a/bzrlib/transport/http/_urllib2_wrappers.py	2007-03-14 16:39:24 +0000
+++ b/bzrlib/transport/http/_urllib2_wrappers.py	2007-04-15 15:57:08 +0000
@@ -16,7 +16,7 @@
 
 """Implementaion of urllib2 tailored to bzr needs
 
-This file re-implements the urllib2 class hierarchy with custom classes.
+This file complements the urllib2 class hierarchy with custom classes.
 
 For instance, we create a new HTTPConnection and HTTPSConnection that inherit
 from the original urllib2.HTTP(s)Connection objects, but also have a new base
@@ -28,10 +28,11 @@
 We have a custom Response class, which lets us maintain a keep-alive
 connection even for requests that urllib2 doesn't expect to contain body data.
 
-And a custom Request class that lets us track redirections, and send
-authentication data without requiring an extra round trip to get rejected by
-the server. We also create a Request hierarchy, to make it clear what type
-of request is being made.
+And a custom Request class that lets us track redirections, and
+handle authentication schemes.
+
+We also create a Request hierarchy, to make it clear what type of
+request is being made.
 """
 
 DEBUG = 0
@@ -146,9 +147,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)
         urllib2.Request.__init__(self, url, data, headers,
                                  origin_req_host, unverifiable)
         self.method = method
@@ -158,36 +156,18 @@
         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_scheme = auth_scheme
+        self.user = user
+        self.password = password
 
     def get_method(self):
         return self.method
 
 
-# The urlib2.xxxAuthHandler handle the authentification of the
+# 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
 # typed by the user.
@@ -204,15 +184,11 @@
     internally used. But we need it in order to achieve
     connection sharing. So, we add it to the request just before
     it is processed, and then we override the do_open method for
-    http[s] requests.
+    http[s] requests in AbstractHTTPHandler.
     """
 
     handler_order = 1000 # after all pre-processings
 
-    def get_key(self, connection):
-        """Returns the key for the connection in the cache"""
-        return '%s:%d' % (connection.host, connection.port)
-
     def create_connection(self, request, http_connection_class):
         host = request.get_host()
         if not host:
@@ -282,7 +258,6 @@
                         # urllib2 using capitalize() for headers
                         # instead of title(sp?).
                         'User-agent': 'bzr/%s (urllib)' % bzrlib_version,
-                        # FIXME: pycurl also set the following, understand why
                         'Accept': '*/*',
                         }
 
@@ -718,17 +693,50 @@
 
 
 class HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
-    """Custom basic authentification handler.
+    """Custom basic authentication handler.
 
-    Send the authentification preventively to avoid the the
-    roundtrip associated with the 401 error.
+    Send the authentication preventively to avoid the roundtrip
+    associated with the 401 error.
     """
 
-#    def http_request(self, request):
-#        """Insert an authentification header if information is available"""
-#        if request.auth == 'basic' and request.password is not None:
-#            
-#        return request
+    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.password is not None:
+            request.add_header(self.auth_header,
+                               self.get_auth(request.user, request.password))
+
+    def http_request(self, request):
+        """Insert an authentication header if information is available"""
+        if request.auth_scheme == 'basic' and request.password is not None:
+            self.set_auth(request)
+        return request
+
+    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:
+            # We capture the auth_scheme to be able to send the
+            # authentication header with the next requests
+            # without waiting for a 401 error.
+            # The urllib2.HTTPBasicAuthHandler 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.auth_scheme = 'basic'
+
+        return response
 
 
 class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
@@ -737,7 +745,6 @@
     We don't really process the errors, quite the contrary
     instead, we leave our Transport handle them.
     """
-    handler_order = 1000  # after all other processing
 
     def http_response(self, request, response):
         code, msg, hdrs = response.code, response.msg, response.info()
@@ -770,7 +777,6 @@
             # of a better magic value.
             raise errors.InvalidRange(req.get_full_url(),0)
         else:
-            # TODO: A test is needed to exercise that code path
             raise errors.InvalidHttpResponse(req.get_full_url(),
                                              'Unable to handle http code %d: %s'
                                              % (code, msg))
@@ -792,7 +798,7 @@
         self._opener = urllib2.build_opener( \
             connection, redirect, error,
             ProxyHandler,
-            urllib2.HTTPBasicAuthHandler(self.password_manager),
+            HTTPBasicAuthHandler(self.password_manager),
             #urllib2.HTTPDigestAuthHandler(self.password_manager),
             #urllib2.ProxyBasicAuthHandler,
             #urllib2.ProxyDigestAuthHandler,
@@ -807,4 +813,3 @@
             # handler is used, when and for what.
             import pprint
             pprint.pprint(self._opener.__dict__)
-



More information about the bazaar-commits mailing list