##// END OF EJS Templates
url: add distribution and version to user-agent request header (BC)...
Gregory Szorc -
r29589:486de14e default
parent child Browse files
Show More
@@ -1,522 +1,534
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 if has_https:
155 155 try:
156 156 _create_connection = socket.create_connection
157 157 except AttributeError:
158 158 _GLOBAL_DEFAULT_TIMEOUT = object()
159 159
160 160 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
161 161 source_address=None):
162 162 # lifted from Python 2.6
163 163
164 164 msg = "getaddrinfo returns an empty list"
165 165 host, port = address
166 166 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
167 167 af, socktype, proto, canonname, sa = res
168 168 sock = None
169 169 try:
170 170 sock = socket.socket(af, socktype, proto)
171 171 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
172 172 sock.settimeout(timeout)
173 173 if source_address:
174 174 sock.bind(source_address)
175 175 sock.connect(sa)
176 176 return sock
177 177
178 178 except socket.error as msg:
179 179 if sock is not None:
180 180 sock.close()
181 181
182 182 raise socket.error(msg)
183 183
184 184 class httpconnection(keepalive.HTTPConnection):
185 185 # must be able to send big bundle as stream.
186 186 send = _gen_sendfile(keepalive.HTTPConnection.send)
187 187
188 188 def connect(self):
189 189 if has_https and self.realhostport: # use CONNECT proxy
190 190 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
191 191 self.sock.connect((self.host, self.port))
192 192 if _generic_proxytunnel(self):
193 193 # we do not support client X.509 certificates
194 194 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
195 195 serverhostname=self.host)
196 196 else:
197 197 keepalive.HTTPConnection.connect(self)
198 198
199 199 def getresponse(self):
200 200 proxyres = getattr(self, 'proxyres', None)
201 201 if proxyres:
202 202 if proxyres.will_close:
203 203 self.close()
204 204 self.proxyres = None
205 205 return proxyres
206 206 return keepalive.HTTPConnection.getresponse(self)
207 207
208 208 # general transaction handler to support different ways to handle
209 209 # HTTPS proxying before and after Python 2.6.3.
210 210 def _generic_start_transaction(handler, h, req):
211 211 tunnel_host = getattr(req, '_tunnel_host', None)
212 212 if tunnel_host:
213 213 if tunnel_host[:7] not in ['http://', 'https:/']:
214 214 tunnel_host = 'https://' + tunnel_host
215 215 new_tunnel = True
216 216 else:
217 217 tunnel_host = req.get_selector()
218 218 new_tunnel = False
219 219
220 220 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
221 221 u = util.url(tunnel_host)
222 222 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
223 223 h.realhostport = ':'.join([u.host, (u.port or '443')])
224 224 h.headers = req.headers.copy()
225 225 h.headers.update(handler.parent.addheaders)
226 226 return
227 227
228 228 h.realhostport = None
229 229 h.headers = None
230 230
231 231 def _generic_proxytunnel(self):
232 232 proxyheaders = dict(
233 233 [(x, self.headers[x]) for x in self.headers
234 234 if x.lower().startswith('proxy-')])
235 235 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
236 236 for header in proxyheaders.iteritems():
237 237 self.send('%s: %s\r\n' % header)
238 238 self.send('\r\n')
239 239
240 240 # majority of the following code is duplicated from
241 241 # httplib.HTTPConnection as there are no adequate places to
242 242 # override functions to provide the needed functionality
243 243 res = self.response_class(self.sock,
244 244 strict=self.strict,
245 245 method=self._method)
246 246
247 247 while True:
248 248 version, status, reason = res._read_status()
249 249 if status != httplib.CONTINUE:
250 250 break
251 251 while True:
252 252 skip = res.fp.readline().strip()
253 253 if not skip:
254 254 break
255 255 res.status = status
256 256 res.reason = reason.strip()
257 257
258 258 if res.status == 200:
259 259 while True:
260 260 line = res.fp.readline()
261 261 if line == '\r\n':
262 262 break
263 263 return True
264 264
265 265 if version == 'HTTP/1.0':
266 266 res.version = 10
267 267 elif version.startswith('HTTP/1.'):
268 268 res.version = 11
269 269 elif version == 'HTTP/0.9':
270 270 res.version = 9
271 271 else:
272 272 raise httplib.UnknownProtocol(version)
273 273
274 274 if res.version == 9:
275 275 res.length = None
276 276 res.chunked = 0
277 277 res.will_close = 1
278 278 res.msg = httplib.HTTPMessage(stringio())
279 279 return False
280 280
281 281 res.msg = httplib.HTTPMessage(res.fp)
282 282 res.msg.fp = None
283 283
284 284 # are we using the chunked-style of transfer encoding?
285 285 trenc = res.msg.getheader('transfer-encoding')
286 286 if trenc and trenc.lower() == "chunked":
287 287 res.chunked = 1
288 288 res.chunk_left = None
289 289 else:
290 290 res.chunked = 0
291 291
292 292 # will the connection close at the end of the response?
293 293 res.will_close = res._check_close()
294 294
295 295 # do we have a Content-Length?
296 296 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
297 297 # transfer-encoding is "chunked"
298 298 length = res.msg.getheader('content-length')
299 299 if length and not res.chunked:
300 300 try:
301 301 res.length = int(length)
302 302 except ValueError:
303 303 res.length = None
304 304 else:
305 305 if res.length < 0: # ignore nonsensical negative lengths
306 306 res.length = None
307 307 else:
308 308 res.length = None
309 309
310 310 # does the body have a fixed length? (of zero)
311 311 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
312 312 100 <= status < 200 or # 1xx codes
313 313 res._method == 'HEAD'):
314 314 res.length = 0
315 315
316 316 # if the connection remains open, and we aren't using chunked, and
317 317 # a content-length was not provided, then assume that the connection
318 318 # WILL close.
319 319 if (not res.will_close and
320 320 not res.chunked and
321 321 res.length is None):
322 322 res.will_close = 1
323 323
324 324 self.proxyres = res
325 325
326 326 return False
327 327
328 328 class httphandler(keepalive.HTTPHandler):
329 329 def http_open(self, req):
330 330 return self.do_open(httpconnection, req)
331 331
332 332 def _start_transaction(self, h, req):
333 333 _generic_start_transaction(self, h, req)
334 334 return keepalive.HTTPHandler._start_transaction(self, h, req)
335 335
336 336 if has_https:
337 337 class httpsconnection(httplib.HTTPConnection):
338 338 response_class = keepalive.HTTPResponse
339 339 default_port = httplib.HTTPS_PORT
340 340 # must be able to send big bundle as stream.
341 341 send = _gen_sendfile(keepalive.safesend)
342 342 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
343 343
344 344 def __init__(self, host, port=None, key_file=None, cert_file=None,
345 345 *args, **kwargs):
346 346 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
347 347 self.key_file = key_file
348 348 self.cert_file = cert_file
349 349
350 350 def connect(self):
351 351 self.sock = _create_connection((self.host, self.port))
352 352
353 353 host = self.host
354 354 if self.realhostport: # use CONNECT proxy
355 355 _generic_proxytunnel(self)
356 356 host = self.realhostport.rsplit(':', 1)[0]
357 357 self.sock = sslutil.wrapsocket(
358 358 self.sock, self.key_file, self.cert_file, ui=self.ui,
359 359 serverhostname=host)
360 360 sslutil.validatesocket(self.sock)
361 361
362 362 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
363 363 def __init__(self, ui):
364 364 keepalive.KeepAliveHandler.__init__(self)
365 365 urlreq.httpshandler.__init__(self)
366 366 self.ui = ui
367 367 self.pwmgr = passwordmgr(self.ui,
368 368 self.ui.httppasswordmgrdb)
369 369
370 370 def _start_transaction(self, h, req):
371 371 _generic_start_transaction(self, h, req)
372 372 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
373 373
374 374 def https_open(self, req):
375 375 # req.get_full_url() does not contain credentials and we may
376 376 # need them to match the certificates.
377 377 url = req.get_full_url()
378 378 user, password = self.pwmgr.find_stored_password(url)
379 379 res = httpconnectionmod.readauthforuri(self.ui, url, user)
380 380 if res:
381 381 group, auth = res
382 382 self.auth = auth
383 383 self.ui.debug("using auth.%s.* for authentication\n" % group)
384 384 else:
385 385 self.auth = None
386 386 return self.do_open(self._makeconnection, req)
387 387
388 388 def _makeconnection(self, host, port=None, *args, **kwargs):
389 389 keyfile = None
390 390 certfile = None
391 391
392 392 if len(args) >= 1: # key_file
393 393 keyfile = args[0]
394 394 if len(args) >= 2: # cert_file
395 395 certfile = args[1]
396 396 args = args[2:]
397 397
398 398 # if the user has specified different key/cert files in
399 399 # hgrc, we prefer these
400 400 if self.auth and 'key' in self.auth and 'cert' in self.auth:
401 401 keyfile = self.auth['key']
402 402 certfile = self.auth['cert']
403 403
404 404 conn = httpsconnection(host, port, keyfile, certfile, *args,
405 405 **kwargs)
406 406 conn.ui = self.ui
407 407 return conn
408 408
409 409 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
410 410 def __init__(self, *args, **kwargs):
411 411 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
412 412 self.retried_req = None
413 413
414 414 def reset_retry_count(self):
415 415 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
416 416 # forever. We disable reset_retry_count completely and reset in
417 417 # http_error_auth_reqed instead.
418 418 pass
419 419
420 420 def http_error_auth_reqed(self, auth_header, host, req, headers):
421 421 # Reset the retry counter once for each request.
422 422 if req is not self.retried_req:
423 423 self.retried_req = req
424 424 self.retried = 0
425 425 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
426 426 self, auth_header, host, req, headers)
427 427
428 428 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
429 429 def __init__(self, *args, **kwargs):
430 430 self.auth = None
431 431 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
432 432 self.retried_req = None
433 433
434 434 def http_request(self, request):
435 435 if self.auth:
436 436 request.add_unredirected_header(self.auth_header, self.auth)
437 437
438 438 return request
439 439
440 440 def https_request(self, request):
441 441 if self.auth:
442 442 request.add_unredirected_header(self.auth_header, self.auth)
443 443
444 444 return request
445 445
446 446 def reset_retry_count(self):
447 447 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
448 448 # forever. We disable reset_retry_count completely and reset in
449 449 # http_error_auth_reqed instead.
450 450 pass
451 451
452 452 def http_error_auth_reqed(self, auth_header, host, req, headers):
453 453 # Reset the retry counter once for each request.
454 454 if req is not self.retried_req:
455 455 self.retried_req = req
456 456 self.retried = 0
457 457 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
458 458 self, auth_header, host, req, headers)
459 459
460 460 def retry_http_basic_auth(self, host, req, realm):
461 461 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
462 462 if pw is not None:
463 463 raw = "%s:%s" % (user, pw)
464 464 auth = 'Basic %s' % base64.b64encode(raw).strip()
465 465 if req.headers.get(self.auth_header, None) == auth:
466 466 return None
467 467 self.auth = auth
468 468 req.add_unredirected_header(self.auth_header, auth)
469 469 return self.parent.open(req)
470 470 else:
471 471 return None
472 472
473 473 handlerfuncs = []
474 474
475 475 def opener(ui, authinfo=None):
476 476 '''
477 477 construct an opener suitable for urllib2
478 478 authinfo will be added to the password manager
479 479 '''
480 480 # experimental config: ui.usehttp2
481 481 if ui.configbool('ui', 'usehttp2', False):
482 482 handlers = [
483 483 httpconnectionmod.http2handler(
484 484 ui,
485 485 passwordmgr(ui, ui.httppasswordmgrdb))
486 486 ]
487 487 else:
488 488 handlers = [httphandler()]
489 489 if has_https:
490 490 handlers.append(httpshandler(ui))
491 491
492 492 handlers.append(proxyhandler(ui))
493 493
494 494 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
495 495 if authinfo is not None:
496 496 realm, uris, user, passwd = authinfo
497 497 saveduser, savedpass = passmgr.find_stored_password(uris[0])
498 498 if user != saveduser or passwd:
499 499 passmgr.add_password(realm, uris, user, passwd)
500 500 ui.debug('http auth: user %s, password %s\n' %
501 501 (user, passwd and '*' * len(passwd) or 'not set'))
502 502
503 503 handlers.extend((httpbasicauthhandler(passmgr),
504 504 httpdigestauthhandler(passmgr)))
505 505 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
506 506 opener = urlreq.buildopener(*handlers)
507 507
508 # 1.0 here is the _protocol_ version
509 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
508 # The user agent should should *NOT* be used by servers for e.g.
509 # protocol detection or feature negotiation: there are other
510 # facilities for that.
511 #
512 # "mercurial/proto-1.0" was the original user agent string and
513 # exists for backwards compatibility reasons.
514 #
515 # The "(Mercurial %s)" string contains the distribution
516 # name and version. Other client implementations should choose their
517 # own distribution name. Since servers should not be using the user
518 # agent string for anything, clients should be able to define whatever
519 # user agent they deem appropriate.
520 agent = 'mercurial/proto-1.0 (Mercurial %s)' % util.version()
521 opener.addheaders = [('User-agent', agent)]
510 522 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
511 523 return opener
512 524
513 525 def open(ui, url_, data=None):
514 526 u = util.url(url_)
515 527 if u.scheme:
516 528 u.scheme = u.scheme.lower()
517 529 url_, authinfo = u.authinfo()
518 530 else:
519 531 path = util.normpath(os.path.abspath(url_))
520 532 url_ = 'file://' + urlreq.pathname2url(path)
521 533 authinfo = None
522 534 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now