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