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