PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/httpconnection.py

https://bitbucket.org/mirror/mercurial/
Python | 287 lines | 199 code | 27 blank | 61 comment | 59 complexity | b9004ceab99db701cd37b97c4669770f MD5 | raw file
Possible License(s): GPL-2.0
  1. # httpconnection.py - urllib2 handler for new http support
  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. # Copyright 2011 Google, Inc.
  7. #
  8. # This software may be used and distributed according to the terms of the
  9. # GNU General Public License version 2 or any later version.
  10. import logging
  11. import socket
  12. import urllib
  13. import urllib2
  14. import os
  15. from mercurial import httpclient
  16. from mercurial import sslutil
  17. from mercurial import util
  18. from mercurial.i18n import _
  19. # moved here from url.py to avoid a cycle
  20. class httpsendfile(object):
  21. """This is a wrapper around the objects returned by python's "open".
  22. Its purpose is to send file-like objects via HTTP.
  23. It do however not define a __len__ attribute because the length
  24. might be more than Py_ssize_t can handle.
  25. """
  26. def __init__(self, ui, *args, **kwargs):
  27. # We can't just "self._data = open(*args, **kwargs)" here because there
  28. # is an "open" function defined in this module that shadows the global
  29. # one
  30. self.ui = ui
  31. self._data = open(*args, **kwargs)
  32. self.seek = self._data.seek
  33. self.close = self._data.close
  34. self.write = self._data.write
  35. self.length = os.fstat(self._data.fileno()).st_size
  36. self._pos = 0
  37. self._total = self.length // 1024 * 2
  38. def read(self, *args, **kwargs):
  39. try:
  40. ret = self._data.read(*args, **kwargs)
  41. except EOFError:
  42. self.ui.progress(_('sending'), None)
  43. self._pos += len(ret)
  44. # We pass double the max for total because we currently have
  45. # to send the bundle twice in the case of a server that
  46. # requires authentication. Since we can't know until we try
  47. # once whether authentication will be required, just lie to
  48. # the user and maybe the push succeeds suddenly at 50%.
  49. self.ui.progress(_('sending'), self._pos // 1024,
  50. unit=_('kb'), total=self._total)
  51. return ret
  52. # moved here from url.py to avoid a cycle
  53. def readauthforuri(ui, uri, user):
  54. # Read configuration
  55. config = dict()
  56. for key, val in ui.configitems('auth'):
  57. if '.' not in key:
  58. ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
  59. continue
  60. group, setting = key.rsplit('.', 1)
  61. gdict = config.setdefault(group, dict())
  62. if setting in ('username', 'cert', 'key'):
  63. val = util.expandpath(val)
  64. gdict[setting] = val
  65. # Find the best match
  66. if '://' in uri:
  67. scheme, hostpath = uri.split('://', 1)
  68. else:
  69. # Python 2.4.1 doesn't provide the full URI
  70. scheme, hostpath = 'http', uri
  71. bestuser = None
  72. bestlen = 0
  73. bestauth = None
  74. for group, auth in config.iteritems():
  75. if user and user != auth.get('username', user):
  76. # If a username was set in the URI, the entry username
  77. # must either match it or be unset
  78. continue
  79. prefix = auth.get('prefix')
  80. if not prefix:
  81. continue
  82. p = prefix.split('://', 1)
  83. if len(p) > 1:
  84. schemes, prefix = [p[0]], p[1]
  85. else:
  86. schemes = (auth.get('schemes') or 'https').split()
  87. if (prefix == '*' or hostpath.startswith(prefix)) and \
  88. (len(prefix) > bestlen or (len(prefix) == bestlen and \
  89. not bestuser and 'username' in auth)) \
  90. and scheme in schemes:
  91. bestlen = len(prefix)
  92. bestauth = group, auth
  93. bestuser = auth.get('username')
  94. if user and not bestuser:
  95. auth['username'] = user
  96. return bestauth
  97. # Mercurial (at least until we can remove the old codepath) requires
  98. # that the http response object be sufficiently file-like, so we
  99. # provide a close() method here.
  100. class HTTPResponse(httpclient.HTTPResponse):
  101. def close(self):
  102. pass
  103. class HTTPConnection(httpclient.HTTPConnection):
  104. response_class = HTTPResponse
  105. def request(self, method, uri, body=None, headers={}):
  106. if isinstance(body, httpsendfile):
  107. body.seek(0)
  108. httpclient.HTTPConnection.request(self, method, uri, body=body,
  109. headers=headers)
  110. _configuredlogging = False
  111. LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
  112. # Subclass BOTH of these because otherwise urllib2 "helpfully"
  113. # reinserts them since it notices we don't include any subclasses of
  114. # them.
  115. class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
  116. def __init__(self, ui, pwmgr):
  117. global _configuredlogging
  118. urllib2.AbstractHTTPHandler.__init__(self)
  119. self.ui = ui
  120. self.pwmgr = pwmgr
  121. self._connections = {}
  122. loglevel = ui.config('ui', 'http2debuglevel', default=None)
  123. if loglevel and not _configuredlogging:
  124. _configuredlogging = True
  125. logger = logging.getLogger('mercurial.httpclient')
  126. logger.setLevel(getattr(logging, loglevel.upper()))
  127. handler = logging.StreamHandler()
  128. handler.setFormatter(logging.Formatter(LOGFMT))
  129. logger.addHandler(handler)
  130. def close_all(self):
  131. """Close and remove all connection objects being kept for reuse."""
  132. for openconns in self._connections.values():
  133. for conn in openconns:
  134. conn.close()
  135. self._connections = {}
  136. # shamelessly borrowed from urllib2.AbstractHTTPHandler
  137. def do_open(self, http_class, req, use_ssl):
  138. """Return an addinfourl object for the request, using http_class.
  139. http_class must implement the HTTPConnection API from httplib.
  140. The addinfourl return value is a file-like object. It also
  141. has methods and attributes including:
  142. - info(): return a mimetools.Message object for the headers
  143. - geturl(): return the original request URL
  144. - code: HTTP status code
  145. """
  146. # If using a proxy, the host returned by get_host() is
  147. # actually the proxy. On Python 2.6.1, the real destination
  148. # hostname is encoded in the URI in the urllib2 request
  149. # object. On Python 2.6.5, it's stored in the _tunnel_host
  150. # attribute which has no accessor.
  151. tunhost = getattr(req, '_tunnel_host', None)
  152. host = req.get_host()
  153. if tunhost:
  154. proxyhost = host
  155. host = tunhost
  156. elif req.has_proxy():
  157. proxyhost = req.get_host()
  158. host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
  159. else:
  160. proxyhost = None
  161. if proxyhost:
  162. if ':' in proxyhost:
  163. # Note: this means we'll explode if we try and use an
  164. # IPv6 http proxy. This isn't a regression, so we
  165. # won't worry about it for now.
  166. proxyhost, proxyport = proxyhost.rsplit(':', 1)
  167. else:
  168. proxyport = 3128 # squid default
  169. proxy = (proxyhost, proxyport)
  170. else:
  171. proxy = None
  172. if not host:
  173. raise urllib2.URLError('no host given')
  174. connkey = use_ssl, host, proxy
  175. allconns = self._connections.get(connkey, [])
  176. conns = [c for c in allconns if not c.busy()]
  177. if conns:
  178. h = conns[0]
  179. else:
  180. if allconns:
  181. self.ui.debug('all connections for %s busy, making a new '
  182. 'one\n' % host)
  183. timeout = None
  184. if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
  185. timeout = req.timeout
  186. h = http_class(host, timeout=timeout, proxy_hostport=proxy)
  187. self._connections.setdefault(connkey, []).append(h)
  188. headers = dict(req.headers)
  189. headers.update(req.unredirected_hdrs)
  190. headers = dict(
  191. (name.title(), val) for name, val in headers.items())
  192. try:
  193. path = req.get_selector()
  194. if '://' in path:
  195. path = path.split('://', 1)[1].split('/', 1)[1]
  196. if path[0] != '/':
  197. path = '/' + path
  198. h.request(req.get_method(), path, req.data, headers)
  199. r = h.getresponse()
  200. except socket.error, err: # XXX what error?
  201. raise urllib2.URLError(err)
  202. # Pick apart the HTTPResponse object to get the addinfourl
  203. # object initialized properly.
  204. r.recv = r.read
  205. resp = urllib.addinfourl(r, r.headers, req.get_full_url())
  206. resp.code = r.status
  207. resp.msg = r.reason
  208. return resp
  209. # httplib always uses the given host/port as the socket connect
  210. # target, and then allows full URIs in the request path, which it
  211. # then observes and treats as a signal to do proxying instead.
  212. def http_open(self, req):
  213. if req.get_full_url().startswith('https'):
  214. return self.https_open(req)
  215. def makehttpcon(*args, **kwargs):
  216. k2 = dict(kwargs)
  217. k2['use_ssl'] = False
  218. return HTTPConnection(*args, **k2)
  219. return self.do_open(makehttpcon, req, False)
  220. def https_open(self, req):
  221. # req.get_full_url() does not contain credentials and we may
  222. # need them to match the certificates.
  223. url = req.get_full_url()
  224. user, password = self.pwmgr.find_stored_password(url)
  225. res = readauthforuri(self.ui, url, user)
  226. if res:
  227. group, auth = res
  228. self.auth = auth
  229. self.ui.debug("using auth.%s.* for authentication\n" % group)
  230. else:
  231. self.auth = None
  232. return self.do_open(self._makesslconnection, req, True)
  233. def _makesslconnection(self, host, port=443, *args, **kwargs):
  234. keyfile = None
  235. certfile = None
  236. if args: # key_file
  237. keyfile = args.pop(0)
  238. if args: # cert_file
  239. certfile = args.pop(0)
  240. # if the user has specified different key/cert files in
  241. # hgrc, we prefer these
  242. if self.auth and 'key' in self.auth and 'cert' in self.auth:
  243. keyfile = self.auth['key']
  244. certfile = self.auth['cert']
  245. # let host port take precedence
  246. if ':' in host and '[' not in host or ']:' in host:
  247. host, port = host.rsplit(':', 1)
  248. port = int(port)
  249. if '[' in host:
  250. host = host[1:-1]
  251. kwargs['keyfile'] = keyfile
  252. kwargs['certfile'] = certfile
  253. kwargs.update(sslutil.sslkwargs(self.ui, host))
  254. con = HTTPConnection(host, port, use_ssl=True,
  255. ssl_wrap_socket=sslutil.ssl_wrap_socket,
  256. ssl_validator=sslutil.validator(self.ui, host),
  257. **kwargs)
  258. return con