##// END OF EJS Templates
url: use `iter(callable, sentinel)` instead of while True...
Augie Fackler -
r29729:44ea1275 default
parent child Browse files
Show More
@@ -1,494 +1,490
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import base64
13 13 import os
14 14 import socket
15 15
16 16 from .i18n import _
17 17 from . import (
18 18 error,
19 19 httpconnection as httpconnectionmod,
20 20 keepalive,
21 21 sslutil,
22 22 util,
23 23 )
24 24
25 25 httplib = util.httplib
26 26 stringio = util.stringio
27 27 urlerr = util.urlerr
28 28 urlreq = util.urlreq
29 29
30 30 class passwordmgr(object):
31 31 def __init__(self, ui, passwddb):
32 32 self.ui = ui
33 33 self.passwddb = passwddb
34 34
35 35 def add_password(self, realm, uri, user, passwd):
36 36 return self.passwddb.add_password(realm, uri, user, passwd)
37 37
38 38 def find_user_password(self, realm, authuri):
39 39 authinfo = self.passwddb.find_user_password(realm, authuri)
40 40 user, passwd = authinfo
41 41 if user and passwd:
42 42 self._writedebug(user, passwd)
43 43 return (user, passwd)
44 44
45 45 if not user or not passwd:
46 46 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
47 47 if res:
48 48 group, auth = res
49 49 user, passwd = auth.get('username'), auth.get('password')
50 50 self.ui.debug("using auth.%s.* for authentication\n" % group)
51 51 if not user or not passwd:
52 52 u = util.url(authuri)
53 53 u.query = None
54 54 if not self.ui.interactive():
55 55 raise error.Abort(_('http authorization required for %s') %
56 56 util.hidepassword(str(u)))
57 57
58 58 self.ui.write(_("http authorization required for %s\n") %
59 59 util.hidepassword(str(u)))
60 60 self.ui.write(_("realm: %s\n") % realm)
61 61 if user:
62 62 self.ui.write(_("user: %s\n") % user)
63 63 else:
64 64 user = self.ui.prompt(_("user:"), default=None)
65 65
66 66 if not passwd:
67 67 passwd = self.ui.getpass()
68 68
69 69 self.passwddb.add_password(realm, authuri, user, passwd)
70 70 self._writedebug(user, passwd)
71 71 return (user, passwd)
72 72
73 73 def _writedebug(self, user, passwd):
74 74 msg = _('http auth: user %s, password %s\n')
75 75 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
76 76
77 77 def find_stored_password(self, authuri):
78 78 return self.passwddb.find_user_password(None, authuri)
79 79
80 80 class proxyhandler(urlreq.proxyhandler):
81 81 def __init__(self, ui):
82 82 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
83 83 # XXX proxyauthinfo = None
84 84
85 85 if proxyurl:
86 86 # proxy can be proper url or host[:port]
87 87 if not (proxyurl.startswith('http:') or
88 88 proxyurl.startswith('https:')):
89 89 proxyurl = 'http://' + proxyurl + '/'
90 90 proxy = util.url(proxyurl)
91 91 if not proxy.user:
92 92 proxy.user = ui.config("http_proxy", "user")
93 93 proxy.passwd = ui.config("http_proxy", "passwd")
94 94
95 95 # see if we should use a proxy for this url
96 96 no_list = ["localhost", "127.0.0.1"]
97 97 no_list.extend([p.lower() for
98 98 p in ui.configlist("http_proxy", "no")])
99 99 no_list.extend([p.strip().lower() for
100 100 p in os.getenv("no_proxy", '').split(',')
101 101 if p.strip()])
102 102 # "http_proxy.always" config is for running tests on localhost
103 103 if ui.configbool("http_proxy", "always"):
104 104 self.no_list = []
105 105 else:
106 106 self.no_list = no_list
107 107
108 108 proxyurl = str(proxy)
109 109 proxies = {'http': proxyurl, 'https': proxyurl}
110 110 ui.debug('proxying through http://%s:%s\n' %
111 111 (proxy.host, proxy.port))
112 112 else:
113 113 proxies = {}
114 114
115 115 # urllib2 takes proxy values from the environment and those
116 116 # will take precedence if found. So, if there's a config entry
117 117 # defining a proxy, drop the environment ones
118 118 if ui.config("http_proxy", "host"):
119 119 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
120 120 try:
121 121 if env in os.environ:
122 122 del os.environ[env]
123 123 except OSError:
124 124 pass
125 125
126 126 urlreq.proxyhandler.__init__(self, proxies)
127 127 self.ui = ui
128 128
129 129 def proxy_open(self, req, proxy, type_):
130 130 host = req.get_host().split(':')[0]
131 131 for e in self.no_list:
132 132 if host == e:
133 133 return None
134 134 if e.startswith('*.') and host.endswith(e[2:]):
135 135 return None
136 136 if e.startswith('.') and host.endswith(e[1:]):
137 137 return None
138 138
139 139 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
140 140
141 141 def _gen_sendfile(orgsend):
142 142 def _sendfile(self, data):
143 143 # send a file
144 144 if isinstance(data, httpconnectionmod.httpsendfile):
145 145 # if auth required, some data sent twice, so rewind here
146 146 data.seek(0)
147 147 for chunk in util.filechunkiter(data):
148 148 orgsend(self, chunk)
149 149 else:
150 150 orgsend(self, data)
151 151 return _sendfile
152 152
153 153 has_https = util.safehasattr(urlreq, 'httpshandler')
154 154
155 155 class httpconnection(keepalive.HTTPConnection):
156 156 # must be able to send big bundle as stream.
157 157 send = _gen_sendfile(keepalive.HTTPConnection.send)
158 158
159 159 def getresponse(self):
160 160 proxyres = getattr(self, 'proxyres', None)
161 161 if proxyres:
162 162 if proxyres.will_close:
163 163 self.close()
164 164 self.proxyres = None
165 165 return proxyres
166 166 return keepalive.HTTPConnection.getresponse(self)
167 167
168 168 # general transaction handler to support different ways to handle
169 169 # HTTPS proxying before and after Python 2.6.3.
170 170 def _generic_start_transaction(handler, h, req):
171 171 tunnel_host = getattr(req, '_tunnel_host', None)
172 172 if tunnel_host:
173 173 if tunnel_host[:7] not in ['http://', 'https:/']:
174 174 tunnel_host = 'https://' + tunnel_host
175 175 new_tunnel = True
176 176 else:
177 177 tunnel_host = req.get_selector()
178 178 new_tunnel = False
179 179
180 180 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
181 181 u = util.url(tunnel_host)
182 182 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
183 183 h.realhostport = ':'.join([u.host, (u.port or '443')])
184 184 h.headers = req.headers.copy()
185 185 h.headers.update(handler.parent.addheaders)
186 186 return
187 187
188 188 h.realhostport = None
189 189 h.headers = None
190 190
191 191 def _generic_proxytunnel(self):
192 192 proxyheaders = dict(
193 193 [(x, self.headers[x]) for x in self.headers
194 194 if x.lower().startswith('proxy-')])
195 195 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
196 196 for header in proxyheaders.iteritems():
197 197 self.send('%s: %s\r\n' % header)
198 198 self.send('\r\n')
199 199
200 200 # majority of the following code is duplicated from
201 201 # httplib.HTTPConnection as there are no adequate places to
202 202 # override functions to provide the needed functionality
203 203 res = self.response_class(self.sock,
204 204 strict=self.strict,
205 205 method=self._method)
206 206
207 207 while True:
208 208 version, status, reason = res._read_status()
209 209 if status != httplib.CONTINUE:
210 210 break
211 while True:
212 skip = res.fp.readline().strip()
213 if not skip:
214 break
211 # skip lines that are all whitespace
212 list(iter(lambda: res.fp.readline().strip(), ''))
215 213 res.status = status
216 214 res.reason = reason.strip()
217 215
218 216 if res.status == 200:
219 while True:
220 line = res.fp.readline()
221 if line == '\r\n':
222 break
217 # skip lines until we find a blank line
218 list(iter(res.fp.readline, '\r\n'))
223 219 return True
224 220
225 221 if version == 'HTTP/1.0':
226 222 res.version = 10
227 223 elif version.startswith('HTTP/1.'):
228 224 res.version = 11
229 225 elif version == 'HTTP/0.9':
230 226 res.version = 9
231 227 else:
232 228 raise httplib.UnknownProtocol(version)
233 229
234 230 if res.version == 9:
235 231 res.length = None
236 232 res.chunked = 0
237 233 res.will_close = 1
238 234 res.msg = httplib.HTTPMessage(stringio())
239 235 return False
240 236
241 237 res.msg = httplib.HTTPMessage(res.fp)
242 238 res.msg.fp = None
243 239
244 240 # are we using the chunked-style of transfer encoding?
245 241 trenc = res.msg.getheader('transfer-encoding')
246 242 if trenc and trenc.lower() == "chunked":
247 243 res.chunked = 1
248 244 res.chunk_left = None
249 245 else:
250 246 res.chunked = 0
251 247
252 248 # will the connection close at the end of the response?
253 249 res.will_close = res._check_close()
254 250
255 251 # do we have a Content-Length?
256 252 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
257 253 # transfer-encoding is "chunked"
258 254 length = res.msg.getheader('content-length')
259 255 if length and not res.chunked:
260 256 try:
261 257 res.length = int(length)
262 258 except ValueError:
263 259 res.length = None
264 260 else:
265 261 if res.length < 0: # ignore nonsensical negative lengths
266 262 res.length = None
267 263 else:
268 264 res.length = None
269 265
270 266 # does the body have a fixed length? (of zero)
271 267 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
272 268 100 <= status < 200 or # 1xx codes
273 269 res._method == 'HEAD'):
274 270 res.length = 0
275 271
276 272 # if the connection remains open, and we aren't using chunked, and
277 273 # a content-length was not provided, then assume that the connection
278 274 # WILL close.
279 275 if (not res.will_close and
280 276 not res.chunked and
281 277 res.length is None):
282 278 res.will_close = 1
283 279
284 280 self.proxyres = res
285 281
286 282 return False
287 283
288 284 class httphandler(keepalive.HTTPHandler):
289 285 def http_open(self, req):
290 286 return self.do_open(httpconnection, req)
291 287
292 288 def _start_transaction(self, h, req):
293 289 _generic_start_transaction(self, h, req)
294 290 return keepalive.HTTPHandler._start_transaction(self, h, req)
295 291
296 292 if has_https:
297 293 class httpsconnection(httplib.HTTPConnection):
298 294 response_class = keepalive.HTTPResponse
299 295 default_port = httplib.HTTPS_PORT
300 296 # must be able to send big bundle as stream.
301 297 send = _gen_sendfile(keepalive.safesend)
302 298 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
303 299
304 300 def __init__(self, host, port=None, key_file=None, cert_file=None,
305 301 *args, **kwargs):
306 302 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
307 303 self.key_file = key_file
308 304 self.cert_file = cert_file
309 305
310 306 def connect(self):
311 307 self.sock = socket.create_connection((self.host, self.port))
312 308
313 309 host = self.host
314 310 if self.realhostport: # use CONNECT proxy
315 311 _generic_proxytunnel(self)
316 312 host = self.realhostport.rsplit(':', 1)[0]
317 313 self.sock = sslutil.wrapsocket(
318 314 self.sock, self.key_file, self.cert_file, ui=self.ui,
319 315 serverhostname=host)
320 316 sslutil.validatesocket(self.sock)
321 317
322 318 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
323 319 def __init__(self, ui):
324 320 keepalive.KeepAliveHandler.__init__(self)
325 321 urlreq.httpshandler.__init__(self)
326 322 self.ui = ui
327 323 self.pwmgr = passwordmgr(self.ui,
328 324 self.ui.httppasswordmgrdb)
329 325
330 326 def _start_transaction(self, h, req):
331 327 _generic_start_transaction(self, h, req)
332 328 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
333 329
334 330 def https_open(self, req):
335 331 # req.get_full_url() does not contain credentials and we may
336 332 # need them to match the certificates.
337 333 url = req.get_full_url()
338 334 user, password = self.pwmgr.find_stored_password(url)
339 335 res = httpconnectionmod.readauthforuri(self.ui, url, user)
340 336 if res:
341 337 group, auth = res
342 338 self.auth = auth
343 339 self.ui.debug("using auth.%s.* for authentication\n" % group)
344 340 else:
345 341 self.auth = None
346 342 return self.do_open(self._makeconnection, req)
347 343
348 344 def _makeconnection(self, host, port=None, *args, **kwargs):
349 345 keyfile = None
350 346 certfile = None
351 347
352 348 if len(args) >= 1: # key_file
353 349 keyfile = args[0]
354 350 if len(args) >= 2: # cert_file
355 351 certfile = args[1]
356 352 args = args[2:]
357 353
358 354 # if the user has specified different key/cert files in
359 355 # hgrc, we prefer these
360 356 if self.auth and 'key' in self.auth and 'cert' in self.auth:
361 357 keyfile = self.auth['key']
362 358 certfile = self.auth['cert']
363 359
364 360 conn = httpsconnection(host, port, keyfile, certfile, *args,
365 361 **kwargs)
366 362 conn.ui = self.ui
367 363 return conn
368 364
369 365 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
370 366 def __init__(self, *args, **kwargs):
371 367 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
372 368 self.retried_req = None
373 369
374 370 def reset_retry_count(self):
375 371 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
376 372 # forever. We disable reset_retry_count completely and reset in
377 373 # http_error_auth_reqed instead.
378 374 pass
379 375
380 376 def http_error_auth_reqed(self, auth_header, host, req, headers):
381 377 # Reset the retry counter once for each request.
382 378 if req is not self.retried_req:
383 379 self.retried_req = req
384 380 self.retried = 0
385 381 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
386 382 self, auth_header, host, req, headers)
387 383
388 384 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
389 385 def __init__(self, *args, **kwargs):
390 386 self.auth = None
391 387 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
392 388 self.retried_req = None
393 389
394 390 def http_request(self, request):
395 391 if self.auth:
396 392 request.add_unredirected_header(self.auth_header, self.auth)
397 393
398 394 return request
399 395
400 396 def https_request(self, request):
401 397 if self.auth:
402 398 request.add_unredirected_header(self.auth_header, self.auth)
403 399
404 400 return request
405 401
406 402 def reset_retry_count(self):
407 403 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
408 404 # forever. We disable reset_retry_count completely and reset in
409 405 # http_error_auth_reqed instead.
410 406 pass
411 407
412 408 def http_error_auth_reqed(self, auth_header, host, req, headers):
413 409 # Reset the retry counter once for each request.
414 410 if req is not self.retried_req:
415 411 self.retried_req = req
416 412 self.retried = 0
417 413 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
418 414 self, auth_header, host, req, headers)
419 415
420 416 def retry_http_basic_auth(self, host, req, realm):
421 417 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
422 418 if pw is not None:
423 419 raw = "%s:%s" % (user, pw)
424 420 auth = 'Basic %s' % base64.b64encode(raw).strip()
425 421 if req.get_header(self.auth_header, None) == auth:
426 422 return None
427 423 self.auth = auth
428 424 req.add_unredirected_header(self.auth_header, auth)
429 425 return self.parent.open(req)
430 426 else:
431 427 return None
432 428
433 429 handlerfuncs = []
434 430
435 431 def opener(ui, authinfo=None):
436 432 '''
437 433 construct an opener suitable for urllib2
438 434 authinfo will be added to the password manager
439 435 '''
440 436 # experimental config: ui.usehttp2
441 437 if ui.configbool('ui', 'usehttp2', False):
442 438 handlers = [
443 439 httpconnectionmod.http2handler(
444 440 ui,
445 441 passwordmgr(ui, ui.httppasswordmgrdb))
446 442 ]
447 443 else:
448 444 handlers = [httphandler()]
449 445 if has_https:
450 446 handlers.append(httpshandler(ui))
451 447
452 448 handlers.append(proxyhandler(ui))
453 449
454 450 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
455 451 if authinfo is not None:
456 452 realm, uris, user, passwd = authinfo
457 453 saveduser, savedpass = passmgr.find_stored_password(uris[0])
458 454 if user != saveduser or passwd:
459 455 passmgr.add_password(realm, uris, user, passwd)
460 456 ui.debug('http auth: user %s, password %s\n' %
461 457 (user, passwd and '*' * len(passwd) or 'not set'))
462 458
463 459 handlers.extend((httpbasicauthhandler(passmgr),
464 460 httpdigestauthhandler(passmgr)))
465 461 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
466 462 opener = urlreq.buildopener(*handlers)
467 463
468 464 # The user agent should should *NOT* be used by servers for e.g.
469 465 # protocol detection or feature negotiation: there are other
470 466 # facilities for that.
471 467 #
472 468 # "mercurial/proto-1.0" was the original user agent string and
473 469 # exists for backwards compatibility reasons.
474 470 #
475 471 # The "(Mercurial %s)" string contains the distribution
476 472 # name and version. Other client implementations should choose their
477 473 # own distribution name. Since servers should not be using the user
478 474 # agent string for anything, clients should be able to define whatever
479 475 # user agent they deem appropriate.
480 476 agent = 'mercurial/proto-1.0 (Mercurial %s)' % util.version()
481 477 opener.addheaders = [('User-agent', agent)]
482 478 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
483 479 return opener
484 480
485 481 def open(ui, url_, data=None):
486 482 u = util.url(url_)
487 483 if u.scheme:
488 484 u.scheme = u.scheme.lower()
489 485 url_, authinfo = u.authinfo()
490 486 else:
491 487 path = util.normpath(os.path.abspath(url_))
492 488 url_ = 'file://' + urlreq.pathname2url(path)
493 489 authinfo = None
494 490 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now