|
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) |