mercurial/wireprotoserver.py
changeset 35856 ef3a24a023ec
parent 35750 a39a9df7ecca
child 35857 a42455b3dbf8
equal deleted inserted replaced
35855:69d7fcd91696 35856:ef3a24a023ec
       
     1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
       
     2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
       
     3 #
       
     4 # This software may be used and distributed according to the terms of the
       
     5 # GNU General Public License version 2 or any later version.
       
     6 
       
     7 from __future__ import absolute_import
       
     8 
       
     9 import cgi
       
    10 import struct
       
    11 
       
    12 from .hgweb.common import (
       
    13     HTTP_OK,
       
    14 )
       
    15 from . import (
       
    16     error,
       
    17     pycompat,
       
    18     util,
       
    19     wireproto,
       
    20 )
       
    21 
       
    22 stringio = util.stringio
       
    23 
       
    24 urlerr = util.urlerr
       
    25 urlreq = util.urlreq
       
    26 
       
    27 HGTYPE = 'application/mercurial-0.1'
       
    28 HGTYPE2 = 'application/mercurial-0.2'
       
    29 HGERRTYPE = 'application/hg-error'
       
    30 
       
    31 def decodevaluefromheaders(req, headerprefix):
       
    32     """Decode a long value from multiple HTTP request headers.
       
    33 
       
    34     Returns the value as a bytes, not a str.
       
    35     """
       
    36     chunks = []
       
    37     i = 1
       
    38     prefix = headerprefix.upper().replace(r'-', r'_')
       
    39     while True:
       
    40         v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
       
    41         if v is None:
       
    42             break
       
    43         chunks.append(pycompat.bytesurl(v))
       
    44         i += 1
       
    45 
       
    46     return ''.join(chunks)
       
    47 
       
    48 class webproto(wireproto.abstractserverproto):
       
    49     def __init__(self, req, ui):
       
    50         self.req = req
       
    51         self.response = ''
       
    52         self.ui = ui
       
    53         self.name = 'http'
       
    54 
       
    55     def getargs(self, args):
       
    56         knownargs = self._args()
       
    57         data = {}
       
    58         keys = args.split()
       
    59         for k in keys:
       
    60             if k == '*':
       
    61                 star = {}
       
    62                 for key in knownargs.keys():
       
    63                     if key != 'cmd' and key not in keys:
       
    64                         star[key] = knownargs[key][0]
       
    65                 data['*'] = star
       
    66             else:
       
    67                 data[k] = knownargs[k][0]
       
    68         return [data[k] for k in keys]
       
    69     def _args(self):
       
    70         args = self.req.form.copy()
       
    71         if pycompat.ispy3:
       
    72             args = {k.encode('ascii'): [v.encode('ascii') for v in vs]
       
    73                     for k, vs in args.items()}
       
    74         postlen = int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0))
       
    75         if postlen:
       
    76             args.update(cgi.parse_qs(
       
    77                 self.req.read(postlen), keep_blank_values=True))
       
    78             return args
       
    79 
       
    80         argvalue = decodevaluefromheaders(self.req, r'X-HgArg')
       
    81         args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
       
    82         return args
       
    83     def getfile(self, fp):
       
    84         length = int(self.req.env[r'CONTENT_LENGTH'])
       
    85         # If httppostargs is used, we need to read Content-Length
       
    86         # minus the amount that was consumed by args.
       
    87         length -= int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0))
       
    88         for s in util.filechunkiter(self.req, limit=length):
       
    89             fp.write(s)
       
    90     def redirect(self):
       
    91         self.oldio = self.ui.fout, self.ui.ferr
       
    92         self.ui.ferr = self.ui.fout = stringio()
       
    93     def restore(self):
       
    94         val = self.ui.fout.getvalue()
       
    95         self.ui.ferr, self.ui.fout = self.oldio
       
    96         return val
       
    97 
       
    98     def _client(self):
       
    99         return 'remote:%s:%s:%s' % (
       
   100             self.req.env.get('wsgi.url_scheme') or 'http',
       
   101             urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
       
   102             urlreq.quote(self.req.env.get('REMOTE_USER', '')))
       
   103 
       
   104     def responsetype(self, prefer_uncompressed):
       
   105         """Determine the appropriate response type and compression settings.
       
   106 
       
   107         Returns a tuple of (mediatype, compengine, engineopts).
       
   108         """
       
   109         # Determine the response media type and compression engine based
       
   110         # on the request parameters.
       
   111         protocaps = decodevaluefromheaders(self.req, r'X-HgProto').split(' ')
       
   112 
       
   113         if '0.2' in protocaps:
       
   114             # All clients are expected to support uncompressed data.
       
   115             if prefer_uncompressed:
       
   116                 return HGTYPE2, util._noopengine(), {}
       
   117 
       
   118             # Default as defined by wire protocol spec.
       
   119             compformats = ['zlib', 'none']
       
   120             for cap in protocaps:
       
   121                 if cap.startswith('comp='):
       
   122                     compformats = cap[5:].split(',')
       
   123                     break
       
   124 
       
   125             # Now find an agreed upon compression format.
       
   126             for engine in wireproto.supportedcompengines(self.ui, self,
       
   127                                                          util.SERVERROLE):
       
   128                 if engine.wireprotosupport().name in compformats:
       
   129                     opts = {}
       
   130                     level = self.ui.configint('server',
       
   131                                               '%slevel' % engine.name())
       
   132                     if level is not None:
       
   133                         opts['level'] = level
       
   134 
       
   135                     return HGTYPE2, engine, opts
       
   136 
       
   137             # No mutually supported compression format. Fall back to the
       
   138             # legacy protocol.
       
   139 
       
   140         # Don't allow untrusted settings because disabling compression or
       
   141         # setting a very high compression level could lead to flooding
       
   142         # the server's network or CPU.
       
   143         opts = {'level': self.ui.configint('server', 'zliblevel')}
       
   144         return HGTYPE, util.compengines['zlib'], opts
       
   145 
       
   146 def iscmd(cmd):
       
   147     return cmd in wireproto.commands
       
   148 
       
   149 def call(repo, req, cmd):
       
   150     p = webproto(req, repo.ui)
       
   151 
       
   152     def genversion2(gen, engine, engineopts):
       
   153         # application/mercurial-0.2 always sends a payload header
       
   154         # identifying the compression engine.
       
   155         name = engine.wireprotosupport().name
       
   156         assert 0 < len(name) < 256
       
   157         yield struct.pack('B', len(name))
       
   158         yield name
       
   159 
       
   160         for chunk in gen:
       
   161             yield chunk
       
   162 
       
   163     rsp = wireproto.dispatch(repo, p, cmd)
       
   164     if isinstance(rsp, bytes):
       
   165         req.respond(HTTP_OK, HGTYPE, body=rsp)
       
   166         return []
       
   167     elif isinstance(rsp, wireproto.streamres_legacy):
       
   168         gen = rsp.gen
       
   169         req.respond(HTTP_OK, HGTYPE)
       
   170         return gen
       
   171     elif isinstance(rsp, wireproto.streamres):
       
   172         gen = rsp.gen
       
   173 
       
   174         # This code for compression should not be streamres specific. It
       
   175         # is here because we only compress streamres at the moment.
       
   176         mediatype, engine, engineopts = p.responsetype(rsp.prefer_uncompressed)
       
   177         gen = engine.compressstream(gen, engineopts)
       
   178 
       
   179         if mediatype == HGTYPE2:
       
   180             gen = genversion2(gen, engine, engineopts)
       
   181 
       
   182         req.respond(HTTP_OK, mediatype)
       
   183         return gen
       
   184     elif isinstance(rsp, wireproto.pushres):
       
   185         val = p.restore()
       
   186         rsp = '%d\n%s' % (rsp.res, val)
       
   187         req.respond(HTTP_OK, HGTYPE, body=rsp)
       
   188         return []
       
   189     elif isinstance(rsp, wireproto.pusherr):
       
   190         # drain the incoming bundle
       
   191         req.drain()
       
   192         p.restore()
       
   193         rsp = '0\n%s\n' % rsp.res
       
   194         req.respond(HTTP_OK, HGTYPE, body=rsp)
       
   195         return []
       
   196     elif isinstance(rsp, wireproto.ooberror):
       
   197         rsp = rsp.message
       
   198         req.respond(HTTP_OK, HGERRTYPE, body=rsp)
       
   199         return []
       
   200     raise error.ProgrammingError('hgweb.protocol internal failure', rsp)