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'},