wireproto: implement capabilities for wire protocol v2
authorGregory Szorc <gregory.szorc@gmail.com>
Mon, 09 Apr 2018 11:52:31 -0700
changeset 37533 df4985497986
parent 37532 8475c9bf096d
child 37534 465187fec06f
wireproto: implement capabilities for wire protocol v2 The capabilities mechanism for wire protocol version 2 represents a clean break from version 1. Instead of effectively exchanging a set of capabilities, we're exchanging a rich data structure. This data structure currently contains information about every available command, including its accepted arguments. It also contains information about supported compression formats. Exposing information about supported commands will allow clients to automatically generate bindings to the server. Clients will be able to do things like detect when they are attempting to run a command that isn't known to the server. Exposing the required permissions to run a command can be used by clients to determine if they have privileges to call a command before actually calling it. We could potentially even have clients send credentials preemptively without waiting for the server to deny the command request. Lots of potential here. The data returned by this command will likely evolve heavily. So we shouldn't bikeshed the implementation just yet. Differential Revision: https://phab.mercurial-scm.org/D3200
mercurial/help/internals/wireprotocol.txt
mercurial/wireproto.py
mercurial/wireprotoserver.py
tests/test-ssh-proto.t
tests/test-wireproto-command-capabilities.t
--- a/mercurial/help/internals/wireprotocol.txt	Sun Apr 08 09:45:45 2018 -0700
+++ b/mercurial/help/internals/wireprotocol.txt	Mon Apr 09 11:52:31 2018 -0700
@@ -1690,6 +1690,42 @@
 The response is a map with bytestring keys defining the branch name.
 Values are arrays of bytestring defining raw changeset nodes.
 
+capabilities
+------------
+
+Obtain the server's capabilities.
+
+Receives no arguments.
+
+This command is typically called only as part of the handshake during
+initial connection establishment.
+
+The response is a map with bytestring keys defining server information.
+
+The defined keys are:
+
+commands
+   A map defining available wire protocol commands on this server.
+
+   Keys in the map are the names of commands that can be invoked. Values
+   are maps defining information about that command. The bytestring keys
+   are:
+
+      args
+         An array of argument names accepted by this command.
+      permissions
+         An array of permissions required to execute this command.
+
+compression
+   An array of maps defining available compression format support.
+
+   The array is sorted from most preferred to least preferred.
+
+   Each entry has the following bytestring keys:
+
+      name
+         Name of the compression engine. e.g. ``zstd`` or ``zlib``.
+
 heads
 -----
 
--- a/mercurial/wireproto.py	Sun Apr 08 09:45:45 2018 -0700
+++ b/mercurial/wireproto.py	Mon Apr 09 11:52:31 2018 -0700
@@ -903,7 +903,8 @@
 
 # If you are writing an extension and consider wrapping this function. Wrap
 # `_capabilities` instead.
-@wireprotocommand('capabilities', permission='pull')
+@wireprotocommand('capabilities', permission='pull',
+                  transportpolicy=POLICY_V1_ONLY)
 def capabilities(repo, proto):
     caps = _capabilities(repo, proto)
     return wireprototypes.bytesresponse(' '.join(sorted(caps)))
@@ -1283,6 +1284,31 @@
 
 # Wire protocol version 2 commands only past this point.
 
+def _capabilitiesv2(repo, proto):
+    """Obtain the set of capabilities for version 2 transports.
+
+    These capabilities are distinct from the capabilities for version 1
+    transports.
+    """
+    compression = []
+    for engine in supportedcompengines(repo.ui, util.SERVERROLE):
+        compression.append({
+            b'name': engine.wireprotosupport().name,
+        })
+
+    caps = {
+        'commands': {},
+        'compression': compression,
+    }
+
+    for command, entry in commandsv2.items():
+        caps['commands'][command] = {
+            'args': sorted(entry.args.split()) if entry.args else [],
+            'permissions': [entry.permission],
+        }
+
+    return proto.addcapabilities(repo, caps)
+
 @wireprotocommand('branchmap', permission='pull',
                   transportpolicy=POLICY_V2_ONLY)
 def branchmapv2(repo, proto):
@@ -1291,6 +1317,13 @@
 
     return wireprototypes.cborresponse(branchmap)
 
+@wireprotocommand('capabilities', permission='pull',
+                  transportpolicy=POLICY_V2_ONLY)
+def capabilitiesv2(repo, proto):
+    caps = _capabilitiesv2(repo, proto)
+
+    return wireprototypes.cborresponse(caps)
+
 @wireprotocommand('heads', args='publiconly', permission='pull',
                   transportpolicy=POLICY_V2_ONLY)
 def headsv2(repo, proto, publiconly=False):
--- a/mercurial/wireprotoserver.py	Sun Apr 08 09:45:45 2018 -0700
+++ b/mercurial/wireprotoserver.py	Mon Apr 09 11:52:31 2018 -0700
@@ -831,6 +831,9 @@
     def name(self):
         return wireprototypes.SSHV2
 
+    def addcapabilities(self, repo, caps):
+        return caps
+
 def _runsshserver(ui, repo, fin, fout, ev):
     # This function operates like a state machine of sorts. The following
     # states are defined:
--- a/tests/test-ssh-proto.t	Sun Apr 08 09:45:45 2018 -0700
+++ b/tests/test-ssh-proto.t	Mon Apr 09 11:52:31 2018 -0700
@@ -1120,9 +1120,9 @@
   i> write(6) -> 6:
   i>     hello\n
   o> readline() -> 4:
-  o>     403\n
-  o> readline() -> 403:
-  o>     capabilities: batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset getbundle known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash\n
+  o>     397\n
+  o> readline() -> 397:
+  o>     capabilities: branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset getbundle known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash\n
 
 Multiple upgrades is not allowed
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-wireproto-command-capabilities.t	Mon Apr 09 11:52:31 2018 -0700
@@ -0,0 +1,40 @@
+  $ . $TESTDIR/wireprotohelpers.sh
+
+  $ hg init server
+  $ enablehttpv2 server
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
+  $ cat hg.pid > $DAEMON_PIDS
+
+capabilities request returns an array of capability strings
+
+  $ sendhttpv2peer << EOF
+  > command capabilities
+  > EOF
+  creating http peer for wire protocol version 2
+  sending capabilities command
+  s>     POST /api/exp-http-v2-0001/ro/capabilities HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
+  s>     content-length: 27\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     user-agent: Mercurial debugwireproto\r\n
+  s>     \r\n
+  s>     \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
+  s> makefile('rb', None)
+  s>     HTTP/1.1 200 OK\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
+  s>     Transfer-Encoding: chunked\r\n
+  s>     \r\n
+  s>     *\r\n (glob)
+  s>     *\x00\x01\x00\x02\x01F (glob)
+  s>     \xa2Hcommands\xabEheads\xa2Dargs\x81JpubliconlyKpermissions\x81DpullEknown\xa2Dargs\x81EnodesKpermissions\x81DpullFlookup\xa2Dargs\x81CkeyKpermissions\x81DpullGpushkey\xa2Dargs\x84CkeyInamespaceCnewColdKpermissions\x81DpushHlistkeys\xa2Dargs\x81InamespaceKpermissions\x81DpullHunbundle\xa2Dargs\x81EheadsKpermissions\x81DpushIbranchmap\xa2Dargs\x80Kpermissions\x81DpullIgetbundle\xa2Dargs\x81A*Kpermissions\x81DpullJstream_out\xa2Dargs\x80Kpermissions\x81DpullLcapabilities\xa2Dargs\x80Kpermissions\x81DpullLclonebundles\xa2Dargs\x80Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlib
+  s>     \r\n
+  received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) (glob)
+  s>     0\r\n
+  s>     \r\n
+  response: [{b'commands': {b'branchmap': {b'args': [], b'permissions': [b'pull']}, b'capabilities': {b'args': [], b'permissions': [b'pull']}, b'clonebundles': {b'args': [], b'permissions': [b'pull']}, b'getbundle': {b'args': [b'*'], b'permissions': [b'pull']}, b'heads': {b'args': [b'publiconly'], b'permissions': [b'pull']}, b'known': {b'args': [b'nodes'], b'permissions': [b'pull']}, b'listkeys': {b'args': [b'namespace'], b'permissions': [b'pull']}, b'lookup': {b'args': [b'key'], b'permissions': [b'pull']}, b'pushkey': {b'args': [b'key', b'namespace', b'new', b'old'], b'permissions': [b'push']}, b'stream_out': {b'args': [], b'permissions': [b'pull']}, b'unbundle': {b'args': [b'heads'], b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}]}]
+
+  $ cat error.log