mercurial/wireprotoserver.py
changeset 37557 734515aca84d
parent 37545 93397c4633f6
child 37590 9170df9106a8
equal deleted inserted replaced
37556:b77aa48ba690 37557:734515aca84d
    10 import struct
    10 import struct
    11 import sys
    11 import sys
    12 import threading
    12 import threading
    13 
    13 
    14 from .i18n import _
    14 from .i18n import _
       
    15 from .thirdparty import (
       
    16     cbor,
       
    17 )
    15 from .thirdparty.zope import (
    18 from .thirdparty.zope import (
    16     interface as zi,
    19     interface as zi,
    17 )
    20 )
    18 from . import (
    21 from . import (
    19     encoding,
    22     encoding,
   228         # "unbundle." That assumption is not always valid.
   231         # "unbundle." That assumption is not always valid.
   229         res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
   232         res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
   230 
   233 
   231     return True
   234     return True
   232 
   235 
       
   236 def _availableapis(repo):
       
   237     apis = set()
       
   238 
       
   239     # Registered APIs are made available via config options of the name of
       
   240     # the protocol.
       
   241     for k, v in API_HANDLERS.items():
       
   242         section, option = v['config']
       
   243         if repo.ui.configbool(section, option):
       
   244             apis.add(k)
       
   245 
       
   246     return apis
       
   247 
   233 def handlewsgiapirequest(rctx, req, res, checkperm):
   248 def handlewsgiapirequest(rctx, req, res, checkperm):
   234     """Handle requests to /api/*."""
   249     """Handle requests to /api/*."""
   235     assert req.dispatchparts[0] == b'api'
   250     assert req.dispatchparts[0] == b'api'
   236 
   251 
   237     repo = rctx.repo
   252     repo = rctx.repo
   245         return
   260         return
   246 
   261 
   247     # The URL space is /api/<protocol>/*. The structure of URLs under varies
   262     # The URL space is /api/<protocol>/*. The structure of URLs under varies
   248     # by <protocol>.
   263     # by <protocol>.
   249 
   264 
   250     # Registered APIs are made available via config options of the name of
   265     availableapis = _availableapis(repo)
   251     # the protocol.
       
   252     availableapis = set()
       
   253     for k, v in API_HANDLERS.items():
       
   254         section, option = v['config']
       
   255         if repo.ui.configbool(section, option):
       
   256             availableapis.add(k)
       
   257 
   266 
   258     # Requests to /api/ list available APIs.
   267     # Requests to /api/ list available APIs.
   259     if req.dispatchparts == [b'api']:
   268     if req.dispatchparts == [b'api']:
   260         res.status = b'200 OK'
   269         res.status = b'200 OK'
   261         res.headers[b'Content-Type'] = b'text/plain'
   270         res.headers[b'Content-Type'] = b'text/plain'
   285 
   294 
   286     API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
   295     API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
   287                                    req.dispatchparts[2:])
   296                                    req.dispatchparts[2:])
   288 
   297 
   289 # Maps API name to metadata so custom API can be registered.
   298 # Maps API name to metadata so custom API can be registered.
       
   299 # Keys are:
       
   300 #
       
   301 # config
       
   302 #    Config option that controls whether service is enabled.
       
   303 # handler
       
   304 #    Callable receiving (rctx, req, res, checkperm, urlparts) that is called
       
   305 #    when a request to this API is received.
       
   306 # apidescriptor
       
   307 #    Callable receiving (req, repo) that is called to obtain an API
       
   308 #    descriptor for this service. The response must be serializable to CBOR.
   290 API_HANDLERS = {
   309 API_HANDLERS = {
   291     wireprotov2server.HTTPV2: {
   310     wireprotov2server.HTTPV2: {
   292         'config': ('experimental', 'web.api.http-v2'),
   311         'config': ('experimental', 'web.api.http-v2'),
   293         'handler': wireprotov2server.handlehttpv2request,
   312         'handler': wireprotov2server.handlehttpv2request,
       
   313         'apidescriptor': wireprotov2server.httpv2apidescriptor,
   294     },
   314     },
   295 }
   315 }
   296 
   316 
   297 def _httpresponsetype(ui, proto, prefer_uncompressed):
   317 def _httpresponsetype(ui, proto, prefer_uncompressed):
   298     """Determine the appropriate response type and compression settings.
   318     """Determine the appropriate response type and compression settings.
   325     # setting a very high compression level could lead to flooding
   345     # setting a very high compression level could lead to flooding
   326     # the server's network or CPU.
   346     # the server's network or CPU.
   327     opts = {'level': ui.configint('server', 'zliblevel')}
   347     opts = {'level': ui.configint('server', 'zliblevel')}
   328     return HGTYPE, util.compengines['zlib'], opts
   348     return HGTYPE, util.compengines['zlib'], opts
   329 
   349 
       
   350 def processcapabilitieshandshake(repo, req, res, proto):
       
   351     """Called during a ?cmd=capabilities request.
       
   352 
       
   353     If the client is advertising support for a newer protocol, we send
       
   354     a CBOR response with information about available services. If no
       
   355     advertised services are available, we don't handle the request.
       
   356     """
       
   357     # Fall back to old behavior unless the API server is enabled.
       
   358     if not repo.ui.configbool('experimental', 'web.apiserver'):
       
   359         return False
       
   360 
       
   361     clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
       
   362     protocaps = decodevaluefromheaders(req, b'X-HgProto')
       
   363     if not clientapis or not protocaps:
       
   364         return False
       
   365 
       
   366     # We currently only support CBOR responses.
       
   367     protocaps = set(protocaps.split(' '))
       
   368     if b'cbor' not in protocaps:
       
   369         return False
       
   370 
       
   371     descriptors = {}
       
   372 
       
   373     for api in sorted(set(clientapis.split()) & _availableapis(repo)):
       
   374         handler = API_HANDLERS[api]
       
   375 
       
   376         descriptorfn = handler.get('apidescriptor')
       
   377         if not descriptorfn:
       
   378             continue
       
   379 
       
   380         descriptors[api] = descriptorfn(req, repo)
       
   381 
       
   382     v1caps = wireproto.dispatch(repo, proto, 'capabilities')
       
   383     assert isinstance(v1caps, wireprototypes.bytesresponse)
       
   384 
       
   385     m = {
       
   386         # TODO allow this to be configurable.
       
   387         'apibase': 'api/',
       
   388         'apis': descriptors,
       
   389         'v1capabilities': v1caps.data,
       
   390     }
       
   391 
       
   392     res.status = b'200 OK'
       
   393     res.headers[b'Content-Type'] = b'application/mercurial-cbor'
       
   394     res.setbodybytes(cbor.dumps(m, canonical=True))
       
   395 
       
   396     return True
       
   397 
   330 def _callhttp(repo, req, res, proto, cmd):
   398 def _callhttp(repo, req, res, proto, cmd):
   331     # Avoid cycle involving hg module.
   399     # Avoid cycle involving hg module.
   332     from .hgweb import common as hgwebcommon
   400     from .hgweb import common as hgwebcommon
   333 
   401 
   334     def genversion2(gen, engine, engineopts):
   402     def genversion2(gen, engine, engineopts):
   360                     _('requested wire protocol command is not available over '
   428                     _('requested wire protocol command is not available over '
   361                       'HTTP'))
   429                       'HTTP'))
   362         return
   430         return
   363 
   431 
   364     proto.checkperm(wireproto.commands[cmd].permission)
   432     proto.checkperm(wireproto.commands[cmd].permission)
       
   433 
       
   434     # Possibly handle a modern client wanting to switch protocols.
       
   435     if (cmd == 'capabilities' and
       
   436         processcapabilitieshandshake(repo, req, res, proto)):
       
   437 
       
   438         return
   365 
   439 
   366     rsp = wireproto.dispatch(repo, proto, cmd)
   440     rsp = wireproto.dispatch(repo, proto, cmd)
   367 
   441 
   368     if isinstance(rsp, bytes):
   442     if isinstance(rsp, bytes):
   369         setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
   443         setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)