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