##// END OF EJS Templates
hgweb: refactor 304 handling code...
Gregory Szorc -
r36894:ccb70a77 default
parent child Browse files
Show More
@@ -1,456 +1,455 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
18 HTTP_OK,
20 HTTP_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
21 cspvalues,
20 cspvalues,
22 permhooks,
21 permhooks,
23 )
22 )
24
23
25 from .. import (
24 from .. import (
26 encoding,
25 encoding,
27 error,
26 error,
28 formatter,
27 formatter,
29 hg,
28 hg,
30 hook,
29 hook,
31 profiling,
30 profiling,
32 pycompat,
31 pycompat,
33 repoview,
32 repoview,
34 templatefilters,
33 templatefilters,
35 templater,
34 templater,
36 ui as uimod,
35 ui as uimod,
37 util,
36 util,
38 wireprotoserver,
37 wireprotoserver,
39 )
38 )
40
39
41 from . import (
40 from . import (
42 request as requestmod,
41 request as requestmod,
43 webcommands,
42 webcommands,
44 webutil,
43 webutil,
45 wsgicgi,
44 wsgicgi,
46 )
45 )
47
46
48 archivespecs = util.sortdict((
47 archivespecs = util.sortdict((
49 ('zip', ('application/zip', 'zip', '.zip', None)),
48 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ))
51 ))
53
52
54 def getstyle(req, configfn, templatepath):
53 def getstyle(req, configfn, templatepath):
55 styles = (
54 styles = (
56 req.qsparams.get('style', None),
55 req.qsparams.get('style', None),
57 configfn('web', 'style'),
56 configfn('web', 'style'),
58 'paper',
57 'paper',
59 )
58 )
60 return styles, templater.stylemap(styles, templatepath)
59 return styles, templater.stylemap(styles, templatepath)
61
60
62 def makebreadcrumb(url, prefix=''):
61 def makebreadcrumb(url, prefix=''):
63 '''Return a 'URL breadcrumb' list
62 '''Return a 'URL breadcrumb' list
64
63
65 A 'URL breadcrumb' is a list of URL-name pairs,
64 A 'URL breadcrumb' is a list of URL-name pairs,
66 corresponding to each of the path items on a URL.
65 corresponding to each of the path items on a URL.
67 This can be used to create path navigation entries.
66 This can be used to create path navigation entries.
68 '''
67 '''
69 if url.endswith('/'):
68 if url.endswith('/'):
70 url = url[:-1]
69 url = url[:-1]
71 if prefix:
70 if prefix:
72 url = '/' + prefix + url
71 url = '/' + prefix + url
73 relpath = url
72 relpath = url
74 if relpath.startswith('/'):
73 if relpath.startswith('/'):
75 relpath = relpath[1:]
74 relpath = relpath[1:]
76
75
77 breadcrumb = []
76 breadcrumb = []
78 urlel = url
77 urlel = url
79 pathitems = [''] + relpath.split('/')
78 pathitems = [''] + relpath.split('/')
80 for pathel in reversed(pathitems):
79 for pathel in reversed(pathitems):
81 if not pathel or not urlel:
80 if not pathel or not urlel:
82 break
81 break
83 breadcrumb.append({'url': urlel, 'name': pathel})
82 breadcrumb.append({'url': urlel, 'name': pathel})
84 urlel = os.path.dirname(urlel)
83 urlel = os.path.dirname(urlel)
85 return reversed(breadcrumb)
84 return reversed(breadcrumb)
86
85
87 class requestcontext(object):
86 class requestcontext(object):
88 """Holds state/context for an individual request.
87 """Holds state/context for an individual request.
89
88
90 Servers can be multi-threaded. Holding state on the WSGI application
89 Servers can be multi-threaded. Holding state on the WSGI application
91 is prone to race conditions. Instances of this class exist to hold
90 is prone to race conditions. Instances of this class exist to hold
92 mutable and race-free state for requests.
91 mutable and race-free state for requests.
93 """
92 """
94 def __init__(self, app, repo, req, res):
93 def __init__(self, app, repo, req, res):
95 self.repo = repo
94 self.repo = repo
96 self.reponame = app.reponame
95 self.reponame = app.reponame
97 self.req = req
96 self.req = req
98 self.res = res
97 self.res = res
99
98
100 self.archivespecs = archivespecs
99 self.archivespecs = archivespecs
101
100
102 self.maxchanges = self.configint('web', 'maxchanges')
101 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
102 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
103 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
104 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
105 self.allowpull = self.configbool('web', 'allow-pull')
107
106
108 # we use untrusted=False to prevent a repo owner from using
107 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
108 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
109 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
110 self.templatepath = self.config('web', 'templates', untrusted=False)
112
111
113 # This object is more expensive to build than simple config values.
112 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
113 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
114 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
115 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
116 # of the request.
118 self.websubtable = app.websubtable
117 self.websubtable = app.websubtable
119
118
120 self.csp, self.nonce = cspvalues(self.repo.ui)
119 self.csp, self.nonce = cspvalues(self.repo.ui)
121
120
122 # Trust the settings from the .hg/hgrc files by default.
121 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
122 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
123 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
124 untrusted=untrusted)
126
125
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
126 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
127 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
128 untrusted=untrusted)
130
129
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
130 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
131 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
132 untrusted=untrusted)
134
133
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
134 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
135 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
136 untrusted=untrusted)
138
137
139 def archivelist(self, nodeid):
138 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
139 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
140 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
143
145 def templater(self, req):
144 def templater(self, req):
146 # determine scheme, port and server name
145 # determine scheme, port and server name
147 # this is needed to create absolute urls
146 # this is needed to create absolute urls
148 logourl = self.config('web', 'logourl')
147 logourl = self.config('web', 'logourl')
149 logoimg = self.config('web', 'logoimg')
148 logoimg = self.config('web', 'logoimg')
150 staticurl = (self.config('web', 'staticurl')
149 staticurl = (self.config('web', 'staticurl')
151 or req.apppath + '/static/')
150 or req.apppath + '/static/')
152 if not staticurl.endswith('/'):
151 if not staticurl.endswith('/'):
153 staticurl += '/'
152 staticurl += '/'
154
153
155 # some functions for the templater
154 # some functions for the templater
156
155
157 def motd(**map):
156 def motd(**map):
158 yield self.config('web', 'motd')
157 yield self.config('web', 'motd')
159
158
160 # figure out which style to use
159 # figure out which style to use
161
160
162 vars = {}
161 vars = {}
163 styles, (style, mapfile) = getstyle(req, self.config,
162 styles, (style, mapfile) = getstyle(req, self.config,
164 self.templatepath)
163 self.templatepath)
165 if style == styles[0]:
164 if style == styles[0]:
166 vars['style'] = style
165 vars['style'] = style
167
166
168 sessionvars = webutil.sessionvars(vars, '?')
167 sessionvars = webutil.sessionvars(vars, '?')
169
168
170 if not self.reponame:
169 if not self.reponame:
171 self.reponame = (self.config('web', 'name', '')
170 self.reponame = (self.config('web', 'name', '')
172 or req.reponame
171 or req.reponame
173 or req.apppath
172 or req.apppath
174 or self.repo.root)
173 or self.repo.root)
175
174
176 def websubfilter(text):
175 def websubfilter(text):
177 return templatefilters.websub(text, self.websubtable)
176 return templatefilters.websub(text, self.websubtable)
178
177
179 # create the templater
178 # create the templater
180 # TODO: export all keywords: defaults = templatekw.keywords.copy()
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
181 defaults = {
180 defaults = {
182 'url': req.apppath + '/',
181 'url': req.apppath + '/',
183 'logourl': logourl,
182 'logourl': logourl,
184 'logoimg': logoimg,
183 'logoimg': logoimg,
185 'staticurl': staticurl,
184 'staticurl': staticurl,
186 'urlbase': req.advertisedbaseurl,
185 'urlbase': req.advertisedbaseurl,
187 'repo': self.reponame,
186 'repo': self.reponame,
188 'encoding': encoding.encoding,
187 'encoding': encoding.encoding,
189 'motd': motd,
188 'motd': motd,
190 'sessionvars': sessionvars,
189 'sessionvars': sessionvars,
191 'pathdef': makebreadcrumb(req.apppath),
190 'pathdef': makebreadcrumb(req.apppath),
192 'style': style,
191 'style': style,
193 'nonce': self.nonce,
192 'nonce': self.nonce,
194 }
193 }
195 tres = formatter.templateresources(self.repo.ui, self.repo)
194 tres = formatter.templateresources(self.repo.ui, self.repo)
196 tmpl = templater.templater.frommapfile(mapfile,
195 tmpl = templater.templater.frommapfile(mapfile,
197 filters={'websub': websubfilter},
196 filters={'websub': websubfilter},
198 defaults=defaults,
197 defaults=defaults,
199 resources=tres)
198 resources=tres)
200 return tmpl
199 return tmpl
201
200
202
201
203 class hgweb(object):
202 class hgweb(object):
204 """HTTP server for individual repositories.
203 """HTTP server for individual repositories.
205
204
206 Instances of this class serve HTTP responses for a particular
205 Instances of this class serve HTTP responses for a particular
207 repository.
206 repository.
208
207
209 Instances are typically used as WSGI applications.
208 Instances are typically used as WSGI applications.
210
209
211 Some servers are multi-threaded. On these servers, there may
210 Some servers are multi-threaded. On these servers, there may
212 be multiple active threads inside __call__.
211 be multiple active threads inside __call__.
213 """
212 """
214 def __init__(self, repo, name=None, baseui=None):
213 def __init__(self, repo, name=None, baseui=None):
215 if isinstance(repo, str):
214 if isinstance(repo, str):
216 if baseui:
215 if baseui:
217 u = baseui.copy()
216 u = baseui.copy()
218 else:
217 else:
219 u = uimod.ui.load()
218 u = uimod.ui.load()
220 r = hg.repository(u, repo)
219 r = hg.repository(u, repo)
221 else:
220 else:
222 # we trust caller to give us a private copy
221 # we trust caller to give us a private copy
223 r = repo
222 r = repo
224
223
225 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
227 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
229 # resolve file patterns relative to repo root
228 # resolve file patterns relative to repo root
230 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
232 # displaying bundling progress bar while serving feel wrong and may
231 # displaying bundling progress bar while serving feel wrong and may
233 # break some wsgi implementation.
232 # break some wsgi implementation.
234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
237 self._lastrepo = self._repos[0]
236 self._lastrepo = self._repos[0]
238 hook.redirect(True)
237 hook.redirect(True)
239 self.reponame = name
238 self.reponame = name
240
239
241 def _webifyrepo(self, repo):
240 def _webifyrepo(self, repo):
242 repo = getwebview(repo)
241 repo = getwebview(repo)
243 self.websubtable = webutil.getwebsubs(repo)
242 self.websubtable = webutil.getwebsubs(repo)
244 return repo
243 return repo
245
244
246 @contextlib.contextmanager
245 @contextlib.contextmanager
247 def _obtainrepo(self):
246 def _obtainrepo(self):
248 """Obtain a repo unique to the caller.
247 """Obtain a repo unique to the caller.
249
248
250 Internally we maintain a stack of cachedlocalrepo instances
249 Internally we maintain a stack of cachedlocalrepo instances
251 to be handed out. If one is available, we pop it and return it,
250 to be handed out. If one is available, we pop it and return it,
252 ensuring it is up to date in the process. If one is not available,
251 ensuring it is up to date in the process. If one is not available,
253 we clone the most recently used repo instance and return it.
252 we clone the most recently used repo instance and return it.
254
253
255 It is currently possible for the stack to grow without bounds
254 It is currently possible for the stack to grow without bounds
256 if the server allows infinite threads. However, servers should
255 if the server allows infinite threads. However, servers should
257 have a thread limit, thus establishing our limit.
256 have a thread limit, thus establishing our limit.
258 """
257 """
259 if self._repos:
258 if self._repos:
260 cached = self._repos.pop()
259 cached = self._repos.pop()
261 r, created = cached.fetch()
260 r, created = cached.fetch()
262 else:
261 else:
263 cached = self._lastrepo.copy()
262 cached = self._lastrepo.copy()
264 r, created = cached.fetch()
263 r, created = cached.fetch()
265 if created:
264 if created:
266 r = self._webifyrepo(r)
265 r = self._webifyrepo(r)
267
266
268 self._lastrepo = cached
267 self._lastrepo = cached
269 self.mtime = cached.mtime
268 self.mtime = cached.mtime
270 try:
269 try:
271 yield r
270 yield r
272 finally:
271 finally:
273 self._repos.append(cached)
272 self._repos.append(cached)
274
273
275 def run(self):
274 def run(self):
276 """Start a server from CGI environment.
275 """Start a server from CGI environment.
277
276
278 Modern servers should be using WSGI and should avoid this
277 Modern servers should be using WSGI and should avoid this
279 method, if possible.
278 method, if possible.
280 """
279 """
281 if not encoding.environ.get('GATEWAY_INTERFACE',
280 if not encoding.environ.get('GATEWAY_INTERFACE',
282 '').startswith("CGI/1."):
281 '').startswith("CGI/1."):
283 raise RuntimeError("This function is only intended to be "
282 raise RuntimeError("This function is only intended to be "
284 "called while running as a CGI script.")
283 "called while running as a CGI script.")
285 wsgicgi.launch(self)
284 wsgicgi.launch(self)
286
285
287 def __call__(self, env, respond):
286 def __call__(self, env, respond):
288 """Run the WSGI application.
287 """Run the WSGI application.
289
288
290 This may be called by multiple threads.
289 This may be called by multiple threads.
291 """
290 """
292 req = requestmod.wsgirequest(env, respond)
291 req = requestmod.wsgirequest(env, respond)
293 return self.run_wsgi(req)
292 return self.run_wsgi(req)
294
293
295 def run_wsgi(self, wsgireq):
294 def run_wsgi(self, wsgireq):
296 """Internal method to run the WSGI application.
295 """Internal method to run the WSGI application.
297
296
298 This is typically only called by Mercurial. External consumers
297 This is typically only called by Mercurial. External consumers
299 should be using instances of this class as the WSGI application.
298 should be using instances of this class as the WSGI application.
300 """
299 """
301 with self._obtainrepo() as repo:
300 with self._obtainrepo() as repo:
302 profile = repo.ui.configbool('profiling', 'enabled')
301 profile = repo.ui.configbool('profiling', 'enabled')
303 with profiling.profile(repo.ui, enabled=profile):
302 with profiling.profile(repo.ui, enabled=profile):
304 for r in self._runwsgi(wsgireq, repo):
303 for r in self._runwsgi(wsgireq, repo):
305 yield r
304 yield r
306
305
307 def _runwsgi(self, wsgireq, repo):
306 def _runwsgi(self, wsgireq, repo):
308 req = wsgireq.req
307 req = wsgireq.req
309 res = wsgireq.res
308 res = wsgireq.res
310 rctx = requestcontext(self, repo, req, res)
309 rctx = requestcontext(self, repo, req, res)
311
310
312 # This state is global across all threads.
311 # This state is global across all threads.
313 encoding.encoding = rctx.config('web', 'encoding')
312 encoding.encoding = rctx.config('web', 'encoding')
314 rctx.repo.ui.environ = wsgireq.env
313 rctx.repo.ui.environ = wsgireq.env
315
314
316 if rctx.csp:
315 if rctx.csp:
317 # hgwebdir may have added CSP header. Since we generate our own,
316 # hgwebdir may have added CSP header. Since we generate our own,
318 # replace it.
317 # replace it.
319 wsgireq.headers = [h for h in wsgireq.headers
318 wsgireq.headers = [h for h in wsgireq.headers
320 if h[0] != 'Content-Security-Policy']
319 if h[0] != 'Content-Security-Policy']
321 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
320 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
322 res.headers['Content-Security-Policy'] = rctx.csp
321 res.headers['Content-Security-Policy'] = rctx.csp
323
322
324 handled = wireprotoserver.handlewsgirequest(
323 handled = wireprotoserver.handlewsgirequest(
325 rctx, req, res, self.check_perm)
324 rctx, req, res, self.check_perm)
326 if handled:
325 if handled:
327 return res.sendresponse()
326 return res.sendresponse()
328
327
329 if req.havepathinfo:
328 if req.havepathinfo:
330 query = req.dispatchpath
329 query = req.dispatchpath
331 else:
330 else:
332 query = req.querystring.partition('&')[0].partition(';')[0]
331 query = req.querystring.partition('&')[0].partition(';')[0]
333
332
334 # translate user-visible url structure to internal structure
333 # translate user-visible url structure to internal structure
335
334
336 args = query.split('/', 2)
335 args = query.split('/', 2)
337 if 'cmd' not in req.qsparams and args and args[0]:
336 if 'cmd' not in req.qsparams and args and args[0]:
338 cmd = args.pop(0)
337 cmd = args.pop(0)
339 style = cmd.rfind('-')
338 style = cmd.rfind('-')
340 if style != -1:
339 if style != -1:
341 req.qsparams['style'] = cmd[:style]
340 req.qsparams['style'] = cmd[:style]
342 cmd = cmd[style + 1:]
341 cmd = cmd[style + 1:]
343
342
344 # avoid accepting e.g. style parameter as command
343 # avoid accepting e.g. style parameter as command
345 if util.safehasattr(webcommands, cmd):
344 if util.safehasattr(webcommands, cmd):
346 req.qsparams['cmd'] = cmd
345 req.qsparams['cmd'] = cmd
347
346
348 if cmd == 'static':
347 if cmd == 'static':
349 req.qsparams['file'] = '/'.join(args)
348 req.qsparams['file'] = '/'.join(args)
350 else:
349 else:
351 if args and args[0]:
350 if args and args[0]:
352 node = args.pop(0).replace('%2F', '/')
351 node = args.pop(0).replace('%2F', '/')
353 req.qsparams['node'] = node
352 req.qsparams['node'] = node
354 if args:
353 if args:
355 if 'file' in req.qsparams:
354 if 'file' in req.qsparams:
356 del req.qsparams['file']
355 del req.qsparams['file']
357 for a in args:
356 for a in args:
358 req.qsparams.add('file', a)
357 req.qsparams.add('file', a)
359
358
360 ua = req.headers.get('User-Agent', '')
359 ua = req.headers.get('User-Agent', '')
361 if cmd == 'rev' and 'mercurial' in ua:
360 if cmd == 'rev' and 'mercurial' in ua:
362 req.qsparams['style'] = 'raw'
361 req.qsparams['style'] = 'raw'
363
362
364 if cmd == 'archive':
363 if cmd == 'archive':
365 fn = req.qsparams['node']
364 fn = req.qsparams['node']
366 for type_, spec in rctx.archivespecs.iteritems():
365 for type_, spec in rctx.archivespecs.iteritems():
367 ext = spec[2]
366 ext = spec[2]
368 if fn.endswith(ext):
367 if fn.endswith(ext):
369 req.qsparams['node'] = fn[:-len(ext)]
368 req.qsparams['node'] = fn[:-len(ext)]
370 req.qsparams['type'] = type_
369 req.qsparams['type'] = type_
371 else:
370 else:
372 cmd = req.qsparams.get('cmd', '')
371 cmd = req.qsparams.get('cmd', '')
373
372
374 # process the web interface request
373 # process the web interface request
375
374
376 try:
375 try:
377 tmpl = rctx.templater(req)
376 tmpl = rctx.templater(req)
378 ctype = tmpl('mimetype', encoding=encoding.encoding)
377 ctype = tmpl('mimetype', encoding=encoding.encoding)
379 ctype = templater.stringify(ctype)
378 ctype = templater.stringify(ctype)
380
379
381 # check read permissions non-static content
380 # check read permissions non-static content
382 if cmd != 'static':
381 if cmd != 'static':
383 self.check_perm(rctx, req, None)
382 self.check_perm(rctx, req, None)
384
383
385 if cmd == '':
384 if cmd == '':
386 req.qsparams['cmd'] = tmpl.cache['default']
385 req.qsparams['cmd'] = tmpl.cache['default']
387 cmd = req.qsparams['cmd']
386 cmd = req.qsparams['cmd']
388
387
389 # Don't enable caching if using a CSP nonce because then it wouldn't
388 # Don't enable caching if using a CSP nonce because then it wouldn't
390 # be a nonce.
389 # be a nonce.
391 if rctx.configbool('web', 'cache') and not rctx.nonce:
390 if rctx.configbool('web', 'cache') and not rctx.nonce:
392 tag = 'W/"%d"' % self.mtime
391 tag = 'W/"%d"' % self.mtime
393 if req.headers.get('If-None-Match') == tag:
392 if req.headers.get('If-None-Match') == tag:
394 raise ErrorResponse(HTTP_NOT_MODIFIED)
393 res.status = '304 Not Modified'
394 # Response body not allowed on 304.
395 res.setbodybytes('')
396 return res.sendresponse()
395
397
396 wsgireq.headers.append((r'ETag', pycompat.sysstr(tag)))
398 wsgireq.headers.append((r'ETag', pycompat.sysstr(tag)))
397 res.headers['ETag'] = tag
399 res.headers['ETag'] = tag
398
400
399 if cmd not in webcommands.__all__:
401 if cmd not in webcommands.__all__:
400 msg = 'no such method: %s' % cmd
402 msg = 'no such method: %s' % cmd
401 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
403 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
402 else:
404 else:
403 # Set some globals appropriate for web handlers. Commands can
405 # Set some globals appropriate for web handlers. Commands can
404 # override easily enough.
406 # override easily enough.
405 res.status = '200 Script output follows'
407 res.status = '200 Script output follows'
406 res.headers['Content-Type'] = ctype
408 res.headers['Content-Type'] = ctype
407 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
409 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
408
410
409 if content is res:
411 if content is res:
410 return res.sendresponse()
412 return res.sendresponse()
411 elif content is True:
413 elif content is True:
412 return []
414 return []
413 else:
415 else:
414 wsgireq.respond(HTTP_OK, ctype)
416 wsgireq.respond(HTTP_OK, ctype)
415 return content
417 return content
416
418
417 except (error.LookupError, error.RepoLookupError) as err:
419 except (error.LookupError, error.RepoLookupError) as err:
418 wsgireq.respond(HTTP_NOT_FOUND, ctype)
420 wsgireq.respond(HTTP_NOT_FOUND, ctype)
419 msg = pycompat.bytestr(err)
421 msg = pycompat.bytestr(err)
420 if (util.safehasattr(err, 'name') and
422 if (util.safehasattr(err, 'name') and
421 not isinstance(err, error.ManifestLookupError)):
423 not isinstance(err, error.ManifestLookupError)):
422 msg = 'revision not found: %s' % err.name
424 msg = 'revision not found: %s' % err.name
423 return tmpl('error', error=msg)
425 return tmpl('error', error=msg)
424 except (error.RepoError, error.RevlogError) as inst:
426 except (error.RepoError, error.RevlogError) as inst:
425 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
427 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
426 return tmpl('error', error=pycompat.bytestr(inst))
428 return tmpl('error', error=pycompat.bytestr(inst))
427 except ErrorResponse as inst:
429 except ErrorResponse as inst:
428 wsgireq.respond(inst, ctype)
430 wsgireq.respond(inst, ctype)
429 if inst.code == HTTP_NOT_MODIFIED:
430 # Not allowed to return a body on a 304
431 return ['']
432 return tmpl('error', error=pycompat.bytestr(inst))
431 return tmpl('error', error=pycompat.bytestr(inst))
433
432
434 def check_perm(self, rctx, req, op):
433 def check_perm(self, rctx, req, op):
435 for permhook in permhooks:
434 for permhook in permhooks:
436 permhook(rctx, req, op)
435 permhook(rctx, req, op)
437
436
438 def getwebview(repo):
437 def getwebview(repo):
439 """The 'web.view' config controls changeset filter to hgweb. Possible
438 """The 'web.view' config controls changeset filter to hgweb. Possible
440 values are ``served``, ``visible`` and ``all``. Default is ``served``.
439 values are ``served``, ``visible`` and ``all``. Default is ``served``.
441 The ``served`` filter only shows changesets that can be pulled from the
440 The ``served`` filter only shows changesets that can be pulled from the
442 hgweb instance. The``visible`` filter includes secret changesets but
441 hgweb instance. The``visible`` filter includes secret changesets but
443 still excludes "hidden" one.
442 still excludes "hidden" one.
444
443
445 See the repoview module for details.
444 See the repoview module for details.
446
445
447 The option has been around undocumented since Mercurial 2.5, but no
446 The option has been around undocumented since Mercurial 2.5, but no
448 user ever asked about it. So we better keep it undocumented for now."""
447 user ever asked about it. So we better keep it undocumented for now."""
449 # experimental config: web.view
448 # experimental config: web.view
450 viewconfig = repo.ui.config('web', 'view', untrusted=True)
449 viewconfig = repo.ui.config('web', 'view', untrusted=True)
451 if viewconfig == 'all':
450 if viewconfig == 'all':
452 return repo.unfiltered()
451 return repo.unfiltered()
453 elif viewconfig in repoview.filtertable:
452 elif viewconfig in repoview.filtertable:
454 return repo.filtered(viewconfig)
453 return repo.filtered(viewconfig)
455 else:
454 else:
456 return repo.filtered('served')
455 return repo.filtered('served')
@@ -1,627 +1,651 b''
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import socket
12 import socket
13 import wsgiref.headers as wsgiheaders
13 import wsgiref.headers as wsgiheaders
14 #import wsgiref.validate
14 #import wsgiref.validate
15
15
16 from .common import (
16 from .common import (
17 ErrorResponse,
17 ErrorResponse,
18 HTTP_NOT_MODIFIED,
19 statusmessage,
18 statusmessage,
20 )
19 )
21
20
22 from ..thirdparty import (
21 from ..thirdparty import (
23 attr,
22 attr,
24 )
23 )
25 from .. import (
24 from .. import (
26 error,
25 error,
27 pycompat,
26 pycompat,
28 util,
27 util,
29 )
28 )
30
29
31 class multidict(object):
30 class multidict(object):
32 """A dict like object that can store multiple values for a key.
31 """A dict like object that can store multiple values for a key.
33
32
34 Used to store parsed request parameters.
33 Used to store parsed request parameters.
35
34
36 This is inspired by WebOb's class of the same name.
35 This is inspired by WebOb's class of the same name.
37 """
36 """
38 def __init__(self):
37 def __init__(self):
39 # Stores (key, value) 2-tuples. This isn't the most efficient. But we
38 # Stores (key, value) 2-tuples. This isn't the most efficient. But we
40 # don't rely on parameters that much, so it shouldn't be a perf issue.
39 # don't rely on parameters that much, so it shouldn't be a perf issue.
41 # we can always add dict for fast lookups.
40 # we can always add dict for fast lookups.
42 self._items = []
41 self._items = []
43
42
44 def __getitem__(self, key):
43 def __getitem__(self, key):
45 """Returns the last set value for a key."""
44 """Returns the last set value for a key."""
46 for k, v in reversed(self._items):
45 for k, v in reversed(self._items):
47 if k == key:
46 if k == key:
48 return v
47 return v
49
48
50 raise KeyError(key)
49 raise KeyError(key)
51
50
52 def __setitem__(self, key, value):
51 def __setitem__(self, key, value):
53 """Replace a values for a key with a new value."""
52 """Replace a values for a key with a new value."""
54 try:
53 try:
55 del self[key]
54 del self[key]
56 except KeyError:
55 except KeyError:
57 pass
56 pass
58
57
59 self._items.append((key, value))
58 self._items.append((key, value))
60
59
61 def __delitem__(self, key):
60 def __delitem__(self, key):
62 """Delete all values for a key."""
61 """Delete all values for a key."""
63 oldlen = len(self._items)
62 oldlen = len(self._items)
64
63
65 self._items[:] = [(k, v) for k, v in self._items if k != key]
64 self._items[:] = [(k, v) for k, v in self._items if k != key]
66
65
67 if oldlen == len(self._items):
66 if oldlen == len(self._items):
68 raise KeyError(key)
67 raise KeyError(key)
69
68
70 def __contains__(self, key):
69 def __contains__(self, key):
71 return any(k == key for k, v in self._items)
70 return any(k == key for k, v in self._items)
72
71
73 def __len__(self):
72 def __len__(self):
74 return len(self._items)
73 return len(self._items)
75
74
76 def get(self, key, default=None):
75 def get(self, key, default=None):
77 try:
76 try:
78 return self.__getitem__(key)
77 return self.__getitem__(key)
79 except KeyError:
78 except KeyError:
80 return default
79 return default
81
80
82 def add(self, key, value):
81 def add(self, key, value):
83 """Add a new value for a key. Does not replace existing values."""
82 """Add a new value for a key. Does not replace existing values."""
84 self._items.append((key, value))
83 self._items.append((key, value))
85
84
86 def getall(self, key):
85 def getall(self, key):
87 """Obtains all values for a key."""
86 """Obtains all values for a key."""
88 return [v for k, v in self._items if k == key]
87 return [v for k, v in self._items if k == key]
89
88
90 def getone(self, key):
89 def getone(self, key):
91 """Obtain a single value for a key.
90 """Obtain a single value for a key.
92
91
93 Raises KeyError if key not defined or it has multiple values set.
92 Raises KeyError if key not defined or it has multiple values set.
94 """
93 """
95 vals = self.getall(key)
94 vals = self.getall(key)
96
95
97 if not vals:
96 if not vals:
98 raise KeyError(key)
97 raise KeyError(key)
99
98
100 if len(vals) > 1:
99 if len(vals) > 1:
101 raise KeyError('multiple values for %r' % key)
100 raise KeyError('multiple values for %r' % key)
102
101
103 return vals[0]
102 return vals[0]
104
103
105 def asdictoflists(self):
104 def asdictoflists(self):
106 d = {}
105 d = {}
107 for k, v in self._items:
106 for k, v in self._items:
108 if k in d:
107 if k in d:
109 d[k].append(v)
108 d[k].append(v)
110 else:
109 else:
111 d[k] = [v]
110 d[k] = [v]
112
111
113 return d
112 return d
114
113
115 @attr.s(frozen=True)
114 @attr.s(frozen=True)
116 class parsedrequest(object):
115 class parsedrequest(object):
117 """Represents a parsed WSGI request.
116 """Represents a parsed WSGI request.
118
117
119 Contains both parsed parameters as well as a handle on the input stream.
118 Contains both parsed parameters as well as a handle on the input stream.
120 """
119 """
121
120
122 # Request method.
121 # Request method.
123 method = attr.ib()
122 method = attr.ib()
124 # Full URL for this request.
123 # Full URL for this request.
125 url = attr.ib()
124 url = attr.ib()
126 # URL without any path components. Just <proto>://<host><port>.
125 # URL without any path components. Just <proto>://<host><port>.
127 baseurl = attr.ib()
126 baseurl = attr.ib()
128 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
127 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
129 # of HTTP: Host header for hostname. This is likely what clients used.
128 # of HTTP: Host header for hostname. This is likely what clients used.
130 advertisedurl = attr.ib()
129 advertisedurl = attr.ib()
131 advertisedbaseurl = attr.ib()
130 advertisedbaseurl = attr.ib()
132 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
131 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
133 urlscheme = attr.ib()
132 urlscheme = attr.ib()
134 # Value of REMOTE_USER, if set, or None.
133 # Value of REMOTE_USER, if set, or None.
135 remoteuser = attr.ib()
134 remoteuser = attr.ib()
136 # Value of REMOTE_HOST, if set, or None.
135 # Value of REMOTE_HOST, if set, or None.
137 remotehost = attr.ib()
136 remotehost = attr.ib()
138 # WSGI application path.
137 # WSGI application path.
139 apppath = attr.ib()
138 apppath = attr.ib()
140 # List of path parts to be used for dispatch.
139 # List of path parts to be used for dispatch.
141 dispatchparts = attr.ib()
140 dispatchparts = attr.ib()
142 # URL path component (no query string) used for dispatch.
141 # URL path component (no query string) used for dispatch.
143 dispatchpath = attr.ib()
142 dispatchpath = attr.ib()
144 # Whether there is a path component to this request. This can be true
143 # Whether there is a path component to this request. This can be true
145 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
144 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
146 havepathinfo = attr.ib()
145 havepathinfo = attr.ib()
147 # The name of the repository being accessed.
146 # The name of the repository being accessed.
148 reponame = attr.ib()
147 reponame = attr.ib()
149 # Raw query string (part after "?" in URL).
148 # Raw query string (part after "?" in URL).
150 querystring = attr.ib()
149 querystring = attr.ib()
151 # multidict of query string parameters.
150 # multidict of query string parameters.
152 qsparams = attr.ib()
151 qsparams = attr.ib()
153 # wsgiref.headers.Headers instance. Operates like a dict with case
152 # wsgiref.headers.Headers instance. Operates like a dict with case
154 # insensitive keys.
153 # insensitive keys.
155 headers = attr.ib()
154 headers = attr.ib()
156 # Request body input stream.
155 # Request body input stream.
157 bodyfh = attr.ib()
156 bodyfh = attr.ib()
158
157
159 def parserequestfromenv(env, bodyfh):
158 def parserequestfromenv(env, bodyfh):
160 """Parse URL components from environment variables.
159 """Parse URL components from environment variables.
161
160
162 WSGI defines request attributes via environment variables. This function
161 WSGI defines request attributes via environment variables. This function
163 parses the environment variables into a data structure.
162 parses the environment variables into a data structure.
164 """
163 """
165 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
164 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
166
165
167 # We first validate that the incoming object conforms with the WSGI spec.
166 # We first validate that the incoming object conforms with the WSGI spec.
168 # We only want to be dealing with spec-conforming WSGI implementations.
167 # We only want to be dealing with spec-conforming WSGI implementations.
169 # TODO enable this once we fix internal violations.
168 # TODO enable this once we fix internal violations.
170 #wsgiref.validate.check_environ(env)
169 #wsgiref.validate.check_environ(env)
171
170
172 # PEP-0333 states that environment keys and values are native strings
171 # PEP-0333 states that environment keys and values are native strings
173 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
172 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
174 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
173 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
175 # in Mercurial, so mass convert string keys and values to bytes.
174 # in Mercurial, so mass convert string keys and values to bytes.
176 if pycompat.ispy3:
175 if pycompat.ispy3:
177 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
176 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
178 env = {k: v.encode('latin-1') if isinstance(v, str) else v
177 env = {k: v.encode('latin-1') if isinstance(v, str) else v
179 for k, v in env.iteritems()}
178 for k, v in env.iteritems()}
180
179
181 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
180 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
182 # the environment variables.
181 # the environment variables.
183 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
182 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
184 # how URLs are reconstructed.
183 # how URLs are reconstructed.
185 fullurl = env['wsgi.url_scheme'] + '://'
184 fullurl = env['wsgi.url_scheme'] + '://'
186 advertisedfullurl = fullurl
185 advertisedfullurl = fullurl
187
186
188 def addport(s):
187 def addport(s):
189 if env['wsgi.url_scheme'] == 'https':
188 if env['wsgi.url_scheme'] == 'https':
190 if env['SERVER_PORT'] != '443':
189 if env['SERVER_PORT'] != '443':
191 s += ':' + env['SERVER_PORT']
190 s += ':' + env['SERVER_PORT']
192 else:
191 else:
193 if env['SERVER_PORT'] != '80':
192 if env['SERVER_PORT'] != '80':
194 s += ':' + env['SERVER_PORT']
193 s += ':' + env['SERVER_PORT']
195
194
196 return s
195 return s
197
196
198 if env.get('HTTP_HOST'):
197 if env.get('HTTP_HOST'):
199 fullurl += env['HTTP_HOST']
198 fullurl += env['HTTP_HOST']
200 else:
199 else:
201 fullurl += env['SERVER_NAME']
200 fullurl += env['SERVER_NAME']
202 fullurl = addport(fullurl)
201 fullurl = addport(fullurl)
203
202
204 advertisedfullurl += env['SERVER_NAME']
203 advertisedfullurl += env['SERVER_NAME']
205 advertisedfullurl = addport(advertisedfullurl)
204 advertisedfullurl = addport(advertisedfullurl)
206
205
207 baseurl = fullurl
206 baseurl = fullurl
208 advertisedbaseurl = advertisedfullurl
207 advertisedbaseurl = advertisedfullurl
209
208
210 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
209 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
211 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
210 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
212 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
211 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
213 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
212 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
214
213
215 if env.get('QUERY_STRING'):
214 if env.get('QUERY_STRING'):
216 fullurl += '?' + env['QUERY_STRING']
215 fullurl += '?' + env['QUERY_STRING']
217 advertisedfullurl += '?' + env['QUERY_STRING']
216 advertisedfullurl += '?' + env['QUERY_STRING']
218
217
219 # When dispatching requests, we look at the URL components (PATH_INFO
218 # When dispatching requests, we look at the URL components (PATH_INFO
220 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
219 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
221 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
220 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
222 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
221 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
223 # root. We also exclude its path components from PATH_INFO when resolving
222 # root. We also exclude its path components from PATH_INFO when resolving
224 # the dispatch path.
223 # the dispatch path.
225
224
226 apppath = env['SCRIPT_NAME']
225 apppath = env['SCRIPT_NAME']
227
226
228 if env.get('REPO_NAME'):
227 if env.get('REPO_NAME'):
229 if not apppath.endswith('/'):
228 if not apppath.endswith('/'):
230 apppath += '/'
229 apppath += '/'
231
230
232 apppath += env.get('REPO_NAME')
231 apppath += env.get('REPO_NAME')
233
232
234 if 'PATH_INFO' in env:
233 if 'PATH_INFO' in env:
235 dispatchparts = env['PATH_INFO'].strip('/').split('/')
234 dispatchparts = env['PATH_INFO'].strip('/').split('/')
236
235
237 # Strip out repo parts.
236 # Strip out repo parts.
238 repoparts = env.get('REPO_NAME', '').split('/')
237 repoparts = env.get('REPO_NAME', '').split('/')
239 if dispatchparts[:len(repoparts)] == repoparts:
238 if dispatchparts[:len(repoparts)] == repoparts:
240 dispatchparts = dispatchparts[len(repoparts):]
239 dispatchparts = dispatchparts[len(repoparts):]
241 else:
240 else:
242 dispatchparts = []
241 dispatchparts = []
243
242
244 dispatchpath = '/'.join(dispatchparts)
243 dispatchpath = '/'.join(dispatchparts)
245
244
246 querystring = env.get('QUERY_STRING', '')
245 querystring = env.get('QUERY_STRING', '')
247
246
248 # We store as a list so we have ordering information. We also store as
247 # We store as a list so we have ordering information. We also store as
249 # a dict to facilitate fast lookup.
248 # a dict to facilitate fast lookup.
250 qsparams = multidict()
249 qsparams = multidict()
251 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
250 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
252 qsparams.add(k, v)
251 qsparams.add(k, v)
253
252
254 # HTTP_* keys contain HTTP request headers. The Headers structure should
253 # HTTP_* keys contain HTTP request headers. The Headers structure should
255 # perform case normalization for us. We just rewrite underscore to dash
254 # perform case normalization for us. We just rewrite underscore to dash
256 # so keys match what likely went over the wire.
255 # so keys match what likely went over the wire.
257 headers = []
256 headers = []
258 for k, v in env.iteritems():
257 for k, v in env.iteritems():
259 if k.startswith('HTTP_'):
258 if k.startswith('HTTP_'):
260 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
259 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
261
260
262 headers = wsgiheaders.Headers(headers)
261 headers = wsgiheaders.Headers(headers)
263
262
264 # This is kind of a lie because the HTTP header wasn't explicitly
263 # This is kind of a lie because the HTTP header wasn't explicitly
265 # sent. But for all intents and purposes it should be OK to lie about
264 # sent. But for all intents and purposes it should be OK to lie about
266 # this, since a consumer will either either value to determine how many
265 # this, since a consumer will either either value to determine how many
267 # bytes are available to read.
266 # bytes are available to read.
268 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
267 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
269 headers['Content-Length'] = env['CONTENT_LENGTH']
268 headers['Content-Length'] = env['CONTENT_LENGTH']
270
269
271 # TODO do this once we remove wsgirequest.inp, otherwise we could have
270 # TODO do this once we remove wsgirequest.inp, otherwise we could have
272 # multiple readers from the underlying input stream.
271 # multiple readers from the underlying input stream.
273 #bodyfh = env['wsgi.input']
272 #bodyfh = env['wsgi.input']
274 #if 'Content-Length' in headers:
273 #if 'Content-Length' in headers:
275 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
274 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
276
275
277 return parsedrequest(method=env['REQUEST_METHOD'],
276 return parsedrequest(method=env['REQUEST_METHOD'],
278 url=fullurl, baseurl=baseurl,
277 url=fullurl, baseurl=baseurl,
279 advertisedurl=advertisedfullurl,
278 advertisedurl=advertisedfullurl,
280 advertisedbaseurl=advertisedbaseurl,
279 advertisedbaseurl=advertisedbaseurl,
281 urlscheme=env['wsgi.url_scheme'],
280 urlscheme=env['wsgi.url_scheme'],
282 remoteuser=env.get('REMOTE_USER'),
281 remoteuser=env.get('REMOTE_USER'),
283 remotehost=env.get('REMOTE_HOST'),
282 remotehost=env.get('REMOTE_HOST'),
284 apppath=apppath,
283 apppath=apppath,
285 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
284 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
286 havepathinfo='PATH_INFO' in env,
285 havepathinfo='PATH_INFO' in env,
287 reponame=env.get('REPO_NAME'),
286 reponame=env.get('REPO_NAME'),
288 querystring=querystring,
287 querystring=querystring,
289 qsparams=qsparams,
288 qsparams=qsparams,
290 headers=headers,
289 headers=headers,
291 bodyfh=bodyfh)
290 bodyfh=bodyfh)
292
291
293 class offsettrackingwriter(object):
292 class offsettrackingwriter(object):
294 """A file object like object that is append only and tracks write count.
293 """A file object like object that is append only and tracks write count.
295
294
296 Instances are bound to a callable. This callable is called with data
295 Instances are bound to a callable. This callable is called with data
297 whenever a ``write()`` is attempted.
296 whenever a ``write()`` is attempted.
298
297
299 Instances track the amount of written data so they can answer ``tell()``
298 Instances track the amount of written data so they can answer ``tell()``
300 requests.
299 requests.
301
300
302 The intent of this class is to wrap the ``write()`` function returned by
301 The intent of this class is to wrap the ``write()`` function returned by
303 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
302 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
304 not a file object, it doesn't implement other file object methods.
303 not a file object, it doesn't implement other file object methods.
305 """
304 """
306 def __init__(self, writefn):
305 def __init__(self, writefn):
307 self._write = writefn
306 self._write = writefn
308 self._offset = 0
307 self._offset = 0
309
308
310 def write(self, s):
309 def write(self, s):
311 res = self._write(s)
310 res = self._write(s)
312 # Some Python objects don't report the number of bytes written.
311 # Some Python objects don't report the number of bytes written.
313 if res is None:
312 if res is None:
314 self._offset += len(s)
313 self._offset += len(s)
315 else:
314 else:
316 self._offset += res
315 self._offset += res
317
316
318 def flush(self):
317 def flush(self):
319 pass
318 pass
320
319
321 def tell(self):
320 def tell(self):
322 return self._offset
321 return self._offset
323
322
324 class wsgiresponse(object):
323 class wsgiresponse(object):
325 """Represents a response to a WSGI request.
324 """Represents a response to a WSGI request.
326
325
327 A response consists of a status line, headers, and a body.
326 A response consists of a status line, headers, and a body.
328
327
329 Consumers must populate the ``status`` and ``headers`` fields and
328 Consumers must populate the ``status`` and ``headers`` fields and
330 make a call to a ``setbody*()`` method before the response can be
329 make a call to a ``setbody*()`` method before the response can be
331 issued.
330 issued.
332
331
333 When it is time to start sending the response over the wire,
332 When it is time to start sending the response over the wire,
334 ``sendresponse()`` is called. It handles emitting the header portion
333 ``sendresponse()`` is called. It handles emitting the header portion
335 of the response message. It then yields chunks of body data to be
334 of the response message. It then yields chunks of body data to be
336 written to the peer. Typically, the WSGI application itself calls
335 written to the peer. Typically, the WSGI application itself calls
337 and returns the value from ``sendresponse()``.
336 and returns the value from ``sendresponse()``.
338 """
337 """
339
338
340 def __init__(self, req, startresponse):
339 def __init__(self, req, startresponse):
341 """Create an empty response tied to a specific request.
340 """Create an empty response tied to a specific request.
342
341
343 ``req`` is a ``parsedrequest``. ``startresponse`` is the
342 ``req`` is a ``parsedrequest``. ``startresponse`` is the
344 ``start_response`` function passed to the WSGI application.
343 ``start_response`` function passed to the WSGI application.
345 """
344 """
346 self._req = req
345 self._req = req
347 self._startresponse = startresponse
346 self._startresponse = startresponse
348
347
349 self.status = None
348 self.status = None
350 self.headers = wsgiheaders.Headers([])
349 self.headers = wsgiheaders.Headers([])
351
350
352 self._bodybytes = None
351 self._bodybytes = None
353 self._bodygen = None
352 self._bodygen = None
354 self._bodywillwrite = False
353 self._bodywillwrite = False
355 self._started = False
354 self._started = False
356 self._bodywritefn = None
355 self._bodywritefn = None
357
356
358 def _verifybody(self):
357 def _verifybody(self):
359 if (self._bodybytes is not None or self._bodygen is not None
358 if (self._bodybytes is not None or self._bodygen is not None
360 or self._bodywillwrite):
359 or self._bodywillwrite):
361 raise error.ProgrammingError('cannot define body multiple times')
360 raise error.ProgrammingError('cannot define body multiple times')
362
361
363 def setbodybytes(self, b):
362 def setbodybytes(self, b):
364 """Define the response body as static bytes."""
363 """Define the response body as static bytes.
364
365 The empty string signals that there is no response body.
366 """
365 self._verifybody()
367 self._verifybody()
366 self._bodybytes = b
368 self._bodybytes = b
367 self.headers['Content-Length'] = '%d' % len(b)
369 self.headers['Content-Length'] = '%d' % len(b)
368
370
369 def setbodygen(self, gen):
371 def setbodygen(self, gen):
370 """Define the response body as a generator of bytes."""
372 """Define the response body as a generator of bytes."""
371 self._verifybody()
373 self._verifybody()
372 self._bodygen = gen
374 self._bodygen = gen
373
375
374 def setbodywillwrite(self):
376 def setbodywillwrite(self):
375 """Signal an intent to use write() to emit the response body.
377 """Signal an intent to use write() to emit the response body.
376
378
377 **This is the least preferred way to send a body.**
379 **This is the least preferred way to send a body.**
378
380
379 It is preferred for WSGI applications to emit a generator of chunks
381 It is preferred for WSGI applications to emit a generator of chunks
380 constituting the response body. However, some consumers can't emit
382 constituting the response body. However, some consumers can't emit
381 data this way. So, WSGI provides a way to obtain a ``write(data)``
383 data this way. So, WSGI provides a way to obtain a ``write(data)``
382 function that can be used to synchronously perform an unbuffered
384 function that can be used to synchronously perform an unbuffered
383 write.
385 write.
384
386
385 Calling this function signals an intent to produce the body in this
387 Calling this function signals an intent to produce the body in this
386 manner.
388 manner.
387 """
389 """
388 self._verifybody()
390 self._verifybody()
389 self._bodywillwrite = True
391 self._bodywillwrite = True
390
392
391 def sendresponse(self):
393 def sendresponse(self):
392 """Send the generated response to the client.
394 """Send the generated response to the client.
393
395
394 Before this is called, ``status`` must be set and one of
396 Before this is called, ``status`` must be set and one of
395 ``setbodybytes()`` or ``setbodygen()`` must be called.
397 ``setbodybytes()`` or ``setbodygen()`` must be called.
396
398
397 Calling this method multiple times is not allowed.
399 Calling this method multiple times is not allowed.
398 """
400 """
399 if self._started:
401 if self._started:
400 raise error.ProgrammingError('sendresponse() called multiple times')
402 raise error.ProgrammingError('sendresponse() called multiple times')
401
403
402 self._started = True
404 self._started = True
403
405
404 if not self.status:
406 if not self.status:
405 raise error.ProgrammingError('status line not defined')
407 raise error.ProgrammingError('status line not defined')
406
408
407 if (self._bodybytes is None and self._bodygen is None
409 if (self._bodybytes is None and self._bodygen is None
408 and not self._bodywillwrite):
410 and not self._bodywillwrite):
409 raise error.ProgrammingError('response body not defined')
411 raise error.ProgrammingError('response body not defined')
410
412
413 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
414 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
415 # and SHOULD NOT generate other headers unless they could be used
416 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
417 # states that no response body can be issued. Content-Length can
418 # be sent. But if it is present, it should be the size of the response
419 # that wasn't transferred.
420 if self.status.startswith('304 '):
421 # setbodybytes('') will set C-L to 0. This doesn't conform with the
422 # spec. So remove it.
423 if self.headers.get('Content-Length') == '0':
424 del self.headers['Content-Length']
425
426 # Strictly speaking, this is too strict. But until it causes
427 # problems, let's be strict.
428 badheaders = {k for k in self.headers.keys()
429 if k.lower() not in ('date', 'etag', 'expires',
430 'cache-control',
431 'content-location',
432 'vary')}
433 if badheaders:
434 raise error.ProgrammingError(
435 'illegal header on 304 response: %s' %
436 ', '.join(sorted(badheaders)))
437
438 if self._bodygen is not None or self._bodywillwrite:
439 raise error.ProgrammingError("must use setbodybytes('') with "
440 "304 responses")
441
411 # Various HTTP clients (notably httplib) won't read the HTTP response
442 # Various HTTP clients (notably httplib) won't read the HTTP response
412 # until the HTTP request has been sent in full. If servers (us) send a
443 # until the HTTP request has been sent in full. If servers (us) send a
413 # response before the HTTP request has been fully sent, the connection
444 # response before the HTTP request has been fully sent, the connection
414 # may deadlock because neither end is reading.
445 # may deadlock because neither end is reading.
415 #
446 #
416 # We work around this by "draining" the request data before
447 # We work around this by "draining" the request data before
417 # sending any response in some conditions.
448 # sending any response in some conditions.
418 drain = False
449 drain = False
419 close = False
450 close = False
420
451
421 # If the client sent Expect: 100-continue, we assume it is smart enough
452 # If the client sent Expect: 100-continue, we assume it is smart enough
422 # to deal with the server sending a response before reading the request.
453 # to deal with the server sending a response before reading the request.
423 # (httplib doesn't do this.)
454 # (httplib doesn't do this.)
424 if self._req.headers.get('Expect', '').lower() == '100-continue':
455 if self._req.headers.get('Expect', '').lower() == '100-continue':
425 pass
456 pass
426 # Only tend to request methods that have bodies. Strictly speaking,
457 # Only tend to request methods that have bodies. Strictly speaking,
427 # we should sniff for a body. But this is fine for our existing
458 # we should sniff for a body. But this is fine for our existing
428 # WSGI applications.
459 # WSGI applications.
429 elif self._req.method not in ('POST', 'PUT'):
460 elif self._req.method not in ('POST', 'PUT'):
430 pass
461 pass
431 else:
462 else:
432 # If we don't know how much data to read, there's no guarantee
463 # If we don't know how much data to read, there's no guarantee
433 # that we can drain the request responsibly. The WSGI
464 # that we can drain the request responsibly. The WSGI
434 # specification only says that servers *should* ensure the
465 # specification only says that servers *should* ensure the
435 # input stream doesn't overrun the actual request. So there's
466 # input stream doesn't overrun the actual request. So there's
436 # no guarantee that reading until EOF won't corrupt the stream
467 # no guarantee that reading until EOF won't corrupt the stream
437 # state.
468 # state.
438 if not isinstance(self._req.bodyfh, util.cappedreader):
469 if not isinstance(self._req.bodyfh, util.cappedreader):
439 close = True
470 close = True
440 else:
471 else:
441 # We /could/ only drain certain HTTP response codes. But 200 and
472 # We /could/ only drain certain HTTP response codes. But 200 and
442 # non-200 wire protocol responses both require draining. Since
473 # non-200 wire protocol responses both require draining. Since
443 # we have a capped reader in place for all situations where we
474 # we have a capped reader in place for all situations where we
444 # drain, it is safe to read from that stream. We'll either do
475 # drain, it is safe to read from that stream. We'll either do
445 # a drain or no-op if we're already at EOF.
476 # a drain or no-op if we're already at EOF.
446 drain = True
477 drain = True
447
478
448 if close:
479 if close:
449 self.headers['Connection'] = 'Close'
480 self.headers['Connection'] = 'Close'
450
481
451 if drain:
482 if drain:
452 assert isinstance(self._req.bodyfh, util.cappedreader)
483 assert isinstance(self._req.bodyfh, util.cappedreader)
453 while True:
484 while True:
454 chunk = self._req.bodyfh.read(32768)
485 chunk = self._req.bodyfh.read(32768)
455 if not chunk:
486 if not chunk:
456 break
487 break
457
488
458 write = self._startresponse(pycompat.sysstr(self.status),
489 write = self._startresponse(pycompat.sysstr(self.status),
459 self.headers.items())
490 self.headers.items())
460
491
461 if self._bodybytes:
492 if self._bodybytes:
462 yield self._bodybytes
493 yield self._bodybytes
463 elif self._bodygen:
494 elif self._bodygen:
464 for chunk in self._bodygen:
495 for chunk in self._bodygen:
465 yield chunk
496 yield chunk
466 elif self._bodywillwrite:
497 elif self._bodywillwrite:
467 self._bodywritefn = write
498 self._bodywritefn = write
468 else:
499 else:
469 error.ProgrammingError('do not know how to send body')
500 error.ProgrammingError('do not know how to send body')
470
501
471 def getbodyfile(self):
502 def getbodyfile(self):
472 """Obtain a file object like object representing the response body.
503 """Obtain a file object like object representing the response body.
473
504
474 For this to work, you must call ``setbodywillwrite()`` and then
505 For this to work, you must call ``setbodywillwrite()`` and then
475 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
506 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
476 function won't run to completion unless the generator is advanced. The
507 function won't run to completion unless the generator is advanced. The
477 generator yields not items. The easiest way to consume it is with
508 generator yields not items. The easiest way to consume it is with
478 ``list(res.sendresponse())``, which should resolve to an empty list -
509 ``list(res.sendresponse())``, which should resolve to an empty list -
479 ``[]``.
510 ``[]``.
480 """
511 """
481 if not self._bodywillwrite:
512 if not self._bodywillwrite:
482 raise error.ProgrammingError('must call setbodywillwrite() first')
513 raise error.ProgrammingError('must call setbodywillwrite() first')
483
514
484 if not self._started:
515 if not self._started:
485 raise error.ProgrammingError('must call sendresponse() first; did '
516 raise error.ProgrammingError('must call sendresponse() first; did '
486 'you remember to consume it since it '
517 'you remember to consume it since it '
487 'is a generator?')
518 'is a generator?')
488
519
489 assert self._bodywritefn
520 assert self._bodywritefn
490 return offsettrackingwriter(self._bodywritefn)
521 return offsettrackingwriter(self._bodywritefn)
491
522
492 class wsgirequest(object):
523 class wsgirequest(object):
493 """Higher-level API for a WSGI request.
524 """Higher-level API for a WSGI request.
494
525
495 WSGI applications are invoked with 2 arguments. They are used to
526 WSGI applications are invoked with 2 arguments. They are used to
496 instantiate instances of this class, which provides higher-level APIs
527 instantiate instances of this class, which provides higher-level APIs
497 for obtaining request parameters, writing HTTP output, etc.
528 for obtaining request parameters, writing HTTP output, etc.
498 """
529 """
499 def __init__(self, wsgienv, start_response):
530 def __init__(self, wsgienv, start_response):
500 version = wsgienv[r'wsgi.version']
531 version = wsgienv[r'wsgi.version']
501 if (version < (1, 0)) or (version >= (2, 0)):
532 if (version < (1, 0)) or (version >= (2, 0)):
502 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
533 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
503 % version)
534 % version)
504
535
505 inp = wsgienv[r'wsgi.input']
536 inp = wsgienv[r'wsgi.input']
506
537
507 if r'HTTP_CONTENT_LENGTH' in wsgienv:
538 if r'HTTP_CONTENT_LENGTH' in wsgienv:
508 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
539 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
509 elif r'CONTENT_LENGTH' in wsgienv:
540 elif r'CONTENT_LENGTH' in wsgienv:
510 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
541 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
511
542
512 self.err = wsgienv[r'wsgi.errors']
543 self.err = wsgienv[r'wsgi.errors']
513 self.threaded = wsgienv[r'wsgi.multithread']
544 self.threaded = wsgienv[r'wsgi.multithread']
514 self.multiprocess = wsgienv[r'wsgi.multiprocess']
545 self.multiprocess = wsgienv[r'wsgi.multiprocess']
515 self.run_once = wsgienv[r'wsgi.run_once']
546 self.run_once = wsgienv[r'wsgi.run_once']
516 self.env = wsgienv
547 self.env = wsgienv
517 self.req = parserequestfromenv(wsgienv, inp)
548 self.req = parserequestfromenv(wsgienv, inp)
518 self.res = wsgiresponse(self.req, start_response)
549 self.res = wsgiresponse(self.req, start_response)
519 self._start_response = start_response
550 self._start_response = start_response
520 self.server_write = None
551 self.server_write = None
521 self.headers = []
552 self.headers = []
522
553
523 def respond(self, status, type, filename=None, body=None):
554 def respond(self, status, type, filename=None, body=None):
524 if not isinstance(type, str):
555 if not isinstance(type, str):
525 type = pycompat.sysstr(type)
556 type = pycompat.sysstr(type)
526 if self._start_response is not None:
557 if self._start_response is not None:
527 self.headers.append((r'Content-Type', type))
558 self.headers.append((r'Content-Type', type))
528 if filename:
559 if filename:
529 filename = (filename.rpartition('/')[-1]
560 filename = (filename.rpartition('/')[-1]
530 .replace('\\', '\\\\').replace('"', '\\"'))
561 .replace('\\', '\\\\').replace('"', '\\"'))
531 self.headers.append(('Content-Disposition',
562 self.headers.append(('Content-Disposition',
532 'inline; filename="%s"' % filename))
563 'inline; filename="%s"' % filename))
533 if body is not None:
564 if body is not None:
534 self.headers.append((r'Content-Length', str(len(body))))
565 self.headers.append((r'Content-Length', str(len(body))))
535
566
536 for k, v in self.headers:
567 for k, v in self.headers:
537 if not isinstance(v, str):
568 if not isinstance(v, str):
538 raise TypeError('header value must be string: %r' % (v,))
569 raise TypeError('header value must be string: %r' % (v,))
539
570
540 if isinstance(status, ErrorResponse):
571 if isinstance(status, ErrorResponse):
541 self.headers.extend(status.headers)
572 self.headers.extend(status.headers)
542 if status.code == HTTP_NOT_MODIFIED:
543 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
544 # it MUST NOT include any headers other than these and no
545 # body
546 self.headers = [(k, v) for (k, v) in self.headers if
547 k in ('Date', 'ETag', 'Expires',
548 'Cache-Control', 'Vary')]
549 status = statusmessage(status.code, pycompat.bytestr(status))
573 status = statusmessage(status.code, pycompat.bytestr(status))
550 elif status == 200:
574 elif status == 200:
551 status = '200 Script output follows'
575 status = '200 Script output follows'
552 elif isinstance(status, int):
576 elif isinstance(status, int):
553 status = statusmessage(status)
577 status = statusmessage(status)
554
578
555 # Various HTTP clients (notably httplib) won't read the HTTP
579 # Various HTTP clients (notably httplib) won't read the HTTP
556 # response until the HTTP request has been sent in full. If servers
580 # response until the HTTP request has been sent in full. If servers
557 # (us) send a response before the HTTP request has been fully sent,
581 # (us) send a response before the HTTP request has been fully sent,
558 # the connection may deadlock because neither end is reading.
582 # the connection may deadlock because neither end is reading.
559 #
583 #
560 # We work around this by "draining" the request data before
584 # We work around this by "draining" the request data before
561 # sending any response in some conditions.
585 # sending any response in some conditions.
562 drain = False
586 drain = False
563 close = False
587 close = False
564
588
565 # If the client sent Expect: 100-continue, we assume it is smart
589 # If the client sent Expect: 100-continue, we assume it is smart
566 # enough to deal with the server sending a response before reading
590 # enough to deal with the server sending a response before reading
567 # the request. (httplib doesn't do this.)
591 # the request. (httplib doesn't do this.)
568 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
592 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
569 pass
593 pass
570 # Only tend to request methods that have bodies. Strictly speaking,
594 # Only tend to request methods that have bodies. Strictly speaking,
571 # we should sniff for a body. But this is fine for our existing
595 # we should sniff for a body. But this is fine for our existing
572 # WSGI applications.
596 # WSGI applications.
573 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
597 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
574 pass
598 pass
575 else:
599 else:
576 # If we don't know how much data to read, there's no guarantee
600 # If we don't know how much data to read, there's no guarantee
577 # that we can drain the request responsibly. The WSGI
601 # that we can drain the request responsibly. The WSGI
578 # specification only says that servers *should* ensure the
602 # specification only says that servers *should* ensure the
579 # input stream doesn't overrun the actual request. So there's
603 # input stream doesn't overrun the actual request. So there's
580 # no guarantee that reading until EOF won't corrupt the stream
604 # no guarantee that reading until EOF won't corrupt the stream
581 # state.
605 # state.
582 if not isinstance(self.req.bodyfh, util.cappedreader):
606 if not isinstance(self.req.bodyfh, util.cappedreader):
583 close = True
607 close = True
584 else:
608 else:
585 # We /could/ only drain certain HTTP response codes. But 200
609 # We /could/ only drain certain HTTP response codes. But 200
586 # and non-200 wire protocol responses both require draining.
610 # and non-200 wire protocol responses both require draining.
587 # Since we have a capped reader in place for all situations
611 # Since we have a capped reader in place for all situations
588 # where we drain, it is safe to read from that stream. We'll
612 # where we drain, it is safe to read from that stream. We'll
589 # either do a drain or no-op if we're already at EOF.
613 # either do a drain or no-op if we're already at EOF.
590 drain = True
614 drain = True
591
615
592 if close:
616 if close:
593 self.headers.append((r'Connection', r'Close'))
617 self.headers.append((r'Connection', r'Close'))
594
618
595 if drain:
619 if drain:
596 assert isinstance(self.req.bodyfh, util.cappedreader)
620 assert isinstance(self.req.bodyfh, util.cappedreader)
597 while True:
621 while True:
598 chunk = self.req.bodyfh.read(32768)
622 chunk = self.req.bodyfh.read(32768)
599 if not chunk:
623 if not chunk:
600 break
624 break
601
625
602 self.server_write = self._start_response(
626 self.server_write = self._start_response(
603 pycompat.sysstr(status), self.headers)
627 pycompat.sysstr(status), self.headers)
604 self._start_response = None
628 self._start_response = None
605 self.headers = []
629 self.headers = []
606 if body is not None:
630 if body is not None:
607 self.write(body)
631 self.write(body)
608 self.server_write = None
632 self.server_write = None
609
633
610 def write(self, thing):
634 def write(self, thing):
611 if thing:
635 if thing:
612 try:
636 try:
613 self.server_write(thing)
637 self.server_write(thing)
614 except socket.error as inst:
638 except socket.error as inst:
615 if inst[0] != errno.ECONNRESET:
639 if inst[0] != errno.ECONNRESET:
616 raise
640 raise
617
641
618 def flush(self):
642 def flush(self):
619 return None
643 return None
620
644
621 def wsgiapplication(app_maker):
645 def wsgiapplication(app_maker):
622 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
646 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
623 can and should now be used as a WSGI application.'''
647 can and should now be used as a WSGI application.'''
624 application = app_maker()
648 application = app_maker()
625 def run_wsgi(env, respond):
649 def run_wsgi(env, respond):
626 return application(env, respond)
650 return application(env, respond)
627 return run_wsgi
651 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now