PageRenderTime 35ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/httppeer.py

https://bitbucket.org/mirror/mercurial/
Python | 272 lines | 250 code | 13 blank | 9 comment | 17 complexity | 0c21325125bb92924023119f105ce329 MD5 | raw file
Possible License(s): GPL-2.0
  1. # httppeer.py - HTTP repository proxy classes for mercurial
  2. #
  3. # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
  4. # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
  5. #
  6. # This software may be used and distributed according to the terms of the
  7. # GNU General Public License version 2 or any later version.
  8. from node import nullid
  9. from i18n import _
  10. import tempfile
  11. import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
  12. import os, urllib, urllib2, zlib, httplib
  13. import errno, socket
  14. def zgenerator(f):
  15. zd = zlib.decompressobj()
  16. try:
  17. for chunk in util.filechunkiter(f):
  18. while chunk:
  19. yield zd.decompress(chunk, 2**18)
  20. chunk = zd.unconsumed_tail
  21. except httplib.HTTPException:
  22. raise IOError(None, _('connection ended unexpectedly'))
  23. yield zd.flush()
  24. class httppeer(wireproto.wirepeer):
  25. def __init__(self, ui, path):
  26. self.path = path
  27. self.caps = None
  28. self.handler = None
  29. self.urlopener = None
  30. u = util.url(path)
  31. if u.query or u.fragment:
  32. raise util.Abort(_('unsupported URL component: "%s"') %
  33. (u.query or u.fragment))
  34. # urllib cannot handle URLs with embedded user or passwd
  35. self._url, authinfo = u.authinfo()
  36. self.ui = ui
  37. self.ui.debug('using %s\n' % self._url)
  38. self.urlopener = url.opener(ui, authinfo)
  39. def __del__(self):
  40. if self.urlopener:
  41. for h in self.urlopener.handlers:
  42. h.close()
  43. getattr(h, "close_all", lambda : None)()
  44. def url(self):
  45. return self.path
  46. # look up capabilities only when needed
  47. def _fetchcaps(self):
  48. self.caps = set(self._call('capabilities').split())
  49. def _capabilities(self):
  50. if self.caps is None:
  51. try:
  52. self._fetchcaps()
  53. except error.RepoError:
  54. self.caps = set()
  55. self.ui.debug('capabilities: %s\n' %
  56. (' '.join(self.caps or ['none'])))
  57. return self.caps
  58. def lock(self):
  59. raise util.Abort(_('operation not supported over http'))
  60. def _callstream(self, cmd, **args):
  61. if cmd == 'pushkey':
  62. args['data'] = ''
  63. data = args.pop('data', None)
  64. size = 0
  65. if util.safehasattr(data, 'length'):
  66. size = data.length
  67. elif data is not None:
  68. size = len(data)
  69. headers = args.pop('headers', {})
  70. if data is not None and 'Content-Type' not in headers:
  71. headers['Content-Type'] = 'application/mercurial-0.1'
  72. if size and self.ui.configbool('ui', 'usehttp2', False):
  73. headers['Expect'] = '100-Continue'
  74. headers['X-HgHttp2'] = '1'
  75. self.ui.debug("sending %s command\n" % cmd)
  76. q = [('cmd', cmd)]
  77. headersize = 0
  78. if len(args) > 0:
  79. httpheader = self.capable('httpheader')
  80. if httpheader:
  81. headersize = int(httpheader.split(',')[0])
  82. if headersize > 0:
  83. # The headers can typically carry more data than the URL.
  84. encargs = urllib.urlencode(sorted(args.items()))
  85. headerfmt = 'X-HgArg-%s'
  86. contentlen = headersize - len(headerfmt % '000' + ': \r\n')
  87. headernum = 0
  88. for i in xrange(0, len(encargs), contentlen):
  89. headernum += 1
  90. header = headerfmt % str(headernum)
  91. headers[header] = encargs[i:i + contentlen]
  92. varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
  93. headers['Vary'] = ','.join(varyheaders)
  94. else:
  95. q += sorted(args.items())
  96. qs = '?%s' % urllib.urlencode(q)
  97. cu = "%s%s" % (self._url, qs)
  98. req = urllib2.Request(cu, data, headers)
  99. if data is not None:
  100. self.ui.debug("sending %s bytes\n" % size)
  101. req.add_unredirected_header('Content-Length', '%d' % size)
  102. try:
  103. resp = self.urlopener.open(req)
  104. except urllib2.HTTPError, inst:
  105. if inst.code == 401:
  106. raise util.Abort(_('authorization failed'))
  107. raise
  108. except httplib.HTTPException, inst:
  109. self.ui.debug('http error while sending %s command\n' % cmd)
  110. self.ui.traceback()
  111. raise IOError(None, inst)
  112. except IndexError:
  113. # this only happens with Python 2.3, later versions raise URLError
  114. raise util.Abort(_('http error, possibly caused by proxy setting'))
  115. # record the url we got redirected to
  116. resp_url = resp.geturl()
  117. if resp_url.endswith(qs):
  118. resp_url = resp_url[:-len(qs)]
  119. if self._url.rstrip('/') != resp_url.rstrip('/'):
  120. if not self.ui.quiet:
  121. self.ui.warn(_('real URL is %s\n') % resp_url)
  122. self._url = resp_url
  123. try:
  124. proto = resp.getheader('content-type')
  125. except AttributeError:
  126. proto = resp.headers.get('content-type', '')
  127. safeurl = util.hidepassword(self._url)
  128. if proto.startswith('application/hg-error'):
  129. raise error.OutOfBandError(resp.read())
  130. # accept old "text/plain" and "application/hg-changegroup" for now
  131. if not (proto.startswith('application/mercurial-') or
  132. (proto.startswith('text/plain')
  133. and not resp.headers.get('content-length')) or
  134. proto.startswith('application/hg-changegroup')):
  135. self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
  136. raise error.RepoError(
  137. _("'%s' does not appear to be an hg repository:\n"
  138. "---%%<--- (%s)\n%s\n---%%<---\n")
  139. % (safeurl, proto or 'no content-type', resp.read(1024)))
  140. if proto.startswith('application/mercurial-'):
  141. try:
  142. version = proto.split('-', 1)[1]
  143. version_info = tuple([int(n) for n in version.split('.')])
  144. except ValueError:
  145. raise error.RepoError(_("'%s' sent a broken Content-Type "
  146. "header (%s)") % (safeurl, proto))
  147. if version_info > (0, 1):
  148. raise error.RepoError(_("'%s' uses newer protocol %s") %
  149. (safeurl, version))
  150. return resp
  151. def _call(self, cmd, **args):
  152. fp = self._callstream(cmd, **args)
  153. try:
  154. return fp.read()
  155. finally:
  156. # if using keepalive, allow connection to be reused
  157. fp.close()
  158. def _callpush(self, cmd, cg, **args):
  159. # have to stream bundle to a temp file because we do not have
  160. # http 1.1 chunked transfer.
  161. types = self.capable('unbundle')
  162. try:
  163. types = types.split(',')
  164. except AttributeError:
  165. # servers older than d1b16a746db6 will send 'unbundle' as a
  166. # boolean capability. They only support headerless/uncompressed
  167. # bundles.
  168. types = [""]
  169. for x in types:
  170. if x in changegroup.bundletypes:
  171. type = x
  172. break
  173. tempname = changegroup.writebundle(cg, None, type)
  174. fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
  175. headers = {'Content-Type': 'application/mercurial-0.1'}
  176. try:
  177. try:
  178. r = self._call(cmd, data=fp, headers=headers, **args)
  179. vals = r.split('\n', 1)
  180. if len(vals) < 2:
  181. raise error.ResponseError(_("unexpected response:"), r)
  182. return vals
  183. except socket.error, err:
  184. if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
  185. raise util.Abort(_('push failed: %s') % err.args[1])
  186. raise util.Abort(err.args[1])
  187. finally:
  188. fp.close()
  189. os.unlink(tempname)
  190. def _calltwowaystream(self, cmd, fp, **args):
  191. fh = None
  192. filename = None
  193. try:
  194. # dump bundle to disk
  195. fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
  196. fh = os.fdopen(fd, "wb")
  197. d = fp.read(4096)
  198. while d:
  199. fh.write(d)
  200. d = fp.read(4096)
  201. fh.close()
  202. # start http push
  203. fp = httpconnection.httpsendfile(self.ui, filename, "rb")
  204. headers = {'Content-Type': 'application/mercurial-0.1'}
  205. return self._callstream(cmd, data=fp, headers=headers, **args)
  206. finally:
  207. if fh is not None:
  208. fh.close()
  209. os.unlink(filename)
  210. def _callcompressable(self, cmd, **args):
  211. stream = self._callstream(cmd, **args)
  212. return util.chunkbuffer(zgenerator(stream))
  213. def _abort(self, exception):
  214. raise exception
  215. class httpspeer(httppeer):
  216. def __init__(self, ui, path):
  217. if not url.has_https:
  218. raise util.Abort(_('Python support for SSL and HTTPS '
  219. 'is not installed'))
  220. httppeer.__init__(self, ui, path)
  221. def instance(ui, path, create):
  222. if create:
  223. raise util.Abort(_('cannot create new http repository'))
  224. try:
  225. if path.startswith('https:'):
  226. inst = httpspeer(ui, path)
  227. else:
  228. inst = httppeer(ui, path)
  229. try:
  230. # Try to do useful work when checking compatibility.
  231. # Usually saves a roundtrip since we want the caps anyway.
  232. inst._fetchcaps()
  233. except error.RepoError:
  234. # No luck, try older compatibility check.
  235. inst.between([(nullid, nullid)])
  236. return inst
  237. except error.RepoError, httpexception:
  238. try:
  239. r = statichttprepo.instance(ui, "static-" + path, create)
  240. ui.note('(falling back to static-http)\n')
  241. return r
  242. except error.RepoError:
  243. raise httpexception # use the original http RepoError instead