##// END OF EJS Templates
hgweb: parse and store HTTP request headers...
Gregory Szorc -
r36832:f9078c6c default
parent child Browse files
Show More
@@ -1,438 +1,438 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 fromreq = req.form.get('style', [None])[0]
56 fromreq = req.form.get('style', [None])[0]
57 styles = (
57 styles = (
58 fromreq,
58 fromreq,
59 configfn('web', 'style'),
59 configfn('web', 'style'),
60 'paper',
60 'paper',
61 )
61 )
62 return styles, templater.stylemap(styles, templatepath)
62 return styles, templater.stylemap(styles, templatepath)
63
63
64 def makebreadcrumb(url, prefix=''):
64 def makebreadcrumb(url, prefix=''):
65 '''Return a 'URL breadcrumb' list
65 '''Return a 'URL breadcrumb' list
66
66
67 A 'URL breadcrumb' is a list of URL-name pairs,
67 A 'URL breadcrumb' is a list of URL-name pairs,
68 corresponding to each of the path items on a URL.
68 corresponding to each of the path items on a URL.
69 This can be used to create path navigation entries.
69 This can be used to create path navigation entries.
70 '''
70 '''
71 if url.endswith('/'):
71 if url.endswith('/'):
72 url = url[:-1]
72 url = url[:-1]
73 if prefix:
73 if prefix:
74 url = '/' + prefix + url
74 url = '/' + prefix + url
75 relpath = url
75 relpath = url
76 if relpath.startswith('/'):
76 if relpath.startswith('/'):
77 relpath = relpath[1:]
77 relpath = relpath[1:]
78
78
79 breadcrumb = []
79 breadcrumb = []
80 urlel = url
80 urlel = url
81 pathitems = [''] + relpath.split('/')
81 pathitems = [''] + relpath.split('/')
82 for pathel in reversed(pathitems):
82 for pathel in reversed(pathitems):
83 if not pathel or not urlel:
83 if not pathel or not urlel:
84 break
84 break
85 breadcrumb.append({'url': urlel, 'name': pathel})
85 breadcrumb.append({'url': urlel, 'name': pathel})
86 urlel = os.path.dirname(urlel)
86 urlel = os.path.dirname(urlel)
87 return reversed(breadcrumb)
87 return reversed(breadcrumb)
88
88
89 class requestcontext(object):
89 class requestcontext(object):
90 """Holds state/context for an individual request.
90 """Holds state/context for an individual request.
91
91
92 Servers can be multi-threaded. Holding state on the WSGI application
92 Servers can be multi-threaded. Holding state on the WSGI application
93 is prone to race conditions. Instances of this class exist to hold
93 is prone to race conditions. Instances of this class exist to hold
94 mutable and race-free state for requests.
94 mutable and race-free state for requests.
95 """
95 """
96 def __init__(self, app, repo):
96 def __init__(self, app, repo):
97 self.repo = repo
97 self.repo = repo
98 self.reponame = app.reponame
98 self.reponame = app.reponame
99
99
100 self.archivespecs = archivespecs
100 self.archivespecs = archivespecs
101
101
102 self.maxchanges = self.configint('web', 'maxchanges')
102 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
103 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
105 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
106 self.allowpull = self.configbool('web', 'allow-pull')
107
107
108 # we use untrusted=False to prevent a repo owner from using
108 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
109 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
110 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
111 self.templatepath = self.config('web', 'templates', untrusted=False)
112
112
113 # This object is more expensive to build than simple config values.
113 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
114 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
115 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
116 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
117 # of the request.
118 self.websubtable = app.websubtable
118 self.websubtable = app.websubtable
119
119
120 self.csp, self.nonce = cspvalues(self.repo.ui)
120 self.csp, self.nonce = cspvalues(self.repo.ui)
121
121
122 # Trust the settings from the .hg/hgrc files by default.
122 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
124 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
125 untrusted=untrusted)
126
126
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
128 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
129 untrusted=untrusted)
130
130
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
132 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
133 untrusted=untrusted)
134
134
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
136 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
137 untrusted=untrusted)
138
138
139 def archivelist(self, nodeid):
139 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
144
145 def templater(self, wsgireq, req):
145 def templater(self, wsgireq, req):
146 # determine scheme, port and server name
146 # determine scheme, port and server name
147 # this is needed to create absolute urls
147 # this is needed to create absolute urls
148 logourl = self.config('web', 'logourl')
148 logourl = self.config('web', 'logourl')
149 logoimg = self.config('web', 'logoimg')
149 logoimg = self.config('web', 'logoimg')
150 staticurl = (self.config('web', 'staticurl')
150 staticurl = (self.config('web', 'staticurl')
151 or req.apppath + '/static/')
151 or req.apppath + '/static/')
152 if not staticurl.endswith('/'):
152 if not staticurl.endswith('/'):
153 staticurl += '/'
153 staticurl += '/'
154
154
155 # some functions for the templater
155 # some functions for the templater
156
156
157 def motd(**map):
157 def motd(**map):
158 yield self.config('web', 'motd')
158 yield self.config('web', 'motd')
159
159
160 # figure out which style to use
160 # figure out which style to use
161
161
162 vars = {}
162 vars = {}
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
164 self.templatepath)
164 self.templatepath)
165 if style == styles[0]:
165 if style == styles[0]:
166 vars['style'] = style
166 vars['style'] = style
167
167
168 sessionvars = webutil.sessionvars(vars, '?')
168 sessionvars = webutil.sessionvars(vars, '?')
169
169
170 if not self.reponame:
170 if not self.reponame:
171 self.reponame = (self.config('web', 'name', '')
171 self.reponame = (self.config('web', 'name', '')
172 or wsgireq.env.get('REPO_NAME')
172 or wsgireq.env.get('REPO_NAME')
173 or req.apppath or self.repo.root)
173 or req.apppath or self.repo.root)
174
174
175 def websubfilter(text):
175 def websubfilter(text):
176 return templatefilters.websub(text, self.websubtable)
176 return templatefilters.websub(text, self.websubtable)
177
177
178 # create the templater
178 # create the templater
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 defaults = {
180 defaults = {
181 'url': req.apppath + '/',
181 'url': req.apppath + '/',
182 'logourl': logourl,
182 'logourl': logourl,
183 'logoimg': logoimg,
183 'logoimg': logoimg,
184 'staticurl': staticurl,
184 'staticurl': staticurl,
185 'urlbase': req.advertisedbaseurl,
185 'urlbase': req.advertisedbaseurl,
186 'repo': self.reponame,
186 'repo': self.reponame,
187 'encoding': encoding.encoding,
187 'encoding': encoding.encoding,
188 'motd': motd,
188 'motd': motd,
189 'sessionvars': sessionvars,
189 'sessionvars': sessionvars,
190 'pathdef': makebreadcrumb(req.apppath),
190 'pathdef': makebreadcrumb(req.apppath),
191 'style': style,
191 'style': style,
192 'nonce': self.nonce,
192 'nonce': self.nonce,
193 }
193 }
194 tres = formatter.templateresources(self.repo.ui, self.repo)
194 tres = formatter.templateresources(self.repo.ui, self.repo)
195 tmpl = templater.templater.frommapfile(mapfile,
195 tmpl = templater.templater.frommapfile(mapfile,
196 filters={'websub': websubfilter},
196 filters={'websub': websubfilter},
197 defaults=defaults,
197 defaults=defaults,
198 resources=tres)
198 resources=tres)
199 return tmpl
199 return tmpl
200
200
201
201
202 class hgweb(object):
202 class hgweb(object):
203 """HTTP server for individual repositories.
203 """HTTP server for individual repositories.
204
204
205 Instances of this class serve HTTP responses for a particular
205 Instances of this class serve HTTP responses for a particular
206 repository.
206 repository.
207
207
208 Instances are typically used as WSGI applications.
208 Instances are typically used as WSGI applications.
209
209
210 Some servers are multi-threaded. On these servers, there may
210 Some servers are multi-threaded. On these servers, there may
211 be multiple active threads inside __call__.
211 be multiple active threads inside __call__.
212 """
212 """
213 def __init__(self, repo, name=None, baseui=None):
213 def __init__(self, repo, name=None, baseui=None):
214 if isinstance(repo, str):
214 if isinstance(repo, str):
215 if baseui:
215 if baseui:
216 u = baseui.copy()
216 u = baseui.copy()
217 else:
217 else:
218 u = uimod.ui.load()
218 u = uimod.ui.load()
219 r = hg.repository(u, repo)
219 r = hg.repository(u, repo)
220 else:
220 else:
221 # we trust caller to give us a private copy
221 # we trust caller to give us a private copy
222 r = repo
222 r = repo
223
223
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 # resolve file patterns relative to repo root
228 # resolve file patterns relative to repo root
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 # displaying bundling progress bar while serving feel wrong and may
231 # displaying bundling progress bar while serving feel wrong and may
232 # break some wsgi implementation.
232 # break some wsgi implementation.
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 self._lastrepo = self._repos[0]
236 self._lastrepo = self._repos[0]
237 hook.redirect(True)
237 hook.redirect(True)
238 self.reponame = name
238 self.reponame = name
239
239
240 def _webifyrepo(self, repo):
240 def _webifyrepo(self, repo):
241 repo = getwebview(repo)
241 repo = getwebview(repo)
242 self.websubtable = webutil.getwebsubs(repo)
242 self.websubtable = webutil.getwebsubs(repo)
243 return repo
243 return repo
244
244
245 @contextlib.contextmanager
245 @contextlib.contextmanager
246 def _obtainrepo(self):
246 def _obtainrepo(self):
247 """Obtain a repo unique to the caller.
247 """Obtain a repo unique to the caller.
248
248
249 Internally we maintain a stack of cachedlocalrepo instances
249 Internally we maintain a stack of cachedlocalrepo instances
250 to be handed out. If one is available, we pop it and return it,
250 to be handed out. If one is available, we pop it and return it,
251 ensuring it is up to date in the process. If one is not available,
251 ensuring it is up to date in the process. If one is not available,
252 we clone the most recently used repo instance and return it.
252 we clone the most recently used repo instance and return it.
253
253
254 It is currently possible for the stack to grow without bounds
254 It is currently possible for the stack to grow without bounds
255 if the server allows infinite threads. However, servers should
255 if the server allows infinite threads. However, servers should
256 have a thread limit, thus establishing our limit.
256 have a thread limit, thus establishing our limit.
257 """
257 """
258 if self._repos:
258 if self._repos:
259 cached = self._repos.pop()
259 cached = self._repos.pop()
260 r, created = cached.fetch()
260 r, created = cached.fetch()
261 else:
261 else:
262 cached = self._lastrepo.copy()
262 cached = self._lastrepo.copy()
263 r, created = cached.fetch()
263 r, created = cached.fetch()
264 if created:
264 if created:
265 r = self._webifyrepo(r)
265 r = self._webifyrepo(r)
266
266
267 self._lastrepo = cached
267 self._lastrepo = cached
268 self.mtime = cached.mtime
268 self.mtime = cached.mtime
269 try:
269 try:
270 yield r
270 yield r
271 finally:
271 finally:
272 self._repos.append(cached)
272 self._repos.append(cached)
273
273
274 def run(self):
274 def run(self):
275 """Start a server from CGI environment.
275 """Start a server from CGI environment.
276
276
277 Modern servers should be using WSGI and should avoid this
277 Modern servers should be using WSGI and should avoid this
278 method, if possible.
278 method, if possible.
279 """
279 """
280 if not encoding.environ.get('GATEWAY_INTERFACE',
280 if not encoding.environ.get('GATEWAY_INTERFACE',
281 '').startswith("CGI/1."):
281 '').startswith("CGI/1."):
282 raise RuntimeError("This function is only intended to be "
282 raise RuntimeError("This function is only intended to be "
283 "called while running as a CGI script.")
283 "called while running as a CGI script.")
284 wsgicgi.launch(self)
284 wsgicgi.launch(self)
285
285
286 def __call__(self, env, respond):
286 def __call__(self, env, respond):
287 """Run the WSGI application.
287 """Run the WSGI application.
288
288
289 This may be called by multiple threads.
289 This may be called by multiple threads.
290 """
290 """
291 req = requestmod.wsgirequest(env, respond)
291 req = requestmod.wsgirequest(env, respond)
292 return self.run_wsgi(req)
292 return self.run_wsgi(req)
293
293
294 def run_wsgi(self, wsgireq):
294 def run_wsgi(self, wsgireq):
295 """Internal method to run the WSGI application.
295 """Internal method to run the WSGI application.
296
296
297 This is typically only called by Mercurial. External consumers
297 This is typically only called by Mercurial. External consumers
298 should be using instances of this class as the WSGI application.
298 should be using instances of this class as the WSGI application.
299 """
299 """
300 with self._obtainrepo() as repo:
300 with self._obtainrepo() as repo:
301 profile = repo.ui.configbool('profiling', 'enabled')
301 profile = repo.ui.configbool('profiling', 'enabled')
302 with profiling.profile(repo.ui, enabled=profile):
302 with profiling.profile(repo.ui, enabled=profile):
303 for r in self._runwsgi(wsgireq, repo):
303 for r in self._runwsgi(wsgireq, repo):
304 yield r
304 yield r
305
305
306 def _runwsgi(self, wsgireq, repo):
306 def _runwsgi(self, wsgireq, repo):
307 req = requestmod.parserequestfromenv(wsgireq.env)
307 req = requestmod.parserequestfromenv(wsgireq.env)
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
320
321 handled, res = wireprotoserver.handlewsgirequest(
321 handled, res = wireprotoserver.handlewsgirequest(
322 rctx, wsgireq, req, self.check_perm)
322 rctx, wsgireq, req, self.check_perm)
323 if handled:
323 if handled:
324 return res
324 return res
325
325
326 if req.havepathinfo:
326 if req.havepathinfo:
327 query = req.dispatchpath
327 query = req.dispatchpath
328 else:
328 else:
329 query = req.querystring.partition('&')[0].partition(';')[0]
329 query = req.querystring.partition('&')[0].partition(';')[0]
330
330
331 # translate user-visible url structure to internal structure
331 # translate user-visible url structure to internal structure
332
332
333 args = query.split('/', 2)
333 args = query.split('/', 2)
334 if 'cmd' not in wsgireq.form and args and args[0]:
334 if 'cmd' not in wsgireq.form and args and args[0]:
335 cmd = args.pop(0)
335 cmd = args.pop(0)
336 style = cmd.rfind('-')
336 style = cmd.rfind('-')
337 if style != -1:
337 if style != -1:
338 wsgireq.form['style'] = [cmd[:style]]
338 wsgireq.form['style'] = [cmd[:style]]
339 cmd = cmd[style + 1:]
339 cmd = cmd[style + 1:]
340
340
341 # avoid accepting e.g. style parameter as command
341 # avoid accepting e.g. style parameter as command
342 if util.safehasattr(webcommands, cmd):
342 if util.safehasattr(webcommands, cmd):
343 wsgireq.form['cmd'] = [cmd]
343 wsgireq.form['cmd'] = [cmd]
344
344
345 if cmd == 'static':
345 if cmd == 'static':
346 wsgireq.form['file'] = ['/'.join(args)]
346 wsgireq.form['file'] = ['/'.join(args)]
347 else:
347 else:
348 if args and args[0]:
348 if args and args[0]:
349 node = args.pop(0).replace('%2F', '/')
349 node = args.pop(0).replace('%2F', '/')
350 wsgireq.form['node'] = [node]
350 wsgireq.form['node'] = [node]
351 if args:
351 if args:
352 wsgireq.form['file'] = args
352 wsgireq.form['file'] = args
353
353
354 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
354 ua = req.headers.get('User-Agent', '')
355 if cmd == 'rev' and 'mercurial' in ua:
355 if cmd == 'rev' and 'mercurial' in ua:
356 wsgireq.form['style'] = ['raw']
356 wsgireq.form['style'] = ['raw']
357
357
358 if cmd == 'archive':
358 if cmd == 'archive':
359 fn = wsgireq.form['node'][0]
359 fn = wsgireq.form['node'][0]
360 for type_, spec in rctx.archivespecs.iteritems():
360 for type_, spec in rctx.archivespecs.iteritems():
361 ext = spec[2]
361 ext = spec[2]
362 if fn.endswith(ext):
362 if fn.endswith(ext):
363 wsgireq.form['node'] = [fn[:-len(ext)]]
363 wsgireq.form['node'] = [fn[:-len(ext)]]
364 wsgireq.form['type'] = [type_]
364 wsgireq.form['type'] = [type_]
365 else:
365 else:
366 cmd = wsgireq.form.get('cmd', [''])[0]
366 cmd = wsgireq.form.get('cmd', [''])[0]
367
367
368 # process the web interface request
368 # process the web interface request
369
369
370 try:
370 try:
371 tmpl = rctx.templater(wsgireq, req)
371 tmpl = rctx.templater(wsgireq, req)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
373 ctype = templater.stringify(ctype)
373 ctype = templater.stringify(ctype)
374
374
375 # check read permissions non-static content
375 # check read permissions non-static content
376 if cmd != 'static':
376 if cmd != 'static':
377 self.check_perm(rctx, wsgireq, None)
377 self.check_perm(rctx, wsgireq, None)
378
378
379 if cmd == '':
379 if cmd == '':
380 wsgireq.form['cmd'] = [tmpl.cache['default']]
380 wsgireq.form['cmd'] = [tmpl.cache['default']]
381 cmd = wsgireq.form['cmd'][0]
381 cmd = wsgireq.form['cmd'][0]
382
382
383 # Don't enable caching if using a CSP nonce because then it wouldn't
383 # Don't enable caching if using a CSP nonce because then it wouldn't
384 # be a nonce.
384 # be a nonce.
385 if rctx.configbool('web', 'cache') and not rctx.nonce:
385 if rctx.configbool('web', 'cache') and not rctx.nonce:
386 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
386 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
387 if cmd not in webcommands.__all__:
387 if cmd not in webcommands.__all__:
388 msg = 'no such method: %s' % cmd
388 msg = 'no such method: %s' % cmd
389 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
389 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
390 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
390 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
391 rctx.ctype = ctype
391 rctx.ctype = ctype
392 content = webcommands.rawfile(rctx, wsgireq, tmpl)
392 content = webcommands.rawfile(rctx, wsgireq, tmpl)
393 else:
393 else:
394 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
394 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
395 wsgireq.respond(HTTP_OK, ctype)
395 wsgireq.respond(HTTP_OK, ctype)
396
396
397 return content
397 return content
398
398
399 except (error.LookupError, error.RepoLookupError) as err:
399 except (error.LookupError, error.RepoLookupError) as err:
400 wsgireq.respond(HTTP_NOT_FOUND, ctype)
400 wsgireq.respond(HTTP_NOT_FOUND, ctype)
401 msg = pycompat.bytestr(err)
401 msg = pycompat.bytestr(err)
402 if (util.safehasattr(err, 'name') and
402 if (util.safehasattr(err, 'name') and
403 not isinstance(err, error.ManifestLookupError)):
403 not isinstance(err, error.ManifestLookupError)):
404 msg = 'revision not found: %s' % err.name
404 msg = 'revision not found: %s' % err.name
405 return tmpl('error', error=msg)
405 return tmpl('error', error=msg)
406 except (error.RepoError, error.RevlogError) as inst:
406 except (error.RepoError, error.RevlogError) as inst:
407 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
407 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
408 return tmpl('error', error=pycompat.bytestr(inst))
408 return tmpl('error', error=pycompat.bytestr(inst))
409 except ErrorResponse as inst:
409 except ErrorResponse as inst:
410 wsgireq.respond(inst, ctype)
410 wsgireq.respond(inst, ctype)
411 if inst.code == HTTP_NOT_MODIFIED:
411 if inst.code == HTTP_NOT_MODIFIED:
412 # Not allowed to return a body on a 304
412 # Not allowed to return a body on a 304
413 return ['']
413 return ['']
414 return tmpl('error', error=pycompat.bytestr(inst))
414 return tmpl('error', error=pycompat.bytestr(inst))
415
415
416 def check_perm(self, rctx, req, op):
416 def check_perm(self, rctx, req, op):
417 for permhook in permhooks:
417 for permhook in permhooks:
418 permhook(rctx, req, op)
418 permhook(rctx, req, op)
419
419
420 def getwebview(repo):
420 def getwebview(repo):
421 """The 'web.view' config controls changeset filter to hgweb. Possible
421 """The 'web.view' config controls changeset filter to hgweb. Possible
422 values are ``served``, ``visible`` and ``all``. Default is ``served``.
422 values are ``served``, ``visible`` and ``all``. Default is ``served``.
423 The ``served`` filter only shows changesets that can be pulled from the
423 The ``served`` filter only shows changesets that can be pulled from the
424 hgweb instance. The``visible`` filter includes secret changesets but
424 hgweb instance. The``visible`` filter includes secret changesets but
425 still excludes "hidden" one.
425 still excludes "hidden" one.
426
426
427 See the repoview module for details.
427 See the repoview module for details.
428
428
429 The option has been around undocumented since Mercurial 2.5, but no
429 The option has been around undocumented since Mercurial 2.5, but no
430 user ever asked about it. So we better keep it undocumented for now."""
430 user ever asked about it. So we better keep it undocumented for now."""
431 # experimental config: web.view
431 # experimental config: web.view
432 viewconfig = repo.ui.config('web', 'view', untrusted=True)
432 viewconfig = repo.ui.config('web', 'view', untrusted=True)
433 if viewconfig == 'all':
433 if viewconfig == 'all':
434 return repo.unfiltered()
434 return repo.unfiltered()
435 elif viewconfig in repoview.filtertable:
435 elif viewconfig in repoview.filtertable:
436 return repo.filtered(viewconfig)
436 return repo.filtered(viewconfig)
437 else:
437 else:
438 return repo.filtered('served')
438 return repo.filtered('served')
@@ -1,300 +1,315 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 cgi
11 import cgi
12 import errno
12 import errno
13 import socket
13 import socket
14 import wsgiref.headers as wsgiheaders
14 #import wsgiref.validate
15 #import wsgiref.validate
15
16
16 from .common import (
17 from .common import (
17 ErrorResponse,
18 ErrorResponse,
18 HTTP_NOT_MODIFIED,
19 HTTP_NOT_MODIFIED,
19 statusmessage,
20 statusmessage,
20 )
21 )
21
22
22 from ..thirdparty import (
23 from ..thirdparty import (
23 attr,
24 attr,
24 )
25 )
25 from .. import (
26 from .. import (
26 pycompat,
27 pycompat,
27 util,
28 util,
28 )
29 )
29
30
30 shortcuts = {
31 shortcuts = {
31 'cl': [('cmd', ['changelog']), ('rev', None)],
32 'cl': [('cmd', ['changelog']), ('rev', None)],
32 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 'cs': [('cmd', ['changeset']), ('node', None)],
34 'cs': [('cmd', ['changeset']), ('node', None)],
34 'f': [('cmd', ['file']), ('filenode', None)],
35 'f': [('cmd', ['file']), ('filenode', None)],
35 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 'fd': [('cmd', ['filediff']), ('node', None)],
37 'fd': [('cmd', ['filediff']), ('node', None)],
37 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 'ca': [('cmd', ['archive']), ('node', None)],
40 'ca': [('cmd', ['archive']), ('node', None)],
40 'tags': [('cmd', ['tags'])],
41 'tags': [('cmd', ['tags'])],
41 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 'static': [('cmd', ['static']), ('file', None)]
43 'static': [('cmd', ['static']), ('file', None)]
43 }
44 }
44
45
45 def normalize(form):
46 def normalize(form):
46 # first expand the shortcuts
47 # first expand the shortcuts
47 for k in shortcuts:
48 for k in shortcuts:
48 if k in form:
49 if k in form:
49 for name, value in shortcuts[k]:
50 for name, value in shortcuts[k]:
50 if value is None:
51 if value is None:
51 value = form[k]
52 value = form[k]
52 form[name] = value
53 form[name] = value
53 del form[k]
54 del form[k]
54 # And strip the values
55 # And strip the values
55 bytesform = {}
56 bytesform = {}
56 for k, v in form.iteritems():
57 for k, v in form.iteritems():
57 bytesform[pycompat.bytesurl(k)] = [
58 bytesform[pycompat.bytesurl(k)] = [
58 pycompat.bytesurl(i.strip()) for i in v]
59 pycompat.bytesurl(i.strip()) for i in v]
59 return bytesform
60 return bytesform
60
61
61 @attr.s(frozen=True)
62 @attr.s(frozen=True)
62 class parsedrequest(object):
63 class parsedrequest(object):
63 """Represents a parsed WSGI request / static HTTP request parameters."""
64 """Represents a parsed WSGI request / static HTTP request parameters."""
64
65
65 # Full URL for this request.
66 # Full URL for this request.
66 url = attr.ib()
67 url = attr.ib()
67 # URL without any path components. Just <proto>://<host><port>.
68 # URL without any path components. Just <proto>://<host><port>.
68 baseurl = attr.ib()
69 baseurl = attr.ib()
69 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
70 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
70 # of HTTP: Host header for hostname. This is likely what clients used.
71 # of HTTP: Host header for hostname. This is likely what clients used.
71 advertisedurl = attr.ib()
72 advertisedurl = attr.ib()
72 advertisedbaseurl = attr.ib()
73 advertisedbaseurl = attr.ib()
73 # WSGI application path.
74 # WSGI application path.
74 apppath = attr.ib()
75 apppath = attr.ib()
75 # List of path parts to be used for dispatch.
76 # List of path parts to be used for dispatch.
76 dispatchparts = attr.ib()
77 dispatchparts = attr.ib()
77 # URL path component (no query string) used for dispatch.
78 # URL path component (no query string) used for dispatch.
78 dispatchpath = attr.ib()
79 dispatchpath = attr.ib()
79 # Whether there is a path component to this request. This can be true
80 # Whether there is a path component to this request. This can be true
80 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
81 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
81 havepathinfo = attr.ib()
82 havepathinfo = attr.ib()
82 # Raw query string (part after "?" in URL).
83 # Raw query string (part after "?" in URL).
83 querystring = attr.ib()
84 querystring = attr.ib()
84 # List of 2-tuples of query string arguments.
85 # List of 2-tuples of query string arguments.
85 querystringlist = attr.ib()
86 querystringlist = attr.ib()
86 # Dict of query string arguments. Values are lists with at least 1 item.
87 # Dict of query string arguments. Values are lists with at least 1 item.
87 querystringdict = attr.ib()
88 querystringdict = attr.ib()
89 # wsgiref.headers.Headers instance. Operates like a dict with case
90 # insensitive keys.
91 headers = attr.ib()
88
92
89 def parserequestfromenv(env):
93 def parserequestfromenv(env):
90 """Parse URL components from environment variables.
94 """Parse URL components from environment variables.
91
95
92 WSGI defines request attributes via environment variables. This function
96 WSGI defines request attributes via environment variables. This function
93 parses the environment variables into a data structure.
97 parses the environment variables into a data structure.
94 """
98 """
95 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
99 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
96
100
97 # We first validate that the incoming object conforms with the WSGI spec.
101 # We first validate that the incoming object conforms with the WSGI spec.
98 # We only want to be dealing with spec-conforming WSGI implementations.
102 # We only want to be dealing with spec-conforming WSGI implementations.
99 # TODO enable this once we fix internal violations.
103 # TODO enable this once we fix internal violations.
100 #wsgiref.validate.check_environ(env)
104 #wsgiref.validate.check_environ(env)
101
105
102 # PEP-0333 states that environment keys and values are native strings
106 # PEP-0333 states that environment keys and values are native strings
103 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
107 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
104 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
108 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
105 # in Mercurial, so mass convert string keys and values to bytes.
109 # in Mercurial, so mass convert string keys and values to bytes.
106 if pycompat.ispy3:
110 if pycompat.ispy3:
107 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
111 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
108 env = {k: v.encode('latin-1') if isinstance(v, str) else v
112 env = {k: v.encode('latin-1') if isinstance(v, str) else v
109 for k, v in env.iteritems()}
113 for k, v in env.iteritems()}
110
114
111 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
115 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
112 # the environment variables.
116 # the environment variables.
113 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
117 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
114 # how URLs are reconstructed.
118 # how URLs are reconstructed.
115 fullurl = env['wsgi.url_scheme'] + '://'
119 fullurl = env['wsgi.url_scheme'] + '://'
116 advertisedfullurl = fullurl
120 advertisedfullurl = fullurl
117
121
118 def addport(s):
122 def addport(s):
119 if env['wsgi.url_scheme'] == 'https':
123 if env['wsgi.url_scheme'] == 'https':
120 if env['SERVER_PORT'] != '443':
124 if env['SERVER_PORT'] != '443':
121 s += ':' + env['SERVER_PORT']
125 s += ':' + env['SERVER_PORT']
122 else:
126 else:
123 if env['SERVER_PORT'] != '80':
127 if env['SERVER_PORT'] != '80':
124 s += ':' + env['SERVER_PORT']
128 s += ':' + env['SERVER_PORT']
125
129
126 return s
130 return s
127
131
128 if env.get('HTTP_HOST'):
132 if env.get('HTTP_HOST'):
129 fullurl += env['HTTP_HOST']
133 fullurl += env['HTTP_HOST']
130 else:
134 else:
131 fullurl += env['SERVER_NAME']
135 fullurl += env['SERVER_NAME']
132 fullurl = addport(fullurl)
136 fullurl = addport(fullurl)
133
137
134 advertisedfullurl += env['SERVER_NAME']
138 advertisedfullurl += env['SERVER_NAME']
135 advertisedfullurl = addport(advertisedfullurl)
139 advertisedfullurl = addport(advertisedfullurl)
136
140
137 baseurl = fullurl
141 baseurl = fullurl
138 advertisedbaseurl = advertisedfullurl
142 advertisedbaseurl = advertisedfullurl
139
143
140 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
144 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
141 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
145 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
142 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
146 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
143 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
147 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
144
148
145 if env.get('QUERY_STRING'):
149 if env.get('QUERY_STRING'):
146 fullurl += '?' + env['QUERY_STRING']
150 fullurl += '?' + env['QUERY_STRING']
147 advertisedfullurl += '?' + env['QUERY_STRING']
151 advertisedfullurl += '?' + env['QUERY_STRING']
148
152
149 # When dispatching requests, we look at the URL components (PATH_INFO
153 # When dispatching requests, we look at the URL components (PATH_INFO
150 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
154 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
151 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
155 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
152 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
156 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
153 # root. We also exclude its path components from PATH_INFO when resolving
157 # root. We also exclude its path components from PATH_INFO when resolving
154 # the dispatch path.
158 # the dispatch path.
155
159
156 apppath = env['SCRIPT_NAME']
160 apppath = env['SCRIPT_NAME']
157
161
158 if env.get('REPO_NAME'):
162 if env.get('REPO_NAME'):
159 if not apppath.endswith('/'):
163 if not apppath.endswith('/'):
160 apppath += '/'
164 apppath += '/'
161
165
162 apppath += env.get('REPO_NAME')
166 apppath += env.get('REPO_NAME')
163
167
164 if 'PATH_INFO' in env:
168 if 'PATH_INFO' in env:
165 dispatchparts = env['PATH_INFO'].strip('/').split('/')
169 dispatchparts = env['PATH_INFO'].strip('/').split('/')
166
170
167 # Strip out repo parts.
171 # Strip out repo parts.
168 repoparts = env.get('REPO_NAME', '').split('/')
172 repoparts = env.get('REPO_NAME', '').split('/')
169 if dispatchparts[:len(repoparts)] == repoparts:
173 if dispatchparts[:len(repoparts)] == repoparts:
170 dispatchparts = dispatchparts[len(repoparts):]
174 dispatchparts = dispatchparts[len(repoparts):]
171 else:
175 else:
172 dispatchparts = []
176 dispatchparts = []
173
177
174 dispatchpath = '/'.join(dispatchparts)
178 dispatchpath = '/'.join(dispatchparts)
175
179
176 querystring = env.get('QUERY_STRING', '')
180 querystring = env.get('QUERY_STRING', '')
177
181
178 # We store as a list so we have ordering information. We also store as
182 # We store as a list so we have ordering information. We also store as
179 # a dict to facilitate fast lookup.
183 # a dict to facilitate fast lookup.
180 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
184 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
181
185
182 querystringdict = {}
186 querystringdict = {}
183 for k, v in querystringlist:
187 for k, v in querystringlist:
184 if k in querystringdict:
188 if k in querystringdict:
185 querystringdict[k].append(v)
189 querystringdict[k].append(v)
186 else:
190 else:
187 querystringdict[k] = [v]
191 querystringdict[k] = [v]
188
192
193 # HTTP_* keys contain HTTP request headers. The Headers structure should
194 # perform case normalization for us. We just rewrite underscore to dash
195 # so keys match what likely went over the wire.
196 headers = []
197 for k, v in env.iteritems():
198 if k.startswith('HTTP_'):
199 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
200
201 headers = wsgiheaders.Headers(headers)
202
189 return parsedrequest(url=fullurl, baseurl=baseurl,
203 return parsedrequest(url=fullurl, baseurl=baseurl,
190 advertisedurl=advertisedfullurl,
204 advertisedurl=advertisedfullurl,
191 advertisedbaseurl=advertisedbaseurl,
205 advertisedbaseurl=advertisedbaseurl,
192 apppath=apppath,
206 apppath=apppath,
193 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
207 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
194 havepathinfo='PATH_INFO' in env,
208 havepathinfo='PATH_INFO' in env,
195 querystring=querystring,
209 querystring=querystring,
196 querystringlist=querystringlist,
210 querystringlist=querystringlist,
197 querystringdict=querystringdict)
211 querystringdict=querystringdict,
212 headers=headers)
198
213
199 class wsgirequest(object):
214 class wsgirequest(object):
200 """Higher-level API for a WSGI request.
215 """Higher-level API for a WSGI request.
201
216
202 WSGI applications are invoked with 2 arguments. They are used to
217 WSGI applications are invoked with 2 arguments. They are used to
203 instantiate instances of this class, which provides higher-level APIs
218 instantiate instances of this class, which provides higher-level APIs
204 for obtaining request parameters, writing HTTP output, etc.
219 for obtaining request parameters, writing HTTP output, etc.
205 """
220 """
206 def __init__(self, wsgienv, start_response):
221 def __init__(self, wsgienv, start_response):
207 version = wsgienv[r'wsgi.version']
222 version = wsgienv[r'wsgi.version']
208 if (version < (1, 0)) or (version >= (2, 0)):
223 if (version < (1, 0)) or (version >= (2, 0)):
209 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
224 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
210 % version)
225 % version)
211 self.inp = wsgienv[r'wsgi.input']
226 self.inp = wsgienv[r'wsgi.input']
212 self.err = wsgienv[r'wsgi.errors']
227 self.err = wsgienv[r'wsgi.errors']
213 self.threaded = wsgienv[r'wsgi.multithread']
228 self.threaded = wsgienv[r'wsgi.multithread']
214 self.multiprocess = wsgienv[r'wsgi.multiprocess']
229 self.multiprocess = wsgienv[r'wsgi.multiprocess']
215 self.run_once = wsgienv[r'wsgi.run_once']
230 self.run_once = wsgienv[r'wsgi.run_once']
216 self.env = wsgienv
231 self.env = wsgienv
217 self.form = normalize(cgi.parse(self.inp,
232 self.form = normalize(cgi.parse(self.inp,
218 self.env,
233 self.env,
219 keep_blank_values=1))
234 keep_blank_values=1))
220 self._start_response = start_response
235 self._start_response = start_response
221 self.server_write = None
236 self.server_write = None
222 self.headers = []
237 self.headers = []
223
238
224 def __iter__(self):
239 def __iter__(self):
225 return iter([])
240 return iter([])
226
241
227 def read(self, count=-1):
242 def read(self, count=-1):
228 return self.inp.read(count)
243 return self.inp.read(count)
229
244
230 def drain(self):
245 def drain(self):
231 '''need to read all data from request, httplib is half-duplex'''
246 '''need to read all data from request, httplib is half-duplex'''
232 length = int(self.env.get('CONTENT_LENGTH') or 0)
247 length = int(self.env.get('CONTENT_LENGTH') or 0)
233 for s in util.filechunkiter(self.inp, limit=length):
248 for s in util.filechunkiter(self.inp, limit=length):
234 pass
249 pass
235
250
236 def respond(self, status, type, filename=None, body=None):
251 def respond(self, status, type, filename=None, body=None):
237 if not isinstance(type, str):
252 if not isinstance(type, str):
238 type = pycompat.sysstr(type)
253 type = pycompat.sysstr(type)
239 if self._start_response is not None:
254 if self._start_response is not None:
240 self.headers.append((r'Content-Type', type))
255 self.headers.append((r'Content-Type', type))
241 if filename:
256 if filename:
242 filename = (filename.rpartition('/')[-1]
257 filename = (filename.rpartition('/')[-1]
243 .replace('\\', '\\\\').replace('"', '\\"'))
258 .replace('\\', '\\\\').replace('"', '\\"'))
244 self.headers.append(('Content-Disposition',
259 self.headers.append(('Content-Disposition',
245 'inline; filename="%s"' % filename))
260 'inline; filename="%s"' % filename))
246 if body is not None:
261 if body is not None:
247 self.headers.append((r'Content-Length', str(len(body))))
262 self.headers.append((r'Content-Length', str(len(body))))
248
263
249 for k, v in self.headers:
264 for k, v in self.headers:
250 if not isinstance(v, str):
265 if not isinstance(v, str):
251 raise TypeError('header value must be string: %r' % (v,))
266 raise TypeError('header value must be string: %r' % (v,))
252
267
253 if isinstance(status, ErrorResponse):
268 if isinstance(status, ErrorResponse):
254 self.headers.extend(status.headers)
269 self.headers.extend(status.headers)
255 if status.code == HTTP_NOT_MODIFIED:
270 if status.code == HTTP_NOT_MODIFIED:
256 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
271 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
257 # it MUST NOT include any headers other than these and no
272 # it MUST NOT include any headers other than these and no
258 # body
273 # body
259 self.headers = [(k, v) for (k, v) in self.headers if
274 self.headers = [(k, v) for (k, v) in self.headers if
260 k in ('Date', 'ETag', 'Expires',
275 k in ('Date', 'ETag', 'Expires',
261 'Cache-Control', 'Vary')]
276 'Cache-Control', 'Vary')]
262 status = statusmessage(status.code, pycompat.bytestr(status))
277 status = statusmessage(status.code, pycompat.bytestr(status))
263 elif status == 200:
278 elif status == 200:
264 status = '200 Script output follows'
279 status = '200 Script output follows'
265 elif isinstance(status, int):
280 elif isinstance(status, int):
266 status = statusmessage(status)
281 status = statusmessage(status)
267
282
268 self.server_write = self._start_response(
283 self.server_write = self._start_response(
269 pycompat.sysstr(status), self.headers)
284 pycompat.sysstr(status), self.headers)
270 self._start_response = None
285 self._start_response = None
271 self.headers = []
286 self.headers = []
272 if body is not None:
287 if body is not None:
273 self.write(body)
288 self.write(body)
274 self.server_write = None
289 self.server_write = None
275
290
276 def write(self, thing):
291 def write(self, thing):
277 if thing:
292 if thing:
278 try:
293 try:
279 self.server_write(thing)
294 self.server_write(thing)
280 except socket.error as inst:
295 except socket.error as inst:
281 if inst[0] != errno.ECONNRESET:
296 if inst[0] != errno.ECONNRESET:
282 raise
297 raise
283
298
284 def writelines(self, lines):
299 def writelines(self, lines):
285 for line in lines:
300 for line in lines:
286 self.write(line)
301 self.write(line)
287
302
288 def flush(self):
303 def flush(self):
289 return None
304 return None
290
305
291 def close(self):
306 def close(self):
292 return None
307 return None
293
308
294 def wsgiapplication(app_maker):
309 def wsgiapplication(app_maker):
295 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
310 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
296 can and should now be used as a WSGI application.'''
311 can and should now be used as a WSGI application.'''
297 application = app_maker()
312 application = app_maker()
298 def run_wsgi(env, respond):
313 def run_wsgi(env, respond):
299 return application(env, respond)
314 return application(env, respond)
300 return run_wsgi
315 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now