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