Mercurial > public > mercurial-scm > hg
diff mercurial/wireprotoserver.py @ 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 | 6ba5b03f3645 |
line wrap: on
line diff
--- 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