mercurial/wireprotoserver.py
changeset 36804 b9b968e21f78
parent 36801 66de4555cefd
child 36810 886fba199022
equal deleted inserted replaced
36803:8e1556ac01bb 36804:b9b968e21f78
    34 HGERRTYPE = 'application/hg-error'
    34 HGERRTYPE = 'application/hg-error'
    35 
    35 
    36 SSHV1 = wireprototypes.SSHV1
    36 SSHV1 = wireprototypes.SSHV1
    37 SSHV2 = wireprototypes.SSHV2
    37 SSHV2 = wireprototypes.SSHV2
    38 
    38 
    39 def decodevaluefromheaders(req, headerprefix):
    39 def decodevaluefromheaders(wsgireq, headerprefix):
    40     """Decode a long value from multiple HTTP request headers.
    40     """Decode a long value from multiple HTTP request headers.
    41 
    41 
    42     Returns the value as a bytes, not a str.
    42     Returns the value as a bytes, not a str.
    43     """
    43     """
    44     chunks = []
    44     chunks = []
    45     i = 1
    45     i = 1
    46     prefix = headerprefix.upper().replace(r'-', r'_')
    46     prefix = headerprefix.upper().replace(r'-', r'_')
    47     while True:
    47     while True:
    48         v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
    48         v = wsgireq.env.get(r'HTTP_%s_%d' % (prefix, i))
    49         if v is None:
    49         if v is None:
    50             break
    50             break
    51         chunks.append(pycompat.bytesurl(v))
    51         chunks.append(pycompat.bytesurl(v))
    52         i += 1
    52         i += 1
    53 
    53 
    54     return ''.join(chunks)
    54     return ''.join(chunks)
    55 
    55 
    56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
    56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
    57     def __init__(self, req, ui, checkperm):
    57     def __init__(self, wsgireq, ui, checkperm):
    58         self._req = req
    58         self._wsgireq = wsgireq
    59         self._ui = ui
    59         self._ui = ui
    60         self._checkperm = checkperm
    60         self._checkperm = checkperm
    61 
    61 
    62     @property
    62     @property
    63     def name(self):
    63     def name(self):
    77             else:
    77             else:
    78                 data[k] = knownargs[k][0]
    78                 data[k] = knownargs[k][0]
    79         return [data[k] for k in keys]
    79         return [data[k] for k in keys]
    80 
    80 
    81     def _args(self):
    81     def _args(self):
    82         args = util.rapply(pycompat.bytesurl, self._req.form.copy())
    82         args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
    83         postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
    83         postlen = int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0))
    84         if postlen:
    84         if postlen:
    85             args.update(urlreq.parseqs(
    85             args.update(urlreq.parseqs(
    86                 self._req.read(postlen), keep_blank_values=True))
    86                 self._wsgireq.read(postlen), keep_blank_values=True))
    87             return args
    87             return args
    88 
    88 
    89         argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
    89         argvalue = decodevaluefromheaders(self._wsgireq, r'X-HgArg')
    90         args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
    90         args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
    91         return args
    91         return args
    92 
    92 
    93     def forwardpayload(self, fp):
    93     def forwardpayload(self, fp):
    94         if r'HTTP_CONTENT_LENGTH' in self._req.env:
    94         if r'HTTP_CONTENT_LENGTH' in self._wsgireq.env:
    95             length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
    95             length = int(self._wsgireq.env[r'HTTP_CONTENT_LENGTH'])
    96         else:
    96         else:
    97             length = int(self._req.env[r'CONTENT_LENGTH'])
    97             length = int(self._wsgireq.env[r'CONTENT_LENGTH'])
    98         # If httppostargs is used, we need to read Content-Length
    98         # If httppostargs is used, we need to read Content-Length
    99         # minus the amount that was consumed by args.
    99         # minus the amount that was consumed by args.
   100         length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
   100         length -= int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0))
   101         for s in util.filechunkiter(self._req, limit=length):
   101         for s in util.filechunkiter(self._wsgireq, limit=length):
   102             fp.write(s)
   102             fp.write(s)
   103 
   103 
   104     @contextlib.contextmanager
   104     @contextlib.contextmanager
   105     def mayberedirectstdio(self):
   105     def mayberedirectstdio(self):
   106         oldout = self._ui.fout
   106         oldout = self._ui.fout
   116             self._ui.fout = oldout
   116             self._ui.fout = oldout
   117             self._ui.ferr = olderr
   117             self._ui.ferr = olderr
   118 
   118 
   119     def client(self):
   119     def client(self):
   120         return 'remote:%s:%s:%s' % (
   120         return 'remote:%s:%s:%s' % (
   121             self._req.env.get('wsgi.url_scheme') or 'http',
   121             self._wsgireq.env.get('wsgi.url_scheme') or 'http',
   122             urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
   122             urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
   123             urlreq.quote(self._req.env.get('REMOTE_USER', '')))
   123             urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
   124 
   124 
   125     def addcapabilities(self, repo, caps):
   125     def addcapabilities(self, repo, caps):
   126         caps.append('httpheader=%d' %
   126         caps.append('httpheader=%d' %
   127                     repo.ui.configint('server', 'maxhttpheaderlen'))
   127                     repo.ui.configint('server', 'maxhttpheaderlen'))
   128         if repo.ui.configbool('experimental', 'httppostargs'):
   128         if repo.ui.configbool('experimental', 'httppostargs'):
   148 # there are no other known users, so with any luck we can discard this
   148 # there are no other known users, so with any luck we can discard this
   149 # hook if remotefilelog becomes a first-party extension.
   149 # hook if remotefilelog becomes a first-party extension.
   150 def iscmd(cmd):
   150 def iscmd(cmd):
   151     return cmd in wireproto.commands
   151     return cmd in wireproto.commands
   152 
   152 
   153 def parsehttprequest(rctx, req, query, checkperm):
   153 def parsehttprequest(rctx, wsgireq, query, checkperm):
   154     """Parse the HTTP request for a wire protocol request.
   154     """Parse the HTTP request for a wire protocol request.
   155 
   155 
   156     If the current request appears to be a wire protocol request, this
   156     If the current request appears to be a wire protocol request, this
   157     function returns a dict with details about that request, including
   157     function returns a dict with details about that request, including
   158     an ``abstractprotocolserver`` instance suitable for handling the
   158     an ``abstractprotocolserver`` instance suitable for handling the
   159     request. Otherwise, ``None`` is returned.
   159     request. Otherwise, ``None`` is returned.
   160 
   160 
   161     ``req`` is a ``wsgirequest`` instance.
   161     ``wsgireq`` is a ``wsgirequest`` instance.
   162     """
   162     """
   163     repo = rctx.repo
   163     repo = rctx.repo
   164 
   164 
   165     # HTTP version 1 wire protocol requests are denoted by a "cmd" query
   165     # HTTP version 1 wire protocol requests are denoted by a "cmd" query
   166     # string parameter. If it isn't present, this isn't a wire protocol
   166     # string parameter. If it isn't present, this isn't a wire protocol
   167     # request.
   167     # request.
   168     if 'cmd' not in req.form:
   168     if 'cmd' not in wsgireq.form:
   169         return None
   169         return None
   170 
   170 
   171     cmd = req.form['cmd'][0]
   171     cmd = wsgireq.form['cmd'][0]
   172 
   172 
   173     # The "cmd" request parameter is used by both the wire protocol and hgweb.
   173     # The "cmd" request parameter is used by both the wire protocol and hgweb.
   174     # While not all wire protocol commands are available for all transports,
   174     # While not all wire protocol commands are available for all transports,
   175     # if we see a "cmd" value that resembles a known wire protocol command, we
   175     # if we see a "cmd" value that resembles a known wire protocol command, we
   176     # route it to a protocol handler. This is better than routing possible
   176     # route it to a protocol handler. This is better than routing possible
   178     # known wire protocol commands and it is less confusing for machine
   178     # known wire protocol commands and it is less confusing for machine
   179     # clients.
   179     # clients.
   180     if not iscmd(cmd):
   180     if not iscmd(cmd):
   181         return None
   181         return None
   182 
   182 
   183     proto = httpv1protocolhandler(req, repo.ui,
   183     proto = httpv1protocolhandler(wsgireq, repo.ui,
   184                                   lambda perm: checkperm(rctx, req, perm))
   184                                   lambda perm: checkperm(rctx, wsgireq, perm))
   185 
   185 
   186     return {
   186     return {
   187         'cmd': cmd,
   187         'cmd': cmd,
   188         'proto': proto,
   188         'proto': proto,
   189         'dispatch': lambda: _callhttp(repo, req, proto, cmd),
   189         'dispatch': lambda: _callhttp(repo, wsgireq, proto, cmd),
   190         'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
   190         'handleerror': lambda ex: _handlehttperror(ex, wsgireq, cmd),
   191     }
   191     }
   192 
   192 
   193 def _httpresponsetype(ui, req, prefer_uncompressed):
   193 def _httpresponsetype(ui, wsgireq, prefer_uncompressed):
   194     """Determine the appropriate response type and compression settings.
   194     """Determine the appropriate response type and compression settings.
   195 
   195 
   196     Returns a tuple of (mediatype, compengine, engineopts).
   196     Returns a tuple of (mediatype, compengine, engineopts).
   197     """
   197     """
   198     # Determine the response media type and compression engine based
   198     # Determine the response media type and compression engine based
   199     # on the request parameters.
   199     # on the request parameters.
   200     protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
   200     protocaps = decodevaluefromheaders(wsgireq, r'X-HgProto').split(' ')
   201 
   201 
   202     if '0.2' in protocaps:
   202     if '0.2' in protocaps:
   203         # All clients are expected to support uncompressed data.
   203         # All clients are expected to support uncompressed data.
   204         if prefer_uncompressed:
   204         if prefer_uncompressed:
   205             return HGTYPE2, util._noopengine(), {}
   205             return HGTYPE2, util._noopengine(), {}
   228     # setting a very high compression level could lead to flooding
   228     # setting a very high compression level could lead to flooding
   229     # the server's network or CPU.
   229     # the server's network or CPU.
   230     opts = {'level': ui.configint('server', 'zliblevel')}
   230     opts = {'level': ui.configint('server', 'zliblevel')}
   231     return HGTYPE, util.compengines['zlib'], opts
   231     return HGTYPE, util.compengines['zlib'], opts
   232 
   232 
   233 def _callhttp(repo, req, proto, cmd):
   233 def _callhttp(repo, wsgireq, proto, cmd):
   234     def genversion2(gen, engine, engineopts):
   234     def genversion2(gen, engine, engineopts):
   235         # application/mercurial-0.2 always sends a payload header
   235         # application/mercurial-0.2 always sends a payload header
   236         # identifying the compression engine.
   236         # identifying the compression engine.
   237         name = engine.wireprotosupport().name
   237         name = engine.wireprotosupport().name
   238         assert 0 < len(name) < 256
   238         assert 0 < len(name) < 256
   241 
   241 
   242         for chunk in gen:
   242         for chunk in gen:
   243             yield chunk
   243             yield chunk
   244 
   244 
   245     if not wireproto.commands.commandavailable(cmd, proto):
   245     if not wireproto.commands.commandavailable(cmd, proto):
   246         req.respond(HTTP_OK, HGERRTYPE,
   246         wsgireq.respond(HTTP_OK, HGERRTYPE,
   247                     body=_('requested wire protocol command is not available '
   247                         body=_('requested wire protocol command is not '
   248                            'over HTTP'))
   248                                'available over HTTP'))
   249         return []
   249         return []
   250 
   250 
   251     proto.checkperm(wireproto.commands[cmd].permission)
   251     proto.checkperm(wireproto.commands[cmd].permission)
   252 
   252 
   253     rsp = wireproto.dispatch(repo, proto, cmd)
   253     rsp = wireproto.dispatch(repo, proto, cmd)
   254 
   254 
   255     if isinstance(rsp, bytes):
   255     if isinstance(rsp, bytes):
   256         req.respond(HTTP_OK, HGTYPE, body=rsp)
   256         wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
   257         return []
   257         return []
   258     elif isinstance(rsp, wireprototypes.bytesresponse):
   258     elif isinstance(rsp, wireprototypes.bytesresponse):
   259         req.respond(HTTP_OK, HGTYPE, body=rsp.data)
   259         wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
   260         return []
   260         return []
   261     elif isinstance(rsp, wireprototypes.streamreslegacy):
   261     elif isinstance(rsp, wireprototypes.streamreslegacy):
   262         gen = rsp.gen
   262         gen = rsp.gen
   263         req.respond(HTTP_OK, HGTYPE)
   263         wsgireq.respond(HTTP_OK, HGTYPE)
   264         return gen
   264         return gen
   265     elif isinstance(rsp, wireprototypes.streamres):
   265     elif isinstance(rsp, wireprototypes.streamres):
   266         gen = rsp.gen
   266         gen = rsp.gen
   267 
   267 
   268         # This code for compression should not be streamres specific. It
   268         # This code for compression should not be streamres specific. It
   269         # is here because we only compress streamres at the moment.
   269         # is here because we only compress streamres at the moment.
   270         mediatype, engine, engineopts = _httpresponsetype(
   270         mediatype, engine, engineopts = _httpresponsetype(
   271             repo.ui, req, rsp.prefer_uncompressed)
   271             repo.ui, wsgireq, rsp.prefer_uncompressed)
   272         gen = engine.compressstream(gen, engineopts)
   272         gen = engine.compressstream(gen, engineopts)
   273 
   273 
   274         if mediatype == HGTYPE2:
   274         if mediatype == HGTYPE2:
   275             gen = genversion2(gen, engine, engineopts)
   275             gen = genversion2(gen, engine, engineopts)
   276 
   276 
   277         req.respond(HTTP_OK, mediatype)
   277         wsgireq.respond(HTTP_OK, mediatype)
   278         return gen
   278         return gen
   279     elif isinstance(rsp, wireprototypes.pushres):
   279     elif isinstance(rsp, wireprototypes.pushres):
   280         rsp = '%d\n%s' % (rsp.res, rsp.output)
   280         rsp = '%d\n%s' % (rsp.res, rsp.output)
   281         req.respond(HTTP_OK, HGTYPE, body=rsp)
   281         wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
   282         return []
   282         return []
   283     elif isinstance(rsp, wireprototypes.pusherr):
   283     elif isinstance(rsp, wireprototypes.pusherr):
   284         # This is the httplib workaround documented in _handlehttperror().
   284         # This is the httplib workaround documented in _handlehttperror().
   285         req.drain()
   285         wsgireq.drain()
   286 
   286 
   287         rsp = '0\n%s\n' % rsp.res
   287         rsp = '0\n%s\n' % rsp.res
   288         req.respond(HTTP_OK, HGTYPE, body=rsp)
   288         wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
   289         return []
   289         return []
   290     elif isinstance(rsp, wireprototypes.ooberror):
   290     elif isinstance(rsp, wireprototypes.ooberror):
   291         rsp = rsp.message
   291         rsp = rsp.message
   292         req.respond(HTTP_OK, HGERRTYPE, body=rsp)
   292         wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
   293         return []
   293         return []
   294     raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
   294     raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
   295 
   295 
   296 def _handlehttperror(e, req, cmd):
   296 def _handlehttperror(e, wsgireq, cmd):
   297     """Called when an ErrorResponse is raised during HTTP request processing."""
   297     """Called when an ErrorResponse is raised during HTTP request processing."""
   298 
   298 
   299     # Clients using Python's httplib are stateful: the HTTP client
   299     # Clients using Python's httplib are stateful: the HTTP client
   300     # won't process an HTTP response until all request data is
   300     # won't process an HTTP response until all request data is
   301     # sent to the server. The intent of this code is to ensure
   301     # sent to the server. The intent of this code is to ensure
   302     # we always read HTTP request data from the client, thus
   302     # we always read HTTP request data from the client, thus
   303     # ensuring httplib transitions to a state that allows it to read
   303     # ensuring httplib transitions to a state that allows it to read
   304     # the HTTP response. In other words, it helps prevent deadlocks
   304     # the HTTP response. In other words, it helps prevent deadlocks
   305     # on clients using httplib.
   305     # on clients using httplib.
   306 
   306 
   307     if (req.env[r'REQUEST_METHOD'] == r'POST' and
   307     if (wsgireq.env[r'REQUEST_METHOD'] == r'POST' and
   308         # But not if Expect: 100-continue is being used.
   308         # But not if Expect: 100-continue is being used.
   309         (req.env.get('HTTP_EXPECT',
   309         (wsgireq.env.get('HTTP_EXPECT',
   310                      '').lower() != '100-continue') or
   310                          '').lower() != '100-continue') or
   311         # Or the non-httplib HTTP library is being advertised by
   311         # Or the non-httplib HTTP library is being advertised by
   312         # the client.
   312         # the client.
   313         req.env.get('X-HgHttp2', '')):
   313         wsgireq.env.get('X-HgHttp2', '')):
   314         req.drain()
   314         wsgireq.drain()
   315     else:
   315     else:
   316         req.headers.append((r'Connection', r'Close'))
   316         wsgireq.headers.append((r'Connection', r'Close'))
   317 
   317 
   318     # TODO This response body assumes the failed command was
   318     # TODO This response body assumes the failed command was
   319     # "unbundle." That assumption is not always valid.
   319     # "unbundle." That assumption is not always valid.
   320     req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
   320     wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
   321 
   321 
   322     return ''
   322     return ''
   323 
   323 
   324 def _sshv1respondbytes(fout, value):
   324 def _sshv1respondbytes(fout, value):
   325     """Send a bytes response for protocol version 1."""
   325     """Send a bytes response for protocol version 1."""