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