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