protocol.py
210 lines
| 7.0 KiB
| text/x-python
|
PythonLexer
|
r5598 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r5598 | |||
|
r27046 | from __future__ import absolute_import | ||
import cgi | ||||
|
r30764 | import struct | ||
|
r27046 | |||
from .common import ( | ||||
HTTP_OK, | ||||
) | ||||
from .. import ( | ||||
|
r34510 | error, | ||
|
r34743 | pycompat, | ||
|
r27046 | util, | ||
wireproto, | ||||
) | ||||
|
r28861 | stringio = util.stringio | ||
|
r5963 | |||
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
|
r5993 | HGTYPE = 'application/mercurial-0.1' | ||
|
r30764 | HGTYPE2 = 'application/mercurial-0.2' | ||
|
r15017 | HGERRTYPE = 'application/hg-error' | ||
|
r11595 | |||
|
r30759 | def decodevaluefromheaders(req, headerprefix): | ||
|
r34745 | """Decode a long value from multiple HTTP request headers. | ||
Returns the value as a bytes, not a str. | ||||
""" | ||||
|
r30759 | chunks = [] | ||
i = 1 | ||||
|
r34745 | prefix = headerprefix.upper().replace(r'-', r'_') | ||
|
r30759 | while True: | ||
|
r34745 | v = req.env.get(r'HTTP_%s_%d' % (prefix, i)) | ||
|
r30759 | if v is None: | ||
break | ||||
|
r34745 | chunks.append(pycompat.bytesurl(v)) | ||
|
r30759 | i += 1 | ||
return ''.join(chunks) | ||||
|
r20903 | class webproto(wireproto.abstractserverproto): | ||
|
r14614 | def __init__(self, req, ui): | ||
|
r11595 | self.req = req | ||
self.response = '' | ||||
|
r14614 | self.ui = ui | ||
|
r30562 | self.name = 'http' | ||
|
r11595 | def getargs(self, args): | ||
|
r14093 | knownargs = self._args() | ||
|
r11595 | data = {} | ||
keys = args.split() | ||||
for k in keys: | ||||
if k == '*': | ||||
star = {} | ||||
|
r14093 | for key in knownargs.keys(): | ||
|
r13721 | if key != 'cmd' and key not in keys: | ||
|
r14093 | star[key] = knownargs[key][0] | ||
|
r11595 | data['*'] = star | ||
else: | ||||
|
r14093 | data[k] = knownargs[k][0] | ||
|
r11595 | return [data[k] for k in keys] | ||
|
r14093 | def _args(self): | ||
args = self.req.form.copy() | ||||
|
r34743 | if pycompat.ispy3: | ||
args = {k.encode('ascii'): [v.encode('ascii') for v in vs] | ||||
for k, vs in args.items()} | ||||
|
r34744 | postlen = int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0)) | ||
|
r28530 | if postlen: | ||
args.update(cgi.parse_qs( | ||||
self.req.read(postlen), keep_blank_values=True)) | ||||
return args | ||||
|
r30759 | |||
|
r34744 | argvalue = decodevaluefromheaders(self.req, r'X-HgArg') | ||
|
r30759 | args.update(cgi.parse_qs(argvalue, keep_blank_values=True)) | ||
|
r14093 | return args | ||
|
r11621 | def getfile(self, fp): | ||
|
r34741 | length = int(self.req.env[r'CONTENT_LENGTH']) | ||
|
r33820 | # If httppostargs is used, we need to read Content-Length | ||
# minus the amount that was consumed by args. | ||||
|
r34741 | length -= int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0)) | ||
|
r11621 | for s in util.filechunkiter(self.req, limit=length): | ||
fp.write(s) | ||||
def redirect(self): | ||||
|
r14614 | self.oldio = self.ui.fout, self.ui.ferr | ||
|
r28861 | self.ui.ferr = self.ui.fout = stringio() | ||
|
r14614 | def restore(self): | ||
val = self.ui.fout.getvalue() | ||||
self.ui.ferr, self.ui.fout = self.oldio | ||||
return val | ||||
|
r30206 | |||
|
r11595 | def _client(self): | ||
return 'remote:%s:%s:%s' % ( | ||||
self.req.env.get('wsgi.url_scheme') or 'http', | ||||
|
r28883 | urlreq.quote(self.req.env.get('REMOTE_HOST', '')), | ||
urlreq.quote(self.req.env.get('REMOTE_USER', ''))) | ||||
|
r11595 | |||
|
r30764 | def responsetype(self, v1compressible=False): | ||
"""Determine the appropriate response type and compression settings. | ||||
The ``v1compressible`` argument states whether the response with | ||||
application/mercurial-0.1 media types should be zlib compressed. | ||||
Returns a tuple of (mediatype, compengine, engineopts). | ||||
""" | ||||
# For now, if it isn't compressible in the old world, it's never | ||||
# compressible. We can change this to send uncompressed 0.2 payloads | ||||
# later. | ||||
if not v1compressible: | ||||
return HGTYPE, None, None | ||||
# Determine the response media type and compression engine based | ||||
# on the request parameters. | ||||
|
r34744 | protocaps = decodevaluefromheaders(self.req, r'X-HgProto').split(' ') | ||
|
r30764 | |||
if '0.2' in protocaps: | ||||
# Default as defined by wire protocol spec. | ||||
compformats = ['zlib', 'none'] | ||||
for cap in protocaps: | ||||
if cap.startswith('comp='): | ||||
compformats = cap[5:].split(',') | ||||
break | ||||
# Now find an agreed upon compression format. | ||||
for engine in wireproto.supportedcompengines(self.ui, self, | ||||
util.SERVERROLE): | ||||
if engine.wireprotosupport().name in compformats: | ||||
opts = {} | ||||
level = self.ui.configint('server', | ||||
'%slevel' % engine.name()) | ||||
if level is not None: | ||||
opts['level'] = level | ||||
return HGTYPE2, engine, opts | ||||
# No mutually supported compression format. Fall back to the | ||||
# legacy protocol. | ||||
# Don't allow untrusted settings because disabling compression or | ||||
# setting a very high compression level could lead to flooding | ||||
# the server's network or CPU. | ||||
r33225 | opts = {'level': self.ui.configint('server', 'zliblevel')} | |||
|
r30764 | return HGTYPE, util.compengines['zlib'], opts | ||
|
r11595 | def iscmd(cmd): | ||
return cmd in wireproto.commands | ||||
def call(repo, req, cmd): | ||||
|
r14614 | p = webproto(req, repo.ui) | ||
|
r30764 | |||
def genversion2(gen, compress, engine, engineopts): | ||||
# application/mercurial-0.2 always sends a payload header | ||||
# identifying the compression engine. | ||||
name = engine.wireprotosupport().name | ||||
assert 0 < len(name) < 256 | ||||
yield struct.pack('B', len(name)) | ||||
yield name | ||||
if compress: | ||||
for chunk in engine.compressstream(gen, opts=engineopts): | ||||
yield chunk | ||||
else: | ||||
for chunk in gen: | ||||
yield chunk | ||||
|
r11625 | rsp = wireproto.dispatch(repo, p, cmd) | ||
|
r34511 | if isinstance(rsp, bytes): | ||
|
r18352 | req.respond(HTTP_OK, HGTYPE, body=rsp) | ||
return [] | ||||
|
r11626 | elif isinstance(rsp, wireproto.streamres): | ||
|
r30466 | if rsp.reader: | ||
gen = iter(lambda: rsp.reader.read(32768), '') | ||||
else: | ||||
gen = rsp.gen | ||||
|
r30764 | # This code for compression should not be streamres specific. It | ||
# is here because we only compress streamres at the moment. | ||||
mediatype, engine, engineopts = p.responsetype(rsp.v1compressible) | ||||
|
r30466 | |||
|
r30764 | if mediatype == HGTYPE and rsp.v1compressible: | ||
gen = engine.compressstream(gen, engineopts) | ||||
elif mediatype == HGTYPE2: | ||||
gen = genversion2(gen, rsp.v1compressible, engine, engineopts) | ||||
req.respond(HTTP_OK, mediatype) | ||||
|
r30466 | return gen | ||
|
r11626 | elif isinstance(rsp, wireproto.pushres): | ||
|
r14614 | val = p.restore() | ||
|
r18346 | rsp = '%d\n%s' % (rsp.res, val) | ||
|
r18352 | req.respond(HTTP_OK, HGTYPE, body=rsp) | ||
return [] | ||||
|
r12703 | elif isinstance(rsp, wireproto.pusherr): | ||
|
r12704 | # drain the incoming bundle | ||
req.drain() | ||||
|
r14614 | p.restore() | ||
|
r12703 | rsp = '0\n%s\n' % rsp.res | ||
|
r18352 | req.respond(HTTP_OK, HGTYPE, body=rsp) | ||
return [] | ||||
|
r15017 | elif isinstance(rsp, wireproto.ooberror): | ||
rsp = rsp.message | ||||
|
r18352 | req.respond(HTTP_OK, HGERRTYPE, body=rsp) | ||
return [] | ||||
|
r34510 | raise error.ProgrammingError('hgweb.protocol internal failure', rsp) | ||