wireprotoserver.py
357 lines
| 10.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / wireprotoserver.py
Gregory Szorc
|
r35874 | # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
Gregory Szorc
|
r35890 | import abc | ||
Gregory Szorc
|
r35874 | import cgi | ||
import struct | ||||
Gregory Szorc
|
r35877 | import sys | ||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r35877 | from .i18n import _ | ||
Gregory Szorc
|
r35874 | from . import ( | ||
Gregory Szorc
|
r35877 | encoding, | ||
Gregory Szorc
|
r35874 | error, | ||
Gregory Szorc
|
r35877 | hook, | ||
Gregory Szorc
|
r35874 | pycompat, | ||
util, | ||||
wireproto, | ||||
) | ||||
stringio = util.stringio | ||||
urlerr = util.urlerr | ||||
urlreq = util.urlreq | ||||
Gregory Szorc
|
r35876 | HTTP_OK = 200 | ||
Gregory Szorc
|
r35874 | HGTYPE = 'application/mercurial-0.1' | ||
HGTYPE2 = 'application/mercurial-0.2' | ||||
HGERRTYPE = 'application/hg-error' | ||||
Gregory Szorc
|
r35878 | class abstractserverproto(object): | ||
"""abstract class that summarizes the protocol API | ||||
Used as reference and documentation. | ||||
""" | ||||
Gregory Szorc
|
r35890 | __metaclass__ = abc.ABCMeta | ||
Gregory Szorc
|
r35891 | @abc.abstractproperty | ||
def name(self): | ||||
"""The name of the protocol implementation. | ||||
Used for uniquely identifying the transport type. | ||||
""" | ||||
Gregory Szorc
|
r35890 | @abc.abstractmethod | ||
Gregory Szorc
|
r35878 | def getargs(self, args): | ||
"""return the value for arguments in <args> | ||||
returns a list of values (same order as <args>)""" | ||||
Gregory Szorc
|
r35890 | @abc.abstractmethod | ||
Gregory Szorc
|
r35878 | def getfile(self, fp): | ||
"""write the whole content of a file into a file like object | ||||
The file is in the form:: | ||||
(<chunk-size>\n<chunk>)+0\n | ||||
chunk size is the ascii version of the int. | ||||
""" | ||||
Gregory Szorc
|
r35890 | @abc.abstractmethod | ||
Gregory Szorc
|
r35878 | def redirect(self): | ||
"""may setup interception for stdout and stderr | ||||
See also the `restore` method.""" | ||||
# If the `redirect` function does install interception, the `restore` | ||||
# function MUST be defined. If interception is not used, this function | ||||
# MUST NOT be defined. | ||||
# | ||||
# left commented here on purpose | ||||
# | ||||
#def restore(self): | ||||
# """reinstall previous stdout and stderr and return intercepted stdout | ||||
# """ | ||||
# raise NotImplementedError() | ||||
Gregory Szorc
|
r35874 | def decodevaluefromheaders(req, headerprefix): | ||
"""Decode a long value from multiple HTTP request headers. | ||||
Returns the value as a bytes, not a str. | ||||
""" | ||||
chunks = [] | ||||
i = 1 | ||||
prefix = headerprefix.upper().replace(r'-', r'_') | ||||
while True: | ||||
v = req.env.get(r'HTTP_%s_%d' % (prefix, i)) | ||||
if v is None: | ||||
break | ||||
chunks.append(pycompat.bytesurl(v)) | ||||
i += 1 | ||||
return ''.join(chunks) | ||||
Gregory Szorc
|
r35878 | class webproto(abstractserverproto): | ||
Gregory Szorc
|
r35874 | def __init__(self, req, ui): | ||
Gregory Szorc
|
r35884 | self._req = req | ||
self._ui = ui | ||||
Gregory Szorc
|
r35891 | |||
@property | ||||
def name(self): | ||||
return 'http' | ||||
Gregory Szorc
|
r35874 | |||
def getargs(self, args): | ||||
knownargs = self._args() | ||||
data = {} | ||||
keys = args.split() | ||||
for k in keys: | ||||
if k == '*': | ||||
star = {} | ||||
for key in knownargs.keys(): | ||||
if key != 'cmd' and key not in keys: | ||||
star[key] = knownargs[key][0] | ||||
data['*'] = star | ||||
else: | ||||
data[k] = knownargs[k][0] | ||||
return [data[k] for k in keys] | ||||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r35874 | def _args(self): | ||
Yuya Nishihara
|
r35918 | args = util.rapply(pycompat.bytesurl, self._req.form.copy()) | ||
Gregory Szorc
|
r35884 | postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0)) | ||
Gregory Szorc
|
r35874 | if postlen: | ||
args.update(cgi.parse_qs( | ||||
Gregory Szorc
|
r35884 | self._req.read(postlen), keep_blank_values=True)) | ||
Gregory Szorc
|
r35874 | return args | ||
Gregory Szorc
|
r35884 | argvalue = decodevaluefromheaders(self._req, r'X-HgArg') | ||
Gregory Szorc
|
r35874 | args.update(cgi.parse_qs(argvalue, keep_blank_values=True)) | ||
return args | ||||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r35874 | def getfile(self, fp): | ||
Gregory Szorc
|
r35884 | length = int(self._req.env[r'CONTENT_LENGTH']) | ||
Gregory Szorc
|
r35874 | # If httppostargs is used, we need to read Content-Length | ||
# minus the amount that was consumed by args. | ||||
Gregory Szorc
|
r35884 | length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0)) | ||
for s in util.filechunkiter(self._req, limit=length): | ||||
Gregory Szorc
|
r35874 | fp.write(s) | ||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r35874 | def redirect(self): | ||
Gregory Szorc
|
r35884 | self._oldio = self._ui.fout, self._ui.ferr | ||
self._ui.ferr = self._ui.fout = stringio() | ||||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r35874 | def restore(self): | ||
Gregory Szorc
|
r35884 | val = self._ui.fout.getvalue() | ||
self._ui.ferr, self._ui.fout = self._oldio | ||||
Gregory Szorc
|
r35874 | return val | ||
def _client(self): | ||||
return 'remote:%s:%s:%s' % ( | ||||
Gregory Szorc
|
r35884 | self._req.env.get('wsgi.url_scheme') or 'http', | ||
urlreq.quote(self._req.env.get('REMOTE_HOST', '')), | ||||
urlreq.quote(self._req.env.get('REMOTE_USER', ''))) | ||||
Gregory Szorc
|
r35874 | |||
def responsetype(self, prefer_uncompressed): | ||||
"""Determine the appropriate response type and compression settings. | ||||
Returns a tuple of (mediatype, compengine, engineopts). | ||||
""" | ||||
# Determine the response media type and compression engine based | ||||
# on the request parameters. | ||||
Gregory Szorc
|
r35884 | protocaps = decodevaluefromheaders(self._req, r'X-HgProto').split(' ') | ||
Gregory Szorc
|
r35874 | |||
if '0.2' in protocaps: | ||||
# All clients are expected to support uncompressed data. | ||||
if prefer_uncompressed: | ||||
return HGTYPE2, util._noopengine(), {} | ||||
# Default as defined by wire protocol spec. | ||||
compformats = ['zlib', 'none'] | ||||
for cap in protocaps: | ||||
if cap.startswith('comp='): | ||||
compformats = cap[5:].split(',') | ||||
break | ||||
# Now find an agreed upon compression format. | ||||
Gregory Szorc
|
r35884 | for engine in wireproto.supportedcompengines(self._ui, self, | ||
Gregory Szorc
|
r35874 | util.SERVERROLE): | ||
if engine.wireprotosupport().name in compformats: | ||||
opts = {} | ||||
Gregory Szorc
|
r35884 | level = self._ui.configint('server', | ||
Gregory Szorc
|
r35874 | '%slevel' % engine.name()) | ||
if level is not None: | ||||
opts['level'] = level | ||||
return HGTYPE2, engine, opts | ||||
# No mutually supported compression format. Fall back to the | ||||
# legacy protocol. | ||||
# Don't allow untrusted settings because disabling compression or | ||||
# setting a very high compression level could lead to flooding | ||||
# the server's network or CPU. | ||||
Gregory Szorc
|
r35884 | opts = {'level': self._ui.configint('server', 'zliblevel')} | ||
Gregory Szorc
|
r35874 | return HGTYPE, util.compengines['zlib'], opts | ||
def iscmd(cmd): | ||||
return cmd in wireproto.commands | ||||
Gregory Szorc
|
r35875 | def callhttp(repo, req, cmd): | ||
Gregory Szorc
|
r35882 | proto = webproto(req, repo.ui) | ||
Gregory Szorc
|
r35874 | |||
def genversion2(gen, engine, engineopts): | ||||
# application/mercurial-0.2 always sends a payload header | ||||
# identifying the compression engine. | ||||
name = engine.wireprotosupport().name | ||||
assert 0 < len(name) < 256 | ||||
yield struct.pack('B', len(name)) | ||||
yield name | ||||
for chunk in gen: | ||||
yield chunk | ||||
Gregory Szorc
|
r35882 | rsp = wireproto.dispatch(repo, proto, cmd) | ||
Gregory Szorc
|
r35874 | if isinstance(rsp, bytes): | ||
req.respond(HTTP_OK, HGTYPE, body=rsp) | ||||
return [] | ||||
elif isinstance(rsp, wireproto.streamres_legacy): | ||||
gen = rsp.gen | ||||
req.respond(HTTP_OK, HGTYPE) | ||||
return gen | ||||
elif isinstance(rsp, wireproto.streamres): | ||||
gen = rsp.gen | ||||
# This code for compression should not be streamres specific. It | ||||
# is here because we only compress streamres at the moment. | ||||
Gregory Szorc
|
r35882 | mediatype, engine, engineopts = proto.responsetype( | ||
rsp.prefer_uncompressed) | ||||
Gregory Szorc
|
r35874 | gen = engine.compressstream(gen, engineopts) | ||
if mediatype == HGTYPE2: | ||||
gen = genversion2(gen, engine, engineopts) | ||||
req.respond(HTTP_OK, mediatype) | ||||
return gen | ||||
elif isinstance(rsp, wireproto.pushres): | ||||
Gregory Szorc
|
r35882 | val = proto.restore() | ||
Gregory Szorc
|
r35874 | rsp = '%d\n%s' % (rsp.res, val) | ||
req.respond(HTTP_OK, HGTYPE, body=rsp) | ||||
return [] | ||||
elif isinstance(rsp, wireproto.pusherr): | ||||
# drain the incoming bundle | ||||
req.drain() | ||||
Gregory Szorc
|
r35882 | proto.restore() | ||
Gregory Szorc
|
r35874 | rsp = '0\n%s\n' % rsp.res | ||
req.respond(HTTP_OK, HGTYPE, body=rsp) | ||||
return [] | ||||
elif isinstance(rsp, wireproto.ooberror): | ||||
rsp = rsp.message | ||||
req.respond(HTTP_OK, HGERRTYPE, body=rsp) | ||||
return [] | ||||
raise error.ProgrammingError('hgweb.protocol internal failure', rsp) | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35878 | class sshserver(abstractserverproto): | ||
Gregory Szorc
|
r35877 | def __init__(self, ui, repo): | ||
Gregory Szorc
|
r35888 | self._ui = ui | ||
self._repo = repo | ||||
self._fin = ui.fin | ||||
self._fout = ui.fout | ||||
Gregory Szorc
|
r35877 | |||
hook.redirect(True) | ||||
ui.fout = repo.ui.fout = ui.ferr | ||||
# Prevent insertion/deletion of CRs | ||||
Gregory Szorc
|
r35888 | util.setbinary(self._fin) | ||
util.setbinary(self._fout) | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35891 | @property | ||
def name(self): | ||||
return 'ssh' | ||||
Gregory Szorc
|
r35877 | def getargs(self, args): | ||
data = {} | ||||
keys = args.split() | ||||
for n in xrange(len(keys)): | ||||
Gregory Szorc
|
r35888 | argline = self._fin.readline()[:-1] | ||
Gregory Szorc
|
r35877 | arg, l = argline.split() | ||
if arg not in keys: | ||||
raise error.Abort(_("unexpected parameter %r") % arg) | ||||
if arg == '*': | ||||
star = {} | ||||
for k in xrange(int(l)): | ||||
Gregory Szorc
|
r35888 | argline = self._fin.readline()[:-1] | ||
Gregory Szorc
|
r35877 | arg, l = argline.split() | ||
Gregory Szorc
|
r35888 | val = self._fin.read(int(l)) | ||
Gregory Szorc
|
r35877 | star[arg] = val | ||
data['*'] = star | ||||
else: | ||||
Gregory Szorc
|
r35888 | val = self._fin.read(int(l)) | ||
Gregory Szorc
|
r35877 | data[arg] = val | ||
return [data[k] for k in keys] | ||||
def getfile(self, fpout): | ||||
Gregory Szorc
|
r35889 | self._sendresponse('') | ||
Gregory Szorc
|
r35888 | count = int(self._fin.readline()) | ||
Gregory Szorc
|
r35877 | while count: | ||
Gregory Szorc
|
r35888 | fpout.write(self._fin.read(count)) | ||
count = int(self._fin.readline()) | ||||
Gregory Szorc
|
r35877 | |||
def redirect(self): | ||||
pass | ||||
Gregory Szorc
|
r35889 | def _sendresponse(self, v): | ||
Gregory Szorc
|
r35888 | self._fout.write("%d\n" % len(v)) | ||
self._fout.write(v) | ||||
self._fout.flush() | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35889 | def _sendstream(self, source): | ||
Gregory Szorc
|
r35888 | write = self._fout.write | ||
Gregory Szorc
|
r35877 | for chunk in source.gen: | ||
write(chunk) | ||||
Gregory Szorc
|
r35888 | self._fout.flush() | ||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35889 | def _sendpushresponse(self, rsp): | ||
self._sendresponse('') | ||||
self._sendresponse(str(rsp.res)) | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35889 | def _sendpusherror(self, rsp): | ||
self._sendresponse(rsp.res) | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35889 | def _sendooberror(self, rsp): | ||
Gregory Szorc
|
r35888 | self._ui.ferr.write('%s\n-\n' % rsp.message) | ||
self._ui.ferr.flush() | ||||
self._fout.write('\n') | ||||
self._fout.flush() | ||||
Gregory Szorc
|
r35877 | |||
def serve_forever(self): | ||||
Gregory Szorc
|
r35886 | while self.serve_one(): | ||
pass | ||||
Gregory Szorc
|
r35877 | sys.exit(0) | ||
Gregory Szorc
|
r35889 | _handlers = { | ||
str: _sendresponse, | ||||
wireproto.streamres: _sendstream, | ||||
wireproto.streamres_legacy: _sendstream, | ||||
wireproto.pushres: _sendpushresponse, | ||||
wireproto.pusherr: _sendpusherror, | ||||
wireproto.ooberror: _sendooberror, | ||||
Gregory Szorc
|
r35877 | } | ||
def serve_one(self): | ||||
Gregory Szorc
|
r35888 | cmd = self._fin.readline()[:-1] | ||
Gregory Szorc
|
r35877 | if cmd and cmd in wireproto.commands: | ||
Gregory Szorc
|
r35888 | rsp = wireproto.dispatch(self._repo, self, cmd) | ||
Gregory Szorc
|
r35889 | self._handlers[rsp.__class__](self, rsp) | ||
Gregory Szorc
|
r35877 | elif cmd: | ||
Gregory Szorc
|
r35889 | self._sendresponse("") | ||
Gregory Szorc
|
r35877 | return cmd != '' | ||
def _client(self): | ||||
client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0] | ||||
return 'remote:ssh:' + client | ||||