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