34 HGERRTYPE = 'application/hg-error' |
34 HGERRTYPE = 'application/hg-error' |
35 |
35 |
36 SSHV1 = wireprototypes.SSHV1 |
36 SSHV1 = wireprototypes.SSHV1 |
37 SSHV2 = wireprototypes.SSHV2 |
37 SSHV2 = wireprototypes.SSHV2 |
38 |
38 |
39 def decodevaluefromheaders(req, headerprefix): |
39 def decodevaluefromheaders(wsgireq, headerprefix): |
40 """Decode a long value from multiple HTTP request headers. |
40 """Decode a long value from multiple HTTP request headers. |
41 |
41 |
42 Returns the value as a bytes, not a str. |
42 Returns the value as a bytes, not a str. |
43 """ |
43 """ |
44 chunks = [] |
44 chunks = [] |
45 i = 1 |
45 i = 1 |
46 prefix = headerprefix.upper().replace(r'-', r'_') |
46 prefix = headerprefix.upper().replace(r'-', r'_') |
47 while True: |
47 while True: |
48 v = req.env.get(r'HTTP_%s_%d' % (prefix, i)) |
48 v = wsgireq.env.get(r'HTTP_%s_%d' % (prefix, i)) |
49 if v is None: |
49 if v is None: |
50 break |
50 break |
51 chunks.append(pycompat.bytesurl(v)) |
51 chunks.append(pycompat.bytesurl(v)) |
52 i += 1 |
52 i += 1 |
53 |
53 |
54 return ''.join(chunks) |
54 return ''.join(chunks) |
55 |
55 |
56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler): |
56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler): |
57 def __init__(self, req, ui, checkperm): |
57 def __init__(self, wsgireq, ui, checkperm): |
58 self._req = req |
58 self._wsgireq = wsgireq |
59 self._ui = ui |
59 self._ui = ui |
60 self._checkperm = checkperm |
60 self._checkperm = checkperm |
61 |
61 |
62 @property |
62 @property |
63 def name(self): |
63 def name(self): |
77 else: |
77 else: |
78 data[k] = knownargs[k][0] |
78 data[k] = knownargs[k][0] |
79 return [data[k] for k in keys] |
79 return [data[k] for k in keys] |
80 |
80 |
81 def _args(self): |
81 def _args(self): |
82 args = util.rapply(pycompat.bytesurl, self._req.form.copy()) |
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy()) |
83 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0)) |
83 postlen = int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0)) |
84 if postlen: |
84 if postlen: |
85 args.update(urlreq.parseqs( |
85 args.update(urlreq.parseqs( |
86 self._req.read(postlen), keep_blank_values=True)) |
86 self._wsgireq.read(postlen), keep_blank_values=True)) |
87 return args |
87 return args |
88 |
88 |
89 argvalue = decodevaluefromheaders(self._req, r'X-HgArg') |
89 argvalue = decodevaluefromheaders(self._wsgireq, r'X-HgArg') |
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) |
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) |
91 return args |
91 return args |
92 |
92 |
93 def forwardpayload(self, fp): |
93 def forwardpayload(self, fp): |
94 if r'HTTP_CONTENT_LENGTH' in self._req.env: |
94 if r'HTTP_CONTENT_LENGTH' in self._wsgireq.env: |
95 length = int(self._req.env[r'HTTP_CONTENT_LENGTH']) |
95 length = int(self._wsgireq.env[r'HTTP_CONTENT_LENGTH']) |
96 else: |
96 else: |
97 length = int(self._req.env[r'CONTENT_LENGTH']) |
97 length = int(self._wsgireq.env[r'CONTENT_LENGTH']) |
98 # If httppostargs is used, we need to read Content-Length |
98 # If httppostargs is used, we need to read Content-Length |
99 # minus the amount that was consumed by args. |
99 # minus the amount that was consumed by args. |
100 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0)) |
100 length -= int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0)) |
101 for s in util.filechunkiter(self._req, limit=length): |
101 for s in util.filechunkiter(self._wsgireq, limit=length): |
102 fp.write(s) |
102 fp.write(s) |
103 |
103 |
104 @contextlib.contextmanager |
104 @contextlib.contextmanager |
105 def mayberedirectstdio(self): |
105 def mayberedirectstdio(self): |
106 oldout = self._ui.fout |
106 oldout = self._ui.fout |
148 # there are no other known users, so with any luck we can discard this |
148 # there are no other known users, so with any luck we can discard this |
149 # hook if remotefilelog becomes a first-party extension. |
149 # hook if remotefilelog becomes a first-party extension. |
150 def iscmd(cmd): |
150 def iscmd(cmd): |
151 return cmd in wireproto.commands |
151 return cmd in wireproto.commands |
152 |
152 |
153 def parsehttprequest(rctx, req, query, checkperm): |
153 def parsehttprequest(rctx, wsgireq, query, checkperm): |
154 """Parse the HTTP request for a wire protocol request. |
154 """Parse the HTTP request for a wire protocol request. |
155 |
155 |
156 If the current request appears to be a wire protocol request, this |
156 If the current request appears to be a wire protocol request, this |
157 function returns a dict with details about that request, including |
157 function returns a dict with details about that request, including |
158 an ``abstractprotocolserver`` instance suitable for handling the |
158 an ``abstractprotocolserver`` instance suitable for handling the |
159 request. Otherwise, ``None`` is returned. |
159 request. Otherwise, ``None`` is returned. |
160 |
160 |
161 ``req`` is a ``wsgirequest`` instance. |
161 ``wsgireq`` is a ``wsgirequest`` instance. |
162 """ |
162 """ |
163 repo = rctx.repo |
163 repo = rctx.repo |
164 |
164 |
165 # HTTP version 1 wire protocol requests are denoted by a "cmd" query |
165 # HTTP version 1 wire protocol requests are denoted by a "cmd" query |
166 # string parameter. If it isn't present, this isn't a wire protocol |
166 # string parameter. If it isn't present, this isn't a wire protocol |
167 # request. |
167 # request. |
168 if 'cmd' not in req.form: |
168 if 'cmd' not in wsgireq.form: |
169 return None |
169 return None |
170 |
170 |
171 cmd = req.form['cmd'][0] |
171 cmd = wsgireq.form['cmd'][0] |
172 |
172 |
173 # The "cmd" request parameter is used by both the wire protocol and hgweb. |
173 # The "cmd" request parameter is used by both the wire protocol and hgweb. |
174 # While not all wire protocol commands are available for all transports, |
174 # While not all wire protocol commands are available for all transports, |
175 # if we see a "cmd" value that resembles a known wire protocol command, we |
175 # if we see a "cmd" value that resembles a known wire protocol command, we |
176 # route it to a protocol handler. This is better than routing possible |
176 # route it to a protocol handler. This is better than routing possible |
178 # known wire protocol commands and it is less confusing for machine |
178 # known wire protocol commands and it is less confusing for machine |
179 # clients. |
179 # clients. |
180 if not iscmd(cmd): |
180 if not iscmd(cmd): |
181 return None |
181 return None |
182 |
182 |
183 proto = httpv1protocolhandler(req, repo.ui, |
183 proto = httpv1protocolhandler(wsgireq, repo.ui, |
184 lambda perm: checkperm(rctx, req, perm)) |
184 lambda perm: checkperm(rctx, wsgireq, perm)) |
185 |
185 |
186 return { |
186 return { |
187 'cmd': cmd, |
187 'cmd': cmd, |
188 'proto': proto, |
188 'proto': proto, |
189 'dispatch': lambda: _callhttp(repo, req, proto, cmd), |
189 'dispatch': lambda: _callhttp(repo, wsgireq, proto, cmd), |
190 'handleerror': lambda ex: _handlehttperror(ex, req, cmd), |
190 'handleerror': lambda ex: _handlehttperror(ex, wsgireq, cmd), |
191 } |
191 } |
192 |
192 |
193 def _httpresponsetype(ui, req, prefer_uncompressed): |
193 def _httpresponsetype(ui, wsgireq, prefer_uncompressed): |
194 """Determine the appropriate response type and compression settings. |
194 """Determine the appropriate response type and compression settings. |
195 |
195 |
196 Returns a tuple of (mediatype, compengine, engineopts). |
196 Returns a tuple of (mediatype, compengine, engineopts). |
197 """ |
197 """ |
198 # Determine the response media type and compression engine based |
198 # Determine the response media type and compression engine based |
199 # on the request parameters. |
199 # on the request parameters. |
200 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ') |
200 protocaps = decodevaluefromheaders(wsgireq, r'X-HgProto').split(' ') |
201 |
201 |
202 if '0.2' in protocaps: |
202 if '0.2' in protocaps: |
203 # All clients are expected to support uncompressed data. |
203 # All clients are expected to support uncompressed data. |
204 if prefer_uncompressed: |
204 if prefer_uncompressed: |
205 return HGTYPE2, util._noopengine(), {} |
205 return HGTYPE2, util._noopengine(), {} |
241 |
241 |
242 for chunk in gen: |
242 for chunk in gen: |
243 yield chunk |
243 yield chunk |
244 |
244 |
245 if not wireproto.commands.commandavailable(cmd, proto): |
245 if not wireproto.commands.commandavailable(cmd, proto): |
246 req.respond(HTTP_OK, HGERRTYPE, |
246 wsgireq.respond(HTTP_OK, HGERRTYPE, |
247 body=_('requested wire protocol command is not available ' |
247 body=_('requested wire protocol command is not ' |
248 'over HTTP')) |
248 'available over HTTP')) |
249 return [] |
249 return [] |
250 |
250 |
251 proto.checkperm(wireproto.commands[cmd].permission) |
251 proto.checkperm(wireproto.commands[cmd].permission) |
252 |
252 |
253 rsp = wireproto.dispatch(repo, proto, cmd) |
253 rsp = wireproto.dispatch(repo, proto, cmd) |
254 |
254 |
255 if isinstance(rsp, bytes): |
255 if isinstance(rsp, bytes): |
256 req.respond(HTTP_OK, HGTYPE, body=rsp) |
256 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp) |
257 return [] |
257 return [] |
258 elif isinstance(rsp, wireprototypes.bytesresponse): |
258 elif isinstance(rsp, wireprototypes.bytesresponse): |
259 req.respond(HTTP_OK, HGTYPE, body=rsp.data) |
259 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data) |
260 return [] |
260 return [] |
261 elif isinstance(rsp, wireprototypes.streamreslegacy): |
261 elif isinstance(rsp, wireprototypes.streamreslegacy): |
262 gen = rsp.gen |
262 gen = rsp.gen |
263 req.respond(HTTP_OK, HGTYPE) |
263 wsgireq.respond(HTTP_OK, HGTYPE) |
264 return gen |
264 return gen |
265 elif isinstance(rsp, wireprototypes.streamres): |
265 elif isinstance(rsp, wireprototypes.streamres): |
266 gen = rsp.gen |
266 gen = rsp.gen |
267 |
267 |
268 # This code for compression should not be streamres specific. It |
268 # This code for compression should not be streamres specific. It |
269 # is here because we only compress streamres at the moment. |
269 # is here because we only compress streamres at the moment. |
270 mediatype, engine, engineopts = _httpresponsetype( |
270 mediatype, engine, engineopts = _httpresponsetype( |
271 repo.ui, req, rsp.prefer_uncompressed) |
271 repo.ui, wsgireq, rsp.prefer_uncompressed) |
272 gen = engine.compressstream(gen, engineopts) |
272 gen = engine.compressstream(gen, engineopts) |
273 |
273 |
274 if mediatype == HGTYPE2: |
274 if mediatype == HGTYPE2: |
275 gen = genversion2(gen, engine, engineopts) |
275 gen = genversion2(gen, engine, engineopts) |
276 |
276 |
277 req.respond(HTTP_OK, mediatype) |
277 wsgireq.respond(HTTP_OK, mediatype) |
278 return gen |
278 return gen |
279 elif isinstance(rsp, wireprototypes.pushres): |
279 elif isinstance(rsp, wireprototypes.pushres): |
280 rsp = '%d\n%s' % (rsp.res, rsp.output) |
280 rsp = '%d\n%s' % (rsp.res, rsp.output) |
281 req.respond(HTTP_OK, HGTYPE, body=rsp) |
281 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp) |
282 return [] |
282 return [] |
283 elif isinstance(rsp, wireprototypes.pusherr): |
283 elif isinstance(rsp, wireprototypes.pusherr): |
284 # This is the httplib workaround documented in _handlehttperror(). |
284 # This is the httplib workaround documented in _handlehttperror(). |
285 req.drain() |
285 wsgireq.drain() |
286 |
286 |
287 rsp = '0\n%s\n' % rsp.res |
287 rsp = '0\n%s\n' % rsp.res |
288 req.respond(HTTP_OK, HGTYPE, body=rsp) |
288 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp) |
289 return [] |
289 return [] |
290 elif isinstance(rsp, wireprototypes.ooberror): |
290 elif isinstance(rsp, wireprototypes.ooberror): |
291 rsp = rsp.message |
291 rsp = rsp.message |
292 req.respond(HTTP_OK, HGERRTYPE, body=rsp) |
292 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp) |
293 return [] |
293 return [] |
294 raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
294 raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
295 |
295 |
296 def _handlehttperror(e, req, cmd): |
296 def _handlehttperror(e, wsgireq, cmd): |
297 """Called when an ErrorResponse is raised during HTTP request processing.""" |
297 """Called when an ErrorResponse is raised during HTTP request processing.""" |
298 |
298 |
299 # Clients using Python's httplib are stateful: the HTTP client |
299 # Clients using Python's httplib are stateful: the HTTP client |
300 # won't process an HTTP response until all request data is |
300 # won't process an HTTP response until all request data is |
301 # sent to the server. The intent of this code is to ensure |
301 # sent to the server. The intent of this code is to ensure |
302 # we always read HTTP request data from the client, thus |
302 # we always read HTTP request data from the client, thus |
303 # ensuring httplib transitions to a state that allows it to read |
303 # ensuring httplib transitions to a state that allows it to read |
304 # the HTTP response. In other words, it helps prevent deadlocks |
304 # the HTTP response. In other words, it helps prevent deadlocks |
305 # on clients using httplib. |
305 # on clients using httplib. |
306 |
306 |
307 if (req.env[r'REQUEST_METHOD'] == r'POST' and |
307 if (wsgireq.env[r'REQUEST_METHOD'] == r'POST' and |
308 # But not if Expect: 100-continue is being used. |
308 # But not if Expect: 100-continue is being used. |
309 (req.env.get('HTTP_EXPECT', |
309 (wsgireq.env.get('HTTP_EXPECT', |
310 '').lower() != '100-continue') or |
310 '').lower() != '100-continue') or |
311 # Or the non-httplib HTTP library is being advertised by |
311 # Or the non-httplib HTTP library is being advertised by |
312 # the client. |
312 # the client. |
313 req.env.get('X-HgHttp2', '')): |
313 wsgireq.env.get('X-HgHttp2', '')): |
314 req.drain() |
314 wsgireq.drain() |
315 else: |
315 else: |
316 req.headers.append((r'Connection', r'Close')) |
316 wsgireq.headers.append((r'Connection', r'Close')) |
317 |
317 |
318 # TODO This response body assumes the failed command was |
318 # TODO This response body assumes the failed command was |
319 # "unbundle." That assumption is not always valid. |
319 # "unbundle." That assumption is not always valid. |
320 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e)) |
320 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e)) |
321 |
321 |
322 return '' |
322 return '' |
323 |
323 |
324 def _sshv1respondbytes(fout, value): |
324 def _sshv1respondbytes(fout, value): |
325 """Send a bytes response for protocol version 1.""" |
325 """Send a bytes response for protocol version 1.""" |