71
class _BufferedMakefileSocket(object):
73
def __init__(self, sock):
78
class _ReportingFileSocket(object):
80
def __init__(self, filesock, report_activity=None):
81
self.filesock = filesock
82
self._report_activity = report_activity
84
def report_activity(self, size, direction):
85
if self._report_activity:
86
self._report_activity(size, direction)
88
def read(self, size=1):
89
s = self.filesock.read(size)
90
self.report_activity(len(s), 'read')
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)
99
s = self.filesock.readline()
100
self.report_activity(len(s), 'read')
103
def __getattr__(self, name):
104
return getattr(self.filesock, name)
107
class _ReportingSocket(object):
109
def __init__(self, sock, report_activity=None):
111
self._report_activity = report_activity
113
def report_activity(self, size, direction):
114
if self._report_activity:
115
self._report_activity(size, direction)
117
def sendall(self, s, *args):
118
self.sock.sendall(s, *args)
119
self.report_activity(len(s), 'write')
121
def recv(self, *args):
122
s = self.sock.recv(*args)
123
self.report_activity(len(s), 'read')
76
126
def makefile(self, mode='r', bufsize=-1):
77
return self.sock.makefile(mode, 65536)
127
# httplib creates a fileobject that doesn't do buffering, which
128
# makes fp.readline() very expensive because it only reads one byte
129
# at a time. So we wrap the socket in an object that forces
130
# sock.makefile to make a buffered file.
131
fsock = self.sock.makefile(mode, 65536)
132
# And wrap that into a reporting kind of fileobject
133
return _ReportingFileSocket(fsock, self._report_activity)
79
135
def __getattr__(self, name):
80
136
return getattr(self.sock, name)
97
153
# 8k chunks should be fine.
98
154
_discarded_buf_size = 8192
100
def __init__(self, sock, *args, **kwargs):
101
# httplib creates a fileobject that doesn't do buffering, which
102
# makes fp.readline() very expensive because it only reads one byte
103
# at a time. So we wrap the socket in an object that forces
104
# sock.makefile to make a buffered file.
105
sock = _BufferedMakefileSocket(sock)
106
httplib.HTTPResponse.__init__(self, sock, *args, **kwargs)
109
157
"""Begin to read the response from the server.
212
261
# Preserve our preciousss
215
# Let httplib.HTTPConnection do its housekeeping
264
# Let httplib.HTTPConnection do its housekeeping
217
266
# Restore our preciousss
269
def _wrap_socket_for_reporting(self, sock):
270
"""Wrap the socket before anybody use it."""
271
self.sock = _ReportingSocket(sock, self._report_activity)
221
274
class HTTPConnection(AbstractHTTPConnection, httplib.HTTPConnection):
223
276
# XXX: Needs refactoring at the caller level.
224
def __init__(self, host, port=None, proxied_host=None):
225
AbstractHTTPConnection.__init__(self)
277
def __init__(self, host, port=None, proxied_host=None,
278
report_activity=None):
279
AbstractHTTPConnection.__init__(self, report_activity=report_activity)
226
280
# Use strict=True since we don't support HTTP/0.9
227
281
httplib.HTTPConnection.__init__(self, host, port, strict=True)
228
282
self.proxied_host = proxied_host
231
285
if 'http' in debug.debug_flags:
232
286
self._mutter_connect()
233
287
httplib.HTTPConnection.connect(self)
236
# FIXME: Should test for ssl availability
288
self._wrap_socket_for_reporting(self.sock)
291
# Build the appropriate socket wrapper for ssl
293
# python 2.6 introduced a better ssl package
295
_ssl_wrap_socket = ssl.wrap_socket
297
# python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
298
# they use httplib.FakeSocket
299
def _ssl_wrap_socket(sock, key_file, cert_file):
300
ssl_sock = socket.ssl(sock, key_file, cert_file)
301
return httplib.FakeSocket(sock, ssl_sock)
237
304
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
239
306
def __init__(self, host, port=None, key_file=None, cert_file=None,
241
AbstractHTTPConnection.__init__(self)
308
report_activity=None):
309
AbstractHTTPConnection.__init__(self, report_activity=report_activity)
242
310
# Use strict=True since we don't support HTTP/0.9
243
311
httplib.HTTPSConnection.__init__(self, host, port,
244
312
key_file, cert_file, strict=True)
248
316
if 'http' in debug.debug_flags:
249
317
self._mutter_connect()
250
318
httplib.HTTPConnection.connect(self)
319
self._wrap_socket_for_reporting(self.sock)
251
320
if self.proxied_host is None:
252
321
self.connect_to_origin()
254
323
def connect_to_origin(self):
255
ssl = socket.ssl(self.sock, self.key_file, self.cert_file)
256
self.sock = httplib.FakeSocket(self.sock, ssl)
324
ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
325
# Wrap the ssl socket before anybody use it
326
self._wrap_socket_for_reporting(ssl_sock)
259
329
class Request(urllib2.Request):
298
368
def set_proxy(self, proxy, type):
299
369
"""Set the proxy and remember the proxied host."""
300
self.proxied_host = self.get_host()
370
host, port = urllib.splitport(self.get_host())
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
377
conn_class = HTTPConnection
378
port = conn_class.default_port
379
self.proxied_host = '%s:%s' % (host, port)
301
380
urllib2.Request.set_proxy(self, proxy, type)
924
1019
# in such a cycle by default.
925
1020
self._retry_count = None
1022
def _parse_auth_header(self, server_header):
1023
"""Parse the authentication header.
1025
:param server_header: The value of the header sent by the server
1026
describing the authenticaion request.
1028
:return: A tuple (scheme, remainder) scheme being the first word in the
1029
given header (lower cased), remainder may be None.
1032
scheme, remainder = server_header.split(None, 1)
1034
scheme = server_header
1036
return (scheme.lower(), remainder)
927
1038
def update_auth(self, auth, key, value):
928
1039
"""Update a value in auth marking the auth as modified if needed"""
929
1040
old_value = auth.get(key, None)
948
1059
# Let's be ready for next round
949
1060
self._retry_count = None
951
server_header = headers.get(self.auth_required_header, None)
952
if server_header is None:
1062
server_headers = headers.getheaders(self.auth_required_header)
1063
if not server_headers:
953
1064
# The http error MUST have the associated
954
1065
# header. This must never happen in production code.
955
1066
raise KeyError('%s not found' % self.auth_required_header)
957
1068
auth = self.get_auth(request)
958
if auth.get('user', None) is None:
959
# Without a known user, we can't authenticate
962
1069
auth['modified'] = False
963
if self.auth_match(server_header, auth):
964
# auth_match may have modified auth (by adding the
965
# password or changing the realm, for example)
966
if (request.get_header(self.auth_header, None) is not None
967
and not auth['modified']):
968
# We already tried that, give up
972
request.connection.cleanup_pipe()
973
response = self.parent.open(request)
975
self.auth_successful(request, response)
1070
# Put some common info in auth if the caller didn't
1071
if auth.get('path', None) is None:
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
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
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
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:
1107
if self.requires_username and auth.get('user', None) is None:
1108
# Without a known user, we can't authenticate
1112
request.connection.cleanup_pipe()
1113
# Retry the request with an authentication header added
1114
response = self.parent.open(request)
1116
self.auth_successful(request, response)
977
1118
# We are not qualified to handle the authentication.
978
1119
# Note: the authentication error handling will try all
979
1120
# available handlers. If one of them authenticates
1029
1170
self._retry_count = None
1031
1172
def get_user_password(self, auth):
1032
"""Ask user for a password if none is already available."""
1173
"""Ask user for a password if none is already available.
1175
:param auth: authentication info gathered so far (from the initial url
1176
and then during dialog with the server).
1033
1178
auth_conf = config.AuthenticationConfig()
1035
password = auth['password']
1179
user = auth.get('user', None)
1180
password = auth.get('password', None)
1036
1181
realm = auth['realm']
1038
1183
if user is None:
1039
user = auth.get_user(auth['protocol'], auth['host'],
1040
port=auth['port'], path=auth['path'],
1043
# Default to local user
1044
user = getpass.getuser()
1046
if password is None:
1184
user = auth_conf.get_user(auth['protocol'], auth['host'],
1185
port=auth['port'], path=auth['path'],
1186
realm=realm, ask=True,
1187
prompt=self.build_username_prompt(auth))
1188
if user is not None and password is None:
1047
1189
password = auth_conf.get_password(
1048
1190
auth['protocol'], auth['host'], user, port=auth['port'],
1049
1191
path=auth['path'], realm=realm,
1069
1211
prompt += ' password'
1214
def _build_username_prompt(self, auth):
1215
"""Build a prompt taking the protocol used into account.
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.
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.
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'
1072
1232
def http_request(self, request):
1073
1233
"""Insert an authentication header if information is available"""
1074
1234
auth = self.get_auth(request)
1079
1239
https_request = http_request # FIXME: Need test
1242
class NegotiateAuthHandler(AbstractAuthHandler):
1243
"""A authentication handler that handles WWW-Authenticate: Negotiate.
1245
At the moment this handler supports just Kerberos. In the future,
1246
NTLM support may also be added.
1249
scheme = 'negotiate'
1251
requires_username = False
1253
def auth_match(self, header, auth):
1254
scheme, raw_auth = self._parse_auth_header(header)
1255
if scheme != self.scheme:
1257
self.update_auth(auth, 'scheme', scheme)
1258
resp = self._auth_match_kerberos(auth)
1261
# Optionally should try to authenticate using NTLM here
1262
self.update_auth(auth, 'negotiate_response', resp)
1265
def _auth_match_kerberos(self, auth):
1266
"""Try to create a GSSAPI response for authenticating against a host."""
1267
if not have_kerberos:
1269
ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
1271
trace.warning('Unable to create GSSAPI context for %s: %d',
1274
ret = kerberos.authGSSClientStep(vc, "")
1276
trace.mutter('authGSSClientStep failed: %d', ret)
1278
return kerberos.authGSSClientResponse(vc)
1280
def build_auth_header(self, auth, request):
1281
return "Negotiate %s" % auth['negotiate_response']
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)
1082
1291
class BasicAuthHandler(AbstractAuthHandler):
1083
1292
"""A custom basic authentication handler."""
1085
1295
handler_order = 500
1087
1296
auth_regexp = re.compile('realm="([^"]*)"', re.I)
1089
1298
def build_auth_header(self, auth, request):
1091
1300
auth_header = 'Basic ' + raw.encode('base64').strip()
1092
1301
return auth_header
1303
def extract_realm(self, header_value):
1304
match = self.auth_regexp.search(header_value)
1307
realm = match.group(1)
1094
1310
def auth_match(self, header, auth):
1095
scheme, raw_auth = header.split(None, 1)
1096
scheme = scheme.lower()
1097
if scheme != 'basic':
1311
scheme, raw_auth = self._parse_auth_header(header)
1312
if scheme != self.scheme:
1100
match = self.auth_regexp.search(raw_auth)
1315
match, realm = self.extract_realm(raw_auth)
1102
realm = match.groups()
1103
if scheme != 'basic':
1106
1317
# Put useful info into auth
1107
1318
self.update_auth(auth, 'scheme', scheme)
1108
1319
self.update_auth(auth, 'realm', realm)
1109
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):
1110
1322
user, password = self.get_user_password(auth)
1111
1323
self.update_auth(auth, 'user', user)
1112
1324
self.update_auth(auth, 'password', password)
1134
1346
def get_new_cnonce(nonce, nonce_count):
1135
1347
raw = '%s:%d:%s:%s' % (nonce, nonce_count, time.ctime(),
1136
1348
urllib2.randombytes(8))
1137
return sha.new(raw).hexdigest()[:16]
1349
return osutils.sha(raw).hexdigest()[:16]
1140
1352
class DigestAuthHandler(AbstractAuthHandler):
1141
1353
"""A custom digest authentication handler."""
1143
# Before basic as digest is a bit more secure
1356
# Before basic as digest is a bit more secure and should be preferred
1144
1357
handler_order = 490
1146
1359
def auth_params_reusable(self, auth):
1171
1383
# Put useful info into auth
1172
1384
self.update_auth(auth, 'scheme', scheme)
1173
1385
self.update_auth(auth, 'realm', realm)
1174
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:
1175
1387
user, password = self.get_user_password(auth)
1176
1388
self.update_auth(auth, 'user', user)
1177
1389
self.update_auth(auth, 'password', password)
1360
1588
def __init__(self,
1361
1589
connection=ConnectionHandler,
1362
1590
redirect=HTTPRedirectHandler,
1363
error=HTTPErrorProcessor,):
1364
self._opener = urllib2.build_opener( \
1365
connection, redirect, error,
1591
error=HTTPErrorProcessor,
1592
report_activity=None):
1593
self._opener = urllib2.build_opener(
1594
connection(report_activity=report_activity),
1366
1596
ProxyHandler(),
1367
1597
HTTPBasicAuthHandler(),
1368
1598
HTTPDigestAuthHandler(),
1599
HTTPNegotiateAuthHandler(),
1369
1600
ProxyBasicAuthHandler(),
1370
1601
ProxyDigestAuthHandler(),
1602
ProxyNegotiateAuthHandler(),
1373
1605
HTTPDefaultErrorHandler,