##// END OF EJS Templates
hgweb: create dedicated type for WSGI responses...
Gregory Szorc -
r36877:a88d68dc default
parent child Browse files
Show More
@@ -305,6 +305,7 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 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, res = wireprotoserver.handlewsgirequest(
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 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 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 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 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 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 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, None
174 return False
175
175
176 cmd = req.querystringdict['cmd'][0]
176 cmd = req.querystringdict['cmd'][0]
177
177
@@ -183,18 +183,19 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, None
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 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 res = _callhttp(repo, wsgireq, req, proto, cmd)
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, res
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 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 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 wsgireq.respond(HTTP_OK, HGERRTYPE,
288 setresponse(HTTP_OK, HGERRTYPE,
267 body=_('requested wire protocol command is not '
289 _('requested wire protocol command is not available over '
268 'available over HTTP'))
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 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
298 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
277 return []
278 elif isinstance(rsp, wireprototypes.bytesresponse):
299 elif isinstance(rsp, wireprototypes.bytesresponse):
279 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
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,31 +312,19 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 wsgireq.respond(HTTP_OK, mediatype)
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 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
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 []
311 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
326 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
312
327
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
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."""
324 fout.write('%d\n' % len(value))
330 fout.write('%d\n' % len(value))
General Comments 0
You need to be logged in to leave comments. Login now