wireprotoserver.py
644 lines
| 21.5 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
|
r36083 | import contextlib | ||
Gregory Szorc
|
r35874 | 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, | ||||
Gregory Szorc
|
r36090 | wireprototypes, | ||
Gregory Szorc
|
r35874 | ) | ||
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
|
r35994 | # Names of the SSH protocol implementations. | ||
SSHV1 = 'ssh-v1' | ||||
# This is advertised over the wire. Incremental the counter at the end | ||||
# to reflect BC breakages. | ||||
SSHV2 = 'exp-ssh-v2-0001' | ||||
Gregory Szorc
|
r36006 | class baseprotocolhandler(object): | ||
"""Abstract base class for wire protocol handlers. | ||||
Gregory Szorc
|
r35878 | |||
Gregory Szorc
|
r36006 | A wire protocol handler serves as an interface between protocol command | ||
handlers and the wire protocol transport layer. Protocol handlers provide | ||||
methods to read command arguments, redirect stdio for the duration of | ||||
the request, handle response types, etc. | ||||
Gregory Szorc
|
r35878 | """ | ||
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
|
r36087 | def forwardpayload(self, fp): | ||
"""Read the raw payload and forward to a file. | ||||
Gregory Szorc
|
r35878 | |||
Gregory Szorc
|
r36087 | The payload is read in full before the function returns. | ||
Gregory Szorc
|
r35878 | """ | ||
Gregory Szorc
|
r35890 | @abc.abstractmethod | ||
Gregory Szorc
|
r36083 | def mayberedirectstdio(self): | ||
"""Context manager to possibly redirect stdio. | ||||
The context manager yields a file-object like object that receives | ||||
stdout and stderr output when the context manager is active. Or it | ||||
yields ``None`` if no I/O redirection occurs. | ||||
The intent of this context manager is to capture stdio output | ||||
so it may be sent in the response. Some transports support streaming | ||||
stdio to the client in real time. For these transports, stdio output | ||||
won't be captured. | ||||
""" | ||||
Gregory Szorc
|
r36086 | @abc.abstractmethod | ||
def client(self): | ||||
"""Returns a string representation of this client (as bytes).""" | ||||
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
|
r36240 | class httpv1protocolhandler(baseprotocolhandler): | ||
Gregory Szorc
|
r35874 | def __init__(self, req, ui): | ||
Gregory Szorc
|
r35884 | self._req = req | ||
self._ui = ui | ||||
Gregory Szorc
|
r35891 | |||
@property | ||||
def name(self): | ||||
Gregory Szorc
|
r36241 | return 'http-v1' | ||
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: | ||
Gregory Szorc
|
r36094 | args.update(urlreq.parseqs( | ||
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
|
r36094 | args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) | ||
Gregory Szorc
|
r35874 | return args | ||
Gregory Szorc
|
r35881 | |||
Gregory Szorc
|
r36087 | def forwardpayload(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
|
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): | ||
Gregory Szorc
|
r35874 | 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 iscmd(cmd): | ||||
return cmd in wireproto.commands | ||||
Gregory Szorc
|
r36002 | def parsehttprequest(repo, req, query): | ||
"""Parse the HTTP request for a wire protocol request. | ||||
If the current request appears to be a wire protocol request, this | ||||
function returns a dict with details about that request, including | ||||
an ``abstractprotocolserver`` instance suitable for handling the | ||||
request. Otherwise, ``None`` is returned. | ||||
``req`` is a ``wsgirequest`` instance. | ||||
""" | ||||
# 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. | ||||
if r'cmd' not in req.form: | ||||
return None | ||||
cmd = pycompat.sysbytes(req.form[r'cmd'][0]) | ||||
# 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. | ||||
if cmd not in wireproto.commands: | ||||
return None | ||||
Gregory Szorc
|
r36240 | proto = httpv1protocolhandler(req, repo.ui) | ||
Gregory Szorc
|
r35874 | |||
Gregory Szorc
|
r36002 | return { | ||
'cmd': cmd, | ||||
'proto': proto, | ||||
'dispatch': lambda: _callhttp(repo, req, proto, cmd), | ||||
Gregory Szorc
|
r36004 | 'handleerror': lambda ex: _handlehttperror(ex, req, cmd), | ||
Gregory Szorc
|
r36002 | } | ||
Gregory Szorc
|
r36089 | def _httpresponsetype(ui, req, 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. | ||||
protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ') | ||||
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. | ||||
for engine in wireproto.supportedcompengines(ui, util.SERVERROLE): | ||||
if engine.wireprotosupport().name in compformats: | ||||
opts = {} | ||||
level = ui.configint('server', '%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. | ||||
opts = {'level': ui.configint('server', 'zliblevel')} | ||||
return HGTYPE, util.compengines['zlib'], opts | ||||
Gregory Szorc
|
r36002 | def _callhttp(repo, req, proto, cmd): | ||
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
|
r36000 | |||
if not wireproto.commands.commandavailable(cmd, proto): | ||||
req.respond(HTTP_OK, HGERRTYPE, | ||||
body=_('requested wire protocol command is not available ' | ||||
'over HTTP')) | ||||
return [] | ||||
Gregory Szorc
|
r35874 | if isinstance(rsp, bytes): | ||
req.respond(HTTP_OK, HGTYPE, body=rsp) | ||||
return [] | ||||
Gregory Szorc
|
r36091 | elif isinstance(rsp, wireprototypes.bytesresponse): | ||
req.respond(HTTP_OK, HGTYPE, body=rsp.data) | ||||
return [] | ||||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.streamreslegacy): | ||
Gregory Szorc
|
r35874 | gen = rsp.gen | ||
req.respond(HTTP_OK, HGTYPE) | ||||
return 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( | ||
repo.ui, req, 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 | ||||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.pushres): | ||
Gregory Szorc
|
r36084 | rsp = '%d\n%s' % (rsp.res, rsp.output) | ||
Gregory Szorc
|
r35874 | req.respond(HTTP_OK, HGTYPE, body=rsp) | ||
return [] | ||||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.pusherr): | ||
Gregory Szorc
|
r36005 | # This is the httplib workaround documented in _handlehttperror(). | ||
Gregory Szorc
|
r35874 | req.drain() | ||
Gregory Szorc
|
r36005 | |||
Gregory Szorc
|
r35874 | rsp = '0\n%s\n' % rsp.res | ||
req.respond(HTTP_OK, HGTYPE, body=rsp) | ||||
return [] | ||||
Gregory Szorc
|
r36090 | elif isinstance(rsp, wireprototypes.ooberror): | ||
Gregory Szorc
|
r35874 | rsp = rsp.message | ||
req.respond(HTTP_OK, HGERRTYPE, body=rsp) | ||||
return [] | ||||
raise error.ProgrammingError('hgweb.protocol internal failure', rsp) | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r36004 | def _handlehttperror(e, req, cmd): | ||
"""Called when an ErrorResponse is raised during HTTP request processing.""" | ||||
Gregory Szorc
|
r36005 | |||
# Clients using Python's httplib are stateful: the HTTP client | ||||
# won't process an HTTP response until all request data is | ||||
# sent to the server. The intent of this code is to ensure | ||||
# we always read HTTP request data from the client, thus | ||||
# ensuring httplib transitions to a state that allows it to read | ||||
# the HTTP response. In other words, it helps prevent deadlocks | ||||
# on clients using httplib. | ||||
if (req.env[r'REQUEST_METHOD'] == r'POST' and | ||||
# But not if Expect: 100-continue is being used. | ||||
Gregory Szorc
|
r36004 | (req.env.get('HTTP_EXPECT', | ||
'').lower() != '100-continue') or | ||||
Gregory Szorc
|
r36005 | # Or the non-httplib HTTP library is being advertised by | ||
# the client. | ||||
Gregory Szorc
|
r36004 | req.env.get('X-HgHttp2', '')): | ||
req.drain() | ||||
else: | ||||
req.headers.append((r'Connection', r'Close')) | ||||
Gregory Szorc
|
r36005 | # TODO This response body assumes the failed command was | ||
# "unbundle." That assumption is not always valid. | ||||
Gregory Szorc
|
r36004 | req.respond(e, HGTYPE, body='0\n%s\n' % e) | ||
return '' | ||||
Gregory Szorc
|
r36081 | def _sshv1respondbytes(fout, value): | ||
"""Send a bytes response for protocol version 1.""" | ||||
fout.write('%d\n' % len(value)) | ||||
fout.write(value) | ||||
fout.flush() | ||||
def _sshv1respondstream(fout, source): | ||||
write = fout.write | ||||
for chunk in source.gen: | ||||
write(chunk) | ||||
fout.flush() | ||||
def _sshv1respondooberror(fout, ferr, rsp): | ||||
ferr.write(b'%s\n-\n' % rsp) | ||||
ferr.flush() | ||||
fout.write(b'\n') | ||||
fout.flush() | ||||
Gregory Szorc
|
r36082 | class sshv1protocolhandler(baseprotocolhandler): | ||
"""Handler for requests services via version 1 of SSH protocol.""" | ||||
def __init__(self, ui, fin, fout): | ||||
Gregory Szorc
|
r35888 | self._ui = ui | ||
Gregory Szorc
|
r36082 | self._fin = fin | ||
self._fout = fout | ||||
Gregory Szorc
|
r35877 | |||
Gregory Szorc
|
r35891 | @property | ||
def name(self): | ||||
Gregory Szorc
|
r36092 | return SSHV1 | ||
Gregory Szorc
|
r35891 | |||
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] | ||||
Gregory Szorc
|
r36087 | def forwardpayload(self, fpout): | ||
# The file is in the form: | ||||
# | ||||
# <chunk size>\n<chunk> | ||||
# ... | ||||
# 0\n | ||||
Gregory Szorc
|
r36081 | _sshv1respondbytes(self._fout, b'') | ||
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 | |||
Gregory Szorc
|
r36083 | @contextlib.contextmanager | ||
def mayberedirectstdio(self): | ||||
yield None | ||||
Gregory Szorc
|
r36086 | def client(self): | ||
Gregory Szorc
|
r36082 | client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0] | ||
return 'remote:ssh:' + client | ||||
Gregory Szorc
|
r36233 | class sshv2protocolhandler(sshv1protocolhandler): | ||
"""Protocol handler for version 2 of the SSH protocol.""" | ||||
Gregory Szorc
|
r36232 | def _runsshserver(ui, repo, fin, fout): | ||
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. | ||||
# | ||||
# protov2-serving | ||||
# Server is in protocol version 2 serving mode. | ||||
# | ||||
# upgrade-initial | ||||
# The server is going to process an upgrade request. | ||||
# | ||||
# upgrade-v2-filter-legacy-handshake | ||||
# The protocol is being upgraded to version 2. The server is expecting | ||||
# the legacy handshake from version 1. | ||||
# | ||||
# upgrade-v2-finish | ||||
# The upgrade to version 2 of the protocol is imminent. | ||||
# | ||||
# 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. | ||||
# | ||||
# protov1-serving -> upgrade-initial | ||||
# An upgrade request line was seen. | ||||
# | ||||
# upgrade-initial -> upgrade-v2-filter-legacy-handshake | ||||
# Upgrade to version 2 in progress. Server is expecting to | ||||
# process a legacy handshake. | ||||
# | ||||
# upgrade-v2-filter-legacy-handshake -> shutdown | ||||
# Client did not fulfill upgrade handshake requirements. | ||||
# | ||||
# upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish | ||||
# Client fulfilled version 2 upgrade requirements. Finishing that | ||||
# upgrade. | ||||
# | ||||
# upgrade-v2-finish -> protov2-serving | ||||
# Protocol upgrade to version 2 complete. Server can now speak protocol | ||||
# version 2. | ||||
# | ||||
# protov2-serving -> protov1-serving | ||||
# Ths happens by default since protocol version 2 is the same as | ||||
# version 1 except for the handshake. | ||||
Gregory Szorc
|
r36232 | state = 'protov1-serving' | ||
proto = sshv1protocolhandler(ui, fin, fout) | ||||
Gregory Szorc
|
r36233 | protoswitched = False | ||
Gregory Szorc
|
r36232 | |||
while True: | ||||
if state == 'protov1-serving': | ||||
# Commands are issued on new lines. | ||||
request = fin.readline()[:-1] | ||||
# Empty lines signal to terminate the connection. | ||||
if not request: | ||||
state = 'shutdown' | ||||
continue | ||||
Gregory Szorc
|
r36233 | # It looks like a protocol upgrade request. Transition state to | ||
# handle it. | ||||
if request.startswith(b'upgrade '): | ||||
if protoswitched: | ||||
_sshv1respondooberror(fout, ui.ferr, | ||||
b'cannot upgrade protocols multiple ' | ||||
b'times') | ||||
state = 'shutdown' | ||||
continue | ||||
state = 'upgrade-initial' | ||||
continue | ||||
Gregory Szorc
|
r36232 | available = wireproto.commands.commandavailable(request, proto) | ||
# 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 | ||||
rsp = wireproto.dispatch(repo, proto, request) | ||||
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: | ||||
raise error.ProgrammingError('unhandled response type from ' | ||||
'wire protocol command: %s' % rsp) | ||||
Gregory Szorc
|
r36233 | # For now, protocol version 2 serving just goes back to version 1. | ||
elif state == 'protov2-serving': | ||||
state = 'protov1-serving' | ||||
continue | ||||
elif state == 'upgrade-initial': | ||||
# We should never transition into this state if we've switched | ||||
# protocols. | ||||
assert not protoswitched | ||||
assert proto.name == SSHV1 | ||||
# Expected: upgrade <token> <capabilities> | ||||
# If we get something else, the request is malformed. It could be | ||||
# from a future client that has altered the upgrade line content. | ||||
# We treat this as an unknown command. | ||||
try: | ||||
token, caps = request.split(b' ')[1:] | ||||
except ValueError: | ||||
_sshv1respondbytes(fout, b'') | ||||
state = 'protov1-serving' | ||||
continue | ||||
# Send empty response if we don't support upgrading protocols. | ||||
if not ui.configbool('experimental', 'sshserver.support-v2'): | ||||
_sshv1respondbytes(fout, b'') | ||||
state = 'protov1-serving' | ||||
continue | ||||
try: | ||||
caps = urlreq.parseqs(caps) | ||||
except ValueError: | ||||
_sshv1respondbytes(fout, b'') | ||||
state = 'protov1-serving' | ||||
continue | ||||
# We don't see an upgrade request to protocol version 2. Ignore | ||||
# the upgrade request. | ||||
wantedprotos = caps.get(b'proto', [b''])[0] | ||||
if SSHV2 not in wantedprotos: | ||||
_sshv1respondbytes(fout, b'') | ||||
state = 'protov1-serving' | ||||
continue | ||||
# It looks like we can honor this upgrade request to protocol 2. | ||||
# Filter the rest of the handshake protocol request lines. | ||||
state = 'upgrade-v2-filter-legacy-handshake' | ||||
continue | ||||
elif state == 'upgrade-v2-filter-legacy-handshake': | ||||
# Client should have sent legacy handshake after an ``upgrade`` | ||||
# request. Expected lines: | ||||
# | ||||
# hello | ||||
# between | ||||
# pairs 81 | ||||
# 0000...-0000... | ||||
ok = True | ||||
for line in (b'hello', b'between', b'pairs 81'): | ||||
request = fin.readline()[:-1] | ||||
if request != line: | ||||
_sshv1respondooberror(fout, ui.ferr, | ||||
b'malformed handshake protocol: ' | ||||
b'missing %s' % line) | ||||
ok = False | ||||
state = 'shutdown' | ||||
break | ||||
if not ok: | ||||
continue | ||||
request = fin.read(81) | ||||
if request != b'%s-%s' % (b'0' * 40, b'0' * 40): | ||||
_sshv1respondooberror(fout, ui.ferr, | ||||
b'malformed handshake protocol: ' | ||||
b'missing between argument value') | ||||
state = 'shutdown' | ||||
continue | ||||
state = 'upgrade-v2-finish' | ||||
continue | ||||
elif state == 'upgrade-v2-finish': | ||||
# Send the upgrade response. | ||||
fout.write(b'upgraded %s %s\n' % (token, SSHV2)) | ||||
servercaps = wireproto.capabilities(repo, proto) | ||||
rsp = b'capabilities: %s' % servercaps.data | ||||
fout.write(b'%d\n%s\n' % (len(rsp), rsp)) | ||||
fout.flush() | ||||
proto = sshv2protocolhandler(ui, fin, fout) | ||||
protoswitched = True | ||||
state = 'protov2-serving' | ||||
continue | ||||
Gregory Szorc
|
r36232 | elif state == 'shutdown': | ||
break | ||||
else: | ||||
raise error.ProgrammingError('unhandled ssh server state: %s' % | ||||
state) | ||||
Gregory Szorc
|
r36082 | class sshserver(object): | ||
def __init__(self, ui, repo): | ||||
self._ui = ui | ||||
self._repo = repo | ||||
self._fin = ui.fin | ||||
self._fout = ui.fout | ||||
hook.redirect(True) | ||||
ui.fout = repo.ui.fout = ui.ferr | ||||
# Prevent insertion/deletion of CRs | ||||
util.setbinary(self._fin) | ||||
util.setbinary(self._fout) | ||||
Gregory Szorc
|
r35877 | def serve_forever(self): | ||
Gregory Szorc
|
r36232 | _runsshserver(self._ui, self._repo, self._fin, self._fout) | ||
Gregory Szorc
|
r35877 | sys.exit(0) | ||