~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-11-04 18:51:39 UTC
  • mfrom: (2961.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20071104185139-kaio3sneodg2kp71
Authentication ring implementation (read-only)

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
60
55
 
61
56
from bzrlib import __version__ as bzrlib_version
62
57
from bzrlib import (
 
58
    config,
63
59
    errors,
 
60
    transport,
64
61
    ui,
65
62
    )
66
63
 
67
64
 
68
 
 
69
65
# We define our own Response class to keep our httplib pipe clean
70
66
class Response(httplib.HTTPResponse):
71
67
    """Custom HTTPResponse, to avoid the need to decorate.
193
189
        # Unless told otherwise, redirections are not followed
194
190
        self.follow_redirections = False
195
191
        # auth and proxy_auth are dicts containing, at least
196
 
        # (scheme, url, realm, user, password).
 
192
        # (scheme, host, port, realm, user, password, protocol, path).
197
193
        # The dict entries are mostly handled by the AuthHandler.
198
194
        # Some authentication schemes may add more entries.
199
195
        self.auth = {}
240
236
        urllib2.Request.set_proxy(self, proxy, type)
241
237
 
242
238
 
243
 
def extract_credentials(url):
244
 
    """Extracts credentials information from url.
245
 
 
246
 
    Get user and password from url of the form: http://user:pass@host/path
247
 
    :returns: (clean_url, user, password)
248
 
    """
249
 
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
250
 
 
251
 
    if '@' in netloc:
252
 
        auth, netloc = netloc.split('@', 1)
253
 
        if ':' in auth:
254
 
            user, password = auth.split(':', 1)
255
 
        else:
256
 
            user, password = auth, None
257
 
        user = urllib.unquote(user)
258
 
        if password is not None:
259
 
            password = urllib.unquote(password)
260
 
    else:
261
 
        user = None
262
 
        password = None
263
 
 
264
 
    # Build the clean url
265
 
    clean_url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
266
 
 
267
 
    return clean_url, user, password
268
 
 
269
 
def extract_authentication_uri(url):
270
 
    """Extract the authentication uri from any url.
271
 
 
272
 
    In the context of bzr, we simplified the authentication uri
273
 
    to the host only. For the transport lifetime, we allow only
274
 
    one user by realm on a given host. I.e. handling several
275
 
    users for different paths for the same realm should be done
276
 
    at a higher level.
277
 
    """
278
 
    scheme, host, path, query, fragment = urlparse.urlsplit(url)
279
 
    return '%s://%s' % (scheme, host)
280
 
 
281
 
 
282
 
# The AuthHandler classes handle the authentication of the requests, to do
283
 
# that, they need a PasswordManager *at build time*. We also need one to reuse
284
 
# the passwords entered by the user.
285
 
class PasswordManager(urllib2.HTTPPasswordMgrWithDefaultRealm):
286
 
 
287
 
    def __init__(self):
288
 
        urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
289
 
 
290
 
 
291
239
class ConnectionHandler(urllib2.BaseHandler):
292
240
    """Provides connection-sharing by pre-processing requests.
293
241
 
737
685
    handler_order = 100
738
686
    _debuglevel = DEBUG
739
687
 
740
 
    def __init__(self, password_manager, proxies=None):
 
688
    def __init__(self, proxies=None):
741
689
        urllib2.ProxyHandler.__init__(self, proxies)
742
 
        self.password_manager = password_manager
743
690
        # First, let's get rid of urllib2 implementation
744
691
        for type, proxy in self.proxies.items():
745
692
            if self._debuglevel > 0:
782
729
 
783
730
    def proxy_bypass(self, host):
784
731
        """Check if host should be proxied or not"""
785
 
        no_proxy = self.get_proxy_env_var('no', None)
 
732
        no_proxy = self.get_proxy_env_var('no', default_to=None)
786
733
        if no_proxy is None:
787
734
            return False
788
735
        hhost, hport = urllib.splitport(host)
814
761
        # grok user:password@host:port as well as
815
762
        # http://user:password@host:port
816
763
 
817
 
        # Extract credentials from the url and store them in the
818
 
        # password manager so that the proxy AuthHandler can use
819
 
        # them later.
820
 
        proxy, user, password = extract_credentials(proxy)
 
764
        (scheme, user, password,
 
765
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
 
766
 
821
767
        if request.proxy_auth == {}:
822
 
            # No proxy auth parameter are available, we are
823
 
            # handling the first proxied request, intialize.
824
 
            # scheme and realm will be set by the AuthHandler
825
 
            authuri = extract_authentication_uri(proxy)
826
 
            request.proxy_auth = {'user': user, 'password': password,
827
 
                                  'authuri': authuri}
828
 
            if user and password is not None: # '' is a valid password
829
 
                # We default to a realm of None to catch them all.
830
 
                self.password_manager.add_password(None, authuri,
831
 
                                                   user, password)
832
 
        orig_type = request.get_type()
833
 
        scheme, r_scheme = urllib.splittype(proxy)
834
 
        if self._debuglevel > 0:
835
 
            print 'scheme: %s, r_scheme: %s' % (scheme, r_scheme)
836
 
        host, XXX = urllib.splithost(r_scheme)
837
 
        if host is None:
838
 
            raise errors.InvalidURL(proxy,
839
 
                                    'Invalid syntax in proxy env variable')
840
 
        host = urllib.unquote(host)
841
 
        request.set_proxy(host, type)
842
 
        if self._debuglevel > 0:
843
 
            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)
844
784
        return request
845
785
 
846
786
 
882
822
      successful and the request authentication parameters have been updated.
883
823
    """
884
824
 
885
 
    _max_retry = 2
 
825
    _max_retry = 3
886
826
    """We don't want to retry authenticating endlessly"""
887
827
 
888
828
    # The following attributes should be defined by daughter
890
830
    # - auth_required_header:  the header received from the server
891
831
    # - auth_header: the header sent in the request
892
832
 
893
 
    def __init__(self, password_manager):
894
 
        self.password_manager = password_manager
895
 
        self.find_user_password = password_manager.find_user_password
896
 
        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.
897
837
        self._retry_count = None
898
838
 
899
839
    def update_auth(self, auth, key, value):
912
852
        """
913
853
        # Don't try  to authenticate endlessly
914
854
        if self._retry_count is None:
915
 
            # The retry being recusrsive calls, None identify the first try
916
 
            self._retry_count = 0
 
855
            # The retry being recusrsive calls, None identify the first retry
 
856
            self._retry_count = 1
917
857
        else:
918
858
            self._retry_count += 1
919
859
            if self._retry_count > self._max_retry:
935
875
        if self.auth_match(server_header, auth):
936
876
            # auth_match may have modified auth (by adding the
937
877
            # password or changing the realm, for example)
938
 
            if request.get_header(self.auth_header, None) is not None \
939
 
                    and not auth['modified']:
 
878
            if (request.get_header(self.auth_header, None) is not None
 
879
                and not auth['modified']):
940
880
                # We already tried that, give up
941
881
                return None
942
882
 
998
938
        # It may happen that we need to reconnect later, let's be ready
999
939
        self._retry_count = None
1000
940
 
1001
 
    def get_password(self, user, authuri, realm=None):
 
941
    def get_user_password(self, auth):
1002
942
        """Ask user for a password if none is already available."""
1003
 
        user_found, password = self.find_user_password(realm, authuri)
1004
 
        if user_found != user:
1005
 
            # FIXME: write a test for that case
1006
 
            password = None
 
943
        auth_conf = config.AuthenticationConfig()
 
944
        user = auth['user']
 
945
        password = auth['password']
 
946
        realm = auth['realm']
 
947
 
 
948
        if user is None:
 
949
            user = auth.get_user(auth['protocol'], auth['host'],
 
950
                                 port=auth['port'], path=auth['path'],
 
951
                                 realm=realm)
 
952
            if user is None:
 
953
                # Default to local user
 
954
                user = getpass.getuser()
1007
955
 
1008
956
        if password is None:
1009
 
            # Prompt user only if we can't find a password
1010
 
            if realm:
1011
 
                realm_prompt = " Realm: '%s'" % realm
1012
 
            else:
1013
 
                realm_prompt = ''
1014
 
            scheme, host, path, query, fragment = urlparse.urlsplit(authuri)
1015
 
            password = ui.ui_factory.get_password(prompt=self.password_prompt,
1016
 
                                                  user=user, host=host,
1017
 
                                                  realm=realm_prompt)
1018
 
            if password is not None:
1019
 
                self.add_password(realm, authuri, user, password)
1020
 
        return password
 
957
            password = auth_conf.get_password(
 
958
                auth['protocol'], auth['host'], user, port=auth['port'],
 
959
                path=auth['path'], realm=realm,
 
960
                prompt=self.build_password_prompt(auth))
 
961
 
 
962
        return user, password
 
963
 
 
964
    def _build_password_prompt(self, auth):
 
965
        """Build a prompt taking the protocol used into account.
 
966
 
 
967
        The AuthHandler is used by http and https, we want that information in
 
968
        the prompt, so we build the prompt from the authentication dict which
 
969
        contains all the needed parts.
 
970
 
 
971
        Also, hhtp and proxy AuthHandlers present different prompts to the
 
972
        user. The daughter classes hosuld implements a public
 
973
        build_password_prompt using this method.
 
974
        """
 
975
        prompt = '%s' % auth['protocol'].upper() + ' %(user)s@%(host)s'
 
976
        realm = auth['realm']
 
977
        if realm is not None:
 
978
            prompt += ", Realm: '%s'" % realm
 
979
        prompt += ' password'
 
980
        return prompt
1021
981
 
1022
982
    def http_request(self, request):
1023
983
        """Insert an authentication header if information is available"""
1056
1016
            # Put useful info into auth
1057
1017
            self.update_auth(auth, 'scheme', scheme)
1058
1018
            self.update_auth(auth, 'realm', realm)
1059
 
            if auth.get('password',None) is None:
1060
 
                password = self.get_password(auth['user'], auth['authuri'],
1061
 
                                             auth['realm'])
 
1019
            if auth['user'] is None or auth['password'] is None:
 
1020
                user, password = self.get_user_password(auth)
 
1021
                self.update_auth(auth, 'user', user)
1062
1022
                self.update_auth(auth, 'password', password)
1063
1023
        return match is not None
1064
1024
 
1118
1078
            return False
1119
1079
 
1120
1080
        realm = req_auth.get('realm', None)
1121
 
        if auth.get('password',None) is None:
1122
 
            auth['password'] = self.get_password(auth['user'],
1123
 
                                                 auth['authuri'],
1124
 
                                                 realm)
1125
1081
        # Put useful info into auth
 
1082
        self.update_auth(auth, 'scheme', scheme)
 
1083
        self.update_auth(auth, 'realm', realm)
 
1084
        if auth['user'] is None or auth['password'] is None:
 
1085
            user, password = self.get_user_password(auth)
 
1086
            self.update_auth(auth, 'user', user)
 
1087
            self.update_auth(auth, 'password', password)
 
1088
 
1126
1089
        try:
1127
 
            self.update_auth(auth, 'scheme', scheme)
1128
1090
            if req_auth.get('algorithm', None) is not None:
1129
1091
                self.update_auth(auth, 'algorithm', req_auth.get('algorithm'))
1130
 
            self.update_auth(auth, 'realm', realm)
1131
1092
            nonce = req_auth['nonce']
1132
1093
            if auth.get('nonce', None) != nonce:
1133
1094
                # A new nonce, never used
1188
1149
    the auth request attribute.
1189
1150
    """
1190
1151
 
1191
 
    password_prompt = 'HTTP %(user)s@%(host)s%(realm)s password'
1192
1152
    auth_required_header = 'www-authenticate'
1193
1153
    auth_header = 'Authorization'
1194
1154
 
1200
1160
        """Set the auth params for the request"""
1201
1161
        request.auth = auth
1202
1162
 
 
1163
    def build_password_prompt(self, auth):
 
1164
        return self._build_password_prompt(auth)
 
1165
 
1203
1166
    def http_error_401(self, req, fp, code, msg, headers):
1204
1167
        return self.auth_required(req, headers)
1205
1168
 
1212
1175
    the proxy_auth request attribute..
1213
1176
    """
1214
1177
 
1215
 
    password_prompt = 'Proxy %(user)s@%(host)s%(realm)s password'
1216
1178
    auth_required_header = 'proxy-authenticate'
1217
1179
    # FIXME: the correct capitalization is Proxy-Authorization,
1218
1180
    # but python-2.4 urllib2.Request insist on using capitalize()
1227
1189
        """Set the auth params for the request"""
1228
1190
        request.proxy_auth = auth
1229
1191
 
 
1192
    def build_password_prompt(self, auth):
 
1193
        prompt = self._build_password_prompt(auth)
 
1194
        prompt = 'Proxy ' + prompt
 
1195
        return prompt
 
1196
 
1230
1197
    def http_error_407(self, req, fp, code, msg, headers):
1231
1198
        return self.auth_required(req, headers)
1232
1199
 
1307
1274
                 connection=ConnectionHandler,
1308
1275
                 redirect=HTTPRedirectHandler,
1309
1276
                 error=HTTPErrorProcessor,):
1310
 
        self.password_manager = PasswordManager()
1311
1277
        self._opener = urllib2.build_opener( \
1312
1278
            connection, redirect, error,
1313
 
            ProxyHandler(self.password_manager),
1314
 
            HTTPBasicAuthHandler(self.password_manager),
1315
 
            HTTPDigestAuthHandler(self.password_manager),
1316
 
            ProxyBasicAuthHandler(self.password_manager),
1317
 
            ProxyDigestAuthHandler(self.password_manager),
 
1279
            ProxyHandler(),
 
1280
            HTTPBasicAuthHandler(),
 
1281
            HTTPDigestAuthHandler(),
 
1282
            ProxyBasicAuthHandler(),
 
1283
            ProxyDigestAuthHandler(),
1318
1284
            HTTPHandler,
1319
1285
            HTTPSHandler,
1320
1286
            HTTPDefaultErrorHandler,