##// END OF EJS Templates
sshpeer: initial definition and implementation of new SSH protocol...
Gregory Szorc -
r35994:48a3a928 default
parent child Browse files
Show More
@@ -574,6 +574,9 b" coreconfigitem('experimental', 'treemani"
574 coreconfigitem('experimental', 'update.atomic-file',
574 coreconfigitem('experimental', 'update.atomic-file',
575 default=False,
575 default=False,
576 )
576 )
577 coreconfigitem('experimental', 'sshpeer.advertise-v2',
578 default=False,
579 )
577 coreconfigitem('extensions', '.*',
580 coreconfigitem('extensions', '.*',
578 default=None,
581 default=None,
579 generic=True,
582 generic=True,
@@ -218,6 +218,95 b' part of the response payload and not par'
218 after responses. In other words, the length of the response contains the
218 after responses. In other words, the length of the response contains the
219 trailing ``\n``.
219 trailing ``\n``.
220
220
221 Clients supporting version 2 of the SSH transport send a line beginning
222 with ``upgrade`` before the ``hello`` and ``between`` commands. The line
223 (which isn't a well-formed command line because it doesn't consist of a
224 single command name) serves to both communicate the client's intent to
225 switch to transport version 2 (transports are version 1 by default) as
226 well as to advertise the client's transport-level capabilities so the
227 server may satisfy that request immediately.
228
229 The upgrade line has the form:
230
231 upgrade <token> <transport capabilities>
232
233 That is the literal string ``upgrade`` followed by a space, followed by
234 a randomly generated string, followed by a space, followed by a string
235 denoting the client's transport capabilities.
236
237 The token can be anything. However, a random UUID is recommended. (Use
238 of version 4 UUIDs is recommended because version 1 UUIDs can leak the
239 client's MAC address.)
240
241 The transport capabilities string is a URL/percent encoded string
242 containing key-value pairs defining the client's transport-level
243 capabilities. The following capabilities are defined:
244
245 proto
246 A comma-delimited list of transport protocol versions the client
247 supports. e.g. ``ssh-v2``.
248
249 If the server does not recognize the ``upgrade`` line, it should issue
250 an empty response and continue processing the ``hello`` and ``between``
251 commands. Here is an example handshake between a version 2 aware client
252 and a non version 2 aware server:
253
254 c: upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=ssh-v2
255 c: hello\n
256 c: between\n
257 c: pairs 81\n
258 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
259 s: 0\n
260 s: 324\n
261 s: capabilities: lookup changegroupsubset branchmap pushkey known getbundle ...\n
262 s: 1\n
263 s: \n
264
265 (The initial ``0\n`` line from the server indicates an empty response to
266 the unknown ``upgrade ..`` command/line.)
267
268 If the server recognizes the ``upgrade`` line and is willing to satisfy that
269 upgrade request, it replies to with a payload of the following form:
270
271 upgraded <token> <transport name>\n
272
273 This line is the literal string ``upgraded``, a space, the token that was
274 specified by the client in its ``upgrade ...`` request line, a space, and the
275 name of the transport protocol that was chosen by the server. The transport
276 name MUST match one of the names the client specified in the ``proto`` field
277 of its ``upgrade ...`` request line.
278
279 If a server issues an ``upgraded`` response, it MUST also read and ignore
280 the lines associated with the ``hello`` and ``between`` command requests
281 that were issued by the server. It is assumed that the negotiated transport
282 will respond with equivalent requested information following the transport
283 handshake.
284
285 All data following the ``\n`` terminating the ``upgraded`` line is the
286 domain of the negotiated transport. It is common for the data immediately
287 following to contain additional metadata about the state of the transport and
288 the server. However, this isn't strictly speaking part of the transport
289 handshake and isn't covered by this section.
290
291 Here is an example handshake between a version 2 aware client and a version
292 2 aware server:
293
294 c: upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=ssh-v2
295 c: hello\n
296 c: between\n
297 c: pairs 81\n
298 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
299 s: upgraded 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a ssh-v2\n
300 s: <additional transport specific data>
301
302 The client-issued token that is echoed in the response provides a more
303 resilient mechanism for differentiating *banner* output from Mercurial
304 output. In version 1, properly formatted banner output could get confused
305 for Mercurial server output. By submitting a randomly generated token
306 that is then present in the response, the client can look for that token
307 in response lines and have reasonable certainty that the line did not
308 originate from a *banner* message.
309
221 SSH Version 1 Transport
310 SSH Version 1 Transport
222 -----------------------
311 -----------------------
223
312
@@ -281,6 +370,31 b' response.'
281
370
282 The server terminates if it receives an empty command (a ``\n`` character).
371 The server terminates if it receives an empty command (a ``\n`` character).
283
372
373 SSH Version 2 Transport
374 -----------------------
375
376 **Experimental**
377
378 Version 2 of the SSH transport behaves identically to version 1 of the SSH
379 transport with the exception of handshake semantics. See above for how
380 version 2 of the SSH transport is negotiated.
381
382 Immediately following the ``upgraded`` line signaling a switch to version
383 2 of the SSH protocol, the server automatically sends additional details
384 about the capabilities of the remote server. This has the form:
385
386 <integer length of value>\n
387 capabilities: ...\n
388
389 e.g.
390
391 s: upgraded 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a ssh-v2\n
392 s: 240\n
393 s: capabilities: known getbundle batch ...\n
394
395 Following capabilities advertisement, the peers communicate using version
396 1 of the SSH transport.
397
284 Capabilities
398 Capabilities
285 ============
399 ============
286
400
@@ -8,6 +8,7 b''
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 (
@@ -15,6 +16,7 b' from . import ('
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):
@@ -162,15 +164,24 b' def _performhandshake(ui, stdin, stdout,'
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
169 # to one of them.
170 #
171 # Those commands (from oldest to newest) are:
167 #
172 #
168 # The ``hello`` command (which was introduced in Mercurial 0.9.1)
173 # ``between``
169 # instructs the server to advertise its capabilities.
174 # Asks for the set of revisions between a pair of revisions. Command
175 # present in all Mercurial server implementations.
170 #
176 #
171 # The ``between`` command (which has existed in all Mercurial servers
177 # ``hello``
172 # for as long as SSH support has existed), asks for the set of revisions
178 # Instructs the server to advertise its capabilities. Introduced in
173 # between a pair of revisions.
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
@@ -186,6 +197,18 b' def _performhandshake(ui, stdin, stdout,'
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
@@ -195,6 +218,14 b' def _performhandshake(ui, stdin, stdout,'
195
218
196 requestlog = ui.configbool('devel', 'debug.peer-request')
219 requestlog = ui.configbool('devel', 'debug.peer-request')
197
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)
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 = [
@@ -204,6 +235,11 b' def _performhandshake(ui, stdin, stdout,'
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')
@@ -217,12 +253,31 b' def _performhandshake(ui, stdin, stdout,'
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:
@@ -235,20 +290,39 b' def _performhandshake(ui, stdin, stdout,'
235 badresponse()
290 badresponse()
236
291
237 caps = set()
292 caps = set()
238 for l in reversed(lines):
239 # Look for response to ``hello`` command. Scan from the back so
240 # we don't misinterpret banner output as the command reply.
241 if l.startswith('capabilities:'):
242 caps.update(l[:-1].split(':')[1].split())
243 break
244
293
245 # Error if we couldn't find a response to ``hello``. This could
294 # For version 1, we should see a ``capabilities`` line in response to the
246 # mean:
295 # ``hello`` command.
296 if protoname == wireprotoserver.SSHV1:
297 for l in reversed(lines):
298 # Look for response to ``hello`` command. Scan from the back so
299 # we don't misinterpret banner output as the command reply.
300 if l.startswith('capabilities:'):
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
@@ -32,6 +32,12 b" HGTYPE = 'application/mercurial-0.1'"
32 HGTYPE2 = 'application/mercurial-0.2'
32 HGTYPE2 = 'application/mercurial-0.2'
33 HGERRTYPE = 'application/hg-error'
33 HGERRTYPE = 'application/hg-error'
34
34
35 # Names of the SSH protocol implementations.
36 SSHV1 = 'ssh-v1'
37 # This is advertised over the wire. Incremental the counter at the end
38 # to reflect BC breakages.
39 SSHV2 = 'exp-ssh-v2-0001'
40
35 class abstractserverproto(object):
41 class abstractserverproto(object):
36 """abstract class that summarizes the protocol API
42 """abstract class that summarizes the protocol API
37
43
@@ -53,6 +53,35 b' class prehelloserver(wireprotoserver.ssh'
53
53
54 super(prehelloserver, self).serve_forever()
54 super(prehelloserver, self).serve_forever()
55
55
56 class upgradev2server(wireprotoserver.sshserver):
57 """Tests behavior for clients that issue upgrade to version 2."""
58 def serve_forever(self):
59 name = wireprotoserver.SSHV2
60 l = self._fin.readline()
61 assert l.startswith(b'upgrade ')
62 token, caps = l[:-1].split(b' ')[1:]
63 assert caps == b'proto=%s' % name
64
65 # Filter hello and between requests.
66 l = self._fin.readline()
67 assert l == b'hello\n'
68 l = self._fin.readline()
69 assert l == b'between\n'
70 l = self._fin.readline()
71 assert l == 'pairs 81\n'
72 self._fin.read(81)
73
74 # Send the upgrade response.
75 self._fout.write(b'upgraded %s %s\n' % (token, name))
76 servercaps = wireproto.capabilities(self._repo, self)
77 rsp = b'capabilities: %s' % servercaps
78 self._fout.write(b'%d\n' % len(rsp))
79 self._fout.write(rsp)
80 self._fout.write(b'\n')
81 self._fout.flush()
82
83 super(upgradev2server, self).serve_forever()
84
56 def performhandshake(orig, ui, stdin, stdout, stderr):
85 def performhandshake(orig, ui, stdin, stdout, stderr):
57 """Wrapped version of sshpeer._performhandshake to send extra commands."""
86 """Wrapped version of sshpeer._performhandshake to send extra commands."""
58 mode = ui.config(b'sshpeer', b'handshake-mode')
87 mode = ui.config(b'sshpeer', b'handshake-mode')
@@ -85,6 +114,8 b' def extsetup(ui):'
85 wireprotoserver.sshserver = bannerserver
114 wireprotoserver.sshserver = bannerserver
86 elif servermode == b'no-hello':
115 elif servermode == b'no-hello':
87 wireprotoserver.sshserver = prehelloserver
116 wireprotoserver.sshserver = prehelloserver
117 elif servermode == b'upgradev2':
118 wireprotoserver.sshserver = upgradev2server
88 elif servermode:
119 elif servermode:
89 raise error.ProgrammingError(b'unknown server mode: %s' % servermode)
120 raise error.ProgrammingError(b'unknown server mode: %s' % servermode)
90
121
@@ -388,3 +388,107 b' And one with arguments'
388 0
388 0
389 0
389 0
390 0
390 0
391
392 Send an upgrade request to a server that doesn't support that command
393
394 $ hg -R server serve --stdio << EOF
395 > upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=irrelevant1%2Cirrelevant2
396 > hello
397 > between
398 > pairs 81
399 > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
400 > EOF
401 0
402 384
403 capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
404 1
405
406
407 $ hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
408 running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob)
409 sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
410 devel-peer-request: hello
411 sending hello command
412 devel-peer-request: between
413 devel-peer-request: pairs: 81 bytes
414 sending between command
415 remote: 0
416 remote: 384
417 remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
418 remote: 1
419 url: ssh://user@dummy/server
420 local: no
421 pushable: yes
422
423 Send an upgrade request to a server that supports upgrade
424
425 $ SSHSERVERMODE=upgradev2 hg -R server serve --stdio << EOF
426 > upgrade this-is-some-token proto=exp-ssh-v2-0001
427 > hello
428 > between
429 > pairs 81
430 > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
431 > EOF
432 upgraded this-is-some-token exp-ssh-v2-0001
433 383
434 capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
435
436 $ SSHSERVERMODE=upgradev2 hg --config experimental.sshpeer.advertise-v2=true --debug debugpeer ssh://user@dummy/server
437 running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob)
438 sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
439 devel-peer-request: hello
440 sending hello command
441 devel-peer-request: between
442 devel-peer-request: pairs: 81 bytes
443 sending between command
444 protocol upgraded to exp-ssh-v2-0001
445 url: ssh://user@dummy/server
446 local: no
447 pushable: yes
448
449 Verify the peer has capabilities
450
451 $ SSHSERVERMODE=upgradev2 hg --config experimental.sshpeer.advertise-v2=true --debug debugcapabilities ssh://user@dummy/server
452 running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob)
453 sending upgrade request: * proto=exp-ssh-v2-0001 (glob)
454 devel-peer-request: hello
455 sending hello command
456 devel-peer-request: between
457 devel-peer-request: pairs: 81 bytes
458 sending between command
459 protocol upgraded to exp-ssh-v2-0001
460 Main capabilities:
461 batch
462 branchmap
463 $USUAL_BUNDLE2_CAPS_SERVER$
464 changegroupsubset
465 getbundle
466 known
467 lookup
468 pushkey
469 streamreqs=generaldelta,revlogv1
470 unbundle=HG10GZ,HG10BZ,HG10UN
471 unbundlehash
472 Bundle2 capabilities:
473 HG20
474 bookmarks
475 changegroup
476 01
477 02
478 digests
479 md5
480 sha1
481 sha512
482 error
483 abort
484 unsupportedcontent
485 pushraced
486 pushkey
487 hgtagsfnodes
488 listkeys
489 phases
490 heads
491 pushkey
492 remote-changegroup
493 http
494 https
General Comments 0
You need to be logged in to leave comments. Login now