PageRenderTime 56ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/mercurial/url.py

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