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, ) 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/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//*. The structure of URLs under varies + # by . + + # 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/, where 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/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/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/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/, where 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/, where 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/, where 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/, where can be one of the following:\n + s> \n + s> exp-http-v2-0001