|
1 # url.py - HTTP handling for mercurial |
|
2 # |
|
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com> |
|
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br> |
|
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> |
|
6 # |
|
7 # This software may be used and distributed according to the terms of the |
|
8 # GNU General Public License version 2 or any later version. |
|
9 |
|
10 import urllib, urllib2, urlparse, httplib, os, re, socket, cStringIO |
|
11 import __builtin__ |
|
12 from i18n import _ |
|
13 import keepalive, util |
|
14 |
|
15 def _urlunparse(scheme, netloc, path, params, query, fragment, url): |
|
16 '''Handle cases where urlunparse(urlparse(x://)) doesn't preserve the "//"''' |
|
17 result = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) |
|
18 if (scheme and |
|
19 result.startswith(scheme + ':') and |
|
20 not result.startswith(scheme + '://') and |
|
21 url.startswith(scheme + '://') |
|
22 ): |
|
23 result = scheme + '://' + result[len(scheme + ':'):] |
|
24 return result |
|
25 |
|
26 def hidepassword(url): |
|
27 '''hide user credential in a url string''' |
|
28 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) |
|
29 netloc = re.sub('([^:]*):([^@]*)@(.*)', r'\1:***@\3', netloc) |
|
30 return _urlunparse(scheme, netloc, path, params, query, fragment, url) |
|
31 |
|
32 def removeauth(url): |
|
33 '''remove all authentication information from a url string''' |
|
34 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) |
|
35 netloc = netloc[netloc.find('@')+1:] |
|
36 return _urlunparse(scheme, netloc, path, params, query, fragment, url) |
|
37 |
|
38 def netlocsplit(netloc): |
|
39 '''split [user[:passwd]@]host[:port] into 4-tuple.''' |
|
40 |
|
41 a = netloc.find('@') |
|
42 if a == -1: |
|
43 user, passwd = None, None |
|
44 else: |
|
45 userpass, netloc = netloc[:a], netloc[a + 1:] |
|
46 c = userpass.find(':') |
|
47 if c == -1: |
|
48 user, passwd = urllib.unquote(userpass), None |
|
49 else: |
|
50 user = urllib.unquote(userpass[:c]) |
|
51 passwd = urllib.unquote(userpass[c + 1:]) |
|
52 c = netloc.find(':') |
|
53 if c == -1: |
|
54 host, port = netloc, None |
|
55 else: |
|
56 host, port = netloc[:c], netloc[c + 1:] |
|
57 return host, port, user, passwd |
|
58 |
|
59 def netlocunsplit(host, port, user=None, passwd=None): |
|
60 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].''' |
|
61 if port: |
|
62 hostport = host + ':' + port |
|
63 else: |
|
64 hostport = host |
|
65 if user: |
|
66 quote = lambda s: urllib.quote(s, safe='') |
|
67 if passwd: |
|
68 userpass = quote(user) + ':' + quote(passwd) |
|
69 else: |
|
70 userpass = quote(user) |
|
71 return userpass + '@' + hostport |
|
72 return hostport |
|
73 |
|
74 _safe = ('abcdefghijklmnopqrstuvwxyz' |
|
75 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
|
76 '0123456789' '_.-/') |
|
77 _safeset = None |
|
78 _hex = None |
|
79 def quotepath(path): |
|
80 '''quote the path part of a URL |
|
81 |
|
82 This is similar to urllib.quote, but it also tries to avoid |
|
83 quoting things twice (inspired by wget): |
|
84 |
|
85 >>> quotepath('abc def') |
|
86 'abc%20def' |
|
87 >>> quotepath('abc%20def') |
|
88 'abc%20def' |
|
89 >>> quotepath('abc%20 def') |
|
90 'abc%20%20def' |
|
91 >>> quotepath('abc def%20') |
|
92 'abc%20def%20' |
|
93 >>> quotepath('abc def%2') |
|
94 'abc%20def%252' |
|
95 >>> quotepath('abc def%') |
|
96 'abc%20def%25' |
|
97 ''' |
|
98 global _safeset, _hex |
|
99 if _safeset is None: |
|
100 _safeset = set(_safe) |
|
101 _hex = set('abcdefABCDEF0123456789') |
|
102 l = list(path) |
|
103 for i in xrange(len(l)): |
|
104 c = l[i] |
|
105 if (c == '%' and i + 2 < len(l) and |
|
106 l[i + 1] in _hex and l[i + 2] in _hex): |
|
107 pass |
|
108 elif c not in _safeset: |
|
109 l[i] = '%%%02X' % ord(c) |
|
110 return ''.join(l) |
|
111 |
|
112 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm): |
|
113 def __init__(self, ui): |
|
114 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self) |
|
115 self.ui = ui |
|
116 |
|
117 def find_user_password(self, realm, authuri): |
|
118 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( |
|
119 self, realm, authuri) |
|
120 user, passwd = authinfo |
|
121 if user and passwd: |
|
122 self._writedebug(user, passwd) |
|
123 return (user, passwd) |
|
124 |
|
125 if not user: |
|
126 auth = self.readauthtoken(authuri) |
|
127 if auth: |
|
128 user, passwd = auth.get('username'), auth.get('password') |
|
129 if not user or not passwd: |
|
130 if not self.ui.interactive(): |
|
131 raise util.Abort(_('http authorization required')) |
|
132 |
|
133 self.ui.write(_("http authorization required\n")) |
|
134 self.ui.write(_("realm: %s\n") % realm) |
|
135 if user: |
|
136 self.ui.write(_("user: %s\n") % user) |
|
137 else: |
|
138 user = self.ui.prompt(_("user:"), default=None) |
|
139 |
|
140 if not passwd: |
|
141 passwd = self.ui.getpass() |
|
142 |
|
143 self.add_password(realm, authuri, user, passwd) |
|
144 self._writedebug(user, passwd) |
|
145 return (user, passwd) |
|
146 |
|
147 def _writedebug(self, user, passwd): |
|
148 msg = _('http auth: user %s, password %s\n') |
|
149 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set')) |
|
150 |
|
151 def readauthtoken(self, uri): |
|
152 # Read configuration |
|
153 config = dict() |
|
154 for key, val in self.ui.configitems('auth'): |
|
155 if '.' not in key: |
|
156 self.ui.warn(_("ignoring invalid [auth] key '%s'\n") % key) |
|
157 continue |
|
158 group, setting = key.split('.', 1) |
|
159 gdict = config.setdefault(group, dict()) |
|
160 if setting in ('username', 'cert', 'key'): |
|
161 val = util.expandpath(val) |
|
162 gdict[setting] = val |
|
163 |
|
164 # Find the best match |
|
165 scheme, hostpath = uri.split('://', 1) |
|
166 bestlen = 0 |
|
167 bestauth = None |
|
168 for auth in config.itervalues(): |
|
169 prefix = auth.get('prefix') |
|
170 if not prefix: |
|
171 continue |
|
172 p = prefix.split('://', 1) |
|
173 if len(p) > 1: |
|
174 schemes, prefix = [p[0]], p[1] |
|
175 else: |
|
176 schemes = (auth.get('schemes') or 'https').split() |
|
177 if (prefix == '*' or hostpath.startswith(prefix)) and \ |
|
178 len(prefix) > bestlen and scheme in schemes: |
|
179 bestlen = len(prefix) |
|
180 bestauth = auth |
|
181 return bestauth |
|
182 |
|
183 class proxyhandler(urllib2.ProxyHandler): |
|
184 def __init__(self, ui): |
|
185 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy') |
|
186 # XXX proxyauthinfo = None |
|
187 |
|
188 if proxyurl: |
|
189 # proxy can be proper url or host[:port] |
|
190 if not (proxyurl.startswith('http:') or |
|
191 proxyurl.startswith('https:')): |
|
192 proxyurl = 'http://' + proxyurl + '/' |
|
193 snpqf = urlparse.urlsplit(proxyurl) |
|
194 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf |
|
195 hpup = netlocsplit(proxynetloc) |
|
196 |
|
197 proxyhost, proxyport, proxyuser, proxypasswd = hpup |
|
198 if not proxyuser: |
|
199 proxyuser = ui.config("http_proxy", "user") |
|
200 proxypasswd = ui.config("http_proxy", "passwd") |
|
201 |
|
202 # see if we should use a proxy for this url |
|
203 no_list = ["localhost", "127.0.0.1"] |
|
204 no_list.extend([p.lower() for |
|
205 p in ui.configlist("http_proxy", "no")]) |
|
206 no_list.extend([p.strip().lower() for |
|
207 p in os.getenv("no_proxy", '').split(',') |
|
208 if p.strip()]) |
|
209 # "http_proxy.always" config is for running tests on localhost |
|
210 if ui.configbool("http_proxy", "always"): |
|
211 self.no_list = [] |
|
212 else: |
|
213 self.no_list = no_list |
|
214 |
|
215 proxyurl = urlparse.urlunsplit(( |
|
216 proxyscheme, netlocunsplit(proxyhost, proxyport, |
|
217 proxyuser, proxypasswd or ''), |
|
218 proxypath, proxyquery, proxyfrag)) |
|
219 proxies = {'http': proxyurl, 'https': proxyurl} |
|
220 ui.debug('proxying through http://%s:%s\n' % |
|
221 (proxyhost, proxyport)) |
|
222 else: |
|
223 proxies = {} |
|
224 |
|
225 # urllib2 takes proxy values from the environment and those |
|
226 # will take precedence if found, so drop them |
|
227 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]: |
|
228 try: |
|
229 if env in os.environ: |
|
230 del os.environ[env] |
|
231 except OSError: |
|
232 pass |
|
233 |
|
234 urllib2.ProxyHandler.__init__(self, proxies) |
|
235 self.ui = ui |
|
236 |
|
237 def proxy_open(self, req, proxy, type_): |
|
238 host = req.get_host().split(':')[0] |
|
239 if host in self.no_list: |
|
240 return None |
|
241 |
|
242 # work around a bug in Python < 2.4.2 |
|
243 # (it leaves a "\n" at the end of Proxy-authorization headers) |
|
244 baseclass = req.__class__ |
|
245 class _request(baseclass): |
|
246 def add_header(self, key, val): |
|
247 if key.lower() == 'proxy-authorization': |
|
248 val = val.strip() |
|
249 return baseclass.add_header(self, key, val) |
|
250 req.__class__ = _request |
|
251 |
|
252 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_) |
|
253 |
|
254 class httpsendfile(object): |
|
255 """This is a wrapper around the objects returned by python's "open". |
|
256 |
|
257 Its purpose is to send file-like objects via HTTP and, to do so, it |
|
258 defines a __len__ attribute to feed the Content-Length header. |
|
259 """ |
|
260 |
|
261 def __init__(self, *args, **kwargs): |
|
262 # We can't just "self._data = open(*args, **kwargs)" here because there |
|
263 # is an "open" function defined in this module that shadows the global |
|
264 # one |
|
265 self._data = __builtin__.open(*args, **kwargs) |
|
266 self.read = self._data.read |
|
267 self.seek = self._data.seek |
|
268 self.close = self._data.close |
|
269 self.write = self._data.write |
|
270 |
|
271 def __len__(self): |
|
272 return os.fstat(self._data.fileno()).st_size |
|
273 |
|
274 def _gen_sendfile(connection): |
|
275 def _sendfile(self, data): |
|
276 # send a file |
|
277 if isinstance(data, httpsendfile): |
|
278 # if auth required, some data sent twice, so rewind here |
|
279 data.seek(0) |
|
280 for chunk in util.filechunkiter(data): |
|
281 connection.send(self, chunk) |
|
282 else: |
|
283 connection.send(self, data) |
|
284 return _sendfile |
|
285 |
|
286 has_https = hasattr(urllib2, 'HTTPSHandler') |
|
287 if has_https: |
|
288 try: |
|
289 # avoid using deprecated/broken FakeSocket in python 2.6 |
|
290 import ssl |
|
291 _ssl_wrap_socket = ssl.wrap_socket |
|
292 CERT_REQUIRED = ssl.CERT_REQUIRED |
|
293 except ImportError: |
|
294 CERT_REQUIRED = 2 |
|
295 |
|
296 def _ssl_wrap_socket(sock, key_file, cert_file, |
|
297 cert_reqs=CERT_REQUIRED, ca_certs=None): |
|
298 if ca_certs: |
|
299 raise util.Abort(_( |
|
300 'certificate checking requires Python 2.6')) |
|
301 |
|
302 ssl = socket.ssl(sock, key_file, cert_file) |
|
303 return httplib.FakeSocket(sock, ssl) |
|
304 |
|
305 try: |
|
306 _create_connection = socket.create_connection |
|
307 except AttributeError: |
|
308 _GLOBAL_DEFAULT_TIMEOUT = object() |
|
309 |
|
310 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, |
|
311 source_address=None): |
|
312 # lifted from Python 2.6 |
|
313 |
|
314 msg = "getaddrinfo returns an empty list" |
|
315 host, port = address |
|
316 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): |
|
317 af, socktype, proto, canonname, sa = res |
|
318 sock = None |
|
319 try: |
|
320 sock = socket.socket(af, socktype, proto) |
|
321 if timeout is not _GLOBAL_DEFAULT_TIMEOUT: |
|
322 sock.settimeout(timeout) |
|
323 if source_address: |
|
324 sock.bind(source_address) |
|
325 sock.connect(sa) |
|
326 return sock |
|
327 |
|
328 except socket.error, msg: |
|
329 if sock is not None: |
|
330 sock.close() |
|
331 |
|
332 raise socket.error, msg |
|
333 |
|
334 class httpconnection(keepalive.HTTPConnection): |
|
335 # must be able to send big bundle as stream. |
|
336 send = _gen_sendfile(keepalive.HTTPConnection) |
|
337 |
|
338 def connect(self): |
|
339 if has_https and self.realhostport: # use CONNECT proxy |
|
340 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
341 self.sock.connect((self.host, self.port)) |
|
342 if _generic_proxytunnel(self): |
|
343 # we do not support client x509 certificates |
|
344 self.sock = _ssl_wrap_socket(self.sock, None, None) |
|
345 else: |
|
346 keepalive.HTTPConnection.connect(self) |
|
347 |
|
348 def getresponse(self): |
|
349 proxyres = getattr(self, 'proxyres', None) |
|
350 if proxyres: |
|
351 if proxyres.will_close: |
|
352 self.close() |
|
353 self.proxyres = None |
|
354 return proxyres |
|
355 return keepalive.HTTPConnection.getresponse(self) |
|
356 |
|
357 # general transaction handler to support different ways to handle |
|
358 # HTTPS proxying before and after Python 2.6.3. |
|
359 def _generic_start_transaction(handler, h, req): |
|
360 if hasattr(req, '_tunnel_host') and req._tunnel_host: |
|
361 tunnel_host = req._tunnel_host |
|
362 if tunnel_host[:7] not in ['http://', 'https:/']: |
|
363 tunnel_host = 'https://' + tunnel_host |
|
364 new_tunnel = True |
|
365 else: |
|
366 tunnel_host = req.get_selector() |
|
367 new_tunnel = False |
|
368 |
|
369 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy |
|
370 urlparts = urlparse.urlparse(tunnel_host) |
|
371 if new_tunnel or urlparts[0] == 'https': # only use CONNECT for HTTPS |
|
372 realhostport = urlparts[1] |
|
373 if realhostport[-1] == ']' or ':' not in realhostport: |
|
374 realhostport += ':443' |
|
375 |
|
376 h.realhostport = realhostport |
|
377 h.headers = req.headers.copy() |
|
378 h.headers.update(handler.parent.addheaders) |
|
379 return |
|
380 |
|
381 h.realhostport = None |
|
382 h.headers = None |
|
383 |
|
384 def _generic_proxytunnel(self): |
|
385 proxyheaders = dict( |
|
386 [(x, self.headers[x]) for x in self.headers |
|
387 if x.lower().startswith('proxy-')]) |
|
388 self._set_hostport(self.host, self.port) |
|
389 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport) |
|
390 for header in proxyheaders.iteritems(): |
|
391 self.send('%s: %s\r\n' % header) |
|
392 self.send('\r\n') |
|
393 |
|
394 # majority of the following code is duplicated from |
|
395 # httplib.HTTPConnection as there are no adequate places to |
|
396 # override functions to provide the needed functionality |
|
397 res = self.response_class(self.sock, |
|
398 strict=self.strict, |
|
399 method=self._method) |
|
400 |
|
401 while True: |
|
402 version, status, reason = res._read_status() |
|
403 if status != httplib.CONTINUE: |
|
404 break |
|
405 while True: |
|
406 skip = res.fp.readline().strip() |
|
407 if not skip: |
|
408 break |
|
409 res.status = status |
|
410 res.reason = reason.strip() |
|
411 |
|
412 if res.status == 200: |
|
413 while True: |
|
414 line = res.fp.readline() |
|
415 if line == '\r\n': |
|
416 break |
|
417 return True |
|
418 |
|
419 if version == 'HTTP/1.0': |
|
420 res.version = 10 |
|
421 elif version.startswith('HTTP/1.'): |
|
422 res.version = 11 |
|
423 elif version == 'HTTP/0.9': |
|
424 res.version = 9 |
|
425 else: |
|
426 raise httplib.UnknownProtocol(version) |
|
427 |
|
428 if res.version == 9: |
|
429 res.length = None |
|
430 res.chunked = 0 |
|
431 res.will_close = 1 |
|
432 res.msg = httplib.HTTPMessage(cStringIO.StringIO()) |
|
433 return False |
|
434 |
|
435 res.msg = httplib.HTTPMessage(res.fp) |
|
436 res.msg.fp = None |
|
437 |
|
438 # are we using the chunked-style of transfer encoding? |
|
439 trenc = res.msg.getheader('transfer-encoding') |
|
440 if trenc and trenc.lower() == "chunked": |
|
441 res.chunked = 1 |
|
442 res.chunk_left = None |
|
443 else: |
|
444 res.chunked = 0 |
|
445 |
|
446 # will the connection close at the end of the response? |
|
447 res.will_close = res._check_close() |
|
448 |
|
449 # do we have a Content-Length? |
|
450 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked" |
|
451 length = res.msg.getheader('content-length') |
|
452 if length and not res.chunked: |
|
453 try: |
|
454 res.length = int(length) |
|
455 except ValueError: |
|
456 res.length = None |
|
457 else: |
|
458 if res.length < 0: # ignore nonsensical negative lengths |
|
459 res.length = None |
|
460 else: |
|
461 res.length = None |
|
462 |
|
463 # does the body have a fixed length? (of zero) |
|
464 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or |
|
465 100 <= status < 200 or # 1xx codes |
|
466 res._method == 'HEAD'): |
|
467 res.length = 0 |
|
468 |
|
469 # if the connection remains open, and we aren't using chunked, and |
|
470 # a content-length was not provided, then assume that the connection |
|
471 # WILL close. |
|
472 if (not res.will_close and |
|
473 not res.chunked and |
|
474 res.length is None): |
|
475 res.will_close = 1 |
|
476 |
|
477 self.proxyres = res |
|
478 |
|
479 return False |
|
480 |
|
481 class httphandler(keepalive.HTTPHandler): |
|
482 def http_open(self, req): |
|
483 return self.do_open(httpconnection, req) |
|
484 |
|
485 def _start_transaction(self, h, req): |
|
486 _generic_start_transaction(self, h, req) |
|
487 return keepalive.HTTPHandler._start_transaction(self, h, req) |
|
488 |
|
489 def _verifycert(cert, hostname): |
|
490 '''Verify that cert (in socket.getpeercert() format) matches hostname. |
|
491 CRLs and subjectAltName are not handled. |
|
492 |
|
493 Returns error message if any problems are found and None on success. |
|
494 ''' |
|
495 if not cert: |
|
496 return _('no certificate received') |
|
497 dnsname = hostname.lower() |
|
498 for s in cert.get('subject', []): |
|
499 key, value = s[0] |
|
500 if key == 'commonName': |
|
501 certname = value.lower() |
|
502 if (certname == dnsname or |
|
503 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]): |
|
504 return None |
|
505 return _('certificate is for %s') % certname |
|
506 return _('no commonName found in certificate') |
|
507 |
|
508 if has_https: |
|
509 class BetterHTTPS(httplib.HTTPSConnection): |
|
510 send = keepalive.safesend |
|
511 |
|
512 def connect(self): |
|
513 if hasattr(self, 'ui'): |
|
514 cacerts = self.ui.config('web', 'cacerts') |
|
515 else: |
|
516 cacerts = None |
|
517 |
|
518 if cacerts: |
|
519 sock = _create_connection((self.host, self.port)) |
|
520 self.sock = _ssl_wrap_socket(sock, self.key_file, |
|
521 self.cert_file, cert_reqs=CERT_REQUIRED, |
|
522 ca_certs=cacerts) |
|
523 msg = _verifycert(self.sock.getpeercert(), self.host) |
|
524 if msg: |
|
525 raise util.Abort(_('%s certificate error: %s') % |
|
526 (self.host, msg)) |
|
527 self.ui.debug('%s certificate successfully verified\n' % |
|
528 self.host) |
|
529 else: |
|
530 self.ui.warn(_("warning: %s certificate not verified " |
|
531 "(check web.cacerts config setting)\n") % |
|
532 self.host) |
|
533 httplib.HTTPSConnection.connect(self) |
|
534 |
|
535 class httpsconnection(BetterHTTPS): |
|
536 response_class = keepalive.HTTPResponse |
|
537 # must be able to send big bundle as stream. |
|
538 send = _gen_sendfile(BetterHTTPS) |
|
539 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection) |
|
540 |
|
541 def connect(self): |
|
542 if self.realhostport: # use CONNECT proxy |
|
543 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
544 self.sock.connect((self.host, self.port)) |
|
545 if _generic_proxytunnel(self): |
|
546 self.sock = _ssl_wrap_socket(self.sock, self.key_file, |
|
547 self.cert_file) |
|
548 else: |
|
549 BetterHTTPS.connect(self) |
|
550 |
|
551 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): |
|
552 def __init__(self, ui): |
|
553 keepalive.KeepAliveHandler.__init__(self) |
|
554 urllib2.HTTPSHandler.__init__(self) |
|
555 self.ui = ui |
|
556 self.pwmgr = passwordmgr(self.ui) |
|
557 |
|
558 def _start_transaction(self, h, req): |
|
559 _generic_start_transaction(self, h, req) |
|
560 return keepalive.KeepAliveHandler._start_transaction(self, h, req) |
|
561 |
|
562 def https_open(self, req): |
|
563 self.auth = self.pwmgr.readauthtoken(req.get_full_url()) |
|
564 return self.do_open(self._makeconnection, req) |
|
565 |
|
566 def _makeconnection(self, host, port=None, *args, **kwargs): |
|
567 keyfile = None |
|
568 certfile = None |
|
569 |
|
570 if len(args) >= 1: # key_file |
|
571 keyfile = args[0] |
|
572 if len(args) >= 2: # cert_file |
|
573 certfile = args[1] |
|
574 args = args[2:] |
|
575 |
|
576 # if the user has specified different key/cert files in |
|
577 # hgrc, we prefer these |
|
578 if self.auth and 'key' in self.auth and 'cert' in self.auth: |
|
579 keyfile = self.auth['key'] |
|
580 certfile = self.auth['cert'] |
|
581 |
|
582 conn = httpsconnection(host, port, keyfile, certfile, *args, **kwargs) |
|
583 conn.ui = self.ui |
|
584 return conn |
|
585 |
|
586 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler): |
|
587 def __init__(self, *args, **kwargs): |
|
588 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs) |
|
589 self.retried_req = None |
|
590 |
|
591 def reset_retry_count(self): |
|
592 # Python 2.6.5 will call this on 401 or 407 errors and thus loop |
|
593 # forever. We disable reset_retry_count completely and reset in |
|
594 # http_error_auth_reqed instead. |
|
595 pass |
|
596 |
|
597 def http_error_auth_reqed(self, auth_header, host, req, headers): |
|
598 # Reset the retry counter once for each request. |
|
599 if req is not self.retried_req: |
|
600 self.retried_req = req |
|
601 self.retried = 0 |
|
602 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if |
|
603 # it doesn't know about the auth type requested. This can happen if |
|
604 # somebody is using BasicAuth and types a bad password. |
|
605 try: |
|
606 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed( |
|
607 self, auth_header, host, req, headers) |
|
608 except ValueError, inst: |
|
609 arg = inst.args[0] |
|
610 if arg.startswith("AbstractDigestAuthHandler doesn't know "): |
|
611 return |
|
612 raise |
|
613 |
|
614 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler): |
|
615 def __init__(self, *args, **kwargs): |
|
616 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs) |
|
617 self.retried_req = None |
|
618 |
|
619 def reset_retry_count(self): |
|
620 # Python 2.6.5 will call this on 401 or 407 errors and thus loop |
|
621 # forever. We disable reset_retry_count completely and reset in |
|
622 # http_error_auth_reqed instead. |
|
623 pass |
|
624 |
|
625 def http_error_auth_reqed(self, auth_header, host, req, headers): |
|
626 # Reset the retry counter once for each request. |
|
627 if req is not self.retried_req: |
|
628 self.retried_req = req |
|
629 self.retried = 0 |
|
630 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed( |
|
631 self, auth_header, host, req, headers) |
|
632 |
|
633 def getauthinfo(path): |
|
634 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) |
|
635 if not urlpath: |
|
636 urlpath = '/' |
|
637 if scheme != 'file': |
|
638 # XXX: why are we quoting the path again with some smart |
|
639 # heuristic here? Anyway, it cannot be done with file:// |
|
640 # urls since path encoding is os/fs dependent (see |
|
641 # urllib.pathname2url() for details). |
|
642 urlpath = quotepath(urlpath) |
|
643 host, port, user, passwd = netlocsplit(netloc) |
|
644 |
|
645 # urllib cannot handle URLs with embedded user or passwd |
|
646 url = urlparse.urlunsplit((scheme, netlocunsplit(host, port), |
|
647 urlpath, query, frag)) |
|
648 if user: |
|
649 netloc = host |
|
650 if port: |
|
651 netloc += ':' + port |
|
652 # Python < 2.4.3 uses only the netloc to search for a password |
|
653 authinfo = (None, (url, netloc), user, passwd or '') |
|
654 else: |
|
655 authinfo = None |
|
656 return url, authinfo |
|
657 |
|
658 handlerfuncs = [] |
|
659 |
|
660 def opener(ui, authinfo=None): |
|
661 ''' |
|
662 construct an opener suitable for urllib2 |
|
663 authinfo will be added to the password manager |
|
664 ''' |
|
665 handlers = [httphandler()] |
|
666 if has_https: |
|
667 handlers.append(httpshandler(ui)) |
|
668 |
|
669 handlers.append(proxyhandler(ui)) |
|
670 |
|
671 passmgr = passwordmgr(ui) |
|
672 if authinfo is not None: |
|
673 passmgr.add_password(*authinfo) |
|
674 user, passwd = authinfo[2:4] |
|
675 ui.debug('http auth: user %s, password %s\n' % |
|
676 (user, passwd and '*' * len(passwd) or 'not set')) |
|
677 |
|
678 handlers.extend((httpbasicauthhandler(passmgr), |
|
679 httpdigestauthhandler(passmgr))) |
|
680 handlers.extend([h(ui, passmgr) for h in handlerfuncs]) |
|
681 opener = urllib2.build_opener(*handlers) |
|
682 |
|
683 # 1.0 here is the _protocol_ version |
|
684 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')] |
|
685 opener.addheaders.append(('Accept', 'application/mercurial-0.1')) |
|
686 return opener |
|
687 |
|
688 scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://') |
|
689 |
|
690 def open(ui, url, data=None): |
|
691 scheme = None |
|
692 m = scheme_re.search(url) |
|
693 if m: |
|
694 scheme = m.group(1).lower() |
|
695 if not scheme: |
|
696 path = util.normpath(os.path.abspath(url)) |
|
697 url = 'file://' + urllib.pathname2url(path) |
|
698 authinfo = None |
|
699 else: |
|
700 url, authinfo = getauthinfo(url) |
|
701 return opener(ui, authinfo).open(url, data) |