mercurial/sshpeer.py
changeset 35976 48a3a9283f09
parent 35940 556218e08e25
child 35977 625038cb4b1d
equal deleted inserted replaced
35975:40d94ea51402 35976:48a3a9283f09
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
     7 
     7 
     8 from __future__ import absolute_import
     8 from __future__ import absolute_import
     9 
     9 
    10 import re
    10 import re
       
    11 import uuid
    11 
    12 
    12 from .i18n import _
    13 from .i18n import _
    13 from . import (
    14 from . import (
    14     error,
    15     error,
    15     pycompat,
    16     pycompat,
    16     util,
    17     util,
    17     wireproto,
    18     wireproto,
       
    19     wireprotoserver,
    18 )
    20 )
    19 
    21 
    20 def _serverquote(s):
    22 def _serverquote(s):
    21     """quote a string for the remote shell ... which we assume is sh"""
    23     """quote a string for the remote shell ... which we assume is sh"""
    22     if not s:
    24     if not s:
   160     def badresponse():
   162     def badresponse():
   161         msg = _('no suitable response from remote hg')
   163         msg = _('no suitable response from remote hg')
   162         hint = ui.config('ui', 'ssherrorhint')
   164         hint = ui.config('ui', 'ssherrorhint')
   163         raise error.RepoError(msg, hint=hint)
   165         raise error.RepoError(msg, hint=hint)
   164 
   166 
   165     # The handshake consists of sending 2 wire protocol commands:
   167     # The handshake consists of sending wire protocol commands in reverse
   166     # ``hello`` and ``between``.
   168     # order of protocol implementation and then sniffing for a response
   167     #
   169     # to one of them.
   168     # The ``hello`` command (which was introduced in Mercurial 0.9.1)
   170     #
   169     # instructs the server to advertise its capabilities.
   171     # Those commands (from oldest to newest) are:
   170     #
   172     #
   171     # The ``between`` command (which has existed in all Mercurial servers
   173     # ``between``
   172     # for as long as SSH support has existed), asks for the set of revisions
   174     #   Asks for the set of revisions between a pair of revisions. Command
   173     # between a pair of revisions.
   175     #   present in all Mercurial server implementations.
       
   176     #
       
   177     # ``hello``
       
   178     #   Instructs the server to advertise its capabilities. Introduced in
       
   179     #   Mercurial 0.9.1.
       
   180     #
       
   181     # ``upgrade``
       
   182     #   Requests upgrade from default transport protocol version 1 to
       
   183     #   a newer version. Introduced in Mercurial 4.6 as an experimental
       
   184     #   feature.
   174     #
   185     #
   175     # The ``between`` command is issued with a request for the null
   186     # The ``between`` command is issued with a request for the null
   176     # range. If the remote is a Mercurial server, this request will
   187     # range. If the remote is a Mercurial server, this request will
   177     # generate a specific response: ``1\n\n``. This represents the
   188     # generate a specific response: ``1\n\n``. This represents the
   178     # wire protocol encoded value for ``\n``. We look for ``1\n\n``
   189     # wire protocol encoded value for ``\n``. We look for ``1\n\n``
   184     # value. If the server doesn't support ``hello`` (which should be
   195     # value. If the server doesn't support ``hello`` (which should be
   185     # rare), that line will be ``0\n``. Otherwise, the value will contain
   196     # rare), that line will be ``0\n``. Otherwise, the value will contain
   186     # RFC 822 like lines. Of these, the ``capabilities:`` line contains
   197     # RFC 822 like lines. Of these, the ``capabilities:`` line contains
   187     # the capabilities of the server.
   198     # the capabilities of the server.
   188     #
   199     #
       
   200     # The ``upgrade`` command isn't really a command in the traditional
       
   201     # sense of version 1 of the transport because it isn't using the
       
   202     # proper mechanism for formatting insteads: instead, it just encodes
       
   203     # arguments on the line, delimited by spaces.
       
   204     #
       
   205     # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
       
   206     # If the server doesn't support protocol upgrades, it will reply to
       
   207     # this line with ``0\n``. Otherwise, it emits an
       
   208     # ``upgraded <token> <protocol>`` line to both stdout and stderr.
       
   209     # Content immediately following this line describes additional
       
   210     # protocol and server state.
       
   211     #
   189     # In addition to the responses to our command requests, the server
   212     # In addition to the responses to our command requests, the server
   190     # may emit "banner" output on stdout. SSH servers are allowed to
   213     # may emit "banner" output on stdout. SSH servers are allowed to
   191     # print messages to stdout on login. Issuing commands on connection
   214     # print messages to stdout on login. Issuing commands on connection
   192     # allows us to flush this banner output from the server by scanning
   215     # allows us to flush this banner output from the server by scanning
   193     # for output to our well-known ``between`` command. Of course, if
   216     # for output to our well-known ``between`` command. Of course, if
   194     # the banner contains ``1\n\n``, this will throw off our detection.
   217     # the banner contains ``1\n\n``, this will throw off our detection.
   195 
   218 
   196     requestlog = ui.configbool('devel', 'debug.peer-request')
   219     requestlog = ui.configbool('devel', 'debug.peer-request')
       
   220 
       
   221     # Generate a random token to help identify responses to version 2
       
   222     # upgrade request.
       
   223     token = bytes(uuid.uuid4())
       
   224     upgradecaps = [
       
   225         ('proto', wireprotoserver.SSHV2),
       
   226     ]
       
   227     upgradecaps = util.urlreq.urlencode(upgradecaps)
   197 
   228 
   198     try:
   229     try:
   199         pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
   230         pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
   200         handshake = [
   231         handshake = [
   201             'hello\n',
   232             'hello\n',
   202             'between\n',
   233             'between\n',
   203             'pairs %d\n' % len(pairsarg),
   234             'pairs %d\n' % len(pairsarg),
   204             pairsarg,
   235             pairsarg,
   205         ]
   236         ]
   206 
   237 
       
   238         # Request upgrade to version 2 if configured.
       
   239         if ui.configbool('experimental', 'sshpeer.advertise-v2'):
       
   240             ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
       
   241             handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
       
   242 
   207         if requestlog:
   243         if requestlog:
   208             ui.debug('devel-peer-request: hello\n')
   244             ui.debug('devel-peer-request: hello\n')
   209         ui.debug('sending hello command\n')
   245         ui.debug('sending hello command\n')
   210         if requestlog:
   246         if requestlog:
   211             ui.debug('devel-peer-request: between\n')
   247             ui.debug('devel-peer-request: between\n')
   215         stdin.write(''.join(handshake))
   251         stdin.write(''.join(handshake))
   216         stdin.flush()
   252         stdin.flush()
   217     except IOError:
   253     except IOError:
   218         badresponse()
   254         badresponse()
   219 
   255 
       
   256     # Assume version 1 of wire protocol by default.
       
   257     protoname = wireprotoserver.SSHV1
       
   258     reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
       
   259 
   220     lines = ['', 'dummy']
   260     lines = ['', 'dummy']
   221     max_noise = 500
   261     max_noise = 500
   222     while lines[-1] and max_noise:
   262     while lines[-1] and max_noise:
   223         try:
   263         try:
   224             l = stdout.readline()
   264             l = stdout.readline()
   225             _forwardoutput(ui, stderr)
   265             _forwardoutput(ui, stderr)
       
   266 
       
   267             # Look for reply to protocol upgrade request. It has a token
       
   268             # in it, so there should be no false positives.
       
   269             m = reupgraded.match(l)
       
   270             if m:
       
   271                 protoname = m.group(1)
       
   272                 ui.debug('protocol upgraded to %s\n' % protoname)
       
   273                 # If an upgrade was handled, the ``hello`` and ``between``
       
   274                 # requests are ignored. The next output belongs to the
       
   275                 # protocol, so stop scanning lines.
       
   276                 break
       
   277 
       
   278             # Otherwise it could be a banner, ``0\n`` response if server
       
   279             # doesn't support upgrade.
       
   280 
   226             if lines[-1] == '1\n' and l == '\n':
   281             if lines[-1] == '1\n' and l == '\n':
   227                 break
   282                 break
   228             if l:
   283             if l:
   229                 ui.debug('remote: ', l)
   284                 ui.debug('remote: ', l)
   230             lines.append(l)
   285             lines.append(l)
   233             badresponse()
   288             badresponse()
   234     else:
   289     else:
   235         badresponse()
   290         badresponse()
   236 
   291 
   237     caps = set()
   292     caps = set()
   238     for l in reversed(lines):
   293 
   239         # Look for response to ``hello`` command. Scan from the back so
   294     # For version 1, we should see a ``capabilities`` line in response to the
   240         # we don't misinterpret banner output as the command reply.
   295     # ``hello`` command.
   241         if l.startswith('capabilities:'):
   296     if protoname == wireprotoserver.SSHV1:
   242             caps.update(l[:-1].split(':')[1].split())
   297         for l in reversed(lines):
   243             break
   298             # Look for response to ``hello`` command. Scan from the back so
   244 
   299             # we don't misinterpret banner output as the command reply.
   245     # Error if we couldn't find a response to ``hello``. This could
   300             if l.startswith('capabilities:'):
   246     # mean:
   301                 caps.update(l[:-1].split(':')[1].split())
       
   302                 break
       
   303     elif protoname == wireprotoserver.SSHV2:
       
   304         # We see a line with number of bytes to follow and then a value
       
   305         # looking like ``capabilities: *``.
       
   306         line = stdout.readline()
       
   307         try:
       
   308             valuelen = int(line)
       
   309         except ValueError:
       
   310             badresponse()
       
   311 
       
   312         capsline = stdout.read(valuelen)
       
   313         if not capsline.startswith('capabilities: '):
       
   314             badresponse()
       
   315 
       
   316         caps.update(capsline.split(':')[1].split())
       
   317         # Trailing newline.
       
   318         stdout.read(1)
       
   319 
       
   320     # Error if we couldn't find capabilities, this means:
   247     #
   321     #
   248     # 1. Remote isn't a Mercurial server
   322     # 1. Remote isn't a Mercurial server
   249     # 2. Remote is a <0.9.1 Mercurial server
   323     # 2. Remote is a <0.9.1 Mercurial server
   250     # 3. Remote is a future Mercurial server that dropped ``hello``
   324     # 3. Remote is a future Mercurial server that dropped ``hello``
   251     #    support.
   325     #    and other attempted handshake mechanisms.
   252     if not caps:
   326     if not caps:
   253         badresponse()
   327         badresponse()
   254 
   328 
   255     return caps
   329     return caps
   256 
   330