##// END OF EJS Templates
hgweb: use the parsed application path directly...
Gregory Szorc -
r36826:0031e972 default
parent child Browse files
Show More
@@ -1,454 +1,452 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 pycompat.sysbytes(wsgireq.url) + '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 wsgireq.url.strip(r'/') 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': pycompat.sysbytes(wsgireq.url),
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(pycompat.sysbytes(wsgireq.url)),
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 wsgireq.url = pycompat.sysstr(req.apppath)
322
323 if r'PATH_INFO' in wsgireq.env:
321 if r'PATH_INFO' in wsgireq.env:
324 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
322 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
325 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
323 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
326 if parts[:len(repo_parts)] == repo_parts:
324 if parts[:len(repo_parts)] == repo_parts:
327 parts = parts[len(repo_parts):]
325 parts = parts[len(repo_parts):]
328 query = r'/'.join(parts)
326 query = r'/'.join(parts)
329 else:
327 else:
330 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
328 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
331 query = query.partition(r';')[0]
329 query = query.partition(r';')[0]
332
330
333 # Route it to a wire protocol handler if it looks like a wire protocol
331 # Route it to a wire protocol handler if it looks like a wire protocol
334 # request.
332 # request.
335 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
333 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
336 self.check_perm)
334 self.check_perm)
337
335
338 if protohandler:
336 if protohandler:
339 try:
337 try:
340 if query:
338 if query:
341 raise ErrorResponse(HTTP_NOT_FOUND)
339 raise ErrorResponse(HTTP_NOT_FOUND)
342
340
343 return protohandler['dispatch']()
341 return protohandler['dispatch']()
344 except ErrorResponse as inst:
342 except ErrorResponse as inst:
345 return protohandler['handleerror'](inst)
343 return protohandler['handleerror'](inst)
346
344
347 # translate user-visible url structure to internal structure
345 # translate user-visible url structure to internal structure
348
346
349 args = query.split(r'/', 2)
347 args = query.split(r'/', 2)
350 if 'cmd' not in wsgireq.form and args and args[0]:
348 if 'cmd' not in wsgireq.form and args and args[0]:
351 cmd = args.pop(0)
349 cmd = args.pop(0)
352 style = cmd.rfind('-')
350 style = cmd.rfind('-')
353 if style != -1:
351 if style != -1:
354 wsgireq.form['style'] = [cmd[:style]]
352 wsgireq.form['style'] = [cmd[:style]]
355 cmd = cmd[style + 1:]
353 cmd = cmd[style + 1:]
356
354
357 # avoid accepting e.g. style parameter as command
355 # avoid accepting e.g. style parameter as command
358 if util.safehasattr(webcommands, cmd):
356 if util.safehasattr(webcommands, cmd):
359 wsgireq.form['cmd'] = [cmd]
357 wsgireq.form['cmd'] = [cmd]
360
358
361 if cmd == 'static':
359 if cmd == 'static':
362 wsgireq.form['file'] = ['/'.join(args)]
360 wsgireq.form['file'] = ['/'.join(args)]
363 else:
361 else:
364 if args and args[0]:
362 if args and args[0]:
365 node = args.pop(0).replace('%2F', '/')
363 node = args.pop(0).replace('%2F', '/')
366 wsgireq.form['node'] = [node]
364 wsgireq.form['node'] = [node]
367 if args:
365 if args:
368 wsgireq.form['file'] = args
366 wsgireq.form['file'] = args
369
367
370 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
368 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
371 if cmd == 'rev' and 'mercurial' in ua:
369 if cmd == 'rev' and 'mercurial' in ua:
372 wsgireq.form['style'] = ['raw']
370 wsgireq.form['style'] = ['raw']
373
371
374 if cmd == 'archive':
372 if cmd == 'archive':
375 fn = wsgireq.form['node'][0]
373 fn = wsgireq.form['node'][0]
376 for type_, spec in rctx.archivespecs.iteritems():
374 for type_, spec in rctx.archivespecs.iteritems():
377 ext = spec[2]
375 ext = spec[2]
378 if fn.endswith(ext):
376 if fn.endswith(ext):
379 wsgireq.form['node'] = [fn[:-len(ext)]]
377 wsgireq.form['node'] = [fn[:-len(ext)]]
380 wsgireq.form['type'] = [type_]
378 wsgireq.form['type'] = [type_]
381 else:
379 else:
382 cmd = wsgireq.form.get('cmd', [''])[0]
380 cmd = wsgireq.form.get('cmd', [''])[0]
383
381
384 # process the web interface request
382 # process the web interface request
385
383
386 try:
384 try:
387 tmpl = rctx.templater(wsgireq, req)
385 tmpl = rctx.templater(wsgireq, req)
388 ctype = tmpl('mimetype', encoding=encoding.encoding)
386 ctype = tmpl('mimetype', encoding=encoding.encoding)
389 ctype = templater.stringify(ctype)
387 ctype = templater.stringify(ctype)
390
388
391 # check read permissions non-static content
389 # check read permissions non-static content
392 if cmd != 'static':
390 if cmd != 'static':
393 self.check_perm(rctx, wsgireq, None)
391 self.check_perm(rctx, wsgireq, None)
394
392
395 if cmd == '':
393 if cmd == '':
396 wsgireq.form['cmd'] = [tmpl.cache['default']]
394 wsgireq.form['cmd'] = [tmpl.cache['default']]
397 cmd = wsgireq.form['cmd'][0]
395 cmd = wsgireq.form['cmd'][0]
398
396
399 # Don't enable caching if using a CSP nonce because then it wouldn't
397 # Don't enable caching if using a CSP nonce because then it wouldn't
400 # be a nonce.
398 # be a nonce.
401 if rctx.configbool('web', 'cache') and not rctx.nonce:
399 if rctx.configbool('web', 'cache') and not rctx.nonce:
402 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
400 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
403 if cmd not in webcommands.__all__:
401 if cmd not in webcommands.__all__:
404 msg = 'no such method: %s' % cmd
402 msg = 'no such method: %s' % cmd
405 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
403 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
406 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
404 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
407 rctx.ctype = ctype
405 rctx.ctype = ctype
408 content = webcommands.rawfile(rctx, wsgireq, tmpl)
406 content = webcommands.rawfile(rctx, wsgireq, tmpl)
409 else:
407 else:
410 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
408 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
411 wsgireq.respond(HTTP_OK, ctype)
409 wsgireq.respond(HTTP_OK, ctype)
412
410
413 return content
411 return content
414
412
415 except (error.LookupError, error.RepoLookupError) as err:
413 except (error.LookupError, error.RepoLookupError) as err:
416 wsgireq.respond(HTTP_NOT_FOUND, ctype)
414 wsgireq.respond(HTTP_NOT_FOUND, ctype)
417 msg = pycompat.bytestr(err)
415 msg = pycompat.bytestr(err)
418 if (util.safehasattr(err, 'name') and
416 if (util.safehasattr(err, 'name') and
419 not isinstance(err, error.ManifestLookupError)):
417 not isinstance(err, error.ManifestLookupError)):
420 msg = 'revision not found: %s' % err.name
418 msg = 'revision not found: %s' % err.name
421 return tmpl('error', error=msg)
419 return tmpl('error', error=msg)
422 except (error.RepoError, error.RevlogError) as inst:
420 except (error.RepoError, error.RevlogError) as inst:
423 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
421 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
424 return tmpl('error', error=pycompat.bytestr(inst))
422 return tmpl('error', error=pycompat.bytestr(inst))
425 except ErrorResponse as inst:
423 except ErrorResponse as inst:
426 wsgireq.respond(inst, ctype)
424 wsgireq.respond(inst, ctype)
427 if inst.code == HTTP_NOT_MODIFIED:
425 if inst.code == HTTP_NOT_MODIFIED:
428 # Not allowed to return a body on a 304
426 # Not allowed to return a body on a 304
429 return ['']
427 return ['']
430 return tmpl('error', error=pycompat.bytestr(inst))
428 return tmpl('error', error=pycompat.bytestr(inst))
431
429
432 def check_perm(self, rctx, req, op):
430 def check_perm(self, rctx, req, op):
433 for permhook in permhooks:
431 for permhook in permhooks:
434 permhook(rctx, req, op)
432 permhook(rctx, req, op)
435
433
436 def getwebview(repo):
434 def getwebview(repo):
437 """The 'web.view' config controls changeset filter to hgweb. Possible
435 """The 'web.view' config controls changeset filter to hgweb. Possible
438 values are ``served``, ``visible`` and ``all``. Default is ``served``.
436 values are ``served``, ``visible`` and ``all``. Default is ``served``.
439 The ``served`` filter only shows changesets that can be pulled from the
437 The ``served`` filter only shows changesets that can be pulled from the
440 hgweb instance. The``visible`` filter includes secret changesets but
438 hgweb instance. The``visible`` filter includes secret changesets but
441 still excludes "hidden" one.
439 still excludes "hidden" one.
442
440
443 See the repoview module for details.
441 See the repoview module for details.
444
442
445 The option has been around undocumented since Mercurial 2.5, but no
443 The option has been around undocumented since Mercurial 2.5, but no
446 user ever asked about it. So we better keep it undocumented for now."""
444 user ever asked about it. So we better keep it undocumented for now."""
447 # experimental config: web.view
445 # experimental config: web.view
448 viewconfig = repo.ui.config('web', 'view', untrusted=True)
446 viewconfig = repo.ui.config('web', 'view', untrusted=True)
449 if viewconfig == 'all':
447 if viewconfig == 'all':
450 return repo.unfiltered()
448 return repo.unfiltered()
451 elif viewconfig in repoview.filtertable:
449 elif viewconfig in repoview.filtertable:
452 return repo.filtered(viewconfig)
450 return repo.filtered(viewconfig)
453 else:
451 else:
454 return repo.filtered('served')
452 return repo.filtered('served')
@@ -1,280 +1,279 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.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 pycompat,
26 pycompat,
27 util,
27 util,
28 )
28 )
29
29
30 shortcuts = {
30 shortcuts = {
31 'cl': [('cmd', ['changelog']), ('rev', None)],
31 'cl': [('cmd', ['changelog']), ('rev', None)],
32 'sl': [('cmd', ['shortlog']), ('rev', None)],
32 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 'cs': [('cmd', ['changeset']), ('node', None)],
33 'cs': [('cmd', ['changeset']), ('node', None)],
34 'f': [('cmd', ['file']), ('filenode', None)],
34 'f': [('cmd', ['file']), ('filenode', None)],
35 'fl': [('cmd', ['filelog']), ('filenode', None)],
35 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 'fd': [('cmd', ['filediff']), ('node', None)],
36 'fd': [('cmd', ['filediff']), ('node', None)],
37 'fa': [('cmd', ['annotate']), ('filenode', None)],
37 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 'mf': [('cmd', ['manifest']), ('manifest', None)],
38 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 'ca': [('cmd', ['archive']), ('node', None)],
39 'ca': [('cmd', ['archive']), ('node', None)],
40 'tags': [('cmd', ['tags'])],
40 'tags': [('cmd', ['tags'])],
41 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
41 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 'static': [('cmd', ['static']), ('file', None)]
42 'static': [('cmd', ['static']), ('file', None)]
43 }
43 }
44
44
45 def normalize(form):
45 def normalize(form):
46 # first expand the shortcuts
46 # first expand the shortcuts
47 for k in shortcuts:
47 for k in shortcuts:
48 if k in form:
48 if k in form:
49 for name, value in shortcuts[k]:
49 for name, value in shortcuts[k]:
50 if value is None:
50 if value is None:
51 value = form[k]
51 value = form[k]
52 form[name] = value
52 form[name] = value
53 del form[k]
53 del form[k]
54 # And strip the values
54 # And strip the values
55 bytesform = {}
55 bytesform = {}
56 for k, v in form.iteritems():
56 for k, v in form.iteritems():
57 bytesform[pycompat.bytesurl(k)] = [
57 bytesform[pycompat.bytesurl(k)] = [
58 pycompat.bytesurl(i.strip()) for i in v]
58 pycompat.bytesurl(i.strip()) for i in v]
59 return bytesform
59 return bytesform
60
60
61 @attr.s(frozen=True)
61 @attr.s(frozen=True)
62 class parsedrequest(object):
62 class parsedrequest(object):
63 """Represents a parsed WSGI request / static HTTP request parameters."""
63 """Represents a parsed WSGI request / static HTTP request parameters."""
64
64
65 # Full URL for this request.
65 # Full URL for this request.
66 url = attr.ib()
66 url = attr.ib()
67 # URL without any path components. Just <proto>://<host><port>.
67 # URL without any path components. Just <proto>://<host><port>.
68 baseurl = attr.ib()
68 baseurl = attr.ib()
69 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
69 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
70 # of HTTP: Host header for hostname. This is likely what clients used.
70 # of HTTP: Host header for hostname. This is likely what clients used.
71 advertisedurl = attr.ib()
71 advertisedurl = attr.ib()
72 advertisedbaseurl = attr.ib()
72 advertisedbaseurl = attr.ib()
73 # WSGI application path.
73 # WSGI application path.
74 apppath = attr.ib()
74 apppath = attr.ib()
75 # List of path parts to be used for dispatch.
75 # List of path parts to be used for dispatch.
76 dispatchparts = attr.ib()
76 dispatchparts = attr.ib()
77 # URL path component (no query string) used for dispatch.
77 # URL path component (no query string) used for dispatch.
78 dispatchpath = attr.ib()
78 dispatchpath = attr.ib()
79 # Raw query string (part after "?" in URL).
79 # Raw query string (part after "?" in URL).
80 querystring = attr.ib()
80 querystring = attr.ib()
81
81
82 def parserequestfromenv(env):
82 def parserequestfromenv(env):
83 """Parse URL components from environment variables.
83 """Parse URL components from environment variables.
84
84
85 WSGI defines request attributes via environment variables. This function
85 WSGI defines request attributes via environment variables. This function
86 parses the environment variables into a data structure.
86 parses the environment variables into a data structure.
87 """
87 """
88 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
88 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
89
89
90 # We first validate that the incoming object conforms with the WSGI spec.
90 # We first validate that the incoming object conforms with the WSGI spec.
91 # We only want to be dealing with spec-conforming WSGI implementations.
91 # We only want to be dealing with spec-conforming WSGI implementations.
92 # TODO enable this once we fix internal violations.
92 # TODO enable this once we fix internal violations.
93 #wsgiref.validate.check_environ(env)
93 #wsgiref.validate.check_environ(env)
94
94
95 # PEP-0333 states that environment keys and values are native strings
95 # PEP-0333 states that environment keys and values are native strings
96 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
96 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
97 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
97 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
98 # in Mercurial, so mass convert string keys and values to bytes.
98 # in Mercurial, so mass convert string keys and values to bytes.
99 if pycompat.ispy3:
99 if pycompat.ispy3:
100 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
100 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
101 env = {k: v.encode('latin-1') if isinstance(v, str) else v
101 env = {k: v.encode('latin-1') if isinstance(v, str) else v
102 for k, v in env.iteritems()}
102 for k, v in env.iteritems()}
103
103
104 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
104 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
105 # the environment variables.
105 # the environment variables.
106 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
106 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
107 # how URLs are reconstructed.
107 # how URLs are reconstructed.
108 fullurl = env['wsgi.url_scheme'] + '://'
108 fullurl = env['wsgi.url_scheme'] + '://'
109 advertisedfullurl = fullurl
109 advertisedfullurl = fullurl
110
110
111 def addport(s):
111 def addport(s):
112 if env['wsgi.url_scheme'] == 'https':
112 if env['wsgi.url_scheme'] == 'https':
113 if env['SERVER_PORT'] != '443':
113 if env['SERVER_PORT'] != '443':
114 s += ':' + env['SERVER_PORT']
114 s += ':' + env['SERVER_PORT']
115 else:
115 else:
116 if env['SERVER_PORT'] != '80':
116 if env['SERVER_PORT'] != '80':
117 s += ':' + env['SERVER_PORT']
117 s += ':' + env['SERVER_PORT']
118
118
119 return s
119 return s
120
120
121 if env.get('HTTP_HOST'):
121 if env.get('HTTP_HOST'):
122 fullurl += env['HTTP_HOST']
122 fullurl += env['HTTP_HOST']
123 else:
123 else:
124 fullurl += env['SERVER_NAME']
124 fullurl += env['SERVER_NAME']
125 fullurl = addport(fullurl)
125 fullurl = addport(fullurl)
126
126
127 advertisedfullurl += env['SERVER_NAME']
127 advertisedfullurl += env['SERVER_NAME']
128 advertisedfullurl = addport(advertisedfullurl)
128 advertisedfullurl = addport(advertisedfullurl)
129
129
130 baseurl = fullurl
130 baseurl = fullurl
131 advertisedbaseurl = advertisedfullurl
131 advertisedbaseurl = advertisedfullurl
132
132
133 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
133 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
134 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
134 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
135 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
135 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
136 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
136 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
137
137
138 if env.get('QUERY_STRING'):
138 if env.get('QUERY_STRING'):
139 fullurl += '?' + env['QUERY_STRING']
139 fullurl += '?' + env['QUERY_STRING']
140 advertisedfullurl += '?' + env['QUERY_STRING']
140 advertisedfullurl += '?' + env['QUERY_STRING']
141
141
142 # When dispatching requests, we look at the URL components (PATH_INFO
142 # When dispatching requests, we look at the URL components (PATH_INFO
143 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
143 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
144 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
144 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
145 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
145 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
146 # root. We also exclude its path components from PATH_INFO when resolving
146 # root. We also exclude its path components from PATH_INFO when resolving
147 # the dispatch path.
147 # the dispatch path.
148
148
149 # TODO the use of trailing slashes in apppath is arguably wrong. We need it
150 # to appease low-level parts of hgweb_mod for now.
151 apppath = env['SCRIPT_NAME']
149 apppath = env['SCRIPT_NAME']
152 if not apppath.endswith('/'):
153 apppath += '/'
154
150
155 if env.get('REPO_NAME'):
151 if env.get('REPO_NAME'):
156 apppath += env.get('REPO_NAME') + '/'
152 if not apppath.endswith('/'):
153 apppath += '/'
154
155 apppath += env.get('REPO_NAME')
157
156
158 if 'PATH_INFO' in env:
157 if 'PATH_INFO' in env:
159 dispatchparts = env['PATH_INFO'].strip('/').split('/')
158 dispatchparts = env['PATH_INFO'].strip('/').split('/')
160
159
161 # Strip out repo parts.
160 # Strip out repo parts.
162 repoparts = env.get('REPO_NAME', '').split('/')
161 repoparts = env.get('REPO_NAME', '').split('/')
163 if dispatchparts[:len(repoparts)] == repoparts:
162 if dispatchparts[:len(repoparts)] == repoparts:
164 dispatchparts = dispatchparts[len(repoparts):]
163 dispatchparts = dispatchparts[len(repoparts):]
165 else:
164 else:
166 dispatchparts = []
165 dispatchparts = []
167
166
168 dispatchpath = '/'.join(dispatchparts)
167 dispatchpath = '/'.join(dispatchparts)
169
168
170 querystring = env.get('QUERY_STRING', '')
169 querystring = env.get('QUERY_STRING', '')
171
170
172 return parsedrequest(url=fullurl, baseurl=baseurl,
171 return parsedrequest(url=fullurl, baseurl=baseurl,
173 advertisedurl=advertisedfullurl,
172 advertisedurl=advertisedfullurl,
174 advertisedbaseurl=advertisedbaseurl,
173 advertisedbaseurl=advertisedbaseurl,
175 apppath=apppath,
174 apppath=apppath,
176 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
175 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
177 querystring=querystring)
176 querystring=querystring)
178
177
179 class wsgirequest(object):
178 class wsgirequest(object):
180 """Higher-level API for a WSGI request.
179 """Higher-level API for a WSGI request.
181
180
182 WSGI applications are invoked with 2 arguments. They are used to
181 WSGI applications are invoked with 2 arguments. They are used to
183 instantiate instances of this class, which provides higher-level APIs
182 instantiate instances of this class, which provides higher-level APIs
184 for obtaining request parameters, writing HTTP output, etc.
183 for obtaining request parameters, writing HTTP output, etc.
185 """
184 """
186 def __init__(self, wsgienv, start_response):
185 def __init__(self, wsgienv, start_response):
187 version = wsgienv[r'wsgi.version']
186 version = wsgienv[r'wsgi.version']
188 if (version < (1, 0)) or (version >= (2, 0)):
187 if (version < (1, 0)) or (version >= (2, 0)):
189 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
188 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
190 % version)
189 % version)
191 self.inp = wsgienv[r'wsgi.input']
190 self.inp = wsgienv[r'wsgi.input']
192 self.err = wsgienv[r'wsgi.errors']
191 self.err = wsgienv[r'wsgi.errors']
193 self.threaded = wsgienv[r'wsgi.multithread']
192 self.threaded = wsgienv[r'wsgi.multithread']
194 self.multiprocess = wsgienv[r'wsgi.multiprocess']
193 self.multiprocess = wsgienv[r'wsgi.multiprocess']
195 self.run_once = wsgienv[r'wsgi.run_once']
194 self.run_once = wsgienv[r'wsgi.run_once']
196 self.env = wsgienv
195 self.env = wsgienv
197 self.form = normalize(cgi.parse(self.inp,
196 self.form = normalize(cgi.parse(self.inp,
198 self.env,
197 self.env,
199 keep_blank_values=1))
198 keep_blank_values=1))
200 self._start_response = start_response
199 self._start_response = start_response
201 self.server_write = None
200 self.server_write = None
202 self.headers = []
201 self.headers = []
203
202
204 def __iter__(self):
203 def __iter__(self):
205 return iter([])
204 return iter([])
206
205
207 def read(self, count=-1):
206 def read(self, count=-1):
208 return self.inp.read(count)
207 return self.inp.read(count)
209
208
210 def drain(self):
209 def drain(self):
211 '''need to read all data from request, httplib is half-duplex'''
210 '''need to read all data from request, httplib is half-duplex'''
212 length = int(self.env.get('CONTENT_LENGTH') or 0)
211 length = int(self.env.get('CONTENT_LENGTH') or 0)
213 for s in util.filechunkiter(self.inp, limit=length):
212 for s in util.filechunkiter(self.inp, limit=length):
214 pass
213 pass
215
214
216 def respond(self, status, type, filename=None, body=None):
215 def respond(self, status, type, filename=None, body=None):
217 if not isinstance(type, str):
216 if not isinstance(type, str):
218 type = pycompat.sysstr(type)
217 type = pycompat.sysstr(type)
219 if self._start_response is not None:
218 if self._start_response is not None:
220 self.headers.append((r'Content-Type', type))
219 self.headers.append((r'Content-Type', type))
221 if filename:
220 if filename:
222 filename = (filename.rpartition('/')[-1]
221 filename = (filename.rpartition('/')[-1]
223 .replace('\\', '\\\\').replace('"', '\\"'))
222 .replace('\\', '\\\\').replace('"', '\\"'))
224 self.headers.append(('Content-Disposition',
223 self.headers.append(('Content-Disposition',
225 'inline; filename="%s"' % filename))
224 'inline; filename="%s"' % filename))
226 if body is not None:
225 if body is not None:
227 self.headers.append((r'Content-Length', str(len(body))))
226 self.headers.append((r'Content-Length', str(len(body))))
228
227
229 for k, v in self.headers:
228 for k, v in self.headers:
230 if not isinstance(v, str):
229 if not isinstance(v, str):
231 raise TypeError('header value must be string: %r' % (v,))
230 raise TypeError('header value must be string: %r' % (v,))
232
231
233 if isinstance(status, ErrorResponse):
232 if isinstance(status, ErrorResponse):
234 self.headers.extend(status.headers)
233 self.headers.extend(status.headers)
235 if status.code == HTTP_NOT_MODIFIED:
234 if status.code == HTTP_NOT_MODIFIED:
236 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
235 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
237 # it MUST NOT include any headers other than these and no
236 # it MUST NOT include any headers other than these and no
238 # body
237 # body
239 self.headers = [(k, v) for (k, v) in self.headers if
238 self.headers = [(k, v) for (k, v) in self.headers if
240 k in ('Date', 'ETag', 'Expires',
239 k in ('Date', 'ETag', 'Expires',
241 'Cache-Control', 'Vary')]
240 'Cache-Control', 'Vary')]
242 status = statusmessage(status.code, pycompat.bytestr(status))
241 status = statusmessage(status.code, pycompat.bytestr(status))
243 elif status == 200:
242 elif status == 200:
244 status = '200 Script output follows'
243 status = '200 Script output follows'
245 elif isinstance(status, int):
244 elif isinstance(status, int):
246 status = statusmessage(status)
245 status = statusmessage(status)
247
246
248 self.server_write = self._start_response(
247 self.server_write = self._start_response(
249 pycompat.sysstr(status), self.headers)
248 pycompat.sysstr(status), self.headers)
250 self._start_response = None
249 self._start_response = None
251 self.headers = []
250 self.headers = []
252 if body is not None:
251 if body is not None:
253 self.write(body)
252 self.write(body)
254 self.server_write = None
253 self.server_write = None
255
254
256 def write(self, thing):
255 def write(self, thing):
257 if thing:
256 if thing:
258 try:
257 try:
259 self.server_write(thing)
258 self.server_write(thing)
260 except socket.error as inst:
259 except socket.error as inst:
261 if inst[0] != errno.ECONNRESET:
260 if inst[0] != errno.ECONNRESET:
262 raise
261 raise
263
262
264 def writelines(self, lines):
263 def writelines(self, lines):
265 for line in lines:
264 for line in lines:
266 self.write(line)
265 self.write(line)
267
266
268 def flush(self):
267 def flush(self):
269 return None
268 return None
270
269
271 def close(self):
270 def close(self):
272 return None
271 return None
273
272
274 def wsgiapplication(app_maker):
273 def wsgiapplication(app_maker):
275 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
274 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
276 can and should now be used as a WSGI application.'''
275 can and should now be used as a WSGI application.'''
277 application = app_maker()
276 application = app_maker()
278 def run_wsgi(env, respond):
277 def run_wsgi(env, respond):
279 return application(env, respond)
278 return application(env, respond)
280 return run_wsgi
279 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now