##// END OF EJS Templates
wireproto: port heads command to wire protocol v2...
wireproto: port heads command to wire protocol v2 After much thought and consideration, wire protocol version 2's commands will be defined in different functions from the existing commands. This will make it easier to implement these commands because it won't require shoehorning things like response formatting and argument declaration into the same APIs. For example, wire protocol version 1 requires that commands declare a fixed and ordered list of argument names. It isn't really possible to insert new arguments or have optional arguments without breaking backwards compatibility. Wire protocol version 2, however, uses CBOR maps for passing arguments. So arguments a) can be optional b) can be added without BC c) can be strongly typed. This commit starts our trek towards reimplementing the wire protocol for version 2 with the heads command. It is pretty similar to the existing heads command. One added feature is it can be told to operate on only public phase changesets. This is useful for making discovery faster when a repo has tens of thousands of draft phase heads (such as Mozilla's "try" repository). The HTTPv2 server-side protocol has had its `getargs()` implementation updated to reflect that arguments are a map and not a list. Differential Revision: https://phab.mercurial-scm.org/D3179

File last commit:

r37503:0b7475ea default
r37503:0b7475ea default
Show More
wireprotoserver.py
1074 lines | 37.4 KiB | text/x-python | PythonLexer
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
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
wireprotoserver: add context manager mechanism for redirecting stdio...
r36083 import contextlib
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 import struct
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 import sys
Gregory Szorc
wireprotoserver: ability to run an SSH server until an event is set...
r36540 import threading
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 from .i18n import _
Gregory Szorc
wireproto: port heads command to wire protocol v2...
r37503 from .thirdparty import (
cbor,
)
Gregory Szorc
wireproto: port protocol handler to zope.interface...
r37312 from .thirdparty.zope import (
interface as zi,
)
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 from . import (
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 encoding,
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 error,
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 hook,
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 pycompat,
util,
wireproto,
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 wireprotoframing,
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 wireprototypes,
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 )
Yuya Nishihara
procutil: bulk-replace function calls to point to new module
r37138 from .utils import (
procutil,
)
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874
stringio = util.stringio
urlerr = util.urlerr
urlreq = util.urlreq
Gregory Szorc
wireprotoserver: don't import symbol from hgweb.common...
r35876 HTTP_OK = 200
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 HGTYPE = 'application/mercurial-0.1'
HGTYPE2 = 'application/mercurial-0.2'
HGERRTYPE = 'application/hg-error'
Gregory Szorc
wireproto: use CBOR for command requests...
r37308 FRAMINGTYPE = b'application/mercurial-exp-framing-0003'
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874
Gregory Szorc
wireproto: support /api/* URL space for exposing APIs...
r37064 HTTPV2 = wireprototypes.HTTPV2
Gregory Szorc
wireprotoserver: move SSHV1 and SSHV2 constants to wireprototypes...
r36553 SSHV1 = wireprototypes.SSHV1
SSHV2 = wireprototypes.SSHV2
Gregory Szorc
sshpeer: initial definition and implementation of new SSH protocol...
r35994
Gregory Szorc
wireprotoserver: access headers through parsed request...
r36862 def decodevaluefromheaders(req, headerprefix):
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
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
wireprotoserver: access headers through parsed request...
r36862 v = req.headers.get(b'%s-%d' % (headerprefix, i))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 if v is None:
break
chunks.append(pycompat.bytesurl(v))
i += 1
return ''.join(chunks)
Gregory Szorc
wireproto: port protocol handler to zope.interface...
r37312 @zi.implementer(wireprototypes.baseprotocolhandler)
class httpv1protocolhandler(object):
Gregory Szorc
hgweb: expose URL scheme and REMOTE_* attributes...
r36883 def __init__(self, req, ui, checkperm):
Gregory Szorc
wireprotoserver: access headers through parsed request...
r36862 self._req = req
Gregory Szorc
wireprotoserver: make attributes private...
r35884 self._ui = ui
Gregory Szorc
wireproto: formalize permissions checking as part of protocol interface...
r36819 self._checkperm = checkperm
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 self._protocaps = None
Gregory Szorc
wireprotoserver: make name part of protocol interface...
r35891
@property
def name(self):
Gregory Szorc
wireprotoserver: add version to HTTP protocol name (API)...
r36241 return 'http-v1'
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
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
wireprotoserver: add some blank lines between methods...
r35881
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 def _args(self):
Gregory Szorc
hgweb: use a multidict for holding query string parameters...
r36878 args = self._req.qsparams.asdictoflists()
Gregory Szorc
wireprotoserver: access headers through parsed request...
r36862 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 if postlen:
Gregory Szorc
wireprotoserver: define and use parse_qs from urllib...
r36094 args.update(urlreq.parseqs(
Gregory Szorc
hgweb: expose input stream on parsed WSGI request object...
r36873 self._req.bodyfh.read(postlen), keep_blank_values=True))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 return args
Gregory Szorc
wireprotoserver: access headers through parsed request...
r36862 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
Gregory Szorc
wireprotoserver: define and use parse_qs from urllib...
r36094 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 return args
Gregory Szorc
wireprotoserver: add some blank lines between methods...
r35881
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 def getprotocaps(self):
if self._protocaps is None:
value = decodevaluefromheaders(self._req, r'X-HgProto')
self._protocaps = set(value.split(' '))
return self._protocaps
Joerg Sonnenberger
wireproto: allow direct stream processing for unbundle...
r37432 def getpayload(self):
Gregory Szorc
hgweb: handle CONTENT_LENGTH...
r36863 # Existing clients *always* send Content-Length.
length = int(self._req.headers[b'Content-Length'])
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 # If httppostargs is used, we need to read Content-Length
# minus the amount that was consumed by args.
Gregory Szorc
wireprotoserver: access headers through parsed request...
r36862 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
Joerg Sonnenberger
wireproto: allow direct stream processing for unbundle...
r37432 return util.filechunkiter(self._req.bodyfh, limit=length)
Gregory Szorc
wireprotoserver: add some blank lines between methods...
r35881
Gregory Szorc
wireprotoserver: add context manager mechanism for redirecting stdio...
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
wireprotoserver: rename _client to client (API)...
r36086 def client(self):
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 return 'remote:%s:%s:%s' % (
Gregory Szorc
hgweb: expose URL scheme and REMOTE_* attributes...
r36883 self._req.urlscheme,
urlreq.quote(self._req.remotehost or ''),
urlreq.quote(self._req.remoteuser or ''))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874
Gregory Szorc
wireproto: add transport specific capabilities in the transport...
r36631 def addcapabilities(self, repo, caps):
Gregory Szorc
wireproto: nominally don't expose "batch" to version 2 wire transports...
r37071 caps.append(b'batch')
Gregory Szorc
wireproto: add transport specific capabilities in the transport...
r36631 caps.append('httpheader=%d' %
repo.ui.configint('server', 'maxhttpheaderlen'))
if repo.ui.configbool('experimental', 'httppostargs'):
caps.append('httppostargs')
# FUTURE advertise 0.2rx once support is implemented
# FUTURE advertise minrx and mintx after consulting config option
caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
if compengines:
comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
for e in compengines)
caps.append('compression=%s' % comptypes)
return caps
Gregory Szorc
wireproto: formalize permissions checking as part of protocol interface...
r36819 def checkperm(self, perm):
return self._checkperm(perm)
Augie Fackler
wireprotoserver: return to using iscmd() method...
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
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 def iscmd(cmd):
return cmd in wireproto.commands
Gregory Szorc
hgweb: transition permissions hooks to modern request type (API)...
r36893 def handlewsgirequest(rctx, req, res, checkperm):
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830 """Possibly process a wire protocol request.
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830 If the current request is a wire protocol request, the request is
processed by this function.
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002
Gregory Szorc
hgweb: only recognize wire protocol commands from query string (BC)...
r36828 ``req`` is a ``parsedrequest`` instance.
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 ``res`` is a ``wsgiresponse`` instance.
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
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
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002 """
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830 # Avoid cycle involving hg module.
from .hgweb import common as hgwebcommon
Gregory Szorc
wireproto: formalize permissions checking as part of protocol interface...
r36819 repo = rctx.repo
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
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.
Gregory Szorc
hgweb: use a multidict for holding query string parameters...
r36878 if 'cmd' not in req.qsparams:
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 return False
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002
Gregory Szorc
hgweb: use a multidict for holding query string parameters...
r36878 cmd = req.qsparams['cmd']
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
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
wireprotoserver: return to using iscmd() method...
r36249 if not iscmd(cmd):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 return False
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
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
hgweb: create dedicated type for WSGI responses...
r36877 res.status = hgwebcommon.statusmessage(404)
res.headers['Content-Type'] = HGTYPE
# TODO This is not a good response to issue for this request. This
# is mostly for BC for now.
res.setbodybytes('0\n%s\n' % b'Not Found')
return True
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002
Gregory Szorc
hgweb: expose URL scheme and REMOTE_* attributes...
r36883 proto = httpv1protocolhandler(req, repo.ui,
Gregory Szorc
hgweb: transition permissions hooks to modern request type (API)...
r36893 lambda perm: checkperm(rctx, req, perm))
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
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
hgweb: expose URL scheme and REMOTE_* attributes...
r36883 _callhttp(repo, req, res, proto, cmd)
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830 except hgwebcommon.ErrorResponse as e:
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 for k, v in e.headers:
res.headers[k] = v
res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
# TODO This response body assumes the failed command was
# "unbundle." That assumption is not always valid.
res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
Gregory Szorc
wireprotoserver: move all wire protocol handling logic out of hgweb...
r36830
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 return True
Gregory Szorc
wireprotoserver: move protocol parsing and dispatch out of hgweb...
r36002
Gregory Szorc
wireproto: support /api/* URL space for exposing APIs...
r37064 def handlewsgiapirequest(rctx, req, res, checkperm):
"""Handle requests to /api/*."""
assert req.dispatchparts[0] == b'api'
repo = rctx.repo
# This whole URL space is experimental for now. But we want to
# reserve the URL space. So, 404 all URLs if the feature isn't enabled.
if not repo.ui.configbool('experimental', 'web.apiserver'):
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('Experimental API server endpoint not enabled'))
return
# The URL space is /api/<protocol>/*. The structure of URLs under varies
# by <protocol>.
# Registered APIs are made available via config options of the name of
# the protocol.
availableapis = set()
for k, v in API_HANDLERS.items():
section, option = v['config']
if repo.ui.configbool(section, option):
availableapis.add(k)
# Requests to /api/ list available APIs.
if req.dispatchparts == [b'api']:
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
'one of the following:\n')]
if availableapis:
lines.extend(sorted(availableapis))
else:
lines.append(_('(no available APIs)\n'))
res.setbodybytes(b'\n'.join(lines))
return
proto = req.dispatchparts[1]
if proto not in API_HANDLERS:
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
proto, b', '.join(sorted(availableapis))))
return
if proto not in availableapis:
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('API %s not enabled\n') % proto)
return
API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
req.dispatchparts[2:])
def _handlehttpv2request(rctx, req, res, checkperm, urlparts):
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 from .hgweb import common as hgwebcommon
# URL space looks like: <permissions>/<command>, where <permission> can
# be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
# Root URL does nothing meaningful... yet.
if not urlparts:
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('HTTP version 2 API handler'))
return
if len(urlparts) == 1:
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('do not know how to process %s\n') %
req.dispatchpath)
return
permission, command = urlparts[0:2]
if permission not in (b'ro', b'rw'):
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('unknown permission: %s') % permission)
return
Gregory Szorc
wireproto: require POST for all HTTPv2 requests...
r37066 if req.method != 'POST':
res.status = b'405 Method Not Allowed'
res.headers[b'Allow'] = b'POST'
res.setbodybytes(_('commands require POST requests'))
return
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 # At some point we'll want to use our own API instead of recycling the
# behavior of version 1 of the wire protocol...
# TODO return reasonable responses - not responses that overload the
# HTTP status line message for error reporting.
try:
checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
except hgwebcommon.ErrorResponse as e:
res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
for k, v in e.headers:
res.headers[k] = v
res.setbodybytes('permission denied')
return
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 # We have a special endpoint to reflect the request back at the client.
if command == b'debugreflect':
_processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
return
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 # Extra commands that we handle that aren't really wire protocol
# commands. Think extra hard before making this hackery available to
# extension.
extracommands = {'multirequest'}
Gregory Szorc
wireproto: separate commands tables for version 1 and 2 commands...
r37311 if command not in wireproto.commandsv2 and command not in extracommands:
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
return
repo = rctx.repo
ui = repo.ui
proto = httpv2protocolhandler(req, ui)
Gregory Szorc
wireproto: separate commands tables for version 1 and 2 commands...
r37311 if (not wireproto.commandsv2.commandavailable(command, proto)
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 and command not in extracommands):
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('invalid wire protocol command: %s') % command)
return
Gregory Szorc
wireproto: review fixups...
r37147 # TODO consider cases where proxies may add additional Accept headers.
Gregory Szorc
wireproto: define and implement protocol for issuing requests...
r37069 if req.headers.get(b'Accept') != FRAMINGTYPE:
Gregory Szorc
wireproto: define content negotiation for HTTPv2...
r37068 res.status = b'406 Not Acceptable'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
Gregory Szorc
wireproto: define and implement protocol for issuing requests...
r37069 % FRAMINGTYPE)
Gregory Szorc
wireproto: define content negotiation for HTTPv2...
r37068 return
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
Gregory Szorc
wireproto: define content negotiation for HTTPv2...
r37068 res.status = b'415 Unsupported Media Type'
# TODO we should send a response with appropriate media type,
# since client does Accept it.
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('client MUST send Content-Type header with '
Gregory Szorc
wireproto: define and implement protocol for issuing requests...
r37069 'value: %s\n') % FRAMINGTYPE)
Gregory Szorc
wireproto: define content negotiation for HTTPv2...
r37068 return
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 _processhttpv2request(ui, repo, req, res, permission, command, proto)
Gregory Szorc
wireproto: support /api/* URL space for exposing APIs...
r37064
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 def _processhttpv2reflectrequest(ui, repo, req, res):
"""Reads unified frame protocol request and dumps out state to client.
This special endpoint can be used to help debug the wire protocol.
Instead of routing the request through the normal dispatch mechanism,
we instead read all frames, decode them, and feed them into our state
tracker. We then dump the log of all that activity back out to the
client.
"""
import json
# Reflection APIs have a history of being abused, accidentally disclosing
# sensitive data, etc. So we have a config knob.
if not ui.configbool('experimental', 'web.api.debugreflect'):
res.status = b'404 Not Found'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('debugreflect service not available'))
return
# We assume we have a unified framing protocol request body.
reactor = wireprotoframing.serverreactor()
states = []
while True:
frame = wireprotoframing.readframe(req.bodyfh)
if not frame:
states.append(b'received: <no frame>')
break
Gregory Szorc
wireproto: define attr-based classes for representing frames...
r37079 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
frame.requestid,
frame.payload))
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070
Gregory Szorc
wireproto: define attr-based classes for representing frames...
r37079 action, meta = reactor.onframerecv(frame)
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 states.append(json.dumps((action, meta), sort_keys=True,
separators=(', ', ': ')))
Gregory Szorc
wireproto: buffer output frames when in half duplex mode...
r37074 action, meta = reactor.oninputeof()
meta['action'] = action
states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
Gregory Szorc
wireproto: implement basic frame reading and processing...
r37070 res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(b'\n'.join(states))
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
"""Post-validation handler for HTTPv2 requests.
Called when the HTTP request contains unified frame-based protocol
frames for evaluation.
"""
Gregory Szorc
wireproto: buffer output frames when in half duplex mode...
r37074 # TODO Some HTTP clients are full duplex and can receive data before
# the entire request is transmitted. Figure out a way to indicate support
# for that so we can opt into full duplex mode.
reactor = wireprotoframing.serverreactor(deferoutput=True)
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 seencommand = False
Gregory Szorc
wireproto: explicit API to create outgoing streams...
r37305 outstream = reactor.makeoutputstream()
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 while True:
frame = wireprotoframing.readframe(req.bodyfh)
if not frame:
break
Gregory Szorc
wireproto: define attr-based classes for representing frames...
r37079 action, meta = reactor.onframerecv(frame)
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
if action == 'wantframe':
# Need more data before we can do anything.
continue
elif action == 'runcommand':
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
Gregory Szorc
wireproto: explicit API to create outgoing streams...
r37305 reqcommand, reactor, outstream,
meta, issubsequent=seencommand)
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077
if sentoutput:
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 return
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 seencommand = True
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
elif action == 'error':
# TODO define proper error mechanism.
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(meta['message'] + b'\n')
return
else:
raise error.ProgrammingError(
'unhandled action from frame processor: %s' % action)
Gregory Szorc
wireproto: buffer output frames when in half duplex mode...
r37074 action, meta = reactor.oninputeof()
if action == 'sendframes':
# We assume we haven't started sending the response yet. If we're
# wrong, the response type will raise an exception.
res.status = b'200 OK'
res.headers[b'Content-Type'] = FRAMINGTYPE
res.setbodygen(meta['framegen'])
elif action == 'noop':
pass
else:
raise error.ProgrammingError('unhandled action from frame processor: %s'
% action)
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
Gregory Szorc
wireproto: explicit API to create outgoing streams...
r37305 outstream, command, issubsequent):
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 """Dispatch a wire protocol command made from HTTPv2 requests.
The authenticated permission (``authedperm``) along with the original
command from the URL (``reqcommand``) are passed in.
"""
# We already validated that the session has permissions to perform the
# actions in ``authedperm``. In the unified frame protocol, the canonical
# command to run is expressed in a frame. However, the URL also requested
# to run a specific command. We need to be careful that the command we
# run doesn't have permissions requirements greater than what was granted
# by ``authedperm``.
#
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 # Our rule for this is we only allow one command per HTTP request and
# that command must match the command in the URL. However, we make
# an exception for the ``multirequest`` URL. This URL is allowed to
# execute multiple commands. We double check permissions of each command
# as it is invoked to ensure there is no privilege escalation.
# TODO consider allowing multiple commands to regular command URLs
# iff each command is the same.
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
proto = httpv2protocolhandler(req, ui, args=command['args'])
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 if reqcommand == b'multirequest':
Gregory Szorc
wireproto: separate commands tables for version 1 and 2 commands...
r37311 if not wireproto.commandsv2.commandavailable(command['command'], proto):
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 # TODO proper error mechanism
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('wire protocol command not available: %s') %
command['command'])
return True
Gregory Szorc
wireproto: review fixups...
r37147 # TODO don't use assert here, since it may be elided by -O.
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 assert authedperm in (b'ro', b'rw')
Gregory Szorc
wireproto: separate commands tables for version 1 and 2 commands...
r37311 wirecommand = wireproto.commandsv2[command['command']]
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 assert wirecommand.permission in ('push', 'pull')
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 if authedperm == b'ro' and wirecommand.permission != 'pull':
# TODO proper error mechanism
res.status = b'403 Forbidden'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('insufficient permissions to execute '
'command: %s') % command['command'])
return True
# TODO should we also call checkperm() here? Maybe not if we're going
# to overhaul that API. The granted scope from the URL check should
# be good enough.
else:
# Don't allow multiple commands outside of ``multirequest`` URL.
if issubsequent:
# TODO proper error mechanism
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('multiple commands cannot be issued to this '
'URL'))
return True
if reqcommand != command['command']:
# TODO define proper error mechanism
res.status = b'200 OK'
res.headers[b'Content-Type'] = b'text/plain'
res.setbodybytes(_('command in frame must match command in URL'))
return True
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
rsp = wireproto.dispatch(repo, proto, command['command'])
res.status = b'200 OK'
Gregory Szorc
wireproto: define and implement responses in framing protocol...
r37073 res.headers[b'Content-Type'] = FRAMINGTYPE
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
if isinstance(rsp, wireprototypes.bytesresponse):
Gregory Szorc
wireproto: explicit API to create outgoing streams...
r37305 action, meta = reactor.onbytesresponseready(outstream,
Gregory Szorc
wireproto: start to associate frame generation with a stream...
r37303 command['requestid'],
Gregory Szorc
wireproto: add request IDs to frames...
r37075 rsp.data)
Gregory Szorc
wireproto: port heads command to wire protocol v2...
r37503 elif isinstance(rsp, wireprototypes.cborresponse):
encoded = cbor.dumps(rsp.value, canonical=True)
action, meta = reactor.onbytesresponseready(outstream,
command['requestid'],
encoded,
iscbor=True)
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 else:
Gregory Szorc
wireproto: define and implement responses in framing protocol...
r37073 action, meta = reactor.onapplicationerror(
_('unhandled response type from wire proto command'))
if action == 'sendframes':
res.setbodygen(meta['framegen'])
Gregory Szorc
wireproto: service multiple command requests per HTTP request...
r37077 return True
Gregory Szorc
wireproto: buffer output frames when in half duplex mode...
r37074 elif action == 'noop':
Gregory Szorc
wireproto: review fixups...
r37147 return False
Gregory Szorc
wireproto: define and implement responses in framing protocol...
r37073 else:
raise error.ProgrammingError('unhandled event from reactor: %s' %
action)
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072
Gregory Szorc
wireproto: support /api/* URL space for exposing APIs...
r37064 # Maps API name to metadata so custom API can be registered.
API_HANDLERS = {
HTTPV2: {
'config': ('experimental', 'web.api.http-v2'),
'handler': _handlehttpv2request,
},
}
Gregory Szorc
wireproto: port protocol handler to zope.interface...
r37312 @zi.implementer(wireprototypes.baseprotocolhandler)
class httpv2protocolhandler(object):
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 def __init__(self, req, ui, args=None):
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 self._req = req
self._ui = ui
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 self._args = args
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065
@property
def name(self):
return HTTPV2
def getargs(self, args):
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 data = {}
for k in args.split():
if k == '*':
raise NotImplementedError('do not support * args')
Gregory Szorc
wireproto: port heads command to wire protocol v2...
r37503 elif k in self._args:
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 data[k] = self._args[k]
Gregory Szorc
wireproto: port heads command to wire protocol v2...
r37503 return data
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 def getprotocaps(self):
# Protocol capabilities are currently not implemented for HTTP V2.
return set()
Joerg Sonnenberger
wireproto: allow direct stream processing for unbundle...
r37432 def getpayload(self):
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065 raise NotImplementedError
@contextlib.contextmanager
def mayberedirectstdio(self):
raise NotImplementedError
def client(self):
raise NotImplementedError
def addcapabilities(self, repo, caps):
Gregory Szorc
wireproto: implement basic command dispatching for HTTPv2...
r37072 return caps
Gregory Szorc
wireproto: define permissions-based routing of HTTPv2 wire protocol...
r37065
def checkperm(self, perm):
raise NotImplementedError
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 def _httpresponsetype(ui, proto, prefer_uncompressed):
Gregory Szorc
wireprotoserver: move responsetype() out of http handler...
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.
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 if '0.2' in proto.getprotocaps():
Gregory Szorc
wireprotoserver: move responsetype() out of http handler...
r36089 # All clients are expected to support uncompressed data.
if prefer_uncompressed:
return HGTYPE2, util._noopengine(), {}
# Now find an agreed upon compression format.
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 compformats = wireproto.clientcompressionsupport(proto)
Gregory Szorc
wireprotoserver: move responsetype() out of http handler...
r36089 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
hgweb: expose URL scheme and REMOTE_* attributes...
r36883 def _callhttp(repo, req, res, proto, cmd):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 # Avoid cycle involving hg module.
from .hgweb import common as hgwebcommon
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
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
hgweb: create dedicated type for WSGI responses...
r36877 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
if code == HTTP_OK:
res.status = '200 Script output follows'
else:
res.status = hgwebcommon.statusmessage(code)
res.headers['Content-Type'] = contenttype
if bodybytes is not None:
res.setbodybytes(bodybytes)
if bodygen is not None:
res.setbodygen(bodygen)
Gregory Szorc
wireproto: function for testing if wire protocol command is available...
r36000 if not wireproto.commands.commandavailable(cmd, proto):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGERRTYPE,
_('requested wire protocol command is not available over '
'HTTP'))
return
Gregory Szorc
wireproto: function for testing if wire protocol command is available...
r36000
Gregory Szorc
wireproto: formalize permissions checking as part of protocol interface...
r36819 proto.checkperm(wireproto.commands[cmd].permission)
Gregory Szorc
wireprotoserver: check permissions in main dispatch function...
r36817
Gregory Szorc
wireprotoserver: check if command available before calling it...
r36816 rsp = wireproto.dispatch(repo, proto, cmd)
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 if isinstance(rsp, bytes):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
Gregory Szorc
wireproto: introduce type for raw byte responses (API)...
r36091 elif isinstance(rsp, wireprototypes.bytesresponse):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 elif isinstance(rsp, wireprototypes.streamreslegacy):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 elif isinstance(rsp, wireprototypes.streamres):
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
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
wireprotoserver: move responsetype() out of http handler...
r36089 mediatype, engine, engineopts = _httpresponsetype(
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 repo.ui, proto, rsp.prefer_uncompressed)
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 gen = engine.compressstream(gen, engineopts)
if mediatype == HGTYPE2:
gen = genversion2(gen, engine, engineopts)
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, mediatype, bodygen=gen)
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 elif isinstance(rsp, wireprototypes.pushres):
Gregory Szorc
wireproto: use maybecapturestdio() for push responses (API)...
r36084 rsp = '%d\n%s' % (rsp.res, rsp.output)
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 elif isinstance(rsp, wireprototypes.pusherr):
Gregory Szorc
wireprotoserver: rename hgweb.protocol to wireprotoserver (API)...
r35874 rsp = '0\n%s\n' % rsp.res
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 res.drain = True
setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
Gregory Szorc
wireprototypes: move wire protocol response types to new module...
r36090 elif isinstance(rsp, wireprototypes.ooberror):
Gregory Szorc
hgweb: create dedicated type for WSGI responses...
r36877 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
else:
raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
Gregory Szorc
wireprotoserver: move error response handling out of hgweb...
r36004
Gregory Szorc
wireprotoserver: extract SSH response handling functions...
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
wireproto: port protocol handler to zope.interface...
r37312 @zi.implementer(wireprototypes.baseprotocolhandler)
class sshv1protocolhandler(object):
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 """Handler for requests services via version 1 of SSH protocol."""
def __init__(self, ui, fin, fout):
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 self._ui = ui
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 self._fin = fin
self._fout = fout
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 self._protocaps = set()
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877
Gregory Szorc
wireprotoserver: make name part of protocol interface...
r35891 @property
def name(self):
Gregory Szorc
wireprotoserver: move SSHV1 and SSHV2 constants to wireprototypes...
r36553 return wireprototypes.SSHV1
Gregory Szorc
wireprotoserver: make name part of protocol interface...
r35891
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 def getargs(self, args):
data = {}
keys = args.split()
for n in xrange(len(keys)):
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 argline = self._fin.readline()[:-1]
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
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
wireprotoserver: make some instance attributes private...
r35888 argline = self._fin.readline()[:-1]
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 arg, l = argline.split()
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 val = self._fin.read(int(l))
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 star[arg] = val
data['*'] = star
else:
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 val = self._fin.read(int(l))
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 data[arg] = val
return [data[k] for k in keys]
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 def getprotocaps(self):
return self._protocaps
Joerg Sonnenberger
wireproto: allow direct stream processing for unbundle...
r37432 def getpayload(self):
Gregory Szorc
wireproto: document the wonky push protocol for SSH...
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
wireprotoserver: rename getfile() to forwardpayload() (API)...
r36087 # The file is in the form:
#
# <chunk size>\n<chunk>
# ...
# 0\n
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 count = int(self._fin.readline())
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 while count:
Joerg Sonnenberger
wireproto: allow direct stream processing for unbundle...
r37432 yield self._fin.read(count)
Gregory Szorc
wireprotoserver: make some instance attributes private...
r35888 count = int(self._fin.readline())
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877
Gregory Szorc
wireprotoserver: add context manager mechanism for redirecting stdio...
r36083 @contextlib.contextmanager
def mayberedirectstdio(self):
yield None
Gregory Szorc
wireprotoserver: rename _client to client (API)...
r36086 def client(self):
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
return 'remote:ssh:' + client
Gregory Szorc
wireproto: add transport specific capabilities in the transport...
r36631 def addcapabilities(self, repo, caps):
Joerg Sonnenberger
wireproto: provide accessors for client capabilities...
r37411 if self.name == wireprototypes.SSHV1:
caps.append(b'protocaps')
Gregory Szorc
wireproto: nominally don't expose "batch" to version 2 wire transports...
r37071 caps.append(b'batch')
Gregory Szorc
wireproto: add transport specific capabilities in the transport...
r36631 return caps
Gregory Szorc
wireproto: formalize permissions checking as part of protocol interface...
r36819 def checkperm(self, perm):
pass
Gregory Szorc
wireprotoserver: handle SSH protocol version 2 upgrade requests...
r36233 class sshv2protocolhandler(sshv1protocolhandler):
"""Protocol handler for version 2 of the SSH protocol."""
Gregory Szorc
wireprotoserver: identify requests via version 2 of SSH protocol as such...
r36628 @property
def name(self):
return wireprototypes.SSHV2
Gregory Szorc
wireprotoserver: ability to run an SSH server until an event is set...
r36540 def _runsshserver(ui, repo, fin, fout, ev):
Gregory Szorc
wireprotoserver: handle SSH protocol version 2 upgrade requests...
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
wireprotoserver: move SSH server operation to a standalone function...
r36232 state = 'protov1-serving'
proto = sshv1protocolhandler(ui, fin, fout)
Gregory Szorc
wireprotoserver: handle SSH protocol version 2 upgrade requests...
r36233 protoswitched = False
Gregory Szorc
wireprotoserver: move SSH server operation to a standalone function...
r36232
Gregory Szorc
wireprotoserver: ability to run an SSH server until an event is set...
r36540 while not ev.is_set():
Gregory Szorc
wireprotoserver: move SSH server operation to a standalone function...
r36232 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
wireprotoserver: handle SSH protocol version 2 upgrade requests...
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
wireprotoserver: move SSH server operation to a standalone function...
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
wireprotoserver: handle SSH protocol version 2 upgrade requests...
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
Gregory Szorc
wireprotoserver: move SSHV1 and SSHV2 constants to wireprototypes...
r36553 assert proto.name == wireprototypes.SSHV1
Gregory Szorc
wireprotoserver: handle SSH protocol version 2 upgrade requests...
r36233
# 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
wireprotoserver: move SSH server operation to a standalone function...
r36232 elif state == 'shutdown':
break
else:
raise error.ProgrammingError('unhandled ssh server state: %s' %
state)
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 class sshserver(object):
Gregory Szorc
wireprotoserver: support logging SSH server I/O to a file descriptor...
r36543 def __init__(self, ui, repo, logfh=None):
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 self._ui = ui
self._repo = repo
self._fin = ui.fin
self._fout = ui.fout
Gregory Szorc
wireprotoserver: support logging SSH server I/O to a file descriptor...
r36543 # Log write I/O to stdout and stderr if configured.
if logfh:
self._fout = util.makeloggingfileobject(
logfh, self._fout, 'o', logdata=True)
ui.ferr = util.makeloggingfileobject(
logfh, ui.ferr, 'e', logdata=True)
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082 hook.redirect(True)
ui.fout = repo.ui.fout = ui.ferr
# Prevent insertion/deletion of CRs
Yuya Nishihara
procutil: bulk-replace function calls to point to new module
r37138 procutil.setbinary(self._fin)
procutil.setbinary(self._fout)
Gregory Szorc
wireprotoserver: split ssh protocol handler and server...
r36082
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 def serve_forever(self):
Gregory Szorc
wireprotoserver: ability to run an SSH server until an event is set...
r36540 self.serveuntil(threading.Event())
Gregory Szorc
wireprotoserver: move sshserver into module (API)...
r35877 sys.exit(0)
Gregory Szorc
wireprotoserver: ability to run an SSH server until an event is set...
r36540
def serveuntil(self, ev):
"""Serve until a threading.Event is set."""
_runsshserver(self._ui, self._repo, self._fin, self._fout, ev)