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