Mercurial > hg
changeset 36215:464bedc0fdb4
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
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Mon, 12 Feb 2018 16:33:54 -0800 |
parents | 3b3a987bbbaa |
children | 7218e93ade47 |
files | mercurial/configitems.py mercurial/wireprotoserver.py tests/sshprotoext.py tests/test-ssh-proto.t |
diffstat | 4 files changed, 273 insertions(+), 36 deletions(-) [+] |
line wrap: on
line diff
--- 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 + - +