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