~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: 2010-01-12 03:53:21 UTC
  • mfrom: (4948 +trunk)
  • mto: This revision was merged to the branch mainline in revision 4964.
  • Revision ID: andrew.bennetts@canonical.com-20100112035321-hofpz5p10224ryj3
Merge lp:bzr, resolving conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Implementaion of urllib2 tailored to bzr needs
18
18
 
46
46
# actual code more or less do that, tests should be written to
47
47
# ensure that.
48
48
 
 
49
import errno
49
50
import httplib
 
51
try:
 
52
    import kerberos
 
53
except ImportError:
 
54
    have_kerberos = False
 
55
else:
 
56
    have_kerberos = True
50
57
import socket
51
58
import urllib
52
59
import urllib2
64
71
    trace,
65
72
    transport,
66
73
    ui,
 
74
    urlutils,
67
75
    )
68
76
 
69
77
 
73
81
        self.filesock = filesock
74
82
        self._report_activity = report_activity
75
83
 
 
84
    def report_activity(self, size, direction):
 
85
        if self._report_activity:
 
86
            self._report_activity(size, direction)
76
87
 
77
88
    def read(self, size=1):
78
89
        s = self.filesock.read(size)
79
 
        self._report_activity(len(s), 'read')
 
90
        self.report_activity(len(s), 'read')
80
91
        return s
81
92
 
82
 
    def readline(self, size=-1):
83
 
        s = self.filesock.readline(size)
84
 
        self._report_activity(len(s), 'read')
 
93
    def readline(self):
 
94
        # This should be readline(self, size=-1), but httplib in python 2.4 and
 
95
        #  2.5 defines a SSLFile wrapper whose readline method lacks the size
 
96
        #  parameter.  So until we drop support for 2.4 and 2.5 and since we
 
97
        #  don't *need* the size parameter we'll stay with readline(self)
 
98
        #  --  vila 20090209
 
99
        s = self.filesock.readline()
 
100
        self.report_activity(len(s), 'read')
85
101
        return s
86
102
 
87
103
    def __getattr__(self, name):
94
110
        self.sock = sock
95
111
        self._report_activity = report_activity
96
112
 
97
 
    def send(self, s, *args):
98
 
        self.sock.send(s, *args)
99
 
        self._report_activity(len(s), 'write')
 
113
    def report_activity(self, size, direction):
 
114
        if self._report_activity:
 
115
            self._report_activity(size, direction)
100
116
 
101
117
    def sendall(self, s, *args):
102
 
        self.sock.send(s, *args)
103
 
        self._report_activity(len(s), 'write')
 
118
        self.sock.sendall(s, *args)
 
119
        self.report_activity(len(s), 'write')
104
120
 
105
121
    def recv(self, *args):
106
122
        s = self.sock.recv(*args)
107
 
        self._report_activity(len(s), 'read')
 
123
        self.report_activity(len(s), 'read')
108
124
        return s
109
125
 
110
126
    def makefile(self, mode='r', bufsize=-1):
211
227
    # we want to warn. But not below a given thresold.
212
228
    _range_warning_thresold = 1024 * 1024
213
229
 
214
 
    def __init__(self,
215
 
                 report_activity=None):
 
230
    def __init__(self, report_activity=None):
216
231
        self._response = None
217
232
        self._report_activity = report_activity
218
233
        self._ranges_received_whole_file = None
246
261
        # Preserve our preciousss
247
262
        sock = self.sock
248
263
        self.sock = None
249
 
        # Let httplib.HTTPConnection do its housekeeping 
 
264
        # Let httplib.HTTPConnection do its housekeeping
250
265
        self.close()
251
266
        # Restore our preciousss
252
267
        self.sock = sock
352
367
 
353
368
    def set_proxy(self, proxy, type):
354
369
        """Set the proxy and remember the proxied host."""
355
 
        self.proxied_host = self.get_host()
 
370
        host, port = urllib.splitport(self.get_host())
 
371
        if port is None:
 
372
            # We need to set the default port ourselves way before it gets set
 
373
            # in the HTTP[S]Connection object at build time.
 
374
            if self.type == 'https':
 
375
                conn_class = HTTPSConnection
 
376
            else:
 
377
                conn_class = HTTPConnection
 
378
            port = conn_class.default_port
 
379
        self.proxied_host = '%s:%s' % (host, port)
356
380
        urllib2.Request.set_proxy(self, proxy, type)
357
381
 
358
382
 
360
384
 
361
385
    def __init__(self, request):
362
386
        """Constructor
363
 
        
 
387
 
364
388
        :param request: the first request sent to the proxied host, already
365
389
            processed by the opener (i.e. proxied_host is already set).
366
390
        """
534
558
                        request.get_full_url(),
535
559
                        'Bad status line received',
536
560
                        orig_error=exc_val)
 
561
                elif (isinstance(exc_val, socket.error) and len(exc_val.args)
 
562
                      and exc_val.args[0] in (errno.ECONNRESET, 10054)):
 
563
                    raise errors.ConnectionReset(
 
564
                        "Connection lost while sending request.")
537
565
                else:
538
566
                    # All other exception are considered connection related.
539
567
 
687
715
                        connect.proxied_host, self.host))
688
716
            # Housekeeping
689
717
            connection.cleanup_pipe()
690
 
            # Establish the connection encryption 
 
718
            # Establish the connection encryption
691
719
            connection.connect_to_origin()
692
720
            # Propagate the connection to the original request
693
721
            request.connection = connection
910
938
 
911
939
        (scheme, user, password,
912
940
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
 
941
        if not host:
 
942
            raise errors.InvalidURL(proxy, 'No host component')
913
943
 
914
944
        if request.proxy_auth == {}:
915
945
            # No proxy auth parameter are available, we are handling the first
938
968
    preventively set authentication headers after the first
939
969
    successful authentication.
940
970
 
941
 
    This can be used for http and proxy, as well as for basic and
 
971
    This can be used for http and proxy, as well as for basic, negotiate and
942
972
    digest authentications.
943
973
 
944
974
    This provides an unified interface for all authentication handlers
969
999
      successful and the request authentication parameters have been updated.
970
1000
    """
971
1001
 
 
1002
    scheme = None
 
1003
    """The scheme as it appears in the server header (lower cased)"""
 
1004
 
972
1005
    _max_retry = 3
973
1006
    """We don't want to retry authenticating endlessly"""
974
1007
 
 
1008
    requires_username = True
 
1009
    """Whether the auth mechanism requires a username."""
 
1010
 
975
1011
    # The following attributes should be defined by daughter
976
1012
    # classes:
977
1013
    # - auth_required_header:  the header received from the server
983
1019
        # in such a cycle by default.
984
1020
        self._retry_count = None
985
1021
 
 
1022
    def _parse_auth_header(self, server_header):
 
1023
        """Parse the authentication header.
 
1024
 
 
1025
        :param server_header: The value of the header sent by the server
 
1026
            describing the authenticaion request.
 
1027
 
 
1028
        :return: A tuple (scheme, remainder) scheme being the first word in the
 
1029
            given header (lower cased), remainder may be None.
 
1030
        """
 
1031
        try:
 
1032
            scheme, remainder = server_header.split(None, 1)
 
1033
        except ValueError:
 
1034
            scheme = server_header
 
1035
            remainder = None
 
1036
        return (scheme.lower(), remainder)
 
1037
 
986
1038
    def update_auth(self, auth, key, value):
987
1039
        """Update a value in auth marking the auth as modified if needed"""
988
1040
        old_value = auth.get(key, None)
1007
1059
                # Let's be ready for next round
1008
1060
                self._retry_count = None
1009
1061
                return None
1010
 
        server_header = headers.get(self.auth_required_header, None)
1011
 
        if server_header is None:
 
1062
        server_headers = headers.getheaders(self.auth_required_header)
 
1063
        if not server_headers:
1012
1064
            # The http error MUST have the associated
1013
1065
            # header. This must never happen in production code.
1014
1066
            raise KeyError('%s not found' % self.auth_required_header)
1015
1067
 
1016
1068
        auth = self.get_auth(request)
1017
1069
        auth['modified'] = False
1018
 
        if self.auth_match(server_header, auth):
1019
 
            # auth_match may have modified auth (by adding the
1020
 
            # password or changing the realm, for example)
1021
 
            if (request.get_header(self.auth_header, None) is not None
1022
 
                and not auth['modified']):
1023
 
                # We already tried that, give up
1024
 
                return None
1025
 
 
1026
 
            if auth.get('user', None) is None:
1027
 
                # Without a known user, we can't authenticate
1028
 
                return None
1029
 
 
1030
 
            # Housekeeping
1031
 
            request.connection.cleanup_pipe()
1032
 
            response = self.parent.open(request)
1033
 
            if response:
1034
 
                self.auth_successful(request, response)
1035
 
            return response
 
1070
        # Put some common info in auth if the caller didn't
 
1071
        if auth.get('path', None) is None:
 
1072
            (protocol, _, _,
 
1073
             host, port, path) = urlutils.parse_url(request.get_full_url())
 
1074
            self.update_auth(auth, 'protocol', protocol)
 
1075
            self.update_auth(auth, 'host', host)
 
1076
            self.update_auth(auth, 'port', port)
 
1077
            self.update_auth(auth, 'path', path)
 
1078
        # FIXME: the auth handler should be selected at a single place instead
 
1079
        # of letting all handlers try to match all headers, but the current
 
1080
        # design doesn't allow a simple implementation.
 
1081
        for server_header in server_headers:
 
1082
            # Several schemes can be proposed by the server, try to match each
 
1083
            # one in turn
 
1084
            matching_handler = self.auth_match(server_header, auth)
 
1085
            if matching_handler:
 
1086
                # auth_match may have modified auth (by adding the
 
1087
                # password or changing the realm, for example)
 
1088
                if (request.get_header(self.auth_header, None) is not None
 
1089
                    and not auth['modified']):
 
1090
                    # We already tried that, give up
 
1091
                    return None
 
1092
 
 
1093
                # Only the most secure scheme proposed by the server should be
 
1094
                # used, since the handlers use 'handler_order' to describe that
 
1095
                # property, the first handler tried takes precedence, the
 
1096
                # others should not attempt to authenticate if the best one
 
1097
                # failed.
 
1098
                best_scheme = auth.get('best_scheme', None)
 
1099
                if best_scheme is None:
 
1100
                    # At that point, if current handler should doesn't succeed
 
1101
                    # the credentials are wrong (or incomplete), but we know
 
1102
                    # that the associated scheme should be used.
 
1103
                    best_scheme = auth['best_scheme'] = self.scheme
 
1104
                if  best_scheme != self.scheme:
 
1105
                    continue
 
1106
 
 
1107
                if self.requires_username and auth.get('user', None) is None:
 
1108
                    # Without a known user, we can't authenticate
 
1109
                    return None
 
1110
 
 
1111
                # Housekeeping
 
1112
                request.connection.cleanup_pipe()
 
1113
                # Retry the request with an authentication header added
 
1114
                response = self.parent.open(request)
 
1115
                if response:
 
1116
                    self.auth_successful(request, response)
 
1117
                return response
1036
1118
        # We are not qualified to handle the authentication.
1037
1119
        # Note: the authentication error handling will try all
1038
1120
        # available handlers. If one of them authenticates
1058
1140
        (digest's nonce is an example, digest's nonce_count is a
1059
1141
        *counter-example*). Such parameters must be updated by
1060
1142
        using the update_auth() method.
1061
 
        
 
1143
 
1062
1144
        :param header: The authentication header sent by the server.
1063
1145
        :param auth: The auth parameters already known. They may be
1064
1146
             updated.
1094
1176
            and then during dialog with the server).
1095
1177
        """
1096
1178
        auth_conf = config.AuthenticationConfig()
1097
 
        user = auth['user']
1098
 
        password = auth['password']
 
1179
        user = auth.get('user', None)
 
1180
        password = auth.get('password', None)
1099
1181
        realm = auth['realm']
1100
1182
 
1101
1183
        if user is None:
1102
1184
            user = auth_conf.get_user(auth['protocol'], auth['host'],
1103
1185
                                      port=auth['port'], path=auth['path'],
1104
 
                                      realm=realm)
 
1186
                                      realm=realm, ask=True,
 
1187
                                      prompt=self.build_username_prompt(auth))
1105
1188
        if user is not None and password is None:
1106
1189
            password = auth_conf.get_password(
1107
1190
                auth['protocol'], auth['host'], user, port=auth['port'],
1128
1211
        prompt += ' password'
1129
1212
        return prompt
1130
1213
 
 
1214
    def _build_username_prompt(self, auth):
 
1215
        """Build a prompt taking the protocol used into account.
 
1216
 
 
1217
        The AuthHandler is used by http and https, we want that information in
 
1218
        the prompt, so we build the prompt from the authentication dict which
 
1219
        contains all the needed parts.
 
1220
 
 
1221
        Also, http and proxy AuthHandlers present different prompts to the
 
1222
        user. The daughter classes should implements a public
 
1223
        build_username_prompt using this method.
 
1224
        """
 
1225
        prompt = '%s' % auth['protocol'].upper() + ' %(host)s'
 
1226
        realm = auth['realm']
 
1227
        if realm is not None:
 
1228
            prompt += ", Realm: '%s'" % realm
 
1229
        prompt += ' username'
 
1230
        return prompt
 
1231
 
1131
1232
    def http_request(self, request):
1132
1233
        """Insert an authentication header if information is available"""
1133
1234
        auth = self.get_auth(request)
1138
1239
    https_request = http_request # FIXME: Need test
1139
1240
 
1140
1241
 
 
1242
class NegotiateAuthHandler(AbstractAuthHandler):
 
1243
    """A authentication handler that handles WWW-Authenticate: Negotiate.
 
1244
 
 
1245
    At the moment this handler supports just Kerberos. In the future,
 
1246
    NTLM support may also be added.
 
1247
    """
 
1248
 
 
1249
    scheme = 'negotiate'
 
1250
    handler_order = 480
 
1251
    requires_username = False
 
1252
 
 
1253
    def auth_match(self, header, auth):
 
1254
        scheme, raw_auth = self._parse_auth_header(header)
 
1255
        if scheme != self.scheme:
 
1256
            return False
 
1257
        self.update_auth(auth, 'scheme', scheme)
 
1258
        resp = self._auth_match_kerberos(auth)
 
1259
        if resp is None:
 
1260
            return False
 
1261
        # Optionally should try to authenticate using NTLM here
 
1262
        self.update_auth(auth, 'negotiate_response', resp)
 
1263
        return True
 
1264
 
 
1265
    def _auth_match_kerberos(self, auth):
 
1266
        """Try to create a GSSAPI response for authenticating against a host."""
 
1267
        if not have_kerberos:
 
1268
            return None
 
1269
        ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
 
1270
        if ret < 1:
 
1271
            trace.warning('Unable to create GSSAPI context for %s: %d',
 
1272
                auth['host'], ret)
 
1273
            return None
 
1274
        ret = kerberos.authGSSClientStep(vc, "")
 
1275
        if ret < 0:
 
1276
            trace.mutter('authGSSClientStep failed: %d', ret)
 
1277
            return None
 
1278
        return kerberos.authGSSClientResponse(vc)
 
1279
 
 
1280
    def build_auth_header(self, auth, request):
 
1281
        return "Negotiate %s" % auth['negotiate_response']
 
1282
 
 
1283
    def auth_params_reusable(self, auth):
 
1284
        # If the auth scheme is known, it means a previous
 
1285
        # authentication was successful, all information is
 
1286
        # available, no further checks are needed.
 
1287
        return (auth.get('scheme', None) == 'negotiate' and
 
1288
                auth.get('negotiate_response', None) is not None)
 
1289
 
 
1290
 
1141
1291
class BasicAuthHandler(AbstractAuthHandler):
1142
1292
    """A custom basic authentication handler."""
1143
1293
 
 
1294
    scheme = 'basic'
1144
1295
    handler_order = 500
1145
 
 
1146
1296
    auth_regexp = re.compile('realm="([^"]*)"', re.I)
1147
1297
 
1148
1298
    def build_auth_header(self, auth, request):
1150
1300
        auth_header = 'Basic ' + raw.encode('base64').strip()
1151
1301
        return auth_header
1152
1302
 
 
1303
    def extract_realm(self, header_value):
 
1304
        match = self.auth_regexp.search(header_value)
 
1305
        realm = None
 
1306
        if match:
 
1307
            realm = match.group(1)
 
1308
        return match, realm
 
1309
 
1153
1310
    def auth_match(self, header, auth):
1154
 
        scheme, raw_auth = header.split(None, 1)
1155
 
        scheme = scheme.lower()
1156
 
        if scheme != 'basic':
 
1311
        scheme, raw_auth = self._parse_auth_header(header)
 
1312
        if scheme != self.scheme:
1157
1313
            return False
1158
1314
 
1159
 
        match = self.auth_regexp.search(raw_auth)
 
1315
        match, realm = self.extract_realm(raw_auth)
1160
1316
        if match:
1161
 
            realm = match.groups()
1162
 
            if scheme != 'basic':
1163
 
                return False
1164
 
 
1165
1317
            # Put useful info into auth
1166
1318
            self.update_auth(auth, 'scheme', scheme)
1167
1319
            self.update_auth(auth, 'realm', realm)
1168
 
            if auth['user'] is None or auth['password'] is None:
 
1320
            if (auth.get('user', None) is None
 
1321
                or auth.get('password', None) is None):
1169
1322
                user, password = self.get_user_password(auth)
1170
1323
                self.update_auth(auth, 'user', user)
1171
1324
                self.update_auth(auth, 'password', password)
1199
1352
class DigestAuthHandler(AbstractAuthHandler):
1200
1353
    """A custom digest authentication handler."""
1201
1354
 
1202
 
    # Before basic as digest is a bit more secure
 
1355
    scheme = 'digest'
 
1356
    # Before basic as digest is a bit more secure and should be preferred
1203
1357
    handler_order = 490
1204
1358
 
1205
1359
    def auth_params_reusable(self, auth):
1209
1363
        return auth.get('scheme', None) == 'digest'
1210
1364
 
1211
1365
    def auth_match(self, header, auth):
1212
 
        scheme, raw_auth = header.split(None, 1)
1213
 
        scheme = scheme.lower()
1214
 
        if scheme != 'digest':
 
1366
        scheme, raw_auth = self._parse_auth_header(header)
 
1367
        if scheme != self.scheme:
1215
1368
            return False
1216
1369
 
1217
1370
        # Put the requested authentication info into a dict
1230
1383
        # Put useful info into auth
1231
1384
        self.update_auth(auth, 'scheme', scheme)
1232
1385
        self.update_auth(auth, 'realm', realm)
1233
 
        if auth['user'] is None or auth['password'] is None:
 
1386
        if auth.get('user', None) is None or auth.get('password', None) is None:
1234
1387
            user, password = self.get_user_password(auth)
1235
1388
            self.update_auth(auth, 'user', user)
1236
1389
            self.update_auth(auth, 'password', password)
1312
1465
    def build_password_prompt(self, auth):
1313
1466
        return self._build_password_prompt(auth)
1314
1467
 
 
1468
    def build_username_prompt(self, auth):
 
1469
        return self._build_username_prompt(auth)
 
1470
 
1315
1471
    def http_error_401(self, req, fp, code, msg, headers):
1316
1472
        return self.auth_required(req, headers)
1317
1473
 
1343
1499
        prompt = 'Proxy ' + prompt
1344
1500
        return prompt
1345
1501
 
 
1502
    def build_username_prompt(self, auth):
 
1503
        prompt = self._build_username_prompt(auth)
 
1504
        prompt = 'Proxy ' + prompt
 
1505
        return prompt
 
1506
 
1346
1507
    def http_error_407(self, req, fp, code, msg, headers):
1347
1508
        return self.auth_required(req, headers)
1348
1509
 
1363
1524
    """Custom proxy basic authentication handler"""
1364
1525
 
1365
1526
 
 
1527
class HTTPNegotiateAuthHandler(NegotiateAuthHandler, HTTPAuthHandler):
 
1528
    """Custom http negotiate authentication handler"""
 
1529
 
 
1530
 
 
1531
class ProxyNegotiateAuthHandler(NegotiateAuthHandler, ProxyAuthHandler):
 
1532
    """Custom proxy negotiate authentication handler"""
 
1533
 
 
1534
 
1366
1535
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
1367
1536
    """Process HTTP error responses.
1368
1537
 
1427
1596
            ProxyHandler(),
1428
1597
            HTTPBasicAuthHandler(),
1429
1598
            HTTPDigestAuthHandler(),
 
1599
            HTTPNegotiateAuthHandler(),
1430
1600
            ProxyBasicAuthHandler(),
1431
1601
            ProxyDigestAuthHandler(),
 
1602
            ProxyNegotiateAuthHandler(),
1432
1603
            HTTPHandler,
1433
1604
            HTTPSHandler,
1434
1605
            HTTPDefaultErrorHandler,