Show More
@@ -305,6 +305,7 b' class hgweb(object):' | |||||
305 |
|
305 | |||
306 | def _runwsgi(self, wsgireq, repo): |
|
306 | def _runwsgi(self, wsgireq, repo): | |
307 | req = wsgireq.req |
|
307 | req = wsgireq.req | |
|
308 | res = wsgireq.res | |||
308 | rctx = requestcontext(self, repo) |
|
309 | rctx = requestcontext(self, repo) | |
309 |
|
310 | |||
310 | # This state is global across all threads. |
|
311 | # This state is global across all threads. | |
@@ -317,11 +318,12 b' class hgweb(object):' | |||||
317 | wsgireq.headers = [h for h in wsgireq.headers |
|
318 | wsgireq.headers = [h for h in wsgireq.headers | |
318 | if h[0] != 'Content-Security-Policy'] |
|
319 | if h[0] != 'Content-Security-Policy'] | |
319 | wsgireq.headers.append(('Content-Security-Policy', rctx.csp)) |
|
320 | wsgireq.headers.append(('Content-Security-Policy', rctx.csp)) | |
|
321 | res.headers['Content-Security-Policy'] = rctx.csp | |||
320 |
|
322 | |||
321 |
handled |
|
323 | handled = wireprotoserver.handlewsgirequest( | |
322 | rctx, wsgireq, req, self.check_perm) |
|
324 | rctx, wsgireq, req, res, self.check_perm) | |
323 | if handled: |
|
325 | if handled: | |
324 | return res |
|
326 | return res.sendresponse() | |
325 |
|
327 | |||
326 | if req.havepathinfo: |
|
328 | if req.havepathinfo: | |
327 | query = req.dispatchpath |
|
329 | query = req.dispatchpath |
@@ -23,6 +23,7 b' from ..thirdparty import (' | |||||
23 | attr, |
|
23 | attr, | |
24 | ) |
|
24 | ) | |
25 | from .. import ( |
|
25 | from .. import ( | |
|
26 | error, | |||
26 | pycompat, |
|
27 | pycompat, | |
27 | util, |
|
28 | util, | |
28 | ) |
|
29 | ) | |
@@ -201,6 +202,128 b' def parserequestfromenv(env, bodyfh):' | |||||
201 | headers=headers, |
|
202 | headers=headers, | |
202 | bodyfh=bodyfh) |
|
203 | bodyfh=bodyfh) | |
203 |
|
204 | |||
|
205 | class wsgiresponse(object): | |||
|
206 | """Represents a response to a WSGI request. | |||
|
207 | ||||
|
208 | A response consists of a status line, headers, and a body. | |||
|
209 | ||||
|
210 | Consumers must populate the ``status`` and ``headers`` fields and | |||
|
211 | make a call to a ``setbody*()`` method before the response can be | |||
|
212 | issued. | |||
|
213 | ||||
|
214 | When it is time to start sending the response over the wire, | |||
|
215 | ``sendresponse()`` is called. It handles emitting the header portion | |||
|
216 | of the response message. It then yields chunks of body data to be | |||
|
217 | written to the peer. Typically, the WSGI application itself calls | |||
|
218 | and returns the value from ``sendresponse()``. | |||
|
219 | """ | |||
|
220 | ||||
|
221 | def __init__(self, req, startresponse): | |||
|
222 | """Create an empty response tied to a specific request. | |||
|
223 | ||||
|
224 | ``req`` is a ``parsedrequest``. ``startresponse`` is the | |||
|
225 | ``start_response`` function passed to the WSGI application. | |||
|
226 | """ | |||
|
227 | self._req = req | |||
|
228 | self._startresponse = startresponse | |||
|
229 | ||||
|
230 | self.status = None | |||
|
231 | self.headers = wsgiheaders.Headers([]) | |||
|
232 | ||||
|
233 | self._bodybytes = None | |||
|
234 | self._bodygen = None | |||
|
235 | self._started = False | |||
|
236 | ||||
|
237 | def setbodybytes(self, b): | |||
|
238 | """Define the response body as static bytes.""" | |||
|
239 | if self._bodybytes is not None or self._bodygen is not None: | |||
|
240 | raise error.ProgrammingError('cannot define body multiple times') | |||
|
241 | ||||
|
242 | self._bodybytes = b | |||
|
243 | self.headers['Content-Length'] = '%d' % len(b) | |||
|
244 | ||||
|
245 | def setbodygen(self, gen): | |||
|
246 | """Define the response body as a generator of bytes.""" | |||
|
247 | if self._bodybytes is not None or self._bodygen is not None: | |||
|
248 | raise error.ProgrammingError('cannot define body multiple times') | |||
|
249 | ||||
|
250 | self._bodygen = gen | |||
|
251 | ||||
|
252 | def sendresponse(self): | |||
|
253 | """Send the generated response to the client. | |||
|
254 | ||||
|
255 | Before this is called, ``status`` must be set and one of | |||
|
256 | ``setbodybytes()`` or ``setbodygen()`` must be called. | |||
|
257 | ||||
|
258 | Calling this method multiple times is not allowed. | |||
|
259 | """ | |||
|
260 | if self._started: | |||
|
261 | raise error.ProgrammingError('sendresponse() called multiple times') | |||
|
262 | ||||
|
263 | self._started = True | |||
|
264 | ||||
|
265 | if not self.status: | |||
|
266 | raise error.ProgrammingError('status line not defined') | |||
|
267 | ||||
|
268 | if self._bodybytes is None and self._bodygen is None: | |||
|
269 | raise error.ProgrammingError('response body not defined') | |||
|
270 | ||||
|
271 | # Various HTTP clients (notably httplib) won't read the HTTP response | |||
|
272 | # until the HTTP request has been sent in full. If servers (us) send a | |||
|
273 | # response before the HTTP request has been fully sent, the connection | |||
|
274 | # may deadlock because neither end is reading. | |||
|
275 | # | |||
|
276 | # We work around this by "draining" the request data before | |||
|
277 | # sending any response in some conditions. | |||
|
278 | drain = False | |||
|
279 | close = False | |||
|
280 | ||||
|
281 | # If the client sent Expect: 100-continue, we assume it is smart enough | |||
|
282 | # to deal with the server sending a response before reading the request. | |||
|
283 | # (httplib doesn't do this.) | |||
|
284 | if self._req.headers.get('Expect', '').lower() == '100-continue': | |||
|
285 | pass | |||
|
286 | # Only tend to request methods that have bodies. Strictly speaking, | |||
|
287 | # we should sniff for a body. But this is fine for our existing | |||
|
288 | # WSGI applications. | |||
|
289 | elif self._req.method not in ('POST', 'PUT'): | |||
|
290 | pass | |||
|
291 | else: | |||
|
292 | # If we don't know how much data to read, there's no guarantee | |||
|
293 | # that we can drain the request responsibly. The WSGI | |||
|
294 | # specification only says that servers *should* ensure the | |||
|
295 | # input stream doesn't overrun the actual request. So there's | |||
|
296 | # no guarantee that reading until EOF won't corrupt the stream | |||
|
297 | # state. | |||
|
298 | if not isinstance(self._req.bodyfh, util.cappedreader): | |||
|
299 | close = True | |||
|
300 | else: | |||
|
301 | # We /could/ only drain certain HTTP response codes. But 200 and | |||
|
302 | # non-200 wire protocol responses both require draining. Since | |||
|
303 | # we have a capped reader in place for all situations where we | |||
|
304 | # drain, it is safe to read from that stream. We'll either do | |||
|
305 | # a drain or no-op if we're already at EOF. | |||
|
306 | drain = True | |||
|
307 | ||||
|
308 | if close: | |||
|
309 | self.headers['Connection'] = 'Close' | |||
|
310 | ||||
|
311 | if drain: | |||
|
312 | assert isinstance(self._req.bodyfh, util.cappedreader) | |||
|
313 | while True: | |||
|
314 | chunk = self._req.bodyfh.read(32768) | |||
|
315 | if not chunk: | |||
|
316 | break | |||
|
317 | ||||
|
318 | self._startresponse(pycompat.sysstr(self.status), self.headers.items()) | |||
|
319 | if self._bodybytes: | |||
|
320 | yield self._bodybytes | |||
|
321 | elif self._bodygen: | |||
|
322 | for chunk in self._bodygen: | |||
|
323 | yield chunk | |||
|
324 | else: | |||
|
325 | error.ProgrammingError('do not know how to send body') | |||
|
326 | ||||
204 | class wsgirequest(object): |
|
327 | class wsgirequest(object): | |
205 | """Higher-level API for a WSGI request. |
|
328 | """Higher-level API for a WSGI request. | |
206 |
|
329 | |||
@@ -228,6 +351,7 b' class wsgirequest(object):' | |||||
228 | self.env = wsgienv |
|
351 | self.env = wsgienv | |
229 | self.req = parserequestfromenv(wsgienv, inp) |
|
352 | self.req = parserequestfromenv(wsgienv, inp) | |
230 | self.form = self.req.querystringdict |
|
353 | self.form = self.req.querystringdict | |
|
354 | self.res = wsgiresponse(self.req, start_response) | |||
231 | self._start_response = start_response |
|
355 | self._start_response = start_response | |
232 | self.server_write = None |
|
356 | self.server_write = None | |
233 | self.headers = [] |
|
357 | self.headers = [] |
@@ -149,7 +149,7 b' class httpv1protocolhandler(wireprototyp' | |||||
149 | def iscmd(cmd): |
|
149 | def iscmd(cmd): | |
150 | return cmd in wireproto.commands |
|
150 | return cmd in wireproto.commands | |
151 |
|
151 | |||
152 | def handlewsgirequest(rctx, wsgireq, req, checkperm): |
|
152 | def handlewsgirequest(rctx, wsgireq, req, res, checkperm): | |
153 | """Possibly process a wire protocol request. |
|
153 | """Possibly process a wire protocol request. | |
154 |
|
154 | |||
155 | If the current request is a wire protocol request, the request is |
|
155 | If the current request is a wire protocol request, the request is | |
@@ -157,10 +157,10 b' def handlewsgirequest(rctx, wsgireq, req' | |||||
157 |
|
157 | |||
158 | ``wsgireq`` is a ``wsgirequest`` instance. |
|
158 | ``wsgireq`` is a ``wsgirequest`` instance. | |
159 | ``req`` is a ``parsedrequest`` instance. |
|
159 | ``req`` is a ``parsedrequest`` instance. | |
|
160 | ``res`` is a ``wsgiresponse`` instance. | |||
160 |
|
161 | |||
161 | Returns a 2-tuple of (bool, response) where the 1st element indicates |
|
162 | Returns a bool indicating if the request was serviced. If set, the caller | |
162 | whether the request was handled and the 2nd element is a return |
|
163 | should stop processing the request, as a response has already been issued. | |
163 | value for a WSGI application (often a generator of bytes). |
|
|||
164 | """ |
|
164 | """ | |
165 | # Avoid cycle involving hg module. |
|
165 | # Avoid cycle involving hg module. | |
166 | from .hgweb import common as hgwebcommon |
|
166 | from .hgweb import common as hgwebcommon | |
@@ -171,7 +171,7 b' def handlewsgirequest(rctx, wsgireq, req' | |||||
171 | # string parameter. If it isn't present, this isn't a wire protocol |
|
171 | # string parameter. If it isn't present, this isn't a wire protocol | |
172 | # request. |
|
172 | # request. | |
173 | if 'cmd' not in req.querystringdict: |
|
173 | if 'cmd' not in req.querystringdict: | |
174 |
return False |
|
174 | return False | |
175 |
|
175 | |||
176 | cmd = req.querystringdict['cmd'][0] |
|
176 | cmd = req.querystringdict['cmd'][0] | |
177 |
|
177 | |||
@@ -183,18 +183,19 b' def handlewsgirequest(rctx, wsgireq, req' | |||||
183 | # known wire protocol commands and it is less confusing for machine |
|
183 | # known wire protocol commands and it is less confusing for machine | |
184 | # clients. |
|
184 | # clients. | |
185 | if not iscmd(cmd): |
|
185 | if not iscmd(cmd): | |
186 |
return False |
|
186 | return False | |
187 |
|
187 | |||
188 | # The "cmd" query string argument is only valid on the root path of the |
|
188 | # The "cmd" query string argument is only valid on the root path of the | |
189 | # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo |
|
189 | # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo | |
190 | # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request |
|
190 | # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request | |
191 | # in this case. We send an HTTP 404 for backwards compatibility reasons. |
|
191 | # in this case. We send an HTTP 404 for backwards compatibility reasons. | |
192 | if req.dispatchpath: |
|
192 | if req.dispatchpath: | |
193 | res = _handlehttperror( |
|
193 | res.status = hgwebcommon.statusmessage(404) | |
194 | hgwebcommon.ErrorResponse(hgwebcommon.HTTP_NOT_FOUND), wsgireq, |
|
194 | res.headers['Content-Type'] = HGTYPE | |
195 | req) |
|
195 | # TODO This is not a good response to issue for this request. This | |
196 |
|
196 | # is mostly for BC for now. | ||
197 | return True, res |
|
197 | res.setbodybytes('0\n%s\n' % b'Not Found') | |
|
198 | return True | |||
198 |
|
199 | |||
199 | proto = httpv1protocolhandler(wsgireq, req, repo.ui, |
|
200 | proto = httpv1protocolhandler(wsgireq, req, repo.ui, | |
200 | lambda perm: checkperm(rctx, wsgireq, perm)) |
|
201 | lambda perm: checkperm(rctx, wsgireq, perm)) | |
@@ -204,11 +205,16 b' def handlewsgirequest(rctx, wsgireq, req' | |||||
204 | # exception here. So consider refactoring into a exception type that |
|
205 | # exception here. So consider refactoring into a exception type that | |
205 | # is associated with the wire protocol. |
|
206 | # is associated with the wire protocol. | |
206 | try: |
|
207 | try: | |
207 |
|
|
208 | _callhttp(repo, wsgireq, req, res, proto, cmd) | |
208 | except hgwebcommon.ErrorResponse as e: |
|
209 | except hgwebcommon.ErrorResponse as e: | |
209 | res = _handlehttperror(e, wsgireq, req) |
|
210 | for k, v in e.headers: | |
|
211 | res.headers[k] = v | |||
|
212 | res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) | |||
|
213 | # TODO This response body assumes the failed command was | |||
|
214 | # "unbundle." That assumption is not always valid. | |||
|
215 | res.setbodybytes('0\n%s\n' % pycompat.bytestr(e)) | |||
210 |
|
216 | |||
211 |
return True |
|
217 | return True | |
212 |
|
218 | |||
213 | def _httpresponsetype(ui, req, prefer_uncompressed): |
|
219 | def _httpresponsetype(ui, req, prefer_uncompressed): | |
214 | """Determine the appropriate response type and compression settings. |
|
220 | """Determine the appropriate response type and compression settings. | |
@@ -250,7 +256,10 b' def _httpresponsetype(ui, req, prefer_un' | |||||
250 | opts = {'level': ui.configint('server', 'zliblevel')} |
|
256 | opts = {'level': ui.configint('server', 'zliblevel')} | |
251 | return HGTYPE, util.compengines['zlib'], opts |
|
257 | return HGTYPE, util.compengines['zlib'], opts | |
252 |
|
258 | |||
253 | def _callhttp(repo, wsgireq, req, proto, cmd): |
|
259 | def _callhttp(repo, wsgireq, req, res, proto, cmd): | |
|
260 | # Avoid cycle involving hg module. | |||
|
261 | from .hgweb import common as hgwebcommon | |||
|
262 | ||||
254 | def genversion2(gen, engine, engineopts): |
|
263 | def genversion2(gen, engine, engineopts): | |
255 | # application/mercurial-0.2 always sends a payload header |
|
264 | # application/mercurial-0.2 always sends a payload header | |
256 | # identifying the compression engine. |
|
265 | # identifying the compression engine. | |
@@ -262,26 +271,35 b' def _callhttp(repo, wsgireq, req, proto,' | |||||
262 | for chunk in gen: |
|
271 | for chunk in gen: | |
263 | yield chunk |
|
272 | yield chunk | |
264 |
|
273 | |||
|
274 | def setresponse(code, contenttype, bodybytes=None, bodygen=None): | |||
|
275 | if code == HTTP_OK: | |||
|
276 | res.status = '200 Script output follows' | |||
|
277 | else: | |||
|
278 | res.status = hgwebcommon.statusmessage(code) | |||
|
279 | ||||
|
280 | res.headers['Content-Type'] = contenttype | |||
|
281 | ||||
|
282 | if bodybytes is not None: | |||
|
283 | res.setbodybytes(bodybytes) | |||
|
284 | if bodygen is not None: | |||
|
285 | res.setbodygen(bodygen) | |||
|
286 | ||||
265 | if not wireproto.commands.commandavailable(cmd, proto): |
|
287 | if not wireproto.commands.commandavailable(cmd, proto): | |
266 |
|
|
288 | setresponse(HTTP_OK, HGERRTYPE, | |
267 |
|
|
289 | _('requested wire protocol command is not available over ' | |
268 |
|
|
290 | 'HTTP')) | |
269 |
return |
|
291 | return | |
270 |
|
292 | |||
271 | proto.checkperm(wireproto.commands[cmd].permission) |
|
293 | proto.checkperm(wireproto.commands[cmd].permission) | |
272 |
|
294 | |||
273 | rsp = wireproto.dispatch(repo, proto, cmd) |
|
295 | rsp = wireproto.dispatch(repo, proto, cmd) | |
274 |
|
296 | |||
275 | if isinstance(rsp, bytes): |
|
297 | if isinstance(rsp, bytes): | |
276 |
|
|
298 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | |
277 | return [] |
|
|||
278 | elif isinstance(rsp, wireprototypes.bytesresponse): |
|
299 | elif isinstance(rsp, wireprototypes.bytesresponse): | |
279 |
|
|
300 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data) | |
280 | return [] |
|
|||
281 | elif isinstance(rsp, wireprototypes.streamreslegacy): |
|
301 | elif isinstance(rsp, wireprototypes.streamreslegacy): | |
282 | gen = rsp.gen |
|
302 | setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen) | |
283 | wsgireq.respond(HTTP_OK, HGTYPE) |
|
|||
284 | return gen |
|
|||
285 | elif isinstance(rsp, wireprototypes.streamres): |
|
303 | elif isinstance(rsp, wireprototypes.streamres): | |
286 | gen = rsp.gen |
|
304 | gen = rsp.gen | |
287 |
|
305 | |||
@@ -294,30 +312,18 b' def _callhttp(repo, wsgireq, req, proto,' | |||||
294 | if mediatype == HGTYPE2: |
|
312 | if mediatype == HGTYPE2: | |
295 | gen = genversion2(gen, engine, engineopts) |
|
313 | gen = genversion2(gen, engine, engineopts) | |
296 |
|
314 | |||
297 |
|
|
315 | setresponse(HTTP_OK, mediatype, bodygen=gen) | |
298 | return gen |
|
|||
299 | elif isinstance(rsp, wireprototypes.pushres): |
|
316 | elif isinstance(rsp, wireprototypes.pushres): | |
300 | rsp = '%d\n%s' % (rsp.res, rsp.output) |
|
317 | rsp = '%d\n%s' % (rsp.res, rsp.output) | |
301 |
|
|
318 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | |
302 | return [] |
|
|||
303 | elif isinstance(rsp, wireprototypes.pusherr): |
|
319 | elif isinstance(rsp, wireprototypes.pusherr): | |
304 | rsp = '0\n%s\n' % rsp.res |
|
320 | rsp = '0\n%s\n' % rsp.res | |
305 | wsgireq.respond(HTTP_OK, HGTYPE, body=rsp) |
|
321 | res.drain = True | |
306 | return [] |
|
322 | setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) | |
307 | elif isinstance(rsp, wireprototypes.ooberror): |
|
323 | elif isinstance(rsp, wireprototypes.ooberror): | |
308 | rsp = rsp.message |
|
324 | setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) | |
309 | wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp) |
|
325 | else: | |
310 | return [] |
|
326 | raise error.ProgrammingError('hgweb.protocol internal failure', rsp) | |
311 | raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
|
|||
312 |
|
||||
313 | def _handlehttperror(e, wsgireq, req): |
|
|||
314 | """Called when an ErrorResponse is raised during HTTP request processing.""" |
|
|||
315 |
|
||||
316 | # TODO This response body assumes the failed command was |
|
|||
317 | # "unbundle." That assumption is not always valid. |
|
|||
318 | wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e)) |
|
|||
319 |
|
||||
320 | return '' |
|
|||
321 |
|
327 | |||
322 | def _sshv1respondbytes(fout, value): |
|
328 | def _sshv1respondbytes(fout, value): | |
323 | """Send a bytes response for protocol version 1.""" |
|
329 | """Send a bytes response for protocol version 1.""" |
General Comments 0
You need to be logged in to leave comments.
Login now