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