##// END OF EJS Templates
Work around urllib2 digest auth bug with Python < 2.5...
Alexis S. L. Carvalho -
r4678:a814a5b1 default
parent child Browse files
Show More
@@ -1,402 +1,416 b''
1 1 # httprepo.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from node import *
10 10 from remoterepo import *
11 11 from i18n import _
12 12 import hg, os, urllib, urllib2, urlparse, zlib, util, httplib
13 13 import errno, keepalive, tempfile, socket, changegroup
14 14
15 15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
16 16 def __init__(self, ui):
17 17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
18 18 self.ui = ui
19 19
20 20 def find_user_password(self, realm, authuri):
21 21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
22 22 self, realm, authuri)
23 23 user, passwd = authinfo
24 24 if user and passwd:
25 25 return (user, passwd)
26 26
27 27 if not self.ui.interactive:
28 28 raise util.Abort(_('http authorization required'))
29 29
30 30 self.ui.write(_("http authorization required\n"))
31 31 self.ui.status(_("realm: %s\n") % realm)
32 32 if user:
33 33 self.ui.status(_("user: %s\n") % user)
34 34 else:
35 35 user = self.ui.prompt(_("user:"), default=None)
36 36
37 37 if not passwd:
38 38 passwd = self.ui.getpass()
39 39
40 40 self.add_password(realm, authuri, user, passwd)
41 41 return (user, passwd)
42 42
43 43 def netlocsplit(netloc):
44 44 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
45 45
46 46 a = netloc.find('@')
47 47 if a == -1:
48 48 user, passwd = None, None
49 49 else:
50 50 userpass, netloc = netloc[:a], netloc[a+1:]
51 51 c = userpass.find(':')
52 52 if c == -1:
53 53 user, passwd = urllib.unquote(userpass), None
54 54 else:
55 55 user = urllib.unquote(userpass[:c])
56 56 passwd = urllib.unquote(userpass[c+1:])
57 57 c = netloc.find(':')
58 58 if c == -1:
59 59 host, port = netloc, None
60 60 else:
61 61 host, port = netloc[:c], netloc[c+1:]
62 62 return host, port, user, passwd
63 63
64 64 def netlocunsplit(host, port, user=None, passwd=None):
65 65 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
66 66 if port:
67 67 hostport = host + ':' + port
68 68 else:
69 69 hostport = host
70 70 if user:
71 71 if passwd:
72 72 userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
73 73 else:
74 74 userpass = urllib.quote(user)
75 75 return userpass + '@' + hostport
76 76 return hostport
77 77
78 78 # work around a bug in Python < 2.4.2
79 79 # (it leaves a "\n" at the end of Proxy-authorization headers)
80 80 class request(urllib2.Request):
81 81 def add_header(self, key, val):
82 82 if key.lower() == 'proxy-authorization':
83 83 val = val.strip()
84 84 return urllib2.Request.add_header(self, key, val)
85 85
86 86 class httpsendfile(file):
87 87 def __len__(self):
88 88 return os.fstat(self.fileno()).st_size
89 89
90 90 def _gen_sendfile(connection):
91 91 def _sendfile(self, data):
92 92 # send a file
93 93 if isinstance(data, httpsendfile):
94 94 # if auth required, some data sent twice, so rewind here
95 95 data.seek(0)
96 96 for chunk in util.filechunkiter(data):
97 97 connection.send(self, chunk)
98 98 else:
99 99 connection.send(self, data)
100 100 return _sendfile
101 101
102 102 class httpconnection(keepalive.HTTPConnection):
103 103 # must be able to send big bundle as stream.
104 104 send = _gen_sendfile(keepalive.HTTPConnection)
105 105
106 106 class basehttphandler(keepalive.HTTPHandler):
107 107 def http_open(self, req):
108 108 return self.do_open(httpconnection, req)
109 109
110 110 has_https = hasattr(urllib2, 'HTTPSHandler')
111 111 if has_https:
112 112 class httpsconnection(httplib.HTTPSConnection):
113 113 response_class = keepalive.HTTPResponse
114 114 # must be able to send big bundle as stream.
115 115 send = _gen_sendfile(httplib.HTTPSConnection)
116 116
117 117 class httphandler(basehttphandler, urllib2.HTTPSHandler):
118 118 def https_open(self, req):
119 119 return self.do_open(httpsconnection, req)
120 120 else:
121 121 class httphandler(basehttphandler):
122 122 pass
123 123
124 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
125 # it doesn't know about the auth type requested. This can happen if
126 # somebody is using BasicAuth and types a bad password.
127 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
128 def http_error_auth_reqed(self, auth_header, host, req, headers):
129 try:
130 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
131 self, auth_header, host, req, headers)
132 except ValueError, inst:
133 arg = inst.args[0]
134 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
135 return
136 raise
137
124 138 def zgenerator(f):
125 139 zd = zlib.decompressobj()
126 140 try:
127 141 for chunk in util.filechunkiter(f):
128 142 yield zd.decompress(chunk)
129 143 except httplib.HTTPException, inst:
130 144 raise IOError(None, _('connection ended unexpectedly'))
131 145 yield zd.flush()
132 146
133 147 class httprepository(remoterepository):
134 148 def __init__(self, ui, path):
135 149 self.path = path
136 150 self.caps = None
137 151 self.handler = None
138 152 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
139 153 if query or frag:
140 154 raise util.Abort(_('unsupported URL component: "%s"') %
141 155 (query or frag))
142 156 if not urlpath: urlpath = '/'
143 157 host, port, user, passwd = netlocsplit(netloc)
144 158
145 159 # urllib cannot handle URLs with embedded user or passwd
146 160 self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
147 161 urlpath, '', ''))
148 162 self.ui = ui
149 163
150 164 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
151 165 # XXX proxyauthinfo = None
152 166 self.handler = httphandler()
153 167 handlers = [self.handler]
154 168
155 169 if proxyurl:
156 170 # proxy can be proper url or host[:port]
157 171 if not (proxyurl.startswith('http:') or
158 172 proxyurl.startswith('https:')):
159 173 proxyurl = 'http://' + proxyurl + '/'
160 174 snpqf = urlparse.urlsplit(proxyurl)
161 175 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
162 176 hpup = netlocsplit(proxynetloc)
163 177
164 178 proxyhost, proxyport, proxyuser, proxypasswd = hpup
165 179 if not proxyuser:
166 180 proxyuser = ui.config("http_proxy", "user")
167 181 proxypasswd = ui.config("http_proxy", "passwd")
168 182
169 183 # see if we should use a proxy for this url
170 184 no_list = [ "localhost", "127.0.0.1" ]
171 185 no_list.extend([p.lower() for
172 186 p in ui.configlist("http_proxy", "no")])
173 187 no_list.extend([p.strip().lower() for
174 188 p in os.getenv("no_proxy", '').split(',')
175 189 if p.strip()])
176 190 # "http_proxy.always" config is for running tests on localhost
177 191 if (not ui.configbool("http_proxy", "always") and
178 192 host.lower() in no_list):
179 193 ui.debug(_('disabling proxy for %s\n') % host)
180 194 else:
181 195 proxyurl = urlparse.urlunsplit((
182 196 proxyscheme, netlocunsplit(proxyhost, proxyport,
183 197 proxyuser, proxypasswd or ''),
184 198 proxypath, proxyquery, proxyfrag))
185 199 handlers.append(urllib2.ProxyHandler({scheme: proxyurl}))
186 200 ui.debug(_('proxying through http://%s:%s\n') %
187 201 (proxyhost, proxyport))
188 202
189 203 # urllib2 takes proxy values from the environment and those
190 204 # will take precedence if found, so drop them
191 205 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
192 206 try:
193 207 if os.environ.has_key(env):
194 208 del os.environ[env]
195 209 except OSError:
196 210 pass
197 211
198 212 passmgr = passwordmgr(ui)
199 213 if user:
200 214 ui.debug(_('http auth: user %s, password %s\n') %
201 215 (user, passwd and '*' * len(passwd) or 'not set'))
202 216 passmgr.add_password(None, host, user, passwd or '')
203 217
204 218 handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr),
205 urllib2.HTTPDigestAuthHandler(passmgr)))
219 httpdigestauthhandler(passmgr)))
206 220 opener = urllib2.build_opener(*handlers)
207 221
208 222 # 1.0 here is the _protocol_ version
209 223 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
210 224 urllib2.install_opener(opener)
211 225
212 226 def __del__(self):
213 227 if self.handler:
214 228 self.handler.close_all()
215 229 self.handler = None
216 230
217 231 def url(self):
218 232 return self.path
219 233
220 234 # look up capabilities only when needed
221 235
222 236 def get_caps(self):
223 237 if self.caps is None:
224 238 try:
225 239 self.caps = self.do_read('capabilities').split()
226 240 except hg.RepoError:
227 241 self.caps = ()
228 242 self.ui.debug(_('capabilities: %s\n') %
229 243 (' '.join(self.caps or ['none'])))
230 244 return self.caps
231 245
232 246 capabilities = property(get_caps)
233 247
234 248 def lock(self):
235 249 raise util.Abort(_('operation not supported over http'))
236 250
237 251 def do_cmd(self, cmd, **args):
238 252 data = args.pop('data', None)
239 253 headers = args.pop('headers', {})
240 254 self.ui.debug(_("sending %s command\n") % cmd)
241 255 q = {"cmd": cmd}
242 256 q.update(args)
243 257 qs = '?%s' % urllib.urlencode(q)
244 258 cu = "%s%s" % (self._url, qs)
245 259 try:
246 260 if data:
247 261 self.ui.debug(_("sending %s bytes\n") %
248 262 headers.get('content-length', 'X'))
249 263 resp = urllib2.urlopen(request(cu, data, headers))
250 264 except urllib2.HTTPError, inst:
251 265 if inst.code == 401:
252 266 raise util.Abort(_('authorization failed'))
253 267 raise
254 268 except httplib.HTTPException, inst:
255 269 self.ui.debug(_('http error while sending %s command\n') % cmd)
256 270 self.ui.print_exc()
257 271 raise IOError(None, inst)
258 272 except IndexError:
259 273 # this only happens with Python 2.3, later versions raise URLError
260 274 raise util.Abort(_('http error, possibly caused by proxy setting'))
261 275 # record the url we got redirected to
262 276 resp_url = resp.geturl()
263 277 if resp_url.endswith(qs):
264 278 resp_url = resp_url[:-len(qs)]
265 279 if self._url != resp_url:
266 280 self.ui.status(_('real URL is %s\n') % resp_url)
267 281 self._url = resp_url
268 282 try:
269 283 proto = resp.getheader('content-type')
270 284 except AttributeError:
271 285 proto = resp.headers['content-type']
272 286
273 287 # accept old "text/plain" and "application/hg-changegroup" for now
274 288 if not (proto.startswith('application/mercurial-') or
275 289 proto.startswith('text/plain') or
276 290 proto.startswith('application/hg-changegroup')):
277 291 raise hg.RepoError(_("'%s' does not appear to be an hg repository")
278 292 % self._url)
279 293
280 294 if proto.startswith('application/mercurial-'):
281 295 try:
282 296 version = proto.split('-', 1)[1]
283 297 version_info = tuple([int(n) for n in version.split('.')])
284 298 except ValueError:
285 299 raise hg.RepoError(_("'%s' sent a broken Content-type "
286 300 "header (%s)") % (self._url, proto))
287 301 if version_info > (0, 1):
288 302 raise hg.RepoError(_("'%s' uses newer protocol %s") %
289 303 (self._url, version))
290 304
291 305 return resp
292 306
293 307 def do_read(self, cmd, **args):
294 308 fp = self.do_cmd(cmd, **args)
295 309 try:
296 310 return fp.read()
297 311 finally:
298 312 # if using keepalive, allow connection to be reused
299 313 fp.close()
300 314
301 315 def lookup(self, key):
302 316 d = self.do_cmd("lookup", key = key).read()
303 317 success, data = d[:-1].split(' ', 1)
304 318 if int(success):
305 319 return bin(data)
306 320 raise hg.RepoError(data)
307 321
308 322 def heads(self):
309 323 d = self.do_read("heads")
310 324 try:
311 325 return map(bin, d[:-1].split(" "))
312 326 except:
313 327 raise util.UnexpectedOutput(_("unexpected response:"), d)
314 328
315 329 def branches(self, nodes):
316 330 n = " ".join(map(hex, nodes))
317 331 d = self.do_read("branches", nodes=n)
318 332 try:
319 333 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
320 334 return br
321 335 except:
322 336 raise util.UnexpectedOutput(_("unexpected response:"), d)
323 337
324 338 def between(self, pairs):
325 339 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
326 340 d = self.do_read("between", pairs=n)
327 341 try:
328 342 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
329 343 return p
330 344 except:
331 345 raise util.UnexpectedOutput(_("unexpected response:"), d)
332 346
333 347 def changegroup(self, nodes, kind):
334 348 n = " ".join(map(hex, nodes))
335 349 f = self.do_cmd("changegroup", roots=n)
336 350 return util.chunkbuffer(zgenerator(f))
337 351
338 352 def changegroupsubset(self, bases, heads, source):
339 353 baselst = " ".join([hex(n) for n in bases])
340 354 headlst = " ".join([hex(n) for n in heads])
341 355 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
342 356 return util.chunkbuffer(zgenerator(f))
343 357
344 358 def unbundle(self, cg, heads, source):
345 359 # have to stream bundle to a temp file because we do not have
346 360 # http 1.1 chunked transfer.
347 361
348 362 type = ""
349 363 types = self.capable('unbundle')
350 364 # servers older than d1b16a746db6 will send 'unbundle' as a
351 365 # boolean capability
352 366 try:
353 367 types = types.split(',')
354 368 except AttributeError:
355 369 types = [""]
356 370 if types:
357 371 for x in types:
358 372 if x in changegroup.bundletypes:
359 373 type = x
360 374 break
361 375
362 376 tempname = changegroup.writebundle(cg, None, type)
363 377 fp = httpsendfile(tempname, "rb")
364 378 try:
365 379 try:
366 380 rfp = self.do_cmd(
367 381 'unbundle', data=fp,
368 382 headers={'content-type': 'application/octet-stream'},
369 383 heads=' '.join(map(hex, heads)))
370 384 try:
371 385 ret = int(rfp.readline())
372 386 self.ui.write(rfp.read())
373 387 return ret
374 388 finally:
375 389 rfp.close()
376 390 except socket.error, err:
377 391 if err[0] in (errno.ECONNRESET, errno.EPIPE):
378 392 raise util.Abort(_('push failed: %s') % err[1])
379 393 raise util.Abort(err[1])
380 394 finally:
381 395 fp.close()
382 396 os.unlink(tempname)
383 397
384 398 def stream_out(self):
385 399 return self.do_cmd('stream_out')
386 400
387 401 class httpsrepository(httprepository):
388 402 def __init__(self, ui, path):
389 403 if not has_https:
390 404 raise util.Abort(_('Python support for SSL and HTTPS '
391 405 'is not installed'))
392 406 httprepository.__init__(self, ui, path)
393 407
394 408 def instance(ui, path, create):
395 409 if create:
396 410 raise util.Abort(_('cannot create new http repository'))
397 411 if path.startswith('hg:'):
398 412 ui.warn(_("hg:// syntax is deprecated, please use http:// instead\n"))
399 413 path = 'http:' + path[3:]
400 414 if path.startswith('https:'):
401 415 return httpsrepository(ui, path)
402 416 return httprepository(ui, path)
General Comments 0
You need to be logged in to leave comments. Login now