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

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
11 messages Options
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  I will soon be introducing a new version of the HTTP wire protocol.
  One of the things I want to change with it is the URL routing.
  I want to rely on URL paths to define endpoints rather than the
  "cmd" query string argument. That should be pretty straightforward.
 
  I was thinking about what URL space to reserve for the new protocol.
  We /could/ put everything at a top-level path. e.g.
  /wireproto/* or /http-v2-wireproto/*. However, these constrain us
  a bit because they assume there will only be 1 API: version 2 of
  the HTTP wire protocol. I think there is room to grow multiple
  APIs. For example, there may someday be a proper JSON API to query
  or even manipulate the repository. And I don't think we should have
  to create a new top-level URL space for each API nor should we
  attempt to shoehorn each future API into the same shared URL space:
  that would just be too chaotic.
 
  This commits reserves the /api/* URL space for all our future API
  needs. Essentially, all requests to /api/* get routed to a new WSGI
  handler. By default, it 404's the entire URL space unless the
  "api server" feature is enabled. When enabled, requests to "/api"
  list available APIs. URLs of the form /api/<name>/* are reserved for
  a particular named API. Behavior within each API is left up to that
  API. So, we can grow new APIs easily without worrying about URL
  space conflicts.
 
  APIs can be registered by adding entries to a global dict. This allows
  extensions to provide their own APIs should they choose to do so.
  This is probably a premature feature. But IMO the code is easier
  to read if we're not dealing with API-specific behavior like config
  option querying inline.
 
  To prove it works, we implement a very basic API for version 2
  of the HTTP wire protocol. It does nothing of value except
  facilitate testing of the /api/* URL space.
 
  We currently emit plain text responses for all /api/* endpoints.
  There's definitely room to look at Accept and other request headers
  to vary the response format. But we have to start somewhere.

REPOSITORY
  rHG Mercurial

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,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/<name>, where <name> 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/<name>, where <name> 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/<name>, where <name> 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/<name>, where <name> can be one of the following:
+  
+  exp-http-v2-0001 (no-eol)
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/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'/')
+
+# 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
@@ -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,
 )



To: indygreg, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
mharbison72 added a comment.


  Will this cause conflicts with `hg serve -S` or hgwebdir with subrepo/virtual paths that start with 'api'?  (This was the reason I used the LFS URI starting with '.hg/', knowing that isn't committable.)

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
indygreg added a comment.


  In https://phab.mercurial-scm.org/D2834#45666, @mharbison72 wrote:
 
  > Will this cause conflicts with `hg serve -S` or hgwebdir with subrepo/virtual paths that start with 'api'?  (This was the reason I used the LFS URI starting with '.hg/', knowing that isn't committable.)
 
 
  Bleh.
 
  So what I'm hearing is that because of hgwebdir's virtual paths and support for serving subrepos, pretty much the entire URL space is dangerous because it can conflict with the path of a virtual repo or subrepo. I suppose we already have problems with existing URLs, like `/rev/`.
 
  I agree that putting things under `.hg/` in the URL space seems to be a viable workaround.
 
  Changing the URL prefix isn't a big deal as far as the code is concerned. We can make that change at any time since we have no BC guarantees at this juncture.

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
mharbison72 added a comment.


  In https://phab.mercurial-scm.org/D2834#45696, @indygreg wrote:
 
  > In https://phab.mercurial-scm.org/D2834#45666, @mharbison72 wrote:
  >
  > > Will this cause conflicts with `hg serve -S` or hgwebdir with subrepo/virtual paths that start with 'api'?  (This was the reason I used the LFS URI starting with '.hg/', knowing that isn't committable.)
  >
  >
  > Bleh.
  >
  > So what I'm hearing is that because of hgwebdir's virtual paths and support for serving subrepos, pretty much the entire URL space is dangerous because it can conflict with the path of a virtual repo or subrepo. I suppose we already have problems with existing URLs, like `/rev/`.
 
 
  I think so.  I remember when Yuya suggested putting the `/index` virtual file into hgwebdir a year ago, I had to dance around the fact that 'index' could have been a repo being served up.   I'm guessing `/rev/` et al. is less of a problem, because presumably they've existed since the beginning?
 
  > I agree that putting things under `.hg/` in the URL space seems to be a viable workaround.
  >
  > Changing the URL prefix isn't a big deal as far as the code is concerned. We can make that change at any time since we have no BC guarantees at this juncture.
 
  I'm fine with landing this as-is and changing it later if needed.  I haven't checked this myself, I was just going on a vague memory of issues that I ran into, and wondered if you had considered it.

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
indygreg updated this revision to Diff 6998.

REPOSITORY
  rHG Mercurial

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

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 <a href="http://$LOCALIP:$HGPORT/">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 <a href="http://$LOCALIP:$HGPORT/">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/
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'/')
+
+# 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
@@ -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,
 )



To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
indygreg updated this revision to Diff 7009.

REPOSITORY
  rHG Mercurial

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

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 <a href="http://$LOCALIP:$HGPORT/">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 <a href="http://$LOCALIP:$HGPORT/">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
@@ -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,
 )



To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
indygreg added a comment.


  So, assuming we can't introduce new top-level URLs without risking conflicts with subrepos or hgwebdir virtual repos, for at least the wire protocol, assuming we keep the `?cmd=capabilities` mechanism for the initial server handshake, we can add something that defines where API requests should be routed. We can then allow server operators to reconfigure the base URL for these requests.

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers
Cc: mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
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 <a href="http://$LOCALIP:$HGPORT/">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 <a href="http://$LOCALIP:$HGPORT/">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
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG1cfef5693203: wireproto: support /api/* URL space for exposing APIs (authored by indygreg, committed by ).

REPOSITORY
  rHG Mercurial

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

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 <a href="http://$LOCALIP:$HGPORT/">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 <a href="http://$LOCALIP:$HGPORT/">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
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
yuja added a comment.


  Can we expect the BC of hgwebdir will be fixed before the release?
 
  I like the `/api/` namespace, but breaking existing hgweb instances
  wouldn't be acceptable.

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers, durin42
Cc: yuja, mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Reply | Threaded
Open this post in threaded view
|

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

indygreg (Gregory Szorc)
In reply to this post by indygreg (Gregory Szorc)
indygreg added a comment.


  In https://phab.mercurial-scm.org/D2834#47334, @yuja wrote:
 
  > Can we expect the BC of hgwebdir will be fixed before the release?
  >
  > I like the `/api/` namespace, but breaking existing hgweb instances
  >  wouldn't be acceptable.
 
 
  https://phab.mercurial-scm.org/D2936

REPOSITORY
  rHG Mercurial

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

To: indygreg, #hg-reviewers, durin42
Cc: yuja, mharbison72, mercurial-devel
_______________________________________________
Mercurial-devel mailing list
[hidden email]
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel