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