httppeer.py
309 lines
| 11.0 KiB
| text/x-python
|
PythonLexer
/ mercurial / httppeer.py
|
r17192 | # httppeer.py - HTTP repository proxy classes for mercurial | ||
# | ||||
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r25954 | from __future__ import absolute_import | ||
import errno | ||||
import os | ||||
import socket | ||||
|
r21074 | import tempfile | ||
|
r25954 | import zlib | ||
from .i18n import _ | ||||
from .node import nullid | ||||
from . import ( | ||||
|
r28666 | bundle2, | ||
|
r25954 | error, | ||
httpconnection, | ||||
statichttprepo, | ||||
url, | ||||
util, | ||||
wireproto, | ||||
) | ||||
|
r17192 | |||
|
r29455 | httplib = util.httplib | ||
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
|
r17192 | def zgenerator(f): | ||
zd = zlib.decompressobj() | ||||
try: | ||||
for chunk in util.filechunkiter(f): | ||||
while chunk: | ||||
yield zd.decompress(chunk, 2**18) | ||||
chunk = zd.unconsumed_tail | ||||
except httplib.HTTPException: | ||||
raise IOError(None, _('connection ended unexpectedly')) | ||||
yield zd.flush() | ||||
class httppeer(wireproto.wirepeer): | ||||
def __init__(self, ui, path): | ||||
self.path = path | ||||
self.caps = None | ||||
self.handler = None | ||||
self.urlopener = None | ||||
|
r25500 | self.requestbuilder = None | ||
|
r17192 | u = util.url(path) | ||
if u.query or u.fragment: | ||||
|
r26587 | raise error.Abort(_('unsupported URL component: "%s"') % | ||
|
r17192 | (u.query or u.fragment)) | ||
# urllib cannot handle URLs with embedded user or passwd | ||||
self._url, authinfo = u.authinfo() | ||||
self.ui = ui | ||||
self.ui.debug('using %s\n' % self._url) | ||||
self.urlopener = url.opener(ui, authinfo) | ||||
|
r28883 | self.requestbuilder = urlreq.request | ||
|
r17192 | |||
def __del__(self): | ||||
|
r30241 | urlopener = getattr(self, 'urlopener', None) | ||
if urlopener: | ||||
for h in urlopener.handlers: | ||||
|
r17192 | h.close() | ||
getattr(h, "close_all", lambda : None)() | ||||
def url(self): | ||||
return self.path | ||||
# look up capabilities only when needed | ||||
def _fetchcaps(self): | ||||
self.caps = set(self._call('capabilities').split()) | ||||
def _capabilities(self): | ||||
if self.caps is None: | ||||
try: | ||||
self._fetchcaps() | ||||
except error.RepoError: | ||||
self.caps = set() | ||||
self.ui.debug('capabilities: %s\n' % | ||||
(' '.join(self.caps or ['none']))) | ||||
return self.caps | ||||
def lock(self): | ||||
|
r26587 | raise error.Abort(_('operation not supported over http')) | ||
|
r17192 | |||
def _callstream(self, cmd, **args): | ||||
if cmd == 'pushkey': | ||||
args['data'] = '' | ||||
data = args.pop('data', None) | ||||
headers = args.pop('headers', {}) | ||||
self.ui.debug("sending %s command\n" % cmd) | ||||
q = [('cmd', cmd)] | ||||
headersize = 0 | ||||
|
r28530 | # Important: don't use self.capable() here or else you end up | ||
# with infinite recursion when trying to look up capabilities | ||||
# for the first time. | ||||
postargsok = self.caps is not None and 'httppostargs' in self.caps | ||||
# TODO: support for httppostargs when data is a file-like | ||||
# object rather than a basestring | ||||
canmungedata = not data or isinstance(data, basestring) | ||||
if postargsok and canmungedata: | ||||
|
r28883 | strargs = urlreq.urlencode(sorted(args.items())) | ||
|
r28530 | if strargs: | ||
if not data: | ||||
data = strargs | ||||
elif isinstance(data, basestring): | ||||
data = strargs + data | ||||
headers['X-HgArgs-Post'] = len(strargs) | ||||
else: | ||||
|
r28485 | if len(args) > 0: | ||
httpheader = self.capable('httpheader') | ||||
if httpheader: | ||||
headersize = int(httpheader.split(',', 1)[0]) | ||||
if headersize > 0: | ||||
# The headers can typically carry more data than the URL. | ||||
|
r28883 | encargs = urlreq.urlencode(sorted(args.items())) | ||
|
r28485 | headerfmt = 'X-HgArg-%s' | ||
contentlen = headersize - len(headerfmt % '000' + ': \r\n') | ||||
headernum = 0 | ||||
|
r28486 | varyheaders = [] | ||
|
r28485 | for i in xrange(0, len(encargs), contentlen): | ||
headernum += 1 | ||||
header = headerfmt % str(headernum) | ||||
headers[header] = encargs[i:i + contentlen] | ||||
|
r28486 | varyheaders.append(header) | ||
|
r28485 | headers['Vary'] = ','.join(varyheaders) | ||
else: | ||||
q += sorted(args.items()) | ||||
|
r28883 | qs = '?%s' % urlreq.urlencode(q) | ||
|
r17192 | cu = "%s%s" % (self._url, qs) | ||
|
r28484 | size = 0 | ||
if util.safehasattr(data, 'length'): | ||||
size = data.length | ||||
elif data is not None: | ||||
size = len(data) | ||||
if size and self.ui.configbool('ui', 'usehttp2', False): | ||||
headers['Expect'] = '100-Continue' | ||||
headers['X-HgHttp2'] = '1' | ||||
if data is not None and 'Content-Type' not in headers: | ||||
headers['Content-Type'] = 'application/mercurial-0.1' | ||||
|
r25500 | req = self.requestbuilder(cu, data, headers) | ||
|
r17192 | if data is not None: | ||
self.ui.debug("sending %s bytes\n" % size) | ||||
req.add_unredirected_header('Content-Length', '%d' % size) | ||||
try: | ||||
resp = self.urlopener.open(req) | ||||
|
r28883 | except urlerr.httperror as inst: | ||
|
r17192 | if inst.code == 401: | ||
|
r26587 | raise error.Abort(_('authorization failed')) | ||
|
r17192 | raise | ||
|
r25660 | except httplib.HTTPException as inst: | ||
|
r17192 | self.ui.debug('http error while sending %s command\n' % cmd) | ||
self.ui.traceback() | ||||
raise IOError(None, inst) | ||||
except IndexError: | ||||
# this only happens with Python 2.3, later versions raise URLError | ||||
|
r26587 | raise error.Abort(_('http error, possibly caused by proxy setting')) | ||
|
r17192 | # record the url we got redirected to | ||
resp_url = resp.geturl() | ||||
if resp_url.endswith(qs): | ||||
resp_url = resp_url[:-len(qs)] | ||||
if self._url.rstrip('/') != resp_url.rstrip('/'): | ||||
if not self.ui.quiet: | ||||
self.ui.warn(_('real URL is %s\n') % resp_url) | ||||
self._url = resp_url | ||||
try: | ||||
proto = resp.getheader('content-type') | ||||
except AttributeError: | ||||
proto = resp.headers.get('content-type', '') | ||||
safeurl = util.hidepassword(self._url) | ||||
if proto.startswith('application/hg-error'): | ||||
raise error.OutOfBandError(resp.read()) | ||||
# accept old "text/plain" and "application/hg-changegroup" for now | ||||
if not (proto.startswith('application/mercurial-') or | ||||
|
r18737 | (proto.startswith('text/plain') | ||
and not resp.headers.get('content-length')) or | ||||
|
r17192 | proto.startswith('application/hg-changegroup')): | ||
self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu)) | ||||
raise error.RepoError( | ||||
_("'%s' does not appear to be an hg repository:\n" | ||||
"---%%<--- (%s)\n%s\n---%%<---\n") | ||||
|
r18738 | % (safeurl, proto or 'no content-type', resp.read(1024))) | ||
|
r17192 | |||
if proto.startswith('application/mercurial-'): | ||||
try: | ||||
version = proto.split('-', 1)[1] | ||||
version_info = tuple([int(n) for n in version.split('.')]) | ||||
except ValueError: | ||||
raise error.RepoError(_("'%s' sent a broken Content-Type " | ||||
"header (%s)") % (safeurl, proto)) | ||||
if version_info > (0, 1): | ||||
raise error.RepoError(_("'%s' uses newer protocol %s") % | ||||
(safeurl, version)) | ||||
return resp | ||||
def _call(self, cmd, **args): | ||||
fp = self._callstream(cmd, **args) | ||||
try: | ||||
return fp.read() | ||||
finally: | ||||
# if using keepalive, allow connection to be reused | ||||
fp.close() | ||||
def _callpush(self, cmd, cg, **args): | ||||
# have to stream bundle to a temp file because we do not have | ||||
# http 1.1 chunked transfer. | ||||
types = self.capable('unbundle') | ||||
try: | ||||
types = types.split(',') | ||||
except AttributeError: | ||||
# servers older than d1b16a746db6 will send 'unbundle' as a | ||||
# boolean capability. They only support headerless/uncompressed | ||||
# bundles. | ||||
types = [""] | ||||
for x in types: | ||||
|
r28666 | if x in bundle2.bundletypes: | ||
|
r17192 | type = x | ||
break | ||||
|
r28666 | tempname = bundle2.writebundle(self.ui, cg, None, type) | ||
|
r17192 | fp = httpconnection.httpsendfile(self.ui, tempname, "rb") | ||
headers = {'Content-Type': 'application/mercurial-0.1'} | ||||
try: | ||||
|
r25085 | r = self._call(cmd, data=fp, headers=headers, **args) | ||
vals = r.split('\n', 1) | ||||
if len(vals) < 2: | ||||
raise error.ResponseError(_("unexpected response:"), r) | ||||
return vals | ||||
|
r25660 | except socket.error as err: | ||
|
r25085 | if err.args[0] in (errno.ECONNRESET, errno.EPIPE): | ||
|
r26587 | raise error.Abort(_('push failed: %s') % err.args[1]) | ||
raise error.Abort(err.args[1]) | ||||
|
r17192 | finally: | ||
fp.close() | ||||
os.unlink(tempname) | ||||
|
r21074 | def _calltwowaystream(self, cmd, fp, **args): | ||
fh = None | ||||
|
r23086 | fp_ = None | ||
|
r21074 | filename = None | ||
try: | ||||
# dump bundle to disk | ||||
fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg") | ||||
fh = os.fdopen(fd, "wb") | ||||
d = fp.read(4096) | ||||
while d: | ||||
fh.write(d) | ||||
d = fp.read(4096) | ||||
fh.close() | ||||
# start http push | ||||
|
r23086 | fp_ = httpconnection.httpsendfile(self.ui, filename, "rb") | ||
|
r21074 | headers = {'Content-Type': 'application/mercurial-0.1'} | ||
|
r23086 | return self._callstream(cmd, data=fp_, headers=headers, **args) | ||
|
r21074 | finally: | ||
|
r23086 | if fp_ is not None: | ||
fp_.close() | ||||
|
r21074 | if fh is not None: | ||
fh.close() | ||||
os.unlink(filename) | ||||
|
r20905 | def _callcompressable(self, cmd, **args): | ||
|
r27637 | stream = self._callstream(cmd, **args) | ||
|
r17192 | return util.chunkbuffer(zgenerator(stream)) | ||
|
r21188 | def _abort(self, exception): | ||
raise exception | ||||
|
r17192 | class httpspeer(httppeer): | ||
def __init__(self, ui, path): | ||||
if not url.has_https: | ||||
|
r26587 | raise error.Abort(_('Python support for SSL and HTTPS ' | ||
|
r17192 | 'is not installed')) | ||
httppeer.__init__(self, ui, path) | ||||
def instance(ui, path, create): | ||||
if create: | ||||
|
r26587 | raise error.Abort(_('cannot create new http repository')) | ||
|
r17192 | try: | ||
if path.startswith('https:'): | ||||
inst = httpspeer(ui, path) | ||||
else: | ||||
inst = httppeer(ui, path) | ||||
try: | ||||
# Try to do useful work when checking compatibility. | ||||
# Usually saves a roundtrip since we want the caps anyway. | ||||
inst._fetchcaps() | ||||
except error.RepoError: | ||||
# No luck, try older compatibility check. | ||||
inst.between([(nullid, nullid)]) | ||||
return inst | ||||
|
r25660 | except error.RepoError as httpexception: | ||
|
r17192 | try: | ||
r = statichttprepo.instance(ui, "static-" + path, create) | ||||
|
r29241 | ui.note(_('(falling back to static-http)\n')) | ||
|
r17192 | return r | ||
except error.RepoError: | ||||
raise httpexception # use the original http RepoError instead | ||||