changeset 37054:e7a012b60d6e

wireproto: implement basic command dispatching for HTTPv2 Now that we can ingest frames and decode them to requests to run commands, we are able to actually run those commands. So this commit starts to implement that. There are numerous shortcomings. We can't operate on commands with "*" arguments. We can only emit bytesresponse results. We don't yet issue a response in the unified framing protocol. But it's a start. Differential Revision: https://phab.mercurial-scm.org/D2857
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 14 Mar 2018 13:32:31 -0700
parents cd0ca979a8b8
children 61393f888dfe
files mercurial/wireprotoserver.py tests/test-http-api-httpv2.t
diffstat 2 files changed, 114 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/wireprotoserver.py	Wed Mar 14 08:18:15 2018 -0700
+++ b/mercurial/wireprotoserver.py	Wed Mar 14 13:32:31 2018 -0700
@@ -360,10 +360,7 @@
                            'value: %s\n') % FRAMINGTYPE)
         return
 
-    # We don't do anything meaningful yet.
-    res.status = b'200 OK'
-    res.headers[b'Content-Type'] = b'text/plain'
-    res.setbodybytes(b'/'.join(urlparts) + b'\n')
+    _processhttpv2request(ui, repo, req, res, permission, command, proto)
 
 def _processhttpv2reflectrequest(ui, repo, req, res):
     """Reads unified frame protocol request and dumps out state to client.
@@ -408,6 +405,104 @@
     res.headers[b'Content-Type'] = b'text/plain'
     res.setbodybytes(b'\n'.join(states))
 
+def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
+    """Post-validation handler for HTTPv2 requests.
+
+    Called when the HTTP request contains unified frame-based protocol
+    frames for evaluation.
+    """
+    reactor = wireprotoframing.serverreactor()
+    seencommand = False
+
+    while True:
+        frame = wireprotoframing.readframe(req.bodyfh)
+        if not frame:
+            break
+
+        action, meta = reactor.onframerecv(*frame)
+
+        if action == 'wantframe':
+            # Need more data before we can do anything.
+            continue
+        elif action == 'runcommand':
+            # We currently only support running a single command per
+            # HTTP request.
+            if seencommand:
+                # TODO define proper error mechanism.
+                res.status = b'200 OK'
+                res.headers[b'Content-Type'] = b'text/plain'
+                res.setbodybytes(_('support for multiple commands per request '
+                                   'not yet implemented'))
+                return
+
+            _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand,
+                              reactor, meta)
+
+        elif action == 'error':
+            # TODO define proper error mechanism.
+            res.status = b'200 OK'
+            res.headers[b'Content-Type'] = b'text/plain'
+            res.setbodybytes(meta['message'] + b'\n')
+            return
+        else:
+            raise error.ProgrammingError(
+                'unhandled action from frame processor: %s' % action)
+
+def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
+                      command):
+    """Dispatch a wire protocol command made from HTTPv2 requests.
+
+    The authenticated permission (``authedperm``) along with the original
+    command from the URL (``reqcommand``) are passed in.
+    """
+    # We already validated that the session has permissions to perform the
+    # actions in ``authedperm``. In the unified frame protocol, the canonical
+    # command to run is expressed in a frame. However, the URL also requested
+    # to run a specific command. We need to be careful that the command we
+    # run doesn't have permissions requirements greater than what was granted
+    # by ``authedperm``.
+    #
+    # For now, this is no big deal, as we only allow a single command per
+    # request and that command must match the command in the URL. But when
+    # things change, we need to watch out...
+    if reqcommand != command['command']:
+        # TODO define proper error mechanism
+        res.status = b'200 OK'
+        res.headers[b'Content-Type'] = b'text/plain'
+        res.setbodybytes(_('command in frame must match command in URL'))
+        return
+
+    # TODO once we get rid of the command==URL restriction, we'll need to
+    # revalidate command validity and auth here. checkperm,
+    # wireproto.commands.commandavailable(), etc.
+
+    proto = httpv2protocolhandler(req, ui, args=command['args'])
+    assert wireproto.commands.commandavailable(command['command'], proto)
+    wirecommand = wireproto.commands[command['command']]
+
+    assert authedperm in (b'ro', b'rw')
+    assert wirecommand.permission in ('push', 'pull')
+
+    # We already checked this as part of the URL==command check, but
+    # permissions are important, so do it again.
+    if authedperm == b'ro':
+        assert wirecommand.permission == 'pull'
+    elif authedperm == b'rw':
+        # We are allowed to access read-only commands under the rw URL.
+        assert wirecommand.permission in ('push', 'pull')
+
+    rsp = wireproto.dispatch(repo, proto, command['command'])
+
+    # TODO use proper response format.
+    res.status = b'200 OK'
+    res.headers[b'Content-Type'] = b'text/plain'
+
+    if isinstance(rsp, wireprototypes.bytesresponse):
+        res.setbodybytes(rsp.data)
+    else:
+        res.setbodybytes(b'unhandled response type from wire proto '
+                         'command')
+
 # Maps API name to metadata so custom API can be registered.
 API_HANDLERS = {
     HTTPV2: {
@@ -417,16 +512,24 @@
 }
 
 class httpv2protocolhandler(wireprototypes.baseprotocolhandler):
-    def __init__(self, req, ui):
+    def __init__(self, req, ui, args=None):
         self._req = req
         self._ui = ui
+        self._args = args
 
     @property
     def name(self):
         return HTTPV2
 
     def getargs(self, args):
-        raise NotImplementedError
+        data = {}
+        for k in args.split():
+            if k == '*':
+                raise NotImplementedError('do not support * args')
+            else:
+                data[k] = self._args[k]
+
+        return [data[k] for k in args.split()]
 
     def forwardpayload(self, fp):
         raise NotImplementedError
@@ -439,7 +542,7 @@
         raise NotImplementedError
 
     def addcapabilities(self, repo, caps):
-        raise NotImplementedError
+        return caps
 
     def checkperm(self, perm):
         raise NotImplementedError
--- a/tests/test-http-api-httpv2.t	Wed Mar 14 08:18:15 2018 -0700
+++ b/tests/test-http-api-httpv2.t	Wed Mar 14 13:32:31 2018 -0700
@@ -196,9 +196,9 @@
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
   s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 18\r\n
+  s>     Content-Length: 29\r\n
   s>     \r\n
-  s>     ro/customreadonly\n
+  s>     customreadonly bytes response
 
 Request to read-write command fails because server is read-only by default
 
@@ -303,9 +303,9 @@
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
   s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 18\r\n
+  s>     Content-Length: 29\r\n
   s>     \r\n
-  s>     rw/customreadonly\n
+  s>     customreadonly bytes response
 
 Authorized request for unknown command is rejected