D2834: wireproto: support /api/* URL space for exposing APIs

indygreg (Gregory Szorc) phabricator at mercurial-scm.org
Wed Mar 21 18:19:51 EDT 2018


indygreg updated this revision to Diff 7194.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2834?vs=7009&id=7194

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

AFFECTED FILES
  mercurial/configitems.py
  mercurial/hgweb/hgweb_mod.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  tests/test-http-api-httpv2.t
  tests/test-http-api.t

CHANGE DETAILS

diff --git a/tests/test-http-api.t b/tests/test-http-api.t
new file mode 100644
--- /dev/null
+++ b/tests/test-http-api.t
@@ -0,0 +1,201 @@
+  $ send() {
+  >   hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
+  > }
+
+  $ hg init server
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+Request to /api fails unless web.apiserver is enabled
+
+  $ send << EOF
+  > httprequest GET api
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  s> makefile('rb', None)
+  s>     HTTP/1.1 404 Not Found\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: text/plain\r\n
+  s>     Content-Length: 44\r\n
+  s>     \r\n
+  s>     Experimental API server endpoint not enabled
+
+  $ send << EOF
+  > httprequest GET api/
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/ HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  s> makefile('rb', None)
+  s>     HTTP/1.1 404 Not Found\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: text/plain\r\n
+  s>     Content-Length: 44\r\n
+  s>     \r\n
+  s>     Experimental API server endpoint not enabled
+
+Restart server with support for API server
+
+  $ killdaemons.py
+  $ cat > server/.hg/hgrc << EOF
+  > [experimental]
+  > web.apiserver = true
+  > EOF
+
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+/api lists available APIs (empty since none are available by default)
+
+  $ send << EOF
+  > httprequest GET api
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  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: text/plain\r\n
+  s>     Content-Length: 100\r\n
+  s>     \r\n
+  s>     APIs can be accessed at /api/<name>, where <name> can be one of the following:\n
+  s>     \n
+  s>     (no available APIs)\n
+
+  $ send << EOF
+  > httprequest GET api/
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/ HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  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: text/plain\r\n
+  s>     Content-Length: 100\r\n
+  s>     \r\n
+  s>     APIs can be accessed at /api/<name>, where <name> can be one of the following:\n
+  s>     \n
+  s>     (no available APIs)\n
+
+Accessing an unknown API yields a 404
+
+  $ send << EOF
+  > httprequest GET api/unknown
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/unknown HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  s> makefile('rb', None)
+  s>     HTTP/1.1 404 Not Found\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: text/plain\r\n
+  s>     Content-Length: 33\r\n
+  s>     \r\n
+  s>     Unknown API: unknown\n
+  s>     Known APIs: 
+
+Accessing a known but not enabled API yields a different error
+
+  $ send << EOF
+  > httprequest GET api/exp-http-v2-0001
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/exp-http-v2-0001 HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  s> makefile('rb', None)
+  s>     HTTP/1.1 404 Not Found\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: text/plain\r\n
+  s>     Content-Length: 33\r\n
+  s>     \r\n
+  s>     API exp-http-v2-0001 not enabled\n
+
+Restart server with support for HTTP v2 API
+
+  $ killdaemons.py
+  $ cat > server/.hg/hgrc << EOF
+  > [experimental]
+  > web.apiserver = true
+  > web.api.http-v2 = true
+  > EOF
+
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+/api lists the HTTP v2 protocol as available
+
+  $ send << EOF
+  > httprequest GET api
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  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: text/plain\r\n
+  s>     Content-Length: 96\r\n
+  s>     \r\n
+  s>     APIs can be accessed at /api/<name>, where <name> can be one of the following:\n
+  s>     \n
+  s>     exp-http-v2-0001
+
+  $ send << EOF
+  > httprequest GET api/
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/ HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  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: text/plain\r\n
+  s>     Content-Length: 96\r\n
+  s>     \r\n
+  s>     APIs can be accessed at /api/<name>, where <name> can be one of the following:\n
+  s>     \n
+  s>     exp-http-v2-0001
diff --git a/tests/test-http-api-httpv2.t b/tests/test-http-api-httpv2.t
new file mode 100644
--- /dev/null
+++ b/tests/test-http-api-httpv2.t
@@ -0,0 +1,65 @@
+  $ send() {
+  >   hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
+  > }
+
+  $ hg init server
+  $ cat > server/.hg/hgrc << EOF
+  > [experimental]
+  > web.apiserver = true
+  > EOF
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+HTTP v2 protocol not enabled by default
+
+  $ send << EOF
+  > httprequest GET api/exp-http-v2-0001
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/exp-http-v2-0001 HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  s> makefile('rb', None)
+  s>     HTTP/1.1 404 Not Found\r\n
+  s>     Server: testing stub value\r\n
+  s>     Date: $HTTP_DATE$\r\n
+  s>     Content-Type: text/plain\r\n
+  s>     Content-Length: 33\r\n
+  s>     \r\n
+  s>     API exp-http-v2-0001 not enabled\n
+
+Restart server with support for HTTP v2 API
+
+  $ killdaemons.py
+  $ cat > server/.hg/hgrc << EOF
+  > [experimental]
+  > web.apiserver = true
+  > web.api.http-v2 = true
+  > EOF
+
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+Requests simply echo their path (for now)
+
+  $ send << EOF
+  > httprequest GET api/exp-http-v2-0001/path1/path2
+  >     user-agent: test
+  > EOF
+  using raw connection to peer
+  s>     GET /api/exp-http-v2-0001/path1/path2 HTTP/1.1\r\n
+  s>     Accept-Encoding: identity\r\n
+  s>     user-agent: test\r\n
+  s>     host: $LOCALIP:$HGPORT\r\n (glob)
+  s>     \r\n
+  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: text/plain\r\n
+  s>     Content-Length: 12\r\n
+  s>     \r\n
+  s>     path1/path2\n
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -9,9 +9,10 @@
 
 # Names of the SSH protocol implementations.
 SSHV1 = 'ssh-v1'
-# This is advertised over the wire. Incremental the counter at the end
+# These are advertised over the wire. Increment the counters at the end
 # to reflect BC breakages.
 SSHV2 = 'exp-ssh-v2-0001'
+HTTPV2 = 'exp-http-v2-0001'
 
 # All available wire protocol transports.
 TRANSPORTS = {
@@ -26,6 +27,10 @@
     'http-v1': {
         'transport': 'http',
         'version': 1,
+    },
+    HTTPV2: {
+        'transport': 'http',
+        'version': 2,
     }
 }
 
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -33,6 +33,7 @@
 HGTYPE2 = 'application/mercurial-0.2'
 HGERRTYPE = 'application/hg-error'
 
+HTTPV2 = wireprototypes.HTTPV2
 SSHV1 = wireprototypes.SSHV1
 SSHV2 = wireprototypes.SSHV2
 
@@ -214,6 +215,75 @@
 
     return True
 
+def handlewsgiapirequest(rctx, req, res, checkperm):
+    """Handle requests to /api/*."""
+    assert req.dispatchparts[0] == b'api'
+
+    repo = rctx.repo
+
+    # This whole URL space is experimental for now. But we want to
+    # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
+    if not repo.ui.configbool('experimental', 'web.apiserver'):
+        res.status = b'404 Not Found'
+        res.headers[b'Content-Type'] = b'text/plain'
+        res.setbodybytes(_('Experimental API server endpoint not enabled'))
+        return
+
+    # The URL space is /api/<protocol>/*. The structure of URLs under varies
+    # by <protocol>.
+
+    # Registered APIs are made available via config options of the name of
+    # the protocol.
+    availableapis = set()
+    for k, v in API_HANDLERS.items():
+        section, option = v['config']
+        if repo.ui.configbool(section, option):
+            availableapis.add(k)
+
+    # Requests to /api/ list available APIs.
+    if req.dispatchparts == [b'api']:
+        res.status = b'200 OK'
+        res.headers[b'Content-Type'] = b'text/plain'
+        lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
+                   'one of the following:\n')]
+        if availableapis:
+            lines.extend(sorted(availableapis))
+        else:
+            lines.append(_('(no available APIs)\n'))
+        res.setbodybytes(b'\n'.join(lines))
+        return
+
+    proto = req.dispatchparts[1]
+
+    if proto not in API_HANDLERS:
+        res.status = b'404 Not Found'
+        res.headers[b'Content-Type'] = b'text/plain'
+        res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
+            proto, b', '.join(sorted(availableapis))))
+        return
+
+    if proto not in availableapis:
+        res.status = b'404 Not Found'
+        res.headers[b'Content-Type'] = b'text/plain'
+        res.setbodybytes(_('API %s not enabled\n') % proto)
+        return
+
+    API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
+                                   req.dispatchparts[2:])
+
+def _handlehttpv2request(rctx, req, res, checkperm, urlparts):
+    res.status = b'200 OK'
+    res.headers[b'Content-Type'] = b'text/plain'
+    res.setbodybytes(b'/'.join(urlparts) + b'\n')
+
+# Maps API name to metadata so custom API can be registered.
+API_HANDLERS = {
+    HTTPV2: {
+        'config': ('experimental', 'web.api.http-v2'),
+        'handler': _handlehttpv2request,
+    },
+}
+
 def _httpresponsetype(ui, req, prefer_uncompressed):
     """Determine the appropriate response type and compression settings.
 
diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -320,6 +320,13 @@
             # replace it.
             res.headers['Content-Security-Policy'] = rctx.csp
 
+        # /api/* is reserved for various API implementations. Dispatch
+        # accordingly.
+        if req.dispatchparts and req.dispatchparts[0] == b'api':
+            wireprotoserver.handlewsgiapirequest(rctx, req, res,
+                                                 self.check_perm)
+            return res.sendresponse()
+
         handled = wireprotoserver.handlewsgirequest(
             rctx, req, res, self.check_perm)
         if handled:
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -580,6 +580,12 @@
 coreconfigitem('experimental', 'sshpeer.advertise-v2',
     default=False,
 )
+coreconfigitem('experimental', 'web.apiserver',
+    default=False,
+)
+coreconfigitem('experimental', 'web.api.http-v2',
+    default=False,
+)
 coreconfigitem('experimental', 'xdiff',
     default=False,
 )



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


More information about the Mercurial-devel mailing list