diff mercurial/httppeer.py @ 37483:61e405fb6372

wireproto: crude support for version 2 HTTP peer As part of implementing the server-side bits of the wire protocol command handlers for version 2, we want a way to easily test those commands. Currently, we use the "httprequest" action of `hg debugwireproto`. But this requires explicitly specifying the HTTP request headers, low-level frame details, and the data structure to encode with CBOR. That's a lot of boilerplate and a lot of it can change as the wire protocol evolves. `hg debugwireproto` has a mechanism to issue commands via the peer interface. That is *much* easier to use and we prefer to test with that going forward. This commit implements enough parts of the peer API to send basic requests via the HTTP version 2 transport. The peer code is super hacky. Again, the goal is to facilitate server testing, not robustly implement a client. The client code will receive love at a later time. Differential Revision: https://phab.mercurial-scm.org/D3177
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 28 Mar 2018 15:09:34 -0700
parents 3e1688711efd
children aacfca6f9767
line wrap: on
line diff
--- a/mercurial/httppeer.py	Mon Mar 26 15:34:52 2018 -0700
+++ b/mercurial/httppeer.py	Wed Mar 28 15:09:34 2018 -0700
@@ -16,6 +16,9 @@
 import tempfile
 
 from .i18n import _
+from .thirdparty import (
+    cbor,
+)
 from . import (
     bundle2,
     error,
@@ -25,6 +28,8 @@
     url as urlmod,
     util,
     wireproto,
+    wireprotoframing,
+    wireprotoserver,
 )
 
 httplib = util.httplib
@@ -467,6 +472,95 @@
     def _abort(self, exception):
         raise exception
 
+# TODO implement interface for version 2 peers
+class httpv2peer(object):
+    def __init__(self, ui, repourl, opener):
+        self.ui = ui
+
+        if repourl.endswith('/'):
+            repourl = repourl[:-1]
+
+        self.url = repourl
+        self._opener = opener
+        # This is an its own attribute to facilitate extensions overriding
+        # the default type.
+        self._requestbuilder = urlreq.request
+
+    def close(self):
+        pass
+
+    # TODO require to be part of a batched primitive, use futures.
+    def _call(self, name, **args):
+        """Call a wire protocol command with arguments."""
+
+        # TODO permissions should come from capabilities results.
+        permission = wireproto.commandsv2[name].permission
+        if permission not in ('push', 'pull'):
+            raise error.ProgrammingError('unknown permission type: %s' %
+                                         permission)
+
+        permission = {
+            'push': 'rw',
+            'pull': 'ro',
+        }[permission]
+
+        url = '%s/api/%s/%s/%s' % (self.url, wireprotoserver.HTTPV2, permission,
+                                   name)
+
+        # TODO modify user-agent to reflect v2.
+        headers = {
+            r'Accept': wireprotoserver.FRAMINGTYPE,
+            r'Content-Type': wireprotoserver.FRAMINGTYPE,
+        }
+
+        # TODO this should be part of a generic peer for the frame-based
+        # protocol.
+        stream = wireprotoframing.stream(1)
+        frames = wireprotoframing.createcommandframes(stream, 1,
+                                                      name, args)
+
+        body = b''.join(map(bytes, frames))
+        req = self._requestbuilder(pycompat.strurl(url), body, headers)
+        req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
+
+        # TODO unify this code with httppeer.
+        try:
+            res = self._opener.open(req)
+        except urlerr.httperror as e:
+            if e.code == 401:
+                raise error.Abort(_('authorization failed'))
+
+            raise
+        except httplib.HTTPException as e:
+            self.ui.traceback()
+            raise IOError(None, e)
+
+        # TODO validate response type, wrap response to handle I/O errors.
+        # TODO more robust frame receiver.
+        results = []
+
+        while True:
+            frame = wireprotoframing.readframe(res)
+            if frame is None:
+                break
+
+            self.ui.note(_('received %r\n') % frame)
+
+            if frame.typeid == wireprotoframing.FRAME_TYPE_BYTES_RESPONSE:
+                if frame.flags & wireprotoframing.FLAG_BYTES_RESPONSE_CBOR:
+                    payload = util.bytesio(frame.payload)
+
+                    decoder = cbor.CBORDecoder(payload)
+                    while payload.tell() + 1 < len(frame.payload):
+                        results.append(decoder.decode())
+                else:
+                    results.append(frame.payload)
+            else:
+                error.ProgrammingError('unhandled frame type: %d' %
+                                       frame.typeid)
+
+        return results
+
 def makepeer(ui, path):
     u = util.url(path)
     if u.query or u.fragment: