##// END OF EJS Templates
url: drop awful hack around bug in Python 2.4...
Pierre-Yves David -
r25207:63583914 default
parent child Browse files
Show More
@@ -1,510 +1,500 b''
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import urllib, urllib2, httplib, os, socket, cStringIO, base64
11 11 from i18n import _
12 12 import keepalive, util, sslutil
13 13 import httpconnection as httpconnectionmod
14 14
15 15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
16 16 def __init__(self, ui):
17 17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
18 18 self.ui = ui
19 19
20 20 def find_user_password(self, realm, authuri):
21 21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
22 22 self, realm, authuri)
23 23 user, passwd = authinfo
24 24 if user and passwd:
25 25 self._writedebug(user, passwd)
26 26 return (user, passwd)
27 27
28 28 if not user or not passwd:
29 29 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
30 30 if res:
31 31 group, auth = res
32 32 user, passwd = auth.get('username'), auth.get('password')
33 33 self.ui.debug("using auth.%s.* for authentication\n" % group)
34 34 if not user or not passwd:
35 35 u = util.url(authuri)
36 36 u.query = None
37 37 if not self.ui.interactive():
38 38 raise util.Abort(_('http authorization required for %s') %
39 39 util.hidepassword(str(u)))
40 40
41 41 self.ui.write(_("http authorization required for %s\n") %
42 42 util.hidepassword(str(u)))
43 43 self.ui.write(_("realm: %s\n") % realm)
44 44 if user:
45 45 self.ui.write(_("user: %s\n") % user)
46 46 else:
47 47 user = self.ui.prompt(_("user:"), default=None)
48 48
49 49 if not passwd:
50 50 passwd = self.ui.getpass()
51 51
52 52 self.add_password(realm, authuri, user, passwd)
53 53 self._writedebug(user, passwd)
54 54 return (user, passwd)
55 55
56 56 def _writedebug(self, user, passwd):
57 57 msg = _('http auth: user %s, password %s\n')
58 58 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
59 59
60 60 def find_stored_password(self, authuri):
61 61 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
62 62 self, None, authuri)
63 63
64 64 class proxyhandler(urllib2.ProxyHandler):
65 65 def __init__(self, ui):
66 66 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
67 67 # XXX proxyauthinfo = None
68 68
69 69 if proxyurl:
70 70 # proxy can be proper url or host[:port]
71 71 if not (proxyurl.startswith('http:') or
72 72 proxyurl.startswith('https:')):
73 73 proxyurl = 'http://' + proxyurl + '/'
74 74 proxy = util.url(proxyurl)
75 75 if not proxy.user:
76 76 proxy.user = ui.config("http_proxy", "user")
77 77 proxy.passwd = ui.config("http_proxy", "passwd")
78 78
79 79 # see if we should use a proxy for this url
80 80 no_list = ["localhost", "127.0.0.1"]
81 81 no_list.extend([p.lower() for
82 82 p in ui.configlist("http_proxy", "no")])
83 83 no_list.extend([p.strip().lower() for
84 84 p in os.getenv("no_proxy", '').split(',')
85 85 if p.strip()])
86 86 # "http_proxy.always" config is for running tests on localhost
87 87 if ui.configbool("http_proxy", "always"):
88 88 self.no_list = []
89 89 else:
90 90 self.no_list = no_list
91 91
92 92 proxyurl = str(proxy)
93 93 proxies = {'http': proxyurl, 'https': proxyurl}
94 94 ui.debug('proxying through http://%s:%s\n' %
95 95 (proxy.host, proxy.port))
96 96 else:
97 97 proxies = {}
98 98
99 99 # urllib2 takes proxy values from the environment and those
100 100 # will take precedence if found. So, if there's a config entry
101 101 # defining a proxy, drop the environment ones
102 102 if ui.config("http_proxy", "host"):
103 103 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
104 104 try:
105 105 if env in os.environ:
106 106 del os.environ[env]
107 107 except OSError:
108 108 pass
109 109
110 110 urllib2.ProxyHandler.__init__(self, proxies)
111 111 self.ui = ui
112 112
113 113 def proxy_open(self, req, proxy, type_):
114 114 host = req.get_host().split(':')[0]
115 115 for e in self.no_list:
116 116 if host == e:
117 117 return None
118 118 if e.startswith('*.') and host.endswith(e[2:]):
119 119 return None
120 120 if e.startswith('.') and host.endswith(e[1:]):
121 121 return None
122 122
123 # work around a bug in Python < 2.4.2
124 # (it leaves a "\n" at the end of Proxy-authorization headers)
125 baseclass = req.__class__
126 class _request(baseclass):
127 def add_header(self, key, val):
128 if key.lower() == 'proxy-authorization':
129 val = val.strip()
130 return baseclass.add_header(self, key, val)
131 req.__class__ = _request
132
133 123 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
134 124
135 125 def _gen_sendfile(orgsend):
136 126 def _sendfile(self, data):
137 127 # send a file
138 128 if isinstance(data, httpconnectionmod.httpsendfile):
139 129 # if auth required, some data sent twice, so rewind here
140 130 data.seek(0)
141 131 for chunk in util.filechunkiter(data):
142 132 orgsend(self, chunk)
143 133 else:
144 134 orgsend(self, data)
145 135 return _sendfile
146 136
147 137 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
148 138 if has_https:
149 139 try:
150 140 _create_connection = socket.create_connection
151 141 except AttributeError:
152 142 _GLOBAL_DEFAULT_TIMEOUT = object()
153 143
154 144 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
155 145 source_address=None):
156 146 # lifted from Python 2.6
157 147
158 148 msg = "getaddrinfo returns an empty list"
159 149 host, port = address
160 150 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
161 151 af, socktype, proto, canonname, sa = res
162 152 sock = None
163 153 try:
164 154 sock = socket.socket(af, socktype, proto)
165 155 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
166 156 sock.settimeout(timeout)
167 157 if source_address:
168 158 sock.bind(source_address)
169 159 sock.connect(sa)
170 160 return sock
171 161
172 162 except socket.error, msg:
173 163 if sock is not None:
174 164 sock.close()
175 165
176 166 raise socket.error(msg)
177 167
178 168 class httpconnection(keepalive.HTTPConnection):
179 169 # must be able to send big bundle as stream.
180 170 send = _gen_sendfile(keepalive.HTTPConnection.send)
181 171
182 172 def connect(self):
183 173 if has_https and self.realhostport: # use CONNECT proxy
184 174 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185 175 self.sock.connect((self.host, self.port))
186 176 if _generic_proxytunnel(self):
187 177 # we do not support client X.509 certificates
188 178 self.sock = sslutil.ssl_wrap_socket(self.sock, None, None,
189 179 serverhostname=self.host)
190 180 else:
191 181 keepalive.HTTPConnection.connect(self)
192 182
193 183 def getresponse(self):
194 184 proxyres = getattr(self, 'proxyres', None)
195 185 if proxyres:
196 186 if proxyres.will_close:
197 187 self.close()
198 188 self.proxyres = None
199 189 return proxyres
200 190 return keepalive.HTTPConnection.getresponse(self)
201 191
202 192 # general transaction handler to support different ways to handle
203 193 # HTTPS proxying before and after Python 2.6.3.
204 194 def _generic_start_transaction(handler, h, req):
205 195 tunnel_host = getattr(req, '_tunnel_host', None)
206 196 if tunnel_host:
207 197 if tunnel_host[:7] not in ['http://', 'https:/']:
208 198 tunnel_host = 'https://' + tunnel_host
209 199 new_tunnel = True
210 200 else:
211 201 tunnel_host = req.get_selector()
212 202 new_tunnel = False
213 203
214 204 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
215 205 u = util.url(tunnel_host)
216 206 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
217 207 h.realhostport = ':'.join([u.host, (u.port or '443')])
218 208 h.headers = req.headers.copy()
219 209 h.headers.update(handler.parent.addheaders)
220 210 return
221 211
222 212 h.realhostport = None
223 213 h.headers = None
224 214
225 215 def _generic_proxytunnel(self):
226 216 proxyheaders = dict(
227 217 [(x, self.headers[x]) for x in self.headers
228 218 if x.lower().startswith('proxy-')])
229 219 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
230 220 for header in proxyheaders.iteritems():
231 221 self.send('%s: %s\r\n' % header)
232 222 self.send('\r\n')
233 223
234 224 # majority of the following code is duplicated from
235 225 # httplib.HTTPConnection as there are no adequate places to
236 226 # override functions to provide the needed functionality
237 227 res = self.response_class(self.sock,
238 228 strict=self.strict,
239 229 method=self._method)
240 230
241 231 while True:
242 232 version, status, reason = res._read_status()
243 233 if status != httplib.CONTINUE:
244 234 break
245 235 while True:
246 236 skip = res.fp.readline().strip()
247 237 if not skip:
248 238 break
249 239 res.status = status
250 240 res.reason = reason.strip()
251 241
252 242 if res.status == 200:
253 243 while True:
254 244 line = res.fp.readline()
255 245 if line == '\r\n':
256 246 break
257 247 return True
258 248
259 249 if version == 'HTTP/1.0':
260 250 res.version = 10
261 251 elif version.startswith('HTTP/1.'):
262 252 res.version = 11
263 253 elif version == 'HTTP/0.9':
264 254 res.version = 9
265 255 else:
266 256 raise httplib.UnknownProtocol(version)
267 257
268 258 if res.version == 9:
269 259 res.length = None
270 260 res.chunked = 0
271 261 res.will_close = 1
272 262 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
273 263 return False
274 264
275 265 res.msg = httplib.HTTPMessage(res.fp)
276 266 res.msg.fp = None
277 267
278 268 # are we using the chunked-style of transfer encoding?
279 269 trenc = res.msg.getheader('transfer-encoding')
280 270 if trenc and trenc.lower() == "chunked":
281 271 res.chunked = 1
282 272 res.chunk_left = None
283 273 else:
284 274 res.chunked = 0
285 275
286 276 # will the connection close at the end of the response?
287 277 res.will_close = res._check_close()
288 278
289 279 # do we have a Content-Length?
290 280 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
291 281 # transfer-encoding is "chunked"
292 282 length = res.msg.getheader('content-length')
293 283 if length and not res.chunked:
294 284 try:
295 285 res.length = int(length)
296 286 except ValueError:
297 287 res.length = None
298 288 else:
299 289 if res.length < 0: # ignore nonsensical negative lengths
300 290 res.length = None
301 291 else:
302 292 res.length = None
303 293
304 294 # does the body have a fixed length? (of zero)
305 295 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
306 296 100 <= status < 200 or # 1xx codes
307 297 res._method == 'HEAD'):
308 298 res.length = 0
309 299
310 300 # if the connection remains open, and we aren't using chunked, and
311 301 # a content-length was not provided, then assume that the connection
312 302 # WILL close.
313 303 if (not res.will_close and
314 304 not res.chunked and
315 305 res.length is None):
316 306 res.will_close = 1
317 307
318 308 self.proxyres = res
319 309
320 310 return False
321 311
322 312 class httphandler(keepalive.HTTPHandler):
323 313 def http_open(self, req):
324 314 return self.do_open(httpconnection, req)
325 315
326 316 def _start_transaction(self, h, req):
327 317 _generic_start_transaction(self, h, req)
328 318 return keepalive.HTTPHandler._start_transaction(self, h, req)
329 319
330 320 if has_https:
331 321 class httpsconnection(httplib.HTTPSConnection):
332 322 response_class = keepalive.HTTPResponse
333 323 # must be able to send big bundle as stream.
334 324 send = _gen_sendfile(keepalive.safesend)
335 325 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
336 326
337 327 def connect(self):
338 328 self.sock = _create_connection((self.host, self.port))
339 329
340 330 host = self.host
341 331 if self.realhostport: # use CONNECT proxy
342 332 _generic_proxytunnel(self)
343 333 host = self.realhostport.rsplit(':', 1)[0]
344 334 self.sock = sslutil.ssl_wrap_socket(
345 335 self.sock, self.key_file, self.cert_file, serverhostname=host,
346 336 **sslutil.sslkwargs(self.ui, host))
347 337 sslutil.validator(self.ui, host)(self.sock)
348 338
349 339 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
350 340 def __init__(self, ui):
351 341 keepalive.KeepAliveHandler.__init__(self)
352 342 urllib2.HTTPSHandler.__init__(self)
353 343 self.ui = ui
354 344 self.pwmgr = passwordmgr(self.ui)
355 345
356 346 def _start_transaction(self, h, req):
357 347 _generic_start_transaction(self, h, req)
358 348 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
359 349
360 350 def https_open(self, req):
361 351 # req.get_full_url() does not contain credentials and we may
362 352 # need them to match the certificates.
363 353 url = req.get_full_url()
364 354 user, password = self.pwmgr.find_stored_password(url)
365 355 res = httpconnectionmod.readauthforuri(self.ui, url, user)
366 356 if res:
367 357 group, auth = res
368 358 self.auth = auth
369 359 self.ui.debug("using auth.%s.* for authentication\n" % group)
370 360 else:
371 361 self.auth = None
372 362 return self.do_open(self._makeconnection, req)
373 363
374 364 def _makeconnection(self, host, port=None, *args, **kwargs):
375 365 keyfile = None
376 366 certfile = None
377 367
378 368 if len(args) >= 1: # key_file
379 369 keyfile = args[0]
380 370 if len(args) >= 2: # cert_file
381 371 certfile = args[1]
382 372 args = args[2:]
383 373
384 374 # if the user has specified different key/cert files in
385 375 # hgrc, we prefer these
386 376 if self.auth and 'key' in self.auth and 'cert' in self.auth:
387 377 keyfile = self.auth['key']
388 378 certfile = self.auth['cert']
389 379
390 380 conn = httpsconnection(host, port, keyfile, certfile, *args,
391 381 **kwargs)
392 382 conn.ui = self.ui
393 383 return conn
394 384
395 385 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
396 386 def __init__(self, *args, **kwargs):
397 387 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
398 388 self.retried_req = None
399 389
400 390 def reset_retry_count(self):
401 391 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
402 392 # forever. We disable reset_retry_count completely and reset in
403 393 # http_error_auth_reqed instead.
404 394 pass
405 395
406 396 def http_error_auth_reqed(self, auth_header, host, req, headers):
407 397 # Reset the retry counter once for each request.
408 398 if req is not self.retried_req:
409 399 self.retried_req = req
410 400 self.retried = 0
411 401 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
412 402 # it doesn't know about the auth type requested. This can happen if
413 403 # somebody is using BasicAuth and types a bad password.
414 404 try:
415 405 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
416 406 self, auth_header, host, req, headers)
417 407 except ValueError, inst:
418 408 arg = inst.args[0]
419 409 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
420 410 return
421 411 raise
422 412
423 413 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
424 414 def __init__(self, *args, **kwargs):
425 415 self.auth = None
426 416 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
427 417 self.retried_req = None
428 418
429 419 def http_request(self, request):
430 420 if self.auth:
431 421 request.add_unredirected_header(self.auth_header, self.auth)
432 422
433 423 return request
434 424
435 425 def https_request(self, request):
436 426 if self.auth:
437 427 request.add_unredirected_header(self.auth_header, self.auth)
438 428
439 429 return request
440 430
441 431 def reset_retry_count(self):
442 432 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
443 433 # forever. We disable reset_retry_count completely and reset in
444 434 # http_error_auth_reqed instead.
445 435 pass
446 436
447 437 def http_error_auth_reqed(self, auth_header, host, req, headers):
448 438 # Reset the retry counter once for each request.
449 439 if req is not self.retried_req:
450 440 self.retried_req = req
451 441 self.retried = 0
452 442 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
453 443 self, auth_header, host, req, headers)
454 444
455 445 def retry_http_basic_auth(self, host, req, realm):
456 446 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
457 447 if pw is not None:
458 448 raw = "%s:%s" % (user, pw)
459 449 auth = 'Basic %s' % base64.b64encode(raw).strip()
460 450 if req.headers.get(self.auth_header, None) == auth:
461 451 return None
462 452 self.auth = auth
463 453 req.add_unredirected_header(self.auth_header, auth)
464 454 return self.parent.open(req)
465 455 else:
466 456 return None
467 457
468 458 handlerfuncs = []
469 459
470 460 def opener(ui, authinfo=None):
471 461 '''
472 462 construct an opener suitable for urllib2
473 463 authinfo will be added to the password manager
474 464 '''
475 465 if ui.configbool('ui', 'usehttp2', False):
476 466 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
477 467 else:
478 468 handlers = [httphandler()]
479 469 if has_https:
480 470 handlers.append(httpshandler(ui))
481 471
482 472 handlers.append(proxyhandler(ui))
483 473
484 474 passmgr = passwordmgr(ui)
485 475 if authinfo is not None:
486 476 passmgr.add_password(*authinfo)
487 477 user, passwd = authinfo[2:4]
488 478 ui.debug('http auth: user %s, password %s\n' %
489 479 (user, passwd and '*' * len(passwd) or 'not set'))
490 480
491 481 handlers.extend((httpbasicauthhandler(passmgr),
492 482 httpdigestauthhandler(passmgr)))
493 483 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
494 484 opener = urllib2.build_opener(*handlers)
495 485
496 486 # 1.0 here is the _protocol_ version
497 487 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
498 488 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
499 489 return opener
500 490
501 491 def open(ui, url_, data=None):
502 492 u = util.url(url_)
503 493 if u.scheme:
504 494 u.scheme = u.scheme.lower()
505 495 url_, authinfo = u.authinfo()
506 496 else:
507 497 path = util.normpath(os.path.abspath(url_))
508 498 url_ = 'file://' + urllib.pathname2url(path)
509 499 authinfo = None
510 500 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now