wireprotoserver.py
561 lines
| 17.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / wireprotoserver.py
Gregory Szorc
|
r35874 | # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||
Raphaël Gomès
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
Gregory Szorc
|
r35874 | # | ||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r36083 | import contextlib | ||
Gregory Szorc
|
r35874 | import struct | ||
Gregory Szorc
|
r36540 | import threading | ||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r35877 | from .i18n import _ | ||
Gregory Szorc
|
r35874 | from . import ( | ||
Gregory Szorc
|
r35877 | encoding, | ||
Gregory Szorc
|
r35874 | error, | ||
pycompat, | ||||
util, | ||||
Gregory Szorc
|
r36090 | wireprototypes, | ||
Gregory Szorc
|
r37803 | wireprotov1server, | ||
Gregory Szorc
|
r35874 | ) | ||
Augie Fackler
|
r43346 | from .interfaces import util as interfaceutil | ||
Yuya Nishihara
|
r37138 | from .utils import ( | ||
r42208 | compression, | |||
Matt Harbison
|
r47524 | stringutil, | ||
Yuya Nishihara
|
r37138 | ) | ||
Gregory Szorc
|
r35874 | |||
stringio = util.stringio | ||||
urlerr = util.urlerr | ||||
urlreq = util.urlreq | ||||
Gregory Szorc
|
r35876 | HTTP_OK = 200 | ||
Augie Fackler
|
r43347 | HGTYPE = b'application/mercurial-0.1' | ||
HGTYPE2 = b'application/mercurial-0.2' | ||||
HGERRTYPE = b'application/hg-error' | ||||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r36553 | SSHV1 = wireprototypes.SSHV1 | ||
Gregory Szorc
|
r35994 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36862 | def decodevaluefromheaders(req, headerprefix): | ||
Gregory Szorc
|
r35874 | """Decode a long value from multiple HTTP request headers. | ||
Returns the value as a bytes, not a str. | ||||
""" | ||||
chunks = [] | ||||
i = 1 | ||||
while True: | ||||
Gregory Szorc
|
r36862 | v = req.headers.get(b'%s-%d' % (headerprefix, i)) | ||
Gregory Szorc
|
r35874 | if v is None: | ||
break | ||||
chunks.append(pycompat.bytesurl(v)) | ||||
i += 1 | ||||
Augie Fackler
|
r43347 | return b''.join(chunks) | ||
Gregory Szorc
|
r35874 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r37828 | @interfaceutil.implementer(wireprototypes.baseprotocolhandler) | ||
Gregory Szorc
|
r49801 | class httpv1protocolhandler: | ||
Gregory Szorc
|
r36883 | def __init__(self, req, ui, checkperm): | ||
Gregory Szorc
|
r36862 | self._req = req | ||
Gregory Szorc
|
r35884 | self._ui = ui | ||
Gregory Szorc
|
r36819 | self._checkperm = checkperm | ||
Joerg Sonnenberger
|
r37411 | self._protocaps = None | ||
Gregory Szorc
|
r35891 | |||
@property | ||||
def name(self): | ||||
Augie Fackler
|
r43347 | return b'http-v1' | ||
Gregory Szorc
|
r35874 | |||
def getargs(self, args): | ||||
knownargs = self._args() | ||||
data = {} | ||||
keys = args.split() | ||||
for k in keys: | ||||
Augie Fackler
|
r43347 | if k == b'*': | ||
Gregory Szorc
|
r35874 | star = {} | ||
for key in knownargs.keys(): | ||||
Augie Fackler
|
r43347 | if key != b'cmd' and key not in keys: | ||
Gregory Szorc
|
r35874 | star[key] = knownargs[key][0] | ||
Augie Fackler
|
r43347 | data[b'*'] = star | ||
Gregory Szorc
|
r35874 | else: | ||
data[k] = knownargs[k][0] | ||||
return [data[k] for k in keys] | ||||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r35874 | def _args(self): | ||
Gregory Szorc
|
r36878 | args = self._req.qsparams.asdictoflists() | ||
Gregory Szorc
|
r36862 | postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0)) | ||
Gregory Szorc
|
r35874 | if postlen: | ||
Augie Fackler
|
r43346 | args.update( | ||
urlreq.parseqs( | ||||
self._req.bodyfh.read(postlen), keep_blank_values=True | ||||
) | ||||
) | ||||
Gregory Szorc
|
r35874 | return args | ||
Gregory Szorc
|
r36862 | argvalue = decodevaluefromheaders(self._req, b'X-HgArg') | ||
Gregory Szorc
|
r36094 | args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) | ||
Gregory Szorc
|
r35874 | return args | ||
Gregory Szorc
|
r35881 | |||
Joerg Sonnenberger
|
r37411 | def getprotocaps(self): | ||
if self._protocaps is None: | ||||
Augie Fackler
|
r37608 | value = decodevaluefromheaders(self._req, b'X-HgProto') | ||
Augie Fackler
|
r43347 | self._protocaps = set(value.split(b' ')) | ||
Joerg Sonnenberger
|
r37411 | return self._protocaps | ||
Joerg Sonnenberger
|
r37432 | def getpayload(self): | ||
Gregory Szorc
|
r36863 | # Existing clients *always* send Content-Length. | ||
length = int(self._req.headers[b'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
|
r36862 | length -= int(self._req.headers.get(b'X-HgArgs-Post', 0)) | ||
Joerg Sonnenberger
|
r37432 | return util.filechunkiter(self._req.bodyfh, limit=length) | ||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r36083 | @contextlib.contextmanager | ||
def mayberedirectstdio(self): | ||||
oldout = self._ui.fout | ||||
olderr = self._ui.ferr | ||||
out = util.stringio() | ||||
try: | ||||
self._ui.fout = out | ||||
self._ui.ferr = out | ||||
yield out | ||||
finally: | ||||
self._ui.fout = oldout | ||||
self._ui.ferr = olderr | ||||
Gregory Szorc
|
r36086 | def client(self): | ||
Augie Fackler
|
r43347 | return b'remote:%s:%s:%s' % ( | ||
Gregory Szorc
|
r36883 | self._req.urlscheme, | ||
Augie Fackler
|
r43347 | urlreq.quote(self._req.remotehost or b''), | ||
urlreq.quote(self._req.remoteuser or b''), | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r36631 | def addcapabilities(self, repo, caps): | ||
Gregory Szorc
|
r37071 | caps.append(b'batch') | ||
Augie Fackler
|
r43346 | caps.append( | ||
Augie Fackler
|
r43347 | b'httpheader=%d' % repo.ui.configint(b'server', b'maxhttpheaderlen') | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | if repo.ui.configbool(b'experimental', b'httppostargs'): | ||
caps.append(b'httppostargs') | ||||
Gregory Szorc
|
r36631 | |||
# FUTURE advertise 0.2rx once support is implemented | ||||
# FUTURE advertise minrx and mintx after consulting config option | ||||
Augie Fackler
|
r43347 | caps.append(b'httpmediatype=0.1rx,0.1tx,0.2tx') | ||
Gregory Szorc
|
r36631 | |||
Augie Fackler
|
r43346 | compengines = wireprototypes.supportedcompengines( | ||
repo.ui, compression.SERVERROLE | ||||
) | ||||
Gregory Szorc
|
r36631 | if compengines: | ||
Augie Fackler
|
r43347 | comptypes = b','.join( | ||
Augie Fackler
|
r43346 | urlreq.quote(e.wireprotosupport().name) for e in compengines | ||
) | ||||
Augie Fackler
|
r43347 | caps.append(b'compression=%s' % comptypes) | ||
Gregory Szorc
|
r36631 | |||
return caps | ||||
Gregory Szorc
|
r36819 | def checkperm(self, perm): | ||
return self._checkperm(perm) | ||||
Augie Fackler
|
r43346 | |||
Augie Fackler
|
r36249 | # This method exists mostly so that extensions like remotefilelog can | ||
# disable a kludgey legacy method only over http. As of early 2018, | ||||
# there are no other known users, so with any luck we can discard this | ||||
# hook if remotefilelog becomes a first-party extension. | ||||
Gregory Szorc
|
r35874 | def iscmd(cmd): | ||
Gregory Szorc
|
r37803 | return cmd in wireprotov1server.commands | ||
Gregory Szorc
|
r35874 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36893 | def handlewsgirequest(rctx, req, res, checkperm): | ||
Gregory Szorc
|
r36830 | """Possibly process a wire protocol request. | ||
Gregory Szorc
|
r36002 | |||
Gregory Szorc
|
r36830 | If the current request is a wire protocol request, the request is | ||
processed by this function. | ||||
Gregory Szorc
|
r36002 | |||
Gregory Szorc
|
r36828 | ``req`` is a ``parsedrequest`` instance. | ||
Gregory Szorc
|
r36877 | ``res`` is a ``wsgiresponse`` instance. | ||
Gregory Szorc
|
r36830 | |||
Gregory Szorc
|
r36877 | Returns a bool indicating if the request was serviced. If set, the caller | ||
should stop processing the request, as a response has already been issued. | ||||
Gregory Szorc
|
r36002 | """ | ||
Gregory Szorc
|
r36830 | # Avoid cycle involving hg module. | ||
from .hgweb import common as hgwebcommon | ||||
Gregory Szorc
|
r36819 | repo = rctx.repo | ||
Gregory Szorc
|
r36002 | # HTTP version 1 wire protocol requests are denoted by a "cmd" query | ||
# string parameter. If it isn't present, this isn't a wire protocol | ||||
# request. | ||||
Augie Fackler
|
r43347 | if b'cmd' not in req.qsparams: | ||
Gregory Szorc
|
r36877 | return False | ||
Gregory Szorc
|
r36002 | |||
Augie Fackler
|
r43347 | cmd = req.qsparams[b'cmd'] | ||
Gregory Szorc
|
r36002 | |||
# The "cmd" request parameter is used by both the wire protocol and hgweb. | ||||
# While not all wire protocol commands are available for all transports, | ||||
# if we see a "cmd" value that resembles a known wire protocol command, we | ||||
# route it to a protocol handler. This is better than routing possible | ||||
# wire protocol requests to hgweb because it prevents hgweb from using | ||||
# known wire protocol commands and it is less confusing for machine | ||||
# clients. | ||||
Augie Fackler
|
r36249 | if not iscmd(cmd): | ||
Gregory Szorc
|
r36877 | return False | ||
Gregory Szorc
|
r36830 | |||
# The "cmd" query string argument is only valid on the root path of the | ||||
# repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo | ||||
# like ``/blah?cmd=foo`` are not allowed. So don't recognize the request | ||||
# in this case. We send an HTTP 404 for backwards compatibility reasons. | ||||
if req.dispatchpath: | ||||
Gregory Szorc
|
r36877 | res.status = hgwebcommon.statusmessage(404) | ||
Augie Fackler
|
r43347 | res.headers[b'Content-Type'] = HGTYPE | ||
Gregory Szorc
|
r36877 | # TODO This is not a good response to issue for this request. This | ||
# is mostly for BC for now. | ||||
Augie Fackler
|
r43347 | res.setbodybytes(b'0\n%s\n' % b'Not Found') | ||
Gregory Szorc
|
r36877 | return True | ||
Gregory Szorc
|
r36002 | |||
Augie Fackler
|
r43346 | proto = httpv1protocolhandler( | ||
req, repo.ui, lambda perm: checkperm(rctx, req, perm) | ||||
) | ||||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r36830 | # The permissions checker should be the only thing that can raise an | ||
# ErrorResponse. It is kind of a layer violation to catch an hgweb | ||||
# exception here. So consider refactoring into a exception type that | ||||
# is associated with the wire protocol. | ||||
try: | ||||
Gregory Szorc
|
r36883 | _callhttp(repo, req, res, proto, cmd) | ||
Gregory Szorc
|
r36830 | except hgwebcommon.ErrorResponse as e: | ||
Gregory Szorc
|
r36877 | for k, v in e.headers: | ||
res.headers[k] = v | ||||
Matt Harbison
|
r47524 | res.status = hgwebcommon.statusmessage( | ||
e.code, stringutil.forcebytestr(e) | ||||
) | ||||
Gregory Szorc
|
r36877 | # TODO This response body assumes the failed command was | ||
# "unbundle." That assumption is not always valid. | ||||
Matt Harbison
|
r47524 | res.setbodybytes(b'0\n%s\n' % stringutil.forcebytestr(e)) | ||
Gregory Szorc
|
r36830 | |||
Gregory Szorc
|
r36877 | return True | ||
Gregory Szorc
|
r36002 | |||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r37411 | def _httpresponsetype(ui, proto, prefer_uncompressed): | ||
Gregory Szorc
|
r36089 | """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. | ||||
Augie Fackler
|
r43347 | if b'0.2' in proto.getprotocaps(): | ||
Gregory Szorc
|
r36089 | # All clients are expected to support uncompressed data. | ||
if prefer_uncompressed: | ||||
r42208 | return HGTYPE2, compression._noopengine(), {} | |||
Gregory Szorc
|
r36089 | |||
# Now find an agreed upon compression format. | ||||
Gregory Szorc
|
r37803 | compformats = wireprotov1server.clientcompressionsupport(proto) | ||
Augie Fackler
|
r43346 | for engine in wireprototypes.supportedcompengines( | ||
ui, compression.SERVERROLE | ||||
): | ||||
Gregory Szorc
|
r36089 | if engine.wireprotosupport().name in compformats: | ||
opts = {} | ||||
Augie Fackler
|
r43347 | level = ui.configint(b'server', b'%slevel' % engine.name()) | ||
Gregory Szorc
|
r36089 | if level is not None: | ||
Augie Fackler
|
r43347 | opts[b'level'] = level | ||
Gregory Szorc
|
r36089 | |||
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. | ||||
Augie Fackler
|
r43347 | opts = {b'level': ui.configint(b'server', b'zliblevel')} | ||
return HGTYPE, util.compengines[b'zlib'], opts | ||||
Gregory Szorc
|
r36089 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36883 | def _callhttp(repo, req, res, proto, cmd): | ||
Gregory Szorc
|
r36877 | # Avoid cycle involving hg module. | ||
from .hgweb import common as hgwebcommon | ||||
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 | ||||
Augie Fackler
|
r43347 | yield struct.pack(b'B', len(name)) | ||
Gregory Szorc
|
r35874 | yield name | ||
for chunk in gen: | ||||
yield chunk | ||||
Gregory Szorc
|
r36877 | def setresponse(code, contenttype, bodybytes=None, bodygen=None): | ||
if code == HTTP_OK: | ||||
Augie Fackler
|
r43347 | res.status = b'200 Script output follows' | ||
Gregory Szorc
|
r36877 | else: | ||
res.status = hgwebcommon.statusmessage(code) | ||||
Augie Fackler
|
r43347 | res.headers[b'Content-Type'] = contenttype | ||
Gregory Szorc
|
r36877 | |||
if bodybytes is not None: | ||||
res.setbodybytes(bodybytes) | ||||
if bodygen is not None: | ||||
res.setbodygen(bodygen) | ||||
Gregory Szorc
|
r37803 | if not wireprotov1server.commands.commandavailable(cmd, proto): | ||
Augie Fackler
|
r43346 | setresponse( | ||
HTTP_OK, | ||||
HGERRTYPE, | ||||
Augie Fackler
|
r43347 | _( | ||
b'requested wire protocol command is not available over ' | ||||
b'HTTP' | ||||
), | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r36877 | return | ||
Gregory Szorc
|
r36000 | |||
Gregory Szorc
|
r37803 | proto.checkperm(wireprotov1server.commands[cmd].permission) | ||
Gregory Szorc
|
r36817 | |||
Manuel Jacob
|
r51312 | accesshidden = hgwebcommon.hashiddenaccess(repo, req) | ||
rsp = wireprotov1server.dispatch(repo, proto, cmd, accesshidden) | ||||
Gregory Szorc
|
r36816 | |||
Gregory Szorc
|
r35874 | if isinstance(rsp, bytes): | ||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | ||
Gregory Szorc
|
r36091 | elif isinstance(rsp, wireprototypes.bytesresponse): | ||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data) | ||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.streamreslegacy): | ||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen) | ||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.streamres): | ||
Gregory Szorc
|
r35874 | 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
|
r36089 | mediatype, engine, engineopts = _httpresponsetype( | ||
Augie Fackler
|
r43346 | repo.ui, proto, rsp.prefer_uncompressed | ||
) | ||||
Gregory Szorc
|
r35874 | gen = engine.compressstream(gen, engineopts) | ||
if mediatype == HGTYPE2: | ||||
gen = genversion2(gen, engine, engineopts) | ||||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, mediatype, bodygen=gen) | ||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.pushres): | ||
Augie Fackler
|
r43347 | rsp = b'%d\n%s' % (rsp.res, rsp.output) | ||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | ||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.pusherr): | ||
Augie Fackler
|
r43347 | rsp = b'0\n%s\n' % rsp.res | ||
Gregory Szorc
|
r36877 | res.drain = True | ||
setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | ||||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.ooberror): | ||
Gregory Szorc
|
r36877 | setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) | ||
else: | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'hgweb.protocol internal failure', rsp) | ||
Gregory Szorc
|
r36004 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36081 | def _sshv1respondbytes(fout, value): | ||
"""Send a bytes response for protocol version 1.""" | ||||
Augie Fackler
|
r43347 | fout.write(b'%d\n' % len(value)) | ||
Gregory Szorc
|
r36081 | fout.write(value) | ||
fout.flush() | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36081 | def _sshv1respondstream(fout, source): | ||
write = fout.write | ||||
for chunk in source.gen: | ||||
write(chunk) | ||||
fout.flush() | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36081 | def _sshv1respondooberror(fout, ferr, rsp): | ||
ferr.write(b'%s\n-\n' % rsp) | ||||
ferr.flush() | ||||
fout.write(b'\n') | ||||
fout.flush() | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r37828 | @interfaceutil.implementer(wireprototypes.baseprotocolhandler) | ||
Gregory Szorc
|
r49801 | class sshv1protocolhandler: | ||
Gregory Szorc
|
r36082 | """Handler for requests services via version 1 of SSH protocol.""" | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36082 | def __init__(self, ui, fin, fout): | ||
Gregory Szorc
|
r35888 | self._ui = ui | ||
Gregory Szorc
|
r36082 | self._fin = fin | ||
self._fout = fout | ||||
Joerg Sonnenberger
|
r37411 | self._protocaps = set() | ||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35891 | @property | ||
def name(self): | ||||
Gregory Szorc
|
r36553 | return wireprototypes.SSHV1 | ||
Gregory Szorc
|
r35891 | |||
Gregory Szorc
|
r35877 | def getargs(self, args): | ||
data = {} | ||||
keys = args.split() | ||||
Manuel Jacob
|
r50179 | for n in range(len(keys)): | ||
Gregory Szorc
|
r35888 | argline = self._fin.readline()[:-1] | ||
Gregory Szorc
|
r35877 | arg, l = argline.split() | ||
if arg not in keys: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b"unexpected parameter %r") % arg) | ||
if arg == b'*': | ||||
Gregory Szorc
|
r35877 | star = {} | ||
Manuel Jacob
|
r50179 | for k in range(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 | ||
Augie Fackler
|
r43347 | data[b'*'] = star | ||
Gregory Szorc
|
r35877 | else: | ||
Gregory Szorc
|
r35888 | val = self._fin.read(int(l)) | ||
Gregory Szorc
|
r35877 | data[arg] = val | ||
return [data[k] for k in keys] | ||||
Joerg Sonnenberger
|
r37411 | def getprotocaps(self): | ||
return self._protocaps | ||||
Joerg Sonnenberger
|
r37432 | def getpayload(self): | ||
Gregory Szorc
|
r36390 | # We initially send an empty response. This tells the client it is | ||
# OK to start sending data. If a client sees any other response, it | ||||
# interprets it as an error. | ||||
_sshv1respondbytes(self._fout, b'') | ||||
Gregory Szorc
|
r36087 | # The file is in the form: | ||
# | ||||
# <chunk size>\n<chunk> | ||||
# ... | ||||
# 0\n | ||||
Gregory Szorc
|
r35888 | count = int(self._fin.readline()) | ||
Gregory Szorc
|
r35877 | while count: | ||
Joerg Sonnenberger
|
r37432 | yield self._fin.read(count) | ||
Gregory Szorc
|
r35888 | count = int(self._fin.readline()) | ||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r36083 | @contextlib.contextmanager | ||
def mayberedirectstdio(self): | ||||
yield None | ||||
Gregory Szorc
|
r36086 | def client(self): | ||
Augie Fackler
|
r43347 | client = encoding.environ.get(b'SSH_CLIENT', b'').split(b' ', 1)[0] | ||
return b'remote:ssh:' + client | ||||
Gregory Szorc
|
r36082 | |||
Gregory Szorc
|
r36631 | def addcapabilities(self, repo, caps): | ||
Joerg Sonnenberger
|
r37411 | if self.name == wireprototypes.SSHV1: | ||
caps.append(b'protocaps') | ||||
Gregory Szorc
|
r37071 | caps.append(b'batch') | ||
Gregory Szorc
|
r36631 | return caps | ||
Gregory Szorc
|
r36819 | def checkperm(self, perm): | ||
pass | ||||
Augie Fackler
|
r43346 | |||
Manuel Jacob
|
r51316 | def _runsshserver(ui, repo, fin, fout, ev, accesshidden=False): | ||
Gregory Szorc
|
r36233 | # This function operates like a state machine of sorts. The following | ||
# states are defined: | ||||
# | ||||
# protov1-serving | ||||
# Server is in protocol version 1 serving mode. Commands arrive on | ||||
# new lines. These commands are processed in this state, one command | ||||
# after the other. | ||||
# | ||||
# shutdown | ||||
# The server is shutting down, possibly in reaction to a client event. | ||||
# | ||||
# And here are their transitions: | ||||
# | ||||
# protov1-serving -> shutdown | ||||
# When server receives an empty request or encounters another | ||||
# error. | ||||
Augie Fackler
|
r43347 | state = b'protov1-serving' | ||
Gregory Szorc
|
r36232 | proto = sshv1protocolhandler(ui, fin, fout) | ||
Gregory Szorc
|
r36540 | while not ev.is_set(): | ||
Augie Fackler
|
r43347 | if state == b'protov1-serving': | ||
Gregory Szorc
|
r36232 | # Commands are issued on new lines. | ||
request = fin.readline()[:-1] | ||||
# Empty lines signal to terminate the connection. | ||||
if not request: | ||||
Augie Fackler
|
r43347 | state = b'shutdown' | ||
Gregory Szorc
|
r36232 | continue | ||
Gregory Szorc
|
r37803 | available = wireprotov1server.commands.commandavailable( | ||
Augie Fackler
|
r43346 | request, proto | ||
) | ||||
Gregory Szorc
|
r36232 | |||
# This command isn't available. Send an empty response and go | ||||
# back to waiting for a new command. | ||||
if not available: | ||||
_sshv1respondbytes(fout, b'') | ||||
continue | ||||
Manuel Jacob
|
r51316 | rsp = wireprotov1server.dispatch( | ||
repo, proto, request, accesshidden=accesshidden | ||||
) | ||||
r43166 | repo.ui.fout.flush() | |||
repo.ui.ferr.flush() | ||||
Gregory Szorc
|
r36232 | |||
if isinstance(rsp, bytes): | ||||
_sshv1respondbytes(fout, rsp) | ||||
elif isinstance(rsp, wireprototypes.bytesresponse): | ||||
_sshv1respondbytes(fout, rsp.data) | ||||
elif isinstance(rsp, wireprototypes.streamres): | ||||
_sshv1respondstream(fout, rsp) | ||||
elif isinstance(rsp, wireprototypes.streamreslegacy): | ||||
_sshv1respondstream(fout, rsp) | ||||
elif isinstance(rsp, wireprototypes.pushres): | ||||
_sshv1respondbytes(fout, b'') | ||||
_sshv1respondbytes(fout, b'%d' % rsp.res) | ||||
elif isinstance(rsp, wireprototypes.pusherr): | ||||
_sshv1respondbytes(fout, rsp.res) | ||||
elif isinstance(rsp, wireprototypes.ooberror): | ||||
_sshv1respondooberror(fout, ui.ferr, rsp.message) | ||||
else: | ||||
Augie Fackler
|
r43346 | raise error.ProgrammingError( | ||
Augie Fackler
|
r43347 | b'unhandled response type from ' | ||
b'wire protocol command: %s' % rsp | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r36232 | |||
Augie Fackler
|
r43347 | elif state == b'shutdown': | ||
Gregory Szorc
|
r36232 | break | ||
else: | ||||
Augie Fackler
|
r43346 | raise error.ProgrammingError( | ||
Augie Fackler
|
r43347 | b'unhandled ssh server state: %s' % state | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r36232 | |||
Gregory Szorc
|
r49801 | class sshserver: | ||
Manuel Jacob
|
r51316 | def __init__(self, ui, repo, logfh=None, accesshidden=False): | ||
Gregory Szorc
|
r36082 | self._ui = ui | ||
self._repo = repo | ||||
Manuel Jacob
|
r51316 | self._accesshidden = accesshidden | ||
Arseniy Alekseyev
|
r52465 | self._logfh = logfh | ||
Gregory Szorc
|
r36543 | |||
Gregory Szorc
|
r35877 | def serve_forever(self): | ||
Gregory Szorc
|
r36540 | self.serveuntil(threading.Event()) | ||
def serveuntil(self, ev): | ||||
"""Serve until a threading.Event is set.""" | ||||
Arseniy Alekseyev
|
r52465 | with self._ui.protectedfinout() as (fin, fout): | ||
if self._logfh: | ||||
# Log write I/O to stdout and stderr if configured. | ||||
fout = util.makeloggingfileobject( | ||||
self._logfh, | ||||
fout, | ||||
b'o', | ||||
logdata=True, | ||||
) | ||||
self._ui.ferr = util.makeloggingfileobject( | ||||
self._logfh, | ||||
self._ui.ferr, | ||||
b'e', | ||||
logdata=True, | ||||
) | ||||
_runsshserver( | ||||
self._ui, | ||||
self._repo, | ||||
fin, | ||||
fout, | ||||
ev, | ||||
self._accesshidden, | ||||
) | ||||