##// END OF EJS Templates
url: use url.url in proxyhandler
Brodie Rao -
r13820:65b89e80 default
parent child Browse files
Show More
@@ -1,948 +1,937 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 import urllib, urllib2, urlparse, httplib, os, socket, cStringIO
10 import urllib, urllib2, httplib, os, socket, cStringIO
11 11 import __builtin__
12 12 from i18n import _
13 13 import keepalive, util
14 14
15 15 class url(object):
16 16 """Reliable URL parser.
17 17
18 18 This parses URLs and provides attributes for the following
19 19 components:
20 20
21 21 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
22 22
23 23 Missing components are set to None. The only exception is
24 24 fragment, which is set to '' if present but empty.
25 25
26 26 If parse_fragment is False, fragment is included in query. If
27 27 parse_query is False, query is included in path. If both are
28 28 False, both fragment and query are included in path.
29 29
30 30 See http://www.ietf.org/rfc/rfc2396.txt for more information.
31 31
32 32 Note that for backward compatibility reasons, bundle URLs do not
33 33 take host names. That means 'bundle://../' has a path of '../'.
34 34
35 35 Examples:
36 36
37 37 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
38 38 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
39 39 >>> url('ssh://[::1]:2200//home/joe/repo')
40 40 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
41 41 >>> url('file:///home/joe/repo')
42 42 <url scheme: 'file', path: '/home/joe/repo'>
43 43 >>> url('bundle:foo')
44 44 <url scheme: 'bundle', path: 'foo'>
45 45 >>> url('bundle://../foo')
46 46 <url scheme: 'bundle', path: '../foo'>
47 47 >>> url('c:\\\\foo\\\\bar')
48 48 <url path: 'c:\\\\foo\\\\bar'>
49 49
50 50 Authentication credentials:
51 51
52 52 >>> url('ssh://joe:xyz@x/repo')
53 53 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
54 54 >>> url('ssh://joe@x/repo')
55 55 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
56 56
57 57 Query strings and fragments:
58 58
59 59 >>> url('http://host/a?b#c')
60 60 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
61 61 >>> url('http://host/a?b#c', parse_query=False, parse_fragment=False)
62 62 <url scheme: 'http', host: 'host', path: 'a?b#c'>
63 63 """
64 64
65 65 _safechars = "!~*'()+"
66 66 _safepchars = "/!~*'()+"
67 67
68 68 def __init__(self, path, parse_query=True, parse_fragment=True):
69 69 # We slowly chomp away at path until we have only the path left
70 70 self.scheme = self.user = self.passwd = self.host = None
71 71 self.port = self.path = self.query = self.fragment = None
72 72 self._localpath = True
73 73
74 74 # special case for Windows drive letters
75 75 if has_drive_letter(path):
76 76 self.path = path
77 77 return
78 78
79 79 # For compatibility reasons, we can't handle bundle paths as
80 80 # normal URLS
81 81 if path.startswith('bundle:'):
82 82 self.scheme = 'bundle'
83 83 path = path[7:]
84 84 if path.startswith('//'):
85 85 path = path[2:]
86 86 self.path = path
87 87 return
88 88
89 89 if not path.startswith('/') and ':' in path:
90 90 parts = path.split(':', 1)
91 91 if parts[0]:
92 92 self.scheme, path = parts
93 93 self._localpath = False
94 94
95 95 if not path:
96 96 path = None
97 97 if self._localpath:
98 98 self.path = ''
99 99 return
100 100 else:
101 101 if parse_fragment and '#' in path:
102 102 path, self.fragment = path.split('#', 1)
103 103 if not path:
104 104 path = None
105 105 if self._localpath:
106 106 self.path = path
107 107 return
108 108
109 109 if parse_query and '?' in path:
110 110 path, self.query = path.split('?', 1)
111 111 if not path:
112 112 path = None
113 113 if not self.query:
114 114 self.query = None
115 115
116 116 # // is required to specify a host/authority
117 117 if path and path.startswith('//'):
118 118 parts = path[2:].split('/', 1)
119 119 if len(parts) > 1:
120 120 self.host, path = parts
121 121 path = path
122 122 else:
123 123 self.host = parts[0]
124 124 path = None
125 125 if not self.host:
126 126 self.host = None
127 127 if path:
128 128 path = '/' + path
129 129
130 130 if self.host and '@' in self.host:
131 131 self.user, self.host = self.host.rsplit('@', 1)
132 132 if ':' in self.user:
133 133 self.user, self.passwd = self.user.split(':', 1)
134 134 if not self.host:
135 135 self.host = None
136 136
137 137 # Don't split on colons in IPv6 addresses without ports
138 138 if (self.host and ':' in self.host and
139 139 not (self.host.startswith('[') and self.host.endswith(']'))):
140 140 self.host, self.port = self.host.rsplit(':', 1)
141 141 if not self.host:
142 142 self.host = None
143 143
144 144 if (self.host and self.scheme == 'file' and
145 145 self.host not in ('localhost', '127.0.0.1', '[::1]')):
146 146 raise util.Abort(_('file:// URLs can only refer to localhost'))
147 147
148 148 self.path = path
149 149
150 150 for a in ('user', 'passwd', 'host', 'port',
151 151 'path', 'query', 'fragment'):
152 152 v = getattr(self, a)
153 153 if v is not None:
154 154 setattr(self, a, urllib.unquote(v))
155 155
156 156 def __repr__(self):
157 157 attrs = []
158 158 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
159 159 'query', 'fragment'):
160 160 v = getattr(self, a)
161 161 if v is not None:
162 162 attrs.append('%s: %r' % (a, v))
163 163 return '<url %s>' % ', '.join(attrs)
164 164
165 165 def __str__(self):
166 166 """Join the URL's components back into a URL string.
167 167
168 168 Examples:
169 169
170 170 >>> str(url('http://user:pw@host:80/?foo#bar'))
171 171 'http://user:pw@host:80/?foo#bar'
172 172 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
173 173 'ssh://user:pw@[::1]:2200//home/joe#'
174 174 >>> str(url('http://localhost:80//'))
175 175 'http://localhost:80//'
176 176 >>> str(url('http://localhost:80/'))
177 177 'http://localhost:80/'
178 178 >>> str(url('http://localhost:80'))
179 179 'http://localhost:80/'
180 180 >>> str(url('bundle:foo'))
181 181 'bundle:foo'
182 182 >>> str(url('bundle://../foo'))
183 183 'bundle:../foo'
184 184 >>> str(url('path'))
185 185 'path'
186 186 """
187 187 if self._localpath:
188 188 s = self.path
189 189 if self.scheme == 'bundle':
190 190 s = 'bundle:' + s
191 191 if self.fragment:
192 192 s += '#' + self.fragment
193 193 return s
194 194
195 195 s = self.scheme + ':'
196 196 if (self.user or self.passwd or self.host or
197 197 self.scheme and not self.path):
198 198 s += '//'
199 199 if self.user:
200 200 s += urllib.quote(self.user, safe=self._safechars)
201 201 if self.passwd:
202 202 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
203 203 if self.user or self.passwd:
204 204 s += '@'
205 205 if self.host:
206 206 if not (self.host.startswith('[') and self.host.endswith(']')):
207 207 s += urllib.quote(self.host)
208 208 else:
209 209 s += self.host
210 210 if self.port:
211 211 s += ':' + urllib.quote(self.port)
212 212 if self.host:
213 213 s += '/'
214 214 if self.path:
215 215 s += urllib.quote(self.path, safe=self._safepchars)
216 216 if self.query:
217 217 s += '?' + urllib.quote(self.query, safe=self._safepchars)
218 218 if self.fragment is not None:
219 219 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
220 220 return s
221 221
222 222 def authinfo(self):
223 223 user, passwd = self.user, self.passwd
224 224 try:
225 225 self.user, self.passwd = None, None
226 226 s = str(self)
227 227 finally:
228 228 self.user, self.passwd = user, passwd
229 229 if not self.user:
230 230 return (s, None)
231 231 return (s, (None, (str(self), self.host),
232 232 self.user, self.passwd or ''))
233 233
234 234 def has_scheme(path):
235 235 return bool(url(path).scheme)
236 236
237 237 def has_drive_letter(path):
238 238 return path[1:2] == ':' and path[0:1].isalpha()
239 239
240 240 def hidepassword(u):
241 241 '''hide user credential in a url string'''
242 242 u = url(u)
243 243 if u.passwd:
244 244 u.passwd = '***'
245 245 return str(u)
246 246
247 247 def removeauth(u):
248 248 '''remove all authentication information from a url string'''
249 249 u = url(u)
250 250 u.user = u.passwd = None
251 251 return str(u)
252 252
253 253 def netlocsplit(netloc):
254 254 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
255 255
256 256 a = netloc.find('@')
257 257 if a == -1:
258 258 user, passwd = None, None
259 259 else:
260 260 userpass, netloc = netloc[:a], netloc[a + 1:]
261 261 c = userpass.find(':')
262 262 if c == -1:
263 263 user, passwd = urllib.unquote(userpass), None
264 264 else:
265 265 user = urllib.unquote(userpass[:c])
266 266 passwd = urllib.unquote(userpass[c + 1:])
267 267 c = netloc.find(':')
268 268 if c == -1:
269 269 host, port = netloc, None
270 270 else:
271 271 host, port = netloc[:c], netloc[c + 1:]
272 272 return host, port, user, passwd
273 273
274 274 def netlocunsplit(host, port, user=None, passwd=None):
275 275 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
276 276 if port:
277 277 hostport = host + ':' + port
278 278 else:
279 279 hostport = host
280 280 if user:
281 281 quote = lambda s: urllib.quote(s, safe='')
282 282 if passwd:
283 283 userpass = quote(user) + ':' + quote(passwd)
284 284 else:
285 285 userpass = quote(user)
286 286 return userpass + '@' + hostport
287 287 return hostport
288 288
289 289 def readauthforuri(ui, uri):
290 290 # Read configuration
291 291 config = dict()
292 292 for key, val in ui.configitems('auth'):
293 293 if '.' not in key:
294 294 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
295 295 continue
296 296 group, setting = key.rsplit('.', 1)
297 297 gdict = config.setdefault(group, dict())
298 298 if setting in ('username', 'cert', 'key'):
299 299 val = util.expandpath(val)
300 300 gdict[setting] = val
301 301
302 302 # Find the best match
303 303 scheme, hostpath = uri.split('://', 1)
304 304 bestlen = 0
305 305 bestauth = None
306 306 for group, auth in config.iteritems():
307 307 prefix = auth.get('prefix')
308 308 if not prefix:
309 309 continue
310 310 p = prefix.split('://', 1)
311 311 if len(p) > 1:
312 312 schemes, prefix = [p[0]], p[1]
313 313 else:
314 314 schemes = (auth.get('schemes') or 'https').split()
315 315 if (prefix == '*' or hostpath.startswith(prefix)) and \
316 316 len(prefix) > bestlen and scheme in schemes:
317 317 bestlen = len(prefix)
318 318 bestauth = group, auth
319 319 return bestauth
320 320
321 321 _safe = ('abcdefghijklmnopqrstuvwxyz'
322 322 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
323 323 '0123456789' '_.-/')
324 324 _safeset = None
325 325 _hex = None
326 326 def quotepath(path):
327 327 '''quote the path part of a URL
328 328
329 329 This is similar to urllib.quote, but it also tries to avoid
330 330 quoting things twice (inspired by wget):
331 331
332 332 >>> quotepath('abc def')
333 333 'abc%20def'
334 334 >>> quotepath('abc%20def')
335 335 'abc%20def'
336 336 >>> quotepath('abc%20 def')
337 337 'abc%20%20def'
338 338 >>> quotepath('abc def%20')
339 339 'abc%20def%20'
340 340 >>> quotepath('abc def%2')
341 341 'abc%20def%252'
342 342 >>> quotepath('abc def%')
343 343 'abc%20def%25'
344 344 '''
345 345 global _safeset, _hex
346 346 if _safeset is None:
347 347 _safeset = set(_safe)
348 348 _hex = set('abcdefABCDEF0123456789')
349 349 l = list(path)
350 350 for i in xrange(len(l)):
351 351 c = l[i]
352 352 if (c == '%' and i + 2 < len(l) and
353 353 l[i + 1] in _hex and l[i + 2] in _hex):
354 354 pass
355 355 elif c not in _safeset:
356 356 l[i] = '%%%02X' % ord(c)
357 357 return ''.join(l)
358 358
359 359 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
360 360 def __init__(self, ui):
361 361 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
362 362 self.ui = ui
363 363
364 364 def find_user_password(self, realm, authuri):
365 365 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
366 366 self, realm, authuri)
367 367 user, passwd = authinfo
368 368 if user and passwd:
369 369 self._writedebug(user, passwd)
370 370 return (user, passwd)
371 371
372 372 if not user:
373 373 res = readauthforuri(self.ui, authuri)
374 374 if res:
375 375 group, auth = res
376 376 user, passwd = auth.get('username'), auth.get('password')
377 377 self.ui.debug("using auth.%s.* for authentication\n" % group)
378 378 if not user or not passwd:
379 379 if not self.ui.interactive():
380 380 raise util.Abort(_('http authorization required'))
381 381
382 382 self.ui.write(_("http authorization required\n"))
383 383 self.ui.write(_("realm: %s\n") % realm)
384 384 if user:
385 385 self.ui.write(_("user: %s\n") % user)
386 386 else:
387 387 user = self.ui.prompt(_("user:"), default=None)
388 388
389 389 if not passwd:
390 390 passwd = self.ui.getpass()
391 391
392 392 self.add_password(realm, authuri, user, passwd)
393 393 self._writedebug(user, passwd)
394 394 return (user, passwd)
395 395
396 396 def _writedebug(self, user, passwd):
397 397 msg = _('http auth: user %s, password %s\n')
398 398 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
399 399
400 400 class proxyhandler(urllib2.ProxyHandler):
401 401 def __init__(self, ui):
402 402 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
403 403 # XXX proxyauthinfo = None
404 404
405 405 if proxyurl:
406 406 # proxy can be proper url or host[:port]
407 407 if not (proxyurl.startswith('http:') or
408 408 proxyurl.startswith('https:')):
409 409 proxyurl = 'http://' + proxyurl + '/'
410 snpqf = urlparse.urlsplit(proxyurl)
411 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
412 hpup = netlocsplit(proxynetloc)
413
414 proxyhost, proxyport, proxyuser, proxypasswd = hpup
415 if not proxyuser:
416 proxyuser = ui.config("http_proxy", "user")
417 proxypasswd = ui.config("http_proxy", "passwd")
410 proxy = url(proxyurl)
411 if not proxy.user:
412 proxy.user = ui.config("http_proxy", "user")
413 proxy.passwd = ui.config("http_proxy", "passwd")
418 414
419 415 # see if we should use a proxy for this url
420 416 no_list = ["localhost", "127.0.0.1"]
421 417 no_list.extend([p.lower() for
422 418 p in ui.configlist("http_proxy", "no")])
423 419 no_list.extend([p.strip().lower() for
424 420 p in os.getenv("no_proxy", '').split(',')
425 421 if p.strip()])
426 422 # "http_proxy.always" config is for running tests on localhost
427 423 if ui.configbool("http_proxy", "always"):
428 424 self.no_list = []
429 425 else:
430 426 self.no_list = no_list
431 427
432 proxyurl = urlparse.urlunsplit((
433 proxyscheme, netlocunsplit(proxyhost, proxyport,
434 proxyuser, proxypasswd or ''),
435 proxypath, proxyquery, proxyfrag))
428 proxyurl = str(proxy)
436 429 proxies = {'http': proxyurl, 'https': proxyurl}
437 430 ui.debug('proxying through http://%s:%s\n' %
438 (proxyhost, proxyport))
431 (proxy.host, proxy.port))
439 432 else:
440 433 proxies = {}
441 434
442 435 # urllib2 takes proxy values from the environment and those
443 436 # will take precedence if found, so drop them
444 437 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
445 438 try:
446 439 if env in os.environ:
447 440 del os.environ[env]
448 441 except OSError:
449 442 pass
450 443
451 444 urllib2.ProxyHandler.__init__(self, proxies)
452 445 self.ui = ui
453 446
454 447 def proxy_open(self, req, proxy, type_):
455 448 host = req.get_host().split(':')[0]
456 449 if host in self.no_list:
457 450 return None
458 451
459 452 # work around a bug in Python < 2.4.2
460 453 # (it leaves a "\n" at the end of Proxy-authorization headers)
461 454 baseclass = req.__class__
462 455 class _request(baseclass):
463 456 def add_header(self, key, val):
464 457 if key.lower() == 'proxy-authorization':
465 458 val = val.strip()
466 459 return baseclass.add_header(self, key, val)
467 460 req.__class__ = _request
468 461
469 462 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
470 463
471 464 class httpsendfile(object):
472 465 """This is a wrapper around the objects returned by python's "open".
473 466
474 467 Its purpose is to send file-like objects via HTTP and, to do so, it
475 468 defines a __len__ attribute to feed the Content-Length header.
476 469 """
477 470
478 471 def __init__(self, ui, *args, **kwargs):
479 472 # We can't just "self._data = open(*args, **kwargs)" here because there
480 473 # is an "open" function defined in this module that shadows the global
481 474 # one
482 475 self.ui = ui
483 476 self._data = __builtin__.open(*args, **kwargs)
484 477 self.seek = self._data.seek
485 478 self.close = self._data.close
486 479 self.write = self._data.write
487 480 self._len = os.fstat(self._data.fileno()).st_size
488 481 self._pos = 0
489 482 self._total = len(self) / 1024 * 2
490 483
491 484 def read(self, *args, **kwargs):
492 485 try:
493 486 ret = self._data.read(*args, **kwargs)
494 487 except EOFError:
495 488 self.ui.progress(_('sending'), None)
496 489 self._pos += len(ret)
497 490 # We pass double the max for total because we currently have
498 491 # to send the bundle twice in the case of a server that
499 492 # requires authentication. Since we can't know until we try
500 493 # once whether authentication will be required, just lie to
501 494 # the user and maybe the push succeeds suddenly at 50%.
502 495 self.ui.progress(_('sending'), self._pos / 1024,
503 496 unit=_('kb'), total=self._total)
504 497 return ret
505 498
506 499 def __len__(self):
507 500 return self._len
508 501
509 502 def _gen_sendfile(orgsend):
510 503 def _sendfile(self, data):
511 504 # send a file
512 505 if isinstance(data, httpsendfile):
513 506 # if auth required, some data sent twice, so rewind here
514 507 data.seek(0)
515 508 for chunk in util.filechunkiter(data):
516 509 orgsend(self, chunk)
517 510 else:
518 511 orgsend(self, data)
519 512 return _sendfile
520 513
521 514 has_https = hasattr(urllib2, 'HTTPSHandler')
522 515 if has_https:
523 516 try:
524 517 # avoid using deprecated/broken FakeSocket in python 2.6
525 518 import ssl
526 519 _ssl_wrap_socket = ssl.wrap_socket
527 520 CERT_REQUIRED = ssl.CERT_REQUIRED
528 521 except ImportError:
529 522 CERT_REQUIRED = 2
530 523
531 524 def _ssl_wrap_socket(sock, key_file, cert_file,
532 525 cert_reqs=CERT_REQUIRED, ca_certs=None):
533 526 if ca_certs:
534 527 raise util.Abort(_(
535 528 'certificate checking requires Python 2.6'))
536 529
537 530 ssl = socket.ssl(sock, key_file, cert_file)
538 531 return httplib.FakeSocket(sock, ssl)
539 532
540 533 try:
541 534 _create_connection = socket.create_connection
542 535 except AttributeError:
543 536 _GLOBAL_DEFAULT_TIMEOUT = object()
544 537
545 538 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
546 539 source_address=None):
547 540 # lifted from Python 2.6
548 541
549 542 msg = "getaddrinfo returns an empty list"
550 543 host, port = address
551 544 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
552 545 af, socktype, proto, canonname, sa = res
553 546 sock = None
554 547 try:
555 548 sock = socket.socket(af, socktype, proto)
556 549 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
557 550 sock.settimeout(timeout)
558 551 if source_address:
559 552 sock.bind(source_address)
560 553 sock.connect(sa)
561 554 return sock
562 555
563 556 except socket.error, msg:
564 557 if sock is not None:
565 558 sock.close()
566 559
567 560 raise socket.error, msg
568 561
569 562 class httpconnection(keepalive.HTTPConnection):
570 563 # must be able to send big bundle as stream.
571 564 send = _gen_sendfile(keepalive.HTTPConnection.send)
572 565
573 566 def connect(self):
574 567 if has_https and self.realhostport: # use CONNECT proxy
575 568 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
576 569 self.sock.connect((self.host, self.port))
577 570 if _generic_proxytunnel(self):
578 571 # we do not support client x509 certificates
579 572 self.sock = _ssl_wrap_socket(self.sock, None, None)
580 573 else:
581 574 keepalive.HTTPConnection.connect(self)
582 575
583 576 def getresponse(self):
584 577 proxyres = getattr(self, 'proxyres', None)
585 578 if proxyres:
586 579 if proxyres.will_close:
587 580 self.close()
588 581 self.proxyres = None
589 582 return proxyres
590 583 return keepalive.HTTPConnection.getresponse(self)
591 584
592 585 # general transaction handler to support different ways to handle
593 586 # HTTPS proxying before and after Python 2.6.3.
594 587 def _generic_start_transaction(handler, h, req):
595 588 if hasattr(req, '_tunnel_host') and req._tunnel_host:
596 589 tunnel_host = req._tunnel_host
597 590 if tunnel_host[:7] not in ['http://', 'https:/']:
598 591 tunnel_host = 'https://' + tunnel_host
599 592 new_tunnel = True
600 593 else:
601 594 tunnel_host = req.get_selector()
602 595 new_tunnel = False
603 596
604 597 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
605 urlparts = urlparse.urlparse(tunnel_host)
606 if new_tunnel or urlparts[0] == 'https': # only use CONNECT for HTTPS
607 realhostport = urlparts[1]
608 if realhostport[-1] == ']' or ':' not in realhostport:
609 realhostport += ':443'
610
611 h.realhostport = realhostport
598 u = url(tunnel_host)
599 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
600 h.realhostport = ':'.join([u.host, (u.port or '443')])
612 601 h.headers = req.headers.copy()
613 602 h.headers.update(handler.parent.addheaders)
614 603 return
615 604
616 605 h.realhostport = None
617 606 h.headers = None
618 607
619 608 def _generic_proxytunnel(self):
620 609 proxyheaders = dict(
621 610 [(x, self.headers[x]) for x in self.headers
622 611 if x.lower().startswith('proxy-')])
623 612 self._set_hostport(self.host, self.port)
624 613 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
625 614 for header in proxyheaders.iteritems():
626 615 self.send('%s: %s\r\n' % header)
627 616 self.send('\r\n')
628 617
629 618 # majority of the following code is duplicated from
630 619 # httplib.HTTPConnection as there are no adequate places to
631 620 # override functions to provide the needed functionality
632 621 res = self.response_class(self.sock,
633 622 strict=self.strict,
634 623 method=self._method)
635 624
636 625 while True:
637 626 version, status, reason = res._read_status()
638 627 if status != httplib.CONTINUE:
639 628 break
640 629 while True:
641 630 skip = res.fp.readline().strip()
642 631 if not skip:
643 632 break
644 633 res.status = status
645 634 res.reason = reason.strip()
646 635
647 636 if res.status == 200:
648 637 while True:
649 638 line = res.fp.readline()
650 639 if line == '\r\n':
651 640 break
652 641 return True
653 642
654 643 if version == 'HTTP/1.0':
655 644 res.version = 10
656 645 elif version.startswith('HTTP/1.'):
657 646 res.version = 11
658 647 elif version == 'HTTP/0.9':
659 648 res.version = 9
660 649 else:
661 650 raise httplib.UnknownProtocol(version)
662 651
663 652 if res.version == 9:
664 653 res.length = None
665 654 res.chunked = 0
666 655 res.will_close = 1
667 656 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
668 657 return False
669 658
670 659 res.msg = httplib.HTTPMessage(res.fp)
671 660 res.msg.fp = None
672 661
673 662 # are we using the chunked-style of transfer encoding?
674 663 trenc = res.msg.getheader('transfer-encoding')
675 664 if trenc and trenc.lower() == "chunked":
676 665 res.chunked = 1
677 666 res.chunk_left = None
678 667 else:
679 668 res.chunked = 0
680 669
681 670 # will the connection close at the end of the response?
682 671 res.will_close = res._check_close()
683 672
684 673 # do we have a Content-Length?
685 674 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
686 675 length = res.msg.getheader('content-length')
687 676 if length and not res.chunked:
688 677 try:
689 678 res.length = int(length)
690 679 except ValueError:
691 680 res.length = None
692 681 else:
693 682 if res.length < 0: # ignore nonsensical negative lengths
694 683 res.length = None
695 684 else:
696 685 res.length = None
697 686
698 687 # does the body have a fixed length? (of zero)
699 688 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
700 689 100 <= status < 200 or # 1xx codes
701 690 res._method == 'HEAD'):
702 691 res.length = 0
703 692
704 693 # if the connection remains open, and we aren't using chunked, and
705 694 # a content-length was not provided, then assume that the connection
706 695 # WILL close.
707 696 if (not res.will_close and
708 697 not res.chunked and
709 698 res.length is None):
710 699 res.will_close = 1
711 700
712 701 self.proxyres = res
713 702
714 703 return False
715 704
716 705 class httphandler(keepalive.HTTPHandler):
717 706 def http_open(self, req):
718 707 return self.do_open(httpconnection, req)
719 708
720 709 def _start_transaction(self, h, req):
721 710 _generic_start_transaction(self, h, req)
722 711 return keepalive.HTTPHandler._start_transaction(self, h, req)
723 712
724 713 def _verifycert(cert, hostname):
725 714 '''Verify that cert (in socket.getpeercert() format) matches hostname.
726 715 CRLs is not handled.
727 716
728 717 Returns error message if any problems are found and None on success.
729 718 '''
730 719 if not cert:
731 720 return _('no certificate received')
732 721 dnsname = hostname.lower()
733 722 def matchdnsname(certname):
734 723 return (certname == dnsname or
735 724 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
736 725
737 726 san = cert.get('subjectAltName', [])
738 727 if san:
739 728 certnames = [value.lower() for key, value in san if key == 'DNS']
740 729 for name in certnames:
741 730 if matchdnsname(name):
742 731 return None
743 732 return _('certificate is for %s') % ', '.join(certnames)
744 733
745 734 # subject is only checked when subjectAltName is empty
746 735 for s in cert.get('subject', []):
747 736 key, value = s[0]
748 737 if key == 'commonName':
749 738 try:
750 739 # 'subject' entries are unicode
751 740 certname = value.lower().encode('ascii')
752 741 except UnicodeEncodeError:
753 742 return _('IDN in certificate not supported')
754 743 if matchdnsname(certname):
755 744 return None
756 745 return _('certificate is for %s') % certname
757 746 return _('no commonName or subjectAltName found in certificate')
758 747
759 748 if has_https:
760 749 class httpsconnection(httplib.HTTPSConnection):
761 750 response_class = keepalive.HTTPResponse
762 751 # must be able to send big bundle as stream.
763 752 send = _gen_sendfile(keepalive.safesend)
764 753 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
765 754
766 755 def connect(self):
767 756 self.sock = _create_connection((self.host, self.port))
768 757
769 758 host = self.host
770 759 if self.realhostport: # use CONNECT proxy
771 760 something = _generic_proxytunnel(self)
772 761 host = self.realhostport.rsplit(':', 1)[0]
773 762
774 763 cacerts = self.ui.config('web', 'cacerts')
775 764 hostfingerprint = self.ui.config('hostfingerprints', host)
776 765
777 766 if cacerts and not hostfingerprint:
778 767 cacerts = util.expandpath(cacerts)
779 768 if not os.path.exists(cacerts):
780 769 raise util.Abort(_('could not find '
781 770 'web.cacerts: %s') % cacerts)
782 771 self.sock = _ssl_wrap_socket(self.sock, self.key_file,
783 772 self.cert_file, cert_reqs=CERT_REQUIRED,
784 773 ca_certs=cacerts)
785 774 msg = _verifycert(self.sock.getpeercert(), host)
786 775 if msg:
787 776 raise util.Abort(_('%s certificate error: %s '
788 777 '(use --insecure to connect '
789 778 'insecurely)') % (host, msg))
790 779 self.ui.debug('%s certificate successfully verified\n' % host)
791 780 else:
792 781 self.sock = _ssl_wrap_socket(self.sock, self.key_file,
793 782 self.cert_file)
794 783 if hasattr(self.sock, 'getpeercert'):
795 784 peercert = self.sock.getpeercert(True)
796 785 peerfingerprint = util.sha1(peercert).hexdigest()
797 786 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
798 787 for x in xrange(0, len(peerfingerprint), 2)])
799 788 if hostfingerprint:
800 789 if peerfingerprint.lower() != \
801 790 hostfingerprint.replace(':', '').lower():
802 791 raise util.Abort(_('invalid certificate for %s '
803 792 'with fingerprint %s') %
804 793 (host, nicefingerprint))
805 794 self.ui.debug('%s certificate matched fingerprint %s\n' %
806 795 (host, nicefingerprint))
807 796 else:
808 797 self.ui.warn(_('warning: %s certificate '
809 798 'with fingerprint %s not verified '
810 799 '(check hostfingerprints or web.cacerts '
811 800 'config setting)\n') %
812 801 (host, nicefingerprint))
813 802 else: # python 2.5 ?
814 803 if hostfingerprint:
815 804 raise util.Abort(_('no certificate for %s with '
816 805 'configured hostfingerprint') % host)
817 806 self.ui.warn(_('warning: %s certificate not verified '
818 807 '(check web.cacerts config setting)\n') %
819 808 host)
820 809
821 810 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
822 811 def __init__(self, ui):
823 812 keepalive.KeepAliveHandler.__init__(self)
824 813 urllib2.HTTPSHandler.__init__(self)
825 814 self.ui = ui
826 815 self.pwmgr = passwordmgr(self.ui)
827 816
828 817 def _start_transaction(self, h, req):
829 818 _generic_start_transaction(self, h, req)
830 819 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
831 820
832 821 def https_open(self, req):
833 822 res = readauthforuri(self.ui, req.get_full_url())
834 823 if res:
835 824 group, auth = res
836 825 self.auth = auth
837 826 self.ui.debug("using auth.%s.* for authentication\n" % group)
838 827 else:
839 828 self.auth = None
840 829 return self.do_open(self._makeconnection, req)
841 830
842 831 def _makeconnection(self, host, port=None, *args, **kwargs):
843 832 keyfile = None
844 833 certfile = None
845 834
846 835 if len(args) >= 1: # key_file
847 836 keyfile = args[0]
848 837 if len(args) >= 2: # cert_file
849 838 certfile = args[1]
850 839 args = args[2:]
851 840
852 841 # if the user has specified different key/cert files in
853 842 # hgrc, we prefer these
854 843 if self.auth and 'key' in self.auth and 'cert' in self.auth:
855 844 keyfile = self.auth['key']
856 845 certfile = self.auth['cert']
857 846
858 847 conn = httpsconnection(host, port, keyfile, certfile, *args, **kwargs)
859 848 conn.ui = self.ui
860 849 return conn
861 850
862 851 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
863 852 def __init__(self, *args, **kwargs):
864 853 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
865 854 self.retried_req = None
866 855
867 856 def reset_retry_count(self):
868 857 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
869 858 # forever. We disable reset_retry_count completely and reset in
870 859 # http_error_auth_reqed instead.
871 860 pass
872 861
873 862 def http_error_auth_reqed(self, auth_header, host, req, headers):
874 863 # Reset the retry counter once for each request.
875 864 if req is not self.retried_req:
876 865 self.retried_req = req
877 866 self.retried = 0
878 867 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
879 868 # it doesn't know about the auth type requested. This can happen if
880 869 # somebody is using BasicAuth and types a bad password.
881 870 try:
882 871 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
883 872 self, auth_header, host, req, headers)
884 873 except ValueError, inst:
885 874 arg = inst.args[0]
886 875 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
887 876 return
888 877 raise
889 878
890 879 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
891 880 def __init__(self, *args, **kwargs):
892 881 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
893 882 self.retried_req = None
894 883
895 884 def reset_retry_count(self):
896 885 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
897 886 # forever. We disable reset_retry_count completely and reset in
898 887 # http_error_auth_reqed instead.
899 888 pass
900 889
901 890 def http_error_auth_reqed(self, auth_header, host, req, headers):
902 891 # Reset the retry counter once for each request.
903 892 if req is not self.retried_req:
904 893 self.retried_req = req
905 894 self.retried = 0
906 895 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
907 896 self, auth_header, host, req, headers)
908 897
909 898 handlerfuncs = []
910 899
911 900 def opener(ui, authinfo=None):
912 901 '''
913 902 construct an opener suitable for urllib2
914 903 authinfo will be added to the password manager
915 904 '''
916 905 handlers = [httphandler()]
917 906 if has_https:
918 907 handlers.append(httpshandler(ui))
919 908
920 909 handlers.append(proxyhandler(ui))
921 910
922 911 passmgr = passwordmgr(ui)
923 912 if authinfo is not None:
924 913 passmgr.add_password(*authinfo)
925 914 user, passwd = authinfo[2:4]
926 915 ui.debug('http auth: user %s, password %s\n' %
927 916 (user, passwd and '*' * len(passwd) or 'not set'))
928 917
929 918 handlers.extend((httpbasicauthhandler(passmgr),
930 919 httpdigestauthhandler(passmgr)))
931 920 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
932 921 opener = urllib2.build_opener(*handlers)
933 922
934 923 # 1.0 here is the _protocol_ version
935 924 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
936 925 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
937 926 return opener
938 927
939 928 def open(ui, url_, data=None):
940 929 u = url(url_)
941 930 if u.scheme:
942 931 u.scheme = u.scheme.lower()
943 932 url_, authinfo = u.authinfo()
944 933 else:
945 934 path = util.normpath(os.path.abspath(url_))
946 935 url_ = 'file://' + urllib.pathname2url(path)
947 936 authinfo = None
948 937 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now