~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/_urllib2_wrappers.py

  • Committer: Vincent Ladeuil
  • Date: 2007-10-22 21:19:13 UTC
  • mto: (2961.1.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 2962.
  • Revision ID: v.ladeuil+lp@free.fr-20071022211913-119445k3qco40sx0
Make hhtp proxy aware of AuthenticationConfig (for password).

* bzrlib/transport/http/_urllib2_wrappers.py:
(Request.__init__): Update the auth dict content documentation.
(extract_credentials, extract_authentication_uri): Deleted.
(PasswordManager): Deleted.
(ProxyHandler.set_proxy): Query AuthenticationConfig for the
password. Simplified.
(AbstractAuthHandler): Looong forgotten feedback review about
_retry_count. The behaviour is not changed.
(AbstractAuthHandler.get_password): Change signature now that the
auth dict contains all relevant information. Simplified.
(DigestAuthHandler.auth_match): Delete gratuitous differences with
BasicAuthHandler.
(Opener.__init__): Get rid of PasswordManager.

* bzrlib/transport/http/_urllib.py:
(HttpTransport_urllib._perform): Replace authuri by its components
in the auth dict.

* bzrlib/transport/ftp.py: 
Register 'aftp' for urlparse (revealed by the host empty test in
_split_url).

* bzrlib/transport/__init__.py:
(ConnectedTransport._split_url): Check that host is not empty. Was
first needed to make a proxy test pass, but revealed more bugs in
the test suite.

* bzrlib/tests/test_transport_implementations.py:
(TransportTests.test__reuse_for): Fix bug revealed by the host
empty test in _split_url.

* bzrlib/tests/test_transport.py:
(TestConnectedTransport.test_connection_sharing_propagate_credentials): 
Fix bug revealed by the host empty test in _split_url.

* bzrlib/tests/test_smart_transport.py:
(HTTPTunnellingSmokeTest._test_bulk_data): Fix bug revealed by the
host empty test in _split_url.

* bzrlib/tests/test_http.py:
(TestHttpTransportUrls.test_invalid_http_urls): ConnectionError
can't be raised here.
(TestHttpProxyWhiteBox._proxied_request): No more PasswordManager,
yeah !
(TestProxyHttpServer): Delete FIXME, tests have been added a long
time ago.

* bzrlib/config.py:
(AuthenticationConfig.get_user): Fix silly copy/paste.
(AuthenticationConfig.get_password): Fix oversight.

Show diffs side-by-side

added added

removed removed

Lines of Context:
34
34
 
35
35
DEBUG = 0
36
36
 
37
 
# TODO: It may be possible to share the password_manager across
38
 
# all transports by prefixing the realm by the protocol used
39
 
# (especially if other protocols do not use realms). See
40
 
# PasswordManager below.
41
 
 
42
37
# FIXME: Oversimplifying, two kind of exceptions should be
43
38
# raised, once a request is issued: URLError before we have been
44
39
# able to process the response, HTTPError after that. Process the
62
57
from bzrlib import (
63
58
    config,
64
59
    errors,
 
60
    transport,
65
61
    ui,
66
62
    )
67
63
 
68
64
 
69
 
 
70
65
# We define our own Response class to keep our httplib pipe clean
71
66
class Response(httplib.HTTPResponse):
72
67
    """Custom HTTPResponse, to avoid the need to decorate.
194
189
        # Unless told otherwise, redirections are not followed
195
190
        self.follow_redirections = False
196
191
        # auth and proxy_auth are dicts containing, at least
197
 
        # (scheme, url, realm, user, password).
 
192
        # (scheme, host, port, realm, user, password, protocol, path).
198
193
        # The dict entries are mostly handled by the AuthHandler.
199
194
        # Some authentication schemes may add more entries.
200
195
        self.auth = {}
241
236
        urllib2.Request.set_proxy(self, proxy, type)
242
237
 
243
238
 
244
 
def extract_credentials(url):
245
 
    """Extracts credentials information from url.
246
 
 
247
 
    Get user and password from url of the form: http://user:pass@host/path
248
 
    :returns: (clean_url, user, password)
249
 
    """
250
 
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
251
 
 
252
 
    if '@' in netloc:
253
 
        auth, netloc = netloc.split('@', 1)
254
 
        if ':' in auth:
255
 
            user, password = auth.split(':', 1)
256
 
        else:
257
 
            user, password = auth, None
258
 
        user = urllib.unquote(user)
259
 
        if password is not None:
260
 
            password = urllib.unquote(password)
261
 
    else:
262
 
        user = None
263
 
        password = None
264
 
 
265
 
    # Build the clean url
266
 
    clean_url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
267
 
 
268
 
    return clean_url, user, password
269
 
 
270
 
def extract_authentication_uri(url):
271
 
    """Extract the authentication uri from any url.
272
 
 
273
 
    In the context of bzr, we simplify the authentication uri
274
 
    to the host only. For the transport lifetime, we allow only
275
 
    one user by realm on a given host. I.e. handling several
276
 
    users for different paths for the same realm should be done
277
 
    at a higher level.
278
 
    """
279
 
    scheme, host, path, query, fragment = urlparse.urlsplit(url)
280
 
    return '%s://%s' % (scheme, host)
281
 
 
282
 
 
283
 
# The AuthHandler classes handle the authentication of the requests, to do
284
 
# that, they need a PasswordManager *at build time*. We also need one to reuse
285
 
# the passwords entered by the user.
286
 
class PasswordManager(urllib2.HTTPPasswordMgrWithDefaultRealm):
287
 
 
288
 
    def __init__(self):
289
 
        urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
290
 
 
291
 
 
292
239
class ConnectionHandler(urllib2.BaseHandler):
293
240
    """Provides connection-sharing by pre-processing requests.
294
241
 
738
685
    handler_order = 100
739
686
    _debuglevel = DEBUG
740
687
 
741
 
    def __init__(self, password_manager, proxies=None):
 
688
    def __init__(self, proxies=None):
742
689
        urllib2.ProxyHandler.__init__(self, proxies)
743
 
        self.password_manager = password_manager
744
690
        # First, let's get rid of urllib2 implementation
745
691
        for type, proxy in self.proxies.items():
746
692
            if self._debuglevel > 0:
783
729
 
784
730
    def proxy_bypass(self, host):
785
731
        """Check if host should be proxied or not"""
786
 
        no_proxy = self.get_proxy_env_var('no', None)
 
732
        no_proxy = self.get_proxy_env_var('no', default_to=None)
787
733
        if no_proxy is None:
788
734
            return False
789
735
        hhost, hport = urllib.splitport(host)
815
761
        # grok user:password@host:port as well as
816
762
        # http://user:password@host:port
817
763
 
818
 
        # FIXME: query AuthenticationConfig too
 
764
        (scheme, user, password,
 
765
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
819
766
 
820
 
        # Extract credentials from the url and store them in the
821
 
        # password manager so that the proxy AuthHandler can use
822
 
        # them later.
823
 
        proxy, user, password = extract_credentials(proxy)
824
767
        if request.proxy_auth == {}:
825
 
            # No proxy auth parameter are available, we are
826
 
            # handling the first proxied request, intialize.
827
 
            # scheme and realm will be set by the AuthHandler
828
 
            authuri = extract_authentication_uri(proxy)
829
 
            request.proxy_auth = {'user': user, 'password': password,
830
 
                                  'authuri': authuri}
831
 
            if user and password is not None: # '' is a valid password
832
 
                # We default to a realm of None to catch them all.
833
 
                self.password_manager.add_password(None, authuri,
834
 
                                                   user, password)
835
 
        orig_type = request.get_type()
836
 
        scheme, r_scheme = urllib.splittype(proxy)
837
 
        if self._debuglevel > 0:
838
 
            print 'scheme: %s, r_scheme: %s' % (scheme, r_scheme)
839
 
        host, XXX = urllib.splithost(r_scheme)
840
 
        if host is None:
841
 
            raise errors.InvalidURL(proxy,
842
 
                                    'Invalid syntax in proxy env variable')
843
 
        host = urllib.unquote(host)
844
 
        request.set_proxy(host, type)
845
 
        if self._debuglevel > 0:
846
 
            print 'set_proxy: proxy set to %s://%s' % (type, host)
 
768
            # No proxy auth parameter are available, we are handling the first
 
769
            # proxied request, intialize.  scheme (the authentication scheme)
 
770
            # and realm will be set by the AuthHandler
 
771
            request.proxy_auth = {
 
772
                                  'host': host, 'port': port,
 
773
                                  'user': user, 'password': password,
 
774
                                  'protocol': scheme,
 
775
                                   # We ignore path since we connect to a proxy
 
776
                                  'path': None}
 
777
        if port is None:
 
778
            phost = host
 
779
        else:
 
780
            phost = host + ':%d' % port
 
781
        request.set_proxy(phost, type)
 
782
        if self._debuglevel > 0:
 
783
            print 'set_proxy: proxy set to %s://%s' % (type, phost)
847
784
        return request
848
785
 
849
786
 
885
822
      successful and the request authentication parameters have been updated.
886
823
    """
887
824
 
888
 
    _max_retry = 2
 
825
    _max_retry = 3
889
826
    """We don't want to retry authenticating endlessly"""
890
827
 
891
828
    # The following attributes should be defined by daughter
893
830
    # - auth_required_header:  the header received from the server
894
831
    # - auth_header: the header sent in the request
895
832
 
896
 
    def __init__(self, password_manager):
897
 
        self.password_manager = password_manager
898
 
        self.find_user_password = password_manager.find_user_password
899
 
        self.add_password = password_manager.add_password
 
833
    def __init__(self):
 
834
        # We want to know when we enter into an try/fail cycle of
 
835
        # authentications so we initialize to None to indicate that we aren't
 
836
        # in such a cycle by default.
900
837
        self._retry_count = None
901
838
 
902
839
    def update_auth(self, auth, key, value):
915
852
        """
916
853
        # Don't try  to authenticate endlessly
917
854
        if self._retry_count is None:
918
 
            # The retry being recusrsive calls, None identify the first try
919
 
            self._retry_count = 0
 
855
            # The retry being recusrsive calls, None identify the first retry
 
856
            self._retry_count = 1
920
857
        else:
921
858
            self._retry_count += 1
922
859
            if self._retry_count > self._max_retry:
938
875
        if self.auth_match(server_header, auth):
939
876
            # auth_match may have modified auth (by adding the
940
877
            # password or changing the realm, for example)
941
 
            if request.get_header(self.auth_header, None) is not None \
942
 
                    and not auth['modified']:
 
878
            if (request.get_header(self.auth_header, None) is not None
 
879
                and not auth['modified']):
943
880
                # We already tried that, give up
944
881
                return None
945
882
 
1001
938
        # It may happen that we need to reconnect later, let's be ready
1002
939
        self._retry_count = None
1003
940
 
1004
 
    def get_password(self, user, authuri, realm=None):
 
941
    # FIXME: Rename get_credentials or something and query the auth config for
 
942
    # user too.
 
943
    def get_password(self, auth):
1005
944
        """Ask user for a password if none is already available."""
1006
 
        user_found, password = self.find_user_password(realm, authuri)
1007
 
        if user_found != user:
1008
 
            # FIXME: write a test for that case
1009
 
            password = None
 
945
        user = auth['user']
 
946
        realm = auth['realm']
 
947
        password = auth['password']
1010
948
 
1011
949
        if password is None:
1012
 
            scheme, netloc, path, query, fragment = urlparse.urlsplit(authuri)
1013
 
            if ':' in netloc:
1014
 
                host, port = netloc.rsplit(':', 1)
1015
 
                # port is an int here invalid values have been handled by the
1016
 
                # upper layers
1017
 
                port = int(port)
1018
 
            else:
1019
 
                host = netloc
1020
 
                port = None
1021
 
            auth = config.AuthenticationConfig()
1022
 
            password = auth.get_password(
1023
 
                scheme, host, user, port=port, path=path, realm=realm,
 
950
            auth_conf = config.AuthenticationConfig()
 
951
            password = auth_conf.get_password(
 
952
                auth['protocol'], auth['host'], user, port=auth['port'],
 
953
                path=auth['path'], realm=realm,
1024
954
                prompt=self.password_prompt)
1025
 
            if password is not None:
1026
 
                self.add_password(realm, authuri, user, password)
1027
955
        return password
1028
956
 
1029
957
    def http_request(self, request):
1064
992
            self.update_auth(auth, 'scheme', scheme)
1065
993
            self.update_auth(auth, 'realm', realm)
1066
994
            if auth.get('password',None) is None:
1067
 
                password = self.get_password(auth['user'], auth['authuri'],
1068
 
                                             auth['realm'])
 
995
                password = self.get_password(auth)
1069
996
                self.update_auth(auth, 'password', password)
1070
997
        return match is not None
1071
998
 
1125
1052
            return False
1126
1053
 
1127
1054
        realm = req_auth.get('realm', None)
 
1055
        # Put useful info into auth
 
1056
        self.update_auth(auth, 'scheme', scheme)
 
1057
        self.update_auth(auth, 'realm', realm)
1128
1058
        if auth.get('password',None) is None:
1129
 
            auth['password'] = self.get_password(auth['user'],
1130
 
                                                 auth['authuri'],
1131
 
                                                 realm)
1132
 
        # Put useful info into auth
 
1059
            password = self.get_password(auth)
 
1060
            self.update_auth(auth, 'password', password)
 
1061
 
1133
1062
        try:
1134
 
            self.update_auth(auth, 'scheme', scheme)
1135
1063
            if req_auth.get('algorithm', None) is not None:
1136
1064
                self.update_auth(auth, 'algorithm', req_auth.get('algorithm'))
1137
 
            self.update_auth(auth, 'realm', realm)
1138
1065
            nonce = req_auth['nonce']
1139
1066
            if auth.get('nonce', None) != nonce:
1140
1067
                # A new nonce, never used
1316
1243
                 connection=ConnectionHandler,
1317
1244
                 redirect=HTTPRedirectHandler,
1318
1245
                 error=HTTPErrorProcessor,):
1319
 
        self.password_manager = PasswordManager()
1320
1246
        self._opener = urllib2.build_opener( \
1321
1247
            connection, redirect, error,
1322
 
            ProxyHandler(self.password_manager),
1323
 
            HTTPBasicAuthHandler(self.password_manager),
1324
 
            HTTPDigestAuthHandler(self.password_manager),
1325
 
            ProxyBasicAuthHandler(self.password_manager),
1326
 
            ProxyDigestAuthHandler(self.password_manager),
 
1248
            ProxyHandler(),
 
1249
            HTTPBasicAuthHandler(),
 
1250
            HTTPDigestAuthHandler(),
 
1251
            ProxyBasicAuthHandler(),
 
1252
            ProxyDigestAuthHandler(),
1327
1253
            HTTPHandler,
1328
1254
            HTTPSHandler,
1329
1255
            HTTPDefaultErrorHandler,