wireprotoserver: handle SSH protocol version 2 upgrade requests
This commit teaches the SSH server to recognize the "upgrade"
request line that clients send when they wish to switch the
channel to version 2 of the SSH protocol.
Servers don't honor upgrade requests unless an experimental config
option is set.
Since the built-in server now supports upgrade requests, our test
server to test the handshake has been deleted. Existing tests
use the built-in server and their output doesn't change.
The upgrade is handled in our state machine. The end result is a bit
wonky, as the server transitions back to version 1 state immediately
after upgrading. But this will change as soon as version 2 has an
actual protocol that differs from version 1.
Tests demonstrating that the new server is a bit more strict about
the upgrade handshake have been added.
Differential Revision: https://phab.mercurial-scm.org/D2204
--- a/mercurial/configitems.py Thu Feb 08 15:09:59 2018 -0800
+++ b/mercurial/configitems.py Mon Feb 12 16:33:54 2018 -0800
@@ -556,6 +556,9 @@
coreconfigitem('experimental', 'single-head-per-branch',
default=False,
)
+coreconfigitem('experimental', 'sshserver.support-v2',
+ default=False,
+)
coreconfigitem('experimental', 'spacemovesdown',
default=False,
)
--- a/mercurial/wireprotoserver.py Thu Feb 08 15:09:59 2018 -0800
+++ b/mercurial/wireprotoserver.py Mon Feb 12 16:33:54 2018 -0800
@@ -409,9 +409,65 @@
client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
return 'remote:ssh:' + client
+class sshv2protocolhandler(sshv1protocolhandler):
+ """Protocol handler for version 2 of the SSH protocol."""
+
def _runsshserver(ui, repo, fin, fout):
+ # 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.
+
state = 'protov1-serving'
proto = sshv1protocolhandler(ui, fin, fout)
+ protoswitched = False
while True:
if state == 'protov1-serving':
@@ -423,6 +479,19 @@
state = 'shutdown'
continue
+ # 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
+
available = wireproto.commands.commandavailable(request, proto)
# This command isn't available. Send an empty response and go
@@ -452,6 +521,103 @@
raise error.ProgrammingError('unhandled response type from '
'wire protocol command: %s' % rsp)
+ # 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
+
elif state == 'shutdown':
break
--- a/tests/sshprotoext.py Thu Feb 08 15:09:59 2018 -0800
+++ b/tests/sshprotoext.py Mon Feb 12 16:33:54 2018 -0800
@@ -55,37 +55,6 @@
super(prehelloserver, self).serve_forever()
-class upgradev2server(wireprotoserver.sshserver):
- """Tests behavior for clients that issue upgrade to version 2."""
- def serve_forever(self):
- name = wireprotoserver.SSHV2
- l = self._fin.readline()
- assert l.startswith(b'upgrade ')
- token, caps = l[:-1].split(b' ')[1:]
- assert caps == b'proto=%s' % name
-
- # Filter hello and between requests.
- l = self._fin.readline()
- assert l == b'hello\n'
- l = self._fin.readline()
- assert l == b'between\n'
- l = self._fin.readline()
- assert l == b'pairs 81\n'
- self._fin.read(81)
-
- # Send the upgrade response.
- proto = wireprotoserver.sshv1protocolhandler(self._ui, self._fin,
- self._fout)
- self._fout.write(b'upgraded %s %s\n' % (token, name))
- servercaps = wireproto.capabilities(self._repo, proto)
- rsp = b'capabilities: %s' % servercaps.data
- self._fout.write(b'%d\n' % len(rsp))
- self._fout.write(rsp)
- self._fout.write(b'\n')
- self._fout.flush()
-
- super(upgradev2server, self).serve_forever()
-
def performhandshake(orig, ui, stdin, stdout, stderr):
"""Wrapped version of sshpeer._performhandshake to send extra commands."""
mode = ui.config(b'sshpeer', b'handshake-mode')
@@ -118,8 +87,6 @@
wireprotoserver.sshserver = bannerserver
elif servermode == b'no-hello':
wireprotoserver.sshserver = prehelloserver
- elif servermode == b'upgradev2':
- wireprotoserver.sshserver = upgradev2server
elif servermode:
raise error.ProgrammingError(b'unknown server mode: %s' % servermode)
--- a/tests/test-ssh-proto.t Thu Feb 08 15:09:59 2018 -0800
+++ b/tests/test-ssh-proto.t Mon Feb 12 16:33:54 2018 -0800
@@ -453,9 +453,17 @@
local: no
pushable: yes
+Enable version 2 support on server. We need to do this in hgrc because we can't
+use --config with `hg serve --stdio`.
+
+ $ cat >> server/.hg/hgrc << EOF
+ > [experimental]
+ > sshserver.support-v2 = true
+ > EOF
+
Send an upgrade request to a server that supports upgrade
- $ SSHSERVERMODE=upgradev2 hg -R server serve --stdio << EOF
+ $ hg -R server serve --stdio << EOF
> upgrade this-is-some-token proto=exp-ssh-v2-0001
> hello
> between
@@ -466,7 +474,7 @@
383
capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
- $ SSHSERVERMODE=upgradev2 hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
+ $ hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) (no-windows !)
running * "*\tests/dummyssh" "user@dummy" "hg -R server serve --stdio" (glob) (windows !)
sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
@@ -482,7 +490,7 @@
Verify the peer has capabilities
- $ SSHSERVERMODE=upgradev2 hg --config experimental.sshpeer.advertise-v2=true --debug debugcapabilities ssh://user@dummy/server
+ $ hg --config experimental.sshpeer.advertise-v2=true --debug debugcapabilities ssh://user@dummy/server
running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) (no-windows !)
running * "*\tests/dummyssh" "user@dummy" "hg -R server serve --stdio" (glob) (windows !)
sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
@@ -527,3 +535,96 @@
remote-changegroup
http
https
+
+Command after upgrade to version 2 is processed
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade this-is-some-token proto=exp-ssh-v2-0001
+ > hello
+ > between
+ > pairs 81
+ > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000hello
+ > EOF
+ upgraded this-is-some-token exp-ssh-v2-0001
+ 383
+ capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
+ 384
+ capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
+
+Multiple upgrades is not allowed
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade this-is-some-token proto=exp-ssh-v2-0001
+ > hello
+ > between
+ > pairs 81
+ > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000upgrade another-token proto=irrelevant
+ > hello
+ > EOF
+ upgraded this-is-some-token exp-ssh-v2-0001
+ 383
+ capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
+ cannot upgrade protocols multiple times
+ -
+
+
+Malformed upgrade request line (not exactly 3 space delimited tokens)
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade
+ > EOF
+ 0
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade token
+ > EOF
+ 0
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade token foo=bar extra-token
+ > EOF
+ 0
+
+Upgrade request to unsupported protocol is ignored
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade this-is-some-token proto=unknown1,unknown2
+ > hello
+ > between
+ > pairs 81
+ > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
+ > EOF
+ 0
+ 384
+ capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
+ 1
+
+
+Upgrade request must be followed by hello + between
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade token proto=exp-ssh-v2-0001
+ > invalid
+ > EOF
+ malformed handshake protocol: missing hello
+ -
+
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade token proto=exp-ssh-v2-0001
+ > hello
+ > invalid
+ > EOF
+ malformed handshake protocol: missing between
+ -
+
+
+ $ hg -R server serve --stdio << EOF
+ > upgrade token proto=exp-ssh-v2-0001
+ > hello
+ > between
+ > invalid
+ > EOF
+ malformed handshake protocol: missing pairs 81
+ -
+