Patchwork D2857: wireproto: implement basic command dispatching for HTTPv2

login
register
mail settings
Submitter phabricator
Date March 15, 2018, 1:11 a.m.
Message ID <e637cf4df77cf4653aa579489e883325@localhost.localdomain>
Download mbox | patch
Permalink /patch/29516/
State Not Applicable
Headers show

Comments

phabricator - March 15, 2018, 1:11 a.m.
indygreg updated this revision to Diff 7050.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2857?vs=7033&id=7050

REVISION DETAIL
  https://phab.mercurial-scm.org/D2857

AFFECTED FILES
  mercurial/wireprotoserver.py
  tests/test-http-api-httpv2.t

CHANGE DETAILS




To: indygreg, #hg-reviewers
Cc: mercurial-devel

Patch

diff --git a/tests/test-http-api-httpv2.t b/tests/test-http-api-httpv2.t
--- a/tests/test-http-api-httpv2.t
+++ b/tests/test-http-api-httpv2.t
@@ -181,9 +181,9 @@ 
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
   s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 16\r\n
+  s>     Content-Length: *\r\n (glob)
   s>     \r\n
-  s>     ro/capabilities\n
+  s>     lookup branchmap pushkey known getbundle unbundlehash streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
 
 Request to read-write command fails because server is read-only by default
 
@@ -288,9 +288,9 @@ 
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
   s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 16\r\n
+  s>     Content-Length: *\r\n (glob)
   s>     \r\n
-  s>     rw/capabilities\n
+  s>     lookup branchmap pushkey known getbundle unbundlehash streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN
 
 Authorized request for unknown command is rejected
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -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