##// 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 b' class hgweb(object):'
305 305
306 306 def _runwsgi(self, wsgireq, repo):
307 307 req = wsgireq.req
308 res = wsgireq.res
308 309 rctx = requestcontext(self, repo)
309 310
310 311 # This state is global across all threads.
@@ -317,11 +318,12 b' class hgweb(object):'
317 318 wsgireq.headers = [h for h in wsgireq.headers
318 319 if h[0] != 'Content-Security-Policy']
319 320 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
321 res.headers['Content-Security-Policy'] = rctx.csp
320 322
321 handled, res = wireprotoserver.handlewsgirequest(
322 rctx, wsgireq, req, self.check_perm)
323 handled = wireprotoserver.handlewsgirequest(
324 rctx, wsgireq, req, res, self.check_perm)
323 325 if handled:
324 return res
326 return res.sendresponse()
325 327
326 328 if req.havepathinfo:
327 329 query = req.dispatchpath
@@ -23,6 +23,7 b' from ..thirdparty import ('
23 23 attr,
24 24 )
25 25 from .. import (
26 error,
26 27 pycompat,
27 28 util,
28 29 )
@@ -201,6 +202,128 b' def parserequestfromenv(env, bodyfh):'
201 202 headers=headers,
202 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 327 class wsgirequest(object):
205 328 """Higher-level API for a WSGI request.
206 329
@@ -228,6 +351,7 b' class wsgirequest(object):'
228 351 self.env = wsgienv
229 352 self.req = parserequestfromenv(wsgienv, inp)
230 353 self.form = self.req.querystringdict
354 self.res = wsgiresponse(self.req, start_response)
231 355 self._start_response = start_response
232 356 self.server_write = None
233 357 self.headers = []
@@ -149,7 +149,7 b' class httpv1protocolhandler(wireprototyp'
149 149 def iscmd(cmd):
150 150 return cmd in wireproto.commands
151 151
152 def handlewsgirequest(rctx, wsgireq, req, checkperm):
152 def handlewsgirequest(rctx, wsgireq, req, res, checkperm):
153 153 """Possibly process a wire protocol request.
154 154
155 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 158 ``wsgireq`` is a ``wsgirequest`` instance.
159 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 whether the request was handled and the 2nd element is a return
163 value for a WSGI application (often a generator of bytes).
162 Returns a bool indicating if the request was serviced. If set, the caller
163 should stop processing the request, as a response has already been issued.
164 164 """
165 165 # Avoid cycle involving hg module.
166 166 from .hgweb import common as hgwebcommon
@@ -171,7 +171,7 b' def handlewsgirequest(rctx, wsgireq, req'
171 171 # string parameter. If it isn't present, this isn't a wire protocol
172 172 # request.
173 173 if 'cmd' not in req.querystringdict:
174 return False, None
174 return False
175 175
176 176 cmd = req.querystringdict['cmd'][0]
177 177
@@ -183,18 +183,19 b' def handlewsgirequest(rctx, wsgireq, req'
183 183 # known wire protocol commands and it is less confusing for machine
184 184 # clients.
185 185 if not iscmd(cmd):
186 return False, None
186 return False
187 187
188 188 # The "cmd" query string argument is only valid on the root path of the
189 189 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
190 190 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
191 191 # in this case. We send an HTTP 404 for backwards compatibility reasons.
192 192 if req.dispatchpath:
193 res = _handlehttperror(
194 hgwebcommon.ErrorResponse(hgwebcommon.HTTP_NOT_FOUND), wsgireq,
195 req)
196
197 return True, res
193 res.status = hgwebcommon.statusmessage(404)
194 res.headers['Content-Type'] = HGTYPE
195 # TODO This is not a good response to issue for this request. This
196 # is mostly for BC for now.
197 res.setbodybytes('0\n%s\n' % b'Not Found')
198 return True
198 199
199 200 proto = httpv1protocolhandler(wsgireq, req, repo.ui,
200 201 lambda perm: checkperm(rctx, wsgireq, perm))
@@ -204,11 +205,16 b' def handlewsgirequest(rctx, wsgireq, req'
204 205 # exception here. So consider refactoring into a exception type that
205 206 # is associated with the wire protocol.
206 207 try:
207 res = _callhttp(repo, wsgireq, req, proto, cmd)
208 _callhttp(repo, wsgireq, req, res, proto, cmd)
208 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 219 def _httpresponsetype(ui, req, prefer_uncompressed):
214 220 """Determine the appropriate response type and compression settings.
@@ -250,7 +256,10 b' def _httpresponsetype(ui, req, prefer_un'
250 256 opts = {'level': ui.configint('server', 'zliblevel')}
251 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 263 def genversion2(gen, engine, engineopts):
255 264 # application/mercurial-0.2 always sends a payload header
256 265 # identifying the compression engine.
@@ -262,26 +271,35 b' def _callhttp(repo, wsgireq, req, proto,'
262 271 for chunk in gen:
263 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 287 if not wireproto.commands.commandavailable(cmd, proto):
266 wsgireq.respond(HTTP_OK, HGERRTYPE,
267 body=_('requested wire protocol command is not '
268 'available over HTTP'))
269 return []
288 setresponse(HTTP_OK, HGERRTYPE,
289 _('requested wire protocol command is not available over '
290 'HTTP'))
291 return
270 292
271 293 proto.checkperm(wireproto.commands[cmd].permission)
272 294
273 295 rsp = wireproto.dispatch(repo, proto, cmd)
274 296
275 297 if isinstance(rsp, bytes):
276 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
277 return []
298 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
278 299 elif isinstance(rsp, wireprototypes.bytesresponse):
279 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
280 return []
300 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
281 301 elif isinstance(rsp, wireprototypes.streamreslegacy):
282 gen = rsp.gen
283 wsgireq.respond(HTTP_OK, HGTYPE)
284 return gen
302 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
285 303 elif isinstance(rsp, wireprototypes.streamres):
286 304 gen = rsp.gen
287 305
@@ -294,30 +312,18 b' def _callhttp(repo, wsgireq, req, proto,'
294 312 if mediatype == HGTYPE2:
295 313 gen = genversion2(gen, engine, engineopts)
296 314
297 wsgireq.respond(HTTP_OK, mediatype)
298 return gen
315 setresponse(HTTP_OK, mediatype, bodygen=gen)
299 316 elif isinstance(rsp, wireprototypes.pushres):
300 317 rsp = '%d\n%s' % (rsp.res, rsp.output)
301 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
302 return []
318 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
303 319 elif isinstance(rsp, wireprototypes.pusherr):
304 320 rsp = '0\n%s\n' % rsp.res
305 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
306 return []
321 res.drain = True
322 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
307 323 elif isinstance(rsp, wireprototypes.ooberror):
308 rsp = rsp.message
309 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
310 return []
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 ''
324 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
325 else:
326 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
321 327
322 328 def _sshv1respondbytes(fout, value):
323 329 """Send a bytes response for protocol version 1."""
General Comments 0
You need to be logged in to leave comments. Login now