Mercurial > public > mercurial-scm > hg
comparison mercurial/httprepo.py @ 7270:2db33c1a5654
factor out the url handling from httprepo
Create url.py to handle all the url handling:
- proxy handling
- workaround various python bugs
- handle username/password embedded in the url
author | Benoit Boissinot <benoit.boissinot@ens-lyon.org> |
---|---|
date | Mon, 27 Oct 2008 21:50:01 +0100 |
parents | 95a53961d7a6 |
children | 1f0f84660dea |
comparison
equal
deleted
inserted
replaced
7269:95a53961d7a6 | 7270:2db33c1a5654 |
---|---|
8 | 8 |
9 from node import bin, hex, nullid | 9 from node import bin, hex, nullid |
10 from i18n import _ | 10 from i18n import _ |
11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib | 11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib |
12 import errno, keepalive, socket, changegroup, statichttprepo | 12 import errno, keepalive, socket, changegroup, statichttprepo |
13 | 13 import url |
14 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm): | |
15 def __init__(self, ui): | |
16 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self) | |
17 self.ui = ui | |
18 | |
19 def find_user_password(self, realm, authuri): | |
20 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( | |
21 self, realm, authuri) | |
22 user, passwd = authinfo | |
23 if user and passwd: | |
24 return (user, passwd) | |
25 | |
26 if not self.ui.interactive: | |
27 raise util.Abort(_('http authorization required')) | |
28 | |
29 self.ui.write(_("http authorization required\n")) | |
30 self.ui.status(_("realm: %s\n") % realm) | |
31 if user: | |
32 self.ui.status(_("user: %s\n") % user) | |
33 else: | |
34 user = self.ui.prompt(_("user:"), default=None) | |
35 | |
36 if not passwd: | |
37 passwd = self.ui.getpass() | |
38 | |
39 self.add_password(realm, authuri, user, passwd) | |
40 return (user, passwd) | |
41 | |
42 class proxyhandler(urllib2.ProxyHandler): | |
43 def __init__(self, ui): | |
44 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy') | |
45 # XXX proxyauthinfo = None | |
46 | |
47 if proxyurl: | |
48 # proxy can be proper url or host[:port] | |
49 if not (proxyurl.startswith('http:') or | |
50 proxyurl.startswith('https:')): | |
51 proxyurl = 'http://' + proxyurl + '/' | |
52 snpqf = urlparse.urlsplit(proxyurl) | |
53 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf | |
54 hpup = netlocsplit(proxynetloc) | |
55 | |
56 proxyhost, proxyport, proxyuser, proxypasswd = hpup | |
57 if not proxyuser: | |
58 proxyuser = ui.config("http_proxy", "user") | |
59 proxypasswd = ui.config("http_proxy", "passwd") | |
60 | |
61 # see if we should use a proxy for this url | |
62 no_list = [ "localhost", "127.0.0.1" ] | |
63 no_list.extend([p.lower() for | |
64 p in ui.configlist("http_proxy", "no")]) | |
65 no_list.extend([p.strip().lower() for | |
66 p in os.getenv("no_proxy", '').split(',') | |
67 if p.strip()]) | |
68 # "http_proxy.always" config is for running tests on localhost | |
69 if ui.configbool("http_proxy", "always"): | |
70 self.no_list = [] | |
71 else: | |
72 self.no_list = no_list | |
73 | |
74 proxyurl = urlparse.urlunsplit(( | |
75 proxyscheme, netlocunsplit(proxyhost, proxyport, | |
76 proxyuser, proxypasswd or ''), | |
77 proxypath, proxyquery, proxyfrag)) | |
78 proxies = {'http': proxyurl, 'https': proxyurl} | |
79 ui.debug(_('proxying through http://%s:%s\n') % | |
80 (proxyhost, proxyport)) | |
81 else: | |
82 proxies = {} | |
83 | |
84 # urllib2 takes proxy values from the environment and those | |
85 # will take precedence if found, so drop them | |
86 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]: | |
87 try: | |
88 if env in os.environ: | |
89 del os.environ[env] | |
90 except OSError: | |
91 pass | |
92 | |
93 urllib2.ProxyHandler.__init__(self, proxies) | |
94 self.ui = ui | |
95 | |
96 def proxy_open(self, req, proxy, type): | |
97 host = req.get_host().split(':')[0] | |
98 if host in self.no_list: | |
99 return None | |
100 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type) | |
101 | |
102 def netlocsplit(netloc): | |
103 '''split [user[:passwd]@]host[:port] into 4-tuple.''' | |
104 | |
105 a = netloc.find('@') | |
106 if a == -1: | |
107 user, passwd = None, None | |
108 else: | |
109 userpass, netloc = netloc[:a], netloc[a+1:] | |
110 c = userpass.find(':') | |
111 if c == -1: | |
112 user, passwd = urllib.unquote(userpass), None | |
113 else: | |
114 user = urllib.unquote(userpass[:c]) | |
115 passwd = urllib.unquote(userpass[c+1:]) | |
116 c = netloc.find(':') | |
117 if c == -1: | |
118 host, port = netloc, None | |
119 else: | |
120 host, port = netloc[:c], netloc[c+1:] | |
121 return host, port, user, passwd | |
122 | |
123 def netlocunsplit(host, port, user=None, passwd=None): | |
124 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].''' | |
125 if port: | |
126 hostport = host + ':' + port | |
127 else: | |
128 hostport = host | |
129 if user: | |
130 if passwd: | |
131 userpass = urllib.quote(user) + ':' + urllib.quote(passwd) | |
132 else: | |
133 userpass = urllib.quote(user) | |
134 return userpass + '@' + hostport | |
135 return hostport | |
136 | |
137 # work around a bug in Python < 2.4.2 | |
138 # (it leaves a "\n" at the end of Proxy-authorization headers) | |
139 class request(urllib2.Request): | |
140 def add_header(self, key, val): | |
141 if key.lower() == 'proxy-authorization': | |
142 val = val.strip() | |
143 return urllib2.Request.add_header(self, key, val) | |
144 | |
145 class httpsendfile(file): | |
146 def __len__(self): | |
147 return os.fstat(self.fileno()).st_size | |
148 | |
149 def _gen_sendfile(connection): | |
150 def _sendfile(self, data): | |
151 # send a file | |
152 if isinstance(data, httpsendfile): | |
153 # if auth required, some data sent twice, so rewind here | |
154 data.seek(0) | |
155 for chunk in util.filechunkiter(data): | |
156 connection.send(self, chunk) | |
157 else: | |
158 connection.send(self, data) | |
159 return _sendfile | |
160 | |
161 class httpconnection(keepalive.HTTPConnection): | |
162 # must be able to send big bundle as stream. | |
163 send = _gen_sendfile(keepalive.HTTPConnection) | |
164 | |
165 class httphandler(keepalive.HTTPHandler): | |
166 def http_open(self, req): | |
167 return self.do_open(httpconnection, req) | |
168 | |
169 def __del__(self): | |
170 self.close_all() | |
171 | |
172 has_https = hasattr(urllib2, 'HTTPSHandler') | |
173 if has_https: | |
174 class httpsconnection(httplib.HTTPSConnection): | |
175 response_class = keepalive.HTTPResponse | |
176 # must be able to send big bundle as stream. | |
177 send = _gen_sendfile(httplib.HTTPSConnection) | |
178 | |
179 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): | |
180 def https_open(self, req): | |
181 return self.do_open(httpsconnection, req) | |
182 | |
183 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if | |
184 # it doesn't know about the auth type requested. This can happen if | |
185 # somebody is using BasicAuth and types a bad password. | |
186 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler): | |
187 def http_error_auth_reqed(self, auth_header, host, req, headers): | |
188 try: | |
189 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed( | |
190 self, auth_header, host, req, headers) | |
191 except ValueError, inst: | |
192 arg = inst.args[0] | |
193 if arg.startswith("AbstractDigestAuthHandler doesn't know "): | |
194 return | |
195 raise | |
196 | 14 |
197 def zgenerator(f): | 15 def zgenerator(f): |
198 zd = zlib.decompressobj() | 16 zd = zlib.decompressobj() |
199 try: | 17 try: |
200 for chunk in util.filechunkiter(f): | 18 for chunk in util.filechunkiter(f): |
201 yield zd.decompress(chunk) | 19 yield zd.decompress(chunk) |
202 except httplib.HTTPException, inst: | 20 except httplib.HTTPException, inst: |
203 raise IOError(None, _('connection ended unexpectedly')) | 21 raise IOError(None, _('connection ended unexpectedly')) |
204 yield zd.flush() | 22 yield zd.flush() |
205 | |
206 _safe = ('abcdefghijklmnopqrstuvwxyz' | |
207 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' | |
208 '0123456789' '_.-/') | |
209 _safeset = None | |
210 _hex = None | |
211 def quotepath(path): | |
212 '''quote the path part of a URL | |
213 | |
214 This is similar to urllib.quote, but it also tries to avoid | |
215 quoting things twice (inspired by wget): | |
216 | |
217 >>> quotepath('abc def') | |
218 'abc%20def' | |
219 >>> quotepath('abc%20def') | |
220 'abc%20def' | |
221 >>> quotepath('abc%20 def') | |
222 'abc%20%20def' | |
223 >>> quotepath('abc def%20') | |
224 'abc%20def%20' | |
225 >>> quotepath('abc def%2') | |
226 'abc%20def%252' | |
227 >>> quotepath('abc def%') | |
228 'abc%20def%25' | |
229 ''' | |
230 global _safeset, _hex | |
231 if _safeset is None: | |
232 _safeset = util.set(_safe) | |
233 _hex = util.set('abcdefABCDEF0123456789') | |
234 l = list(path) | |
235 for i in xrange(len(l)): | |
236 c = l[i] | |
237 if c == '%' and i + 2 < len(l) and (l[i+1] in _hex and l[i+2] in _hex): | |
238 pass | |
239 elif c not in _safeset: | |
240 l[i] = '%%%02X' % ord(c) | |
241 return ''.join(l) | |
242 | 23 |
243 class httprepository(repo.repository): | 24 class httprepository(repo.repository): |
244 def __init__(self, ui, path): | 25 def __init__(self, ui, path): |
245 self.path = path | 26 self.path = path |
246 self.caps = None | 27 self.caps = None |
247 self.handler = None | 28 self.handler = None |
248 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) | 29 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) |
249 if query or frag: | 30 if query or frag: |
250 raise util.Abort(_('unsupported URL component: "%s"') % | 31 raise util.Abort(_('unsupported URL component: "%s"') % |
251 (query or frag)) | 32 (query or frag)) |
252 if not urlpath: | |
253 urlpath = '/' | |
254 urlpath = quotepath(urlpath) | |
255 host, port, user, passwd = netlocsplit(netloc) | |
256 | 33 |
257 # urllib cannot handle URLs with embedded user or passwd | 34 # urllib cannot handle URLs with embedded user or passwd |
258 self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port), | 35 self._url, authinfo = url.getauthinfo(path) |
259 urlpath, '', '')) | 36 |
260 self.ui = ui | 37 self.ui = ui |
261 self.ui.debug(_('using %s\n') % self._url) | 38 self.ui.debug(_('using %s\n') % self._url) |
262 | 39 |
263 handlers = [httphandler()] | 40 self.urlopener = url.opener(ui, authinfo) |
264 if has_https: | |
265 handlers.append(httpshandler()) | |
266 | |
267 handlers.append(proxyhandler(ui)) | |
268 | |
269 passmgr = passwordmgr(ui) | |
270 if user: | |
271 ui.debug(_('http auth: user %s, password %s\n') % | |
272 (user, passwd and '*' * len(passwd) or 'not set')) | |
273 netloc = host | |
274 if port: | |
275 netloc += ':' + port | |
276 # Python < 2.4.3 uses only the netloc to search for a password | |
277 passmgr.add_password(None, (self._url, netloc), user, passwd or '') | |
278 | |
279 handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr), | |
280 httpdigestauthhandler(passmgr))) | |
281 opener = urllib2.build_opener(*handlers) | |
282 | |
283 # 1.0 here is the _protocol_ version | |
284 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')] | |
285 opener.addheaders.append(('Accept', 'application/mercurial-0.1')) | |
286 urllib2.install_opener(opener) | |
287 | 41 |
288 def url(self): | 42 def url(self): |
289 return self.path | 43 return self.path |
290 | 44 |
291 # look up capabilities only when needed | 45 # look up capabilities only when needed |
314 qs = '?%s' % urllib.urlencode(q) | 68 qs = '?%s' % urllib.urlencode(q) |
315 cu = "%s%s" % (self._url, qs) | 69 cu = "%s%s" % (self._url, qs) |
316 try: | 70 try: |
317 if data: | 71 if data: |
318 self.ui.debug(_("sending %s bytes\n") % len(data)) | 72 self.ui.debug(_("sending %s bytes\n") % len(data)) |
319 resp = urllib2.urlopen(request(cu, data, headers)) | 73 resp = self.urlopener.open(urllib2.Request(cu, data, headers)) |
320 except urllib2.HTTPError, inst: | 74 except urllib2.HTTPError, inst: |
321 if inst.code == 401: | 75 if inst.code == 401: |
322 raise util.Abort(_('authorization failed')) | 76 raise util.Abort(_('authorization failed')) |
323 raise | 77 raise |
324 except httplib.HTTPException, inst: | 78 except httplib.HTTPException, inst: |
431 if x in changegroup.bundletypes: | 185 if x in changegroup.bundletypes: |
432 type = x | 186 type = x |
433 break | 187 break |
434 | 188 |
435 tempname = changegroup.writebundle(cg, None, type) | 189 tempname = changegroup.writebundle(cg, None, type) |
436 fp = httpsendfile(tempname, "rb") | 190 fp = url.httpsendfile(tempname, "rb") |
437 try: | 191 try: |
438 try: | 192 try: |
439 resp = self.do_read( | 193 resp = self.do_read( |
440 'unbundle', data=fp, | 194 'unbundle', data=fp, |
441 headers={'Content-Type': 'application/octet-stream'}, | 195 headers={'Content-Type': 'application/octet-stream'}, |