~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Andrew Bennetts
  • Date: 2007-11-10 15:09:09 UTC
  • mfrom: (2916.2.17 streamable-containers)
  • mto: This revision was merged to the branch mainline in revision 3174.
  • Revision ID: andrew.bennetts@canonical.com-20071110150909-ik5254kgn930th10
Merge streamable-containers.

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.
113
109
            # close the connection. Some cases are not correctly detected by
114
110
            # httplib.HTTPConnection.getresponse (called by
115
111
            # httplib.HTTPResponse.begin). The CONNECT response for the https
116
 
            # through proxy case is one.
 
112
            # through proxy case is one.  Note: the 'will_close' below refers
 
113
            # to the "true" socket between us and the server, whereas the
 
114
            # 'close()' above refers to the copy of that socket created by
 
115
            # httplib for the response itself. So, in the if above we close the
 
116
            # socket to indicate that we are done with the response whereas
 
117
            # below we keep the socket with the server opened.
117
118
            self.will_close = False
118
119
 
119
120
 
193
194
        # Unless told otherwise, redirections are not followed
194
195
        self.follow_redirections = False
195
196
        # auth and proxy_auth are dicts containing, at least
196
 
        # (scheme, url, realm, user, password).
 
197
        # (scheme, host, port, realm, user, password, protocol, path).
197
198
        # The dict entries are mostly handled by the AuthHandler.
198
199
        # Some authentication schemes may add more entries.
199
200
        self.auth = {}
240
241
        urllib2.Request.set_proxy(self, proxy, type)
241
242
 
242
243
 
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
244
class ConnectionHandler(urllib2.BaseHandler):
292
245
    """Provides connection-sharing by pre-processing requests.
293
246
 
405
358
            raise errors.ConnectionError("Couldn't resolve host '%s'"
406
359
                                         % request.get_origin_req_host(),
407
360
                                         orig_error=exc_val)
 
361
        elif isinstance(exc_val, httplib.ImproperConnectionState):
 
362
            # The httplib pipeline is in incorrect state, it's a bug in our
 
363
            # implementation.
 
364
            raise exc_type, exc_val, exc_tb
408
365
        else:
409
366
            if first_try:
410
367
                if self._debuglevel > 0:
434
391
                else:
435
392
                    # All other exception are considered connection related.
436
393
 
437
 
                    # httplib.HTTPException should indicate a bug in our
438
 
                    # urllib-based implementation, somewhow the httplib
439
 
                    # pipeline is in an incorrect state, we retry in hope that
440
 
                    # this will correct the problem but that may need
441
 
                    # investigation (note that no such bug is known as of
442
 
                    # 20061005 --vila).
443
 
 
444
394
                    # socket errors generally occurs for reasons
445
395
                    # far outside our scope, so closing the
446
396
                    # connection and retrying is the best we can
447
397
                    # do.
448
398
 
449
 
                    # FIXME: and then there is HTTPError raised by:
450
 
                    # - HTTPDefaultErrorHandler (we define our own)
451
 
                    # - HTTPRedirectHandler.redirect_request 
452
 
 
453
399
                    my_exception = errors.ConnectionError(
454
400
                        msg= 'while sending %s %s:' % (request.get_method(),
455
401
                                                       request.get_selector()),
737
683
    handler_order = 100
738
684
    _debuglevel = DEBUG
739
685
 
740
 
    def __init__(self, password_manager, proxies=None):
 
686
    def __init__(self, proxies=None):
741
687
        urllib2.ProxyHandler.__init__(self, proxies)
742
 
        self.password_manager = password_manager
743
688
        # First, let's get rid of urllib2 implementation
744
689
        for type, proxy in self.proxies.items():
745
690
            if self._debuglevel > 0:
782
727
 
783
728
    def proxy_bypass(self, host):
784
729
        """Check if host should be proxied or not"""
785
 
        no_proxy = self.get_proxy_env_var('no', None)
 
730
        no_proxy = self.get_proxy_env_var('no', default_to=None)
786
731
        if no_proxy is None:
787
732
            return False
788
733
        hhost, hport = urllib.splitport(host)
814
759
        # grok user:password@host:port as well as
815
760
        # http://user:password@host:port
816
761
 
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)
 
762
        (scheme, user, password,
 
763
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
 
764
 
821
765
        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)
 
766
            # No proxy auth parameter are available, we are handling the first
 
767
            # proxied request, intialize.  scheme (the authentication scheme)
 
768
            # and realm will be set by the AuthHandler
 
769
            request.proxy_auth = {
 
770
                                  'host': host, 'port': port,
 
771
                                  'user': user, 'password': password,
 
772
                                  'protocol': scheme,
 
773
                                   # We ignore path since we connect to a proxy
 
774
                                  'path': None}
 
775
        if port is None:
 
776
            phost = host
 
777
        else:
 
778
            phost = host + ':%d' % port
 
779
        request.set_proxy(phost, type)
 
780
        if self._debuglevel > 0:
 
781
            print 'set_proxy: proxy set to %s://%s' % (type, phost)
844
782
        return request
845
783
 
846
784
 
882
820
      successful and the request authentication parameters have been updated.
883
821
    """
884
822
 
885
 
    _max_retry = 2
 
823
    _max_retry = 3
886
824
    """We don't want to retry authenticating endlessly"""
887
825
 
888
826
    # The following attributes should be defined by daughter
890
828
    # - auth_required_header:  the header received from the server
891
829
    # - auth_header: the header sent in the request
892
830
 
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
 
831
    def __init__(self):
 
832
        # We want to know when we enter into an try/fail cycle of
 
833
        # authentications so we initialize to None to indicate that we aren't
 
834
        # in such a cycle by default.
897
835
        self._retry_count = None
898
836
 
899
837
    def update_auth(self, auth, key, value):
912
850
        """
913
851
        # Don't try  to authenticate endlessly
914
852
        if self._retry_count is None:
915
 
            # The retry being recusrsive calls, None identify the first try
916
 
            self._retry_count = 0
 
853
            # The retry being recusrsive calls, None identify the first retry
 
854
            self._retry_count = 1
917
855
        else:
918
856
            self._retry_count += 1
919
857
            if self._retry_count > self._max_retry:
935
873
        if self.auth_match(server_header, auth):
936
874
            # auth_match may have modified auth (by adding the
937
875
            # password or changing the realm, for example)
938
 
            if request.get_header(self.auth_header, None) is not None \
939
 
                    and not auth['modified']:
 
876
            if (request.get_header(self.auth_header, None) is not None
 
877
                and not auth['modified']):
940
878
                # We already tried that, give up
941
879
                return None
942
880
 
998
936
        # It may happen that we need to reconnect later, let's be ready
999
937
        self._retry_count = None
1000
938
 
1001
 
    def get_password(self, user, authuri, realm=None):
 
939
    def get_user_password(self, auth):
1002
940
        """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
 
941
        auth_conf = config.AuthenticationConfig()
 
942
        user = auth['user']
 
943
        password = auth['password']
 
944
        realm = auth['realm']
 
945
 
 
946
        if user is None:
 
947
            user = auth.get_user(auth['protocol'], auth['host'],
 
948
                                 port=auth['port'], path=auth['path'],
 
949
                                 realm=realm)
 
950
            if user is None:
 
951
                # Default to local user
 
952
                user = getpass.getuser()
1007
953
 
1008
954
        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
 
955
            password = auth_conf.get_password(
 
956
                auth['protocol'], auth['host'], user, port=auth['port'],
 
957
                path=auth['path'], realm=realm,
 
958
                prompt=self.build_password_prompt(auth))
 
959
 
 
960
        return user, password
 
961
 
 
962
    def _build_password_prompt(self, auth):
 
963
        """Build a prompt taking the protocol used into account.
 
964
 
 
965
        The AuthHandler is used by http and https, we want that information in
 
966
        the prompt, so we build the prompt from the authentication dict which
 
967
        contains all the needed parts.
 
968
 
 
969
        Also, hhtp and proxy AuthHandlers present different prompts to the
 
970
        user. The daughter classes hosuld implements a public
 
971
        build_password_prompt using this method.
 
972
        """
 
973
        prompt = '%s' % auth['protocol'].upper() + ' %(user)s@%(host)s'
 
974
        realm = auth['realm']
 
975
        if realm is not None:
 
976
            prompt += ", Realm: '%s'" % realm
 
977
        prompt += ' password'
 
978
        return prompt
1021
979
 
1022
980
    def http_request(self, request):
1023
981
        """Insert an authentication header if information is available"""
1056
1014
            # Put useful info into auth
1057
1015
            self.update_auth(auth, 'scheme', scheme)
1058
1016
            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'])
 
1017
            if auth['user'] is None or auth['password'] is None:
 
1018
                user, password = self.get_user_password(auth)
 
1019
                self.update_auth(auth, 'user', user)
1062
1020
                self.update_auth(auth, 'password', password)
1063
1021
        return match is not None
1064
1022
 
1118
1076
            return False
1119
1077
 
1120
1078
        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
1079
        # Put useful info into auth
 
1080
        self.update_auth(auth, 'scheme', scheme)
 
1081
        self.update_auth(auth, 'realm', realm)
 
1082
        if auth['user'] is None or auth['password'] is None:
 
1083
            user, password = self.get_user_password(auth)
 
1084
            self.update_auth(auth, 'user', user)
 
1085
            self.update_auth(auth, 'password', password)
 
1086
 
1126
1087
        try:
1127
 
            self.update_auth(auth, 'scheme', scheme)
1128
1088
            if req_auth.get('algorithm', None) is not None:
1129
1089
                self.update_auth(auth, 'algorithm', req_auth.get('algorithm'))
1130
 
            self.update_auth(auth, 'realm', realm)
1131
1090
            nonce = req_auth['nonce']
1132
1091
            if auth.get('nonce', None) != nonce:
1133
1092
                # A new nonce, never used
1188
1147
    the auth request attribute.
1189
1148
    """
1190
1149
 
1191
 
    password_prompt = 'HTTP %(user)s@%(host)s%(realm)s password'
1192
1150
    auth_required_header = 'www-authenticate'
1193
1151
    auth_header = 'Authorization'
1194
1152
 
1200
1158
        """Set the auth params for the request"""
1201
1159
        request.auth = auth
1202
1160
 
 
1161
    def build_password_prompt(self, auth):
 
1162
        return self._build_password_prompt(auth)
 
1163
 
1203
1164
    def http_error_401(self, req, fp, code, msg, headers):
1204
1165
        return self.auth_required(req, headers)
1205
1166
 
1212
1173
    the proxy_auth request attribute..
1213
1174
    """
1214
1175
 
1215
 
    password_prompt = 'Proxy %(user)s@%(host)s%(realm)s password'
1216
1176
    auth_required_header = 'proxy-authenticate'
1217
1177
    # FIXME: the correct capitalization is Proxy-Authorization,
1218
1178
    # but python-2.4 urllib2.Request insist on using capitalize()
1227
1187
        """Set the auth params for the request"""
1228
1188
        request.proxy_auth = auth
1229
1189
 
 
1190
    def build_password_prompt(self, auth):
 
1191
        prompt = self._build_password_prompt(auth)
 
1192
        prompt = 'Proxy ' + prompt
 
1193
        return prompt
 
1194
 
1230
1195
    def http_error_407(self, req, fp, code, msg, headers):
1231
1196
        return self.auth_required(req, headers)
1232
1197
 
1307
1272
                 connection=ConnectionHandler,
1308
1273
                 redirect=HTTPRedirectHandler,
1309
1274
                 error=HTTPErrorProcessor,):
1310
 
        self.password_manager = PasswordManager()
1311
1275
        self._opener = urllib2.build_opener( \
1312
1276
            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),
 
1277
            ProxyHandler(),
 
1278
            HTTPBasicAuthHandler(),
 
1279
            HTTPDigestAuthHandler(),
 
1280
            ProxyBasicAuthHandler(),
 
1281
            ProxyDigestAuthHandler(),
1318
1282
            HTTPHandler,
1319
1283
            HTTPSHandler,
1320
1284
            HTTPDefaultErrorHandler,