diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -577,6 +577,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'/') + +# 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,38 @@ + $ 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 + + $ get-with-headers.py $LOCALIP:$HGPORT api/exp-http-v2-0001 - + 404 Not Found + content-length: 33 + content-type: text/plain + + API exp-http-v2-0001 not enabled + [1] + +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) + + $ get-with-headers.py $LOCALIP:$HGPORT api/exp-http-v2-0001/path1/path2 - + 200 OK + content-length: 12 + content-type: text/plain + + path1/path2/ (no-eol) 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,105 @@ + $ 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 + + $ get-with-headers.py $LOCALIP:$HGPORT api - + 404 Not Found + content-length: 44 + content-type: text/plain + + Experimental API server endpoint not enabled (no-eol) + [1] + + $ get-with-headers.py $LOCALIP:$HGPORT api/ - + 404 Not Found + content-length: 44 + content-type: text/plain + + Experimental API server endpoint not enabled (no-eol) + [1] + +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) + + $ get-with-headers.py $LOCALIP:$HGPORT api - + 200 OK + content-length: 100 + content-type: text/plain + + APIs can be accessed at /api/, where can be one of the following: + + (no available APIs) + + $ get-with-headers.py $LOCALIP:$HGPORT api/ - + 200 OK + content-length: 100 + content-type: text/plain + + APIs can be accessed at /api/, where can be one of the following: + + (no available APIs) + +Accessing an unknown API yields a 404 + + $ get-with-headers.py $LOCALIP:$HGPORT api/unknown - + 404 Not Found + content-length: 33 + content-type: text/plain + + Unknown API: unknown + Known APIs: (no-eol) + [1] + +Accessing a known but not enabled API yields a different error + + $ get-with-headers.py $LOCALIP:$HGPORT api/exp-http-v2-0001 - + 404 Not Found + content-length: 33 + content-type: text/plain + + API exp-http-v2-0001 not enabled + [1] + +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 + + $ get-with-headers.py $LOCALIP:$HGPORT api - + 200 OK + content-length: 96 + content-type: text/plain + + APIs can be accessed at /api/, where can be one of the following: + + exp-http-v2-0001 (no-eol) + + $ get-with-headers.py $LOCALIP:$HGPORT api/ - + 200 OK + content-length: 96 + content-type: text/plain + + APIs can be accessed at /api/, where can be one of the following: + + exp-http-v2-0001 (no-eol)