##// END OF EJS Templates
hgweb: remove wsgirequest (API)...
Gregory Szorc -
r36928:f0a85154 default
parent child Browse files
Show More
@@ -1,453 +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.parserequestfromenv(env)
294 return self.run_wsgi(req)
294 res = requestmod.wsgiresponse(req, respond)
295
295
296 def run_wsgi(self, wsgireq):
296 return self.run_wsgi(req, res)
297
298 def run_wsgi(self, req, res):
297 """Internal method to run the WSGI application.
299 """Internal method to run the WSGI application.
298
300
299 This is typically only called by Mercurial. External consumers
301 This is typically only called by Mercurial. External consumers
300 should be using instances of this class as the WSGI application.
302 should be using instances of this class as the WSGI application.
301 """
303 """
302 with self._obtainrepo() as repo:
304 with self._obtainrepo() as repo:
303 profile = repo.ui.configbool('profiling', 'enabled')
305 profile = repo.ui.configbool('profiling', 'enabled')
304 with profiling.profile(repo.ui, enabled=profile):
306 with profiling.profile(repo.ui, enabled=profile):
305 for r in self._runwsgi(wsgireq, repo):
307 for r in self._runwsgi(req, res, repo):
306 yield r
308 yield r
307
309
308 def _runwsgi(self, wsgireq, repo):
310 def _runwsgi(self, req, res, repo):
309 req = wsgireq.req
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 = req.rawenv
315 rctx.repo.ui.environ = req.rawenv
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 # Old implementations of hgweb supported dispatching the request via
327 # Old implementations of hgweb supported dispatching the request via
328 # the initial query string parameter instead of using PATH_INFO.
328 # the initial query string parameter instead of using PATH_INFO.
329 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
329 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
330 # a value), we use it. Otherwise fall back to the query string.
330 # a value), we use it. Otherwise fall back to the query string.
331 if req.dispatchpath is not None:
331 if req.dispatchpath is not None:
332 query = req.dispatchpath
332 query = req.dispatchpath
333 else:
333 else:
334 query = req.querystring.partition('&')[0].partition(';')[0]
334 query = req.querystring.partition('&')[0].partition(';')[0]
335
335
336 # translate user-visible url structure to internal structure
336 # translate user-visible url structure to internal structure
337
337
338 args = query.split('/', 2)
338 args = query.split('/', 2)
339 if 'cmd' not in req.qsparams and args and args[0]:
339 if 'cmd' not in req.qsparams and args and args[0]:
340 cmd = args.pop(0)
340 cmd = args.pop(0)
341 style = cmd.rfind('-')
341 style = cmd.rfind('-')
342 if style != -1:
342 if style != -1:
343 req.qsparams['style'] = cmd[:style]
343 req.qsparams['style'] = cmd[:style]
344 cmd = cmd[style + 1:]
344 cmd = cmd[style + 1:]
345
345
346 # avoid accepting e.g. style parameter as command
346 # avoid accepting e.g. style parameter as command
347 if util.safehasattr(webcommands, cmd):
347 if util.safehasattr(webcommands, cmd):
348 req.qsparams['cmd'] = cmd
348 req.qsparams['cmd'] = cmd
349
349
350 if cmd == 'static':
350 if cmd == 'static':
351 req.qsparams['file'] = '/'.join(args)
351 req.qsparams['file'] = '/'.join(args)
352 else:
352 else:
353 if args and args[0]:
353 if args and args[0]:
354 node = args.pop(0).replace('%2F', '/')
354 node = args.pop(0).replace('%2F', '/')
355 req.qsparams['node'] = node
355 req.qsparams['node'] = node
356 if args:
356 if args:
357 if 'file' in req.qsparams:
357 if 'file' in req.qsparams:
358 del req.qsparams['file']
358 del req.qsparams['file']
359 for a in args:
359 for a in args:
360 req.qsparams.add('file', a)
360 req.qsparams.add('file', a)
361
361
362 ua = req.headers.get('User-Agent', '')
362 ua = req.headers.get('User-Agent', '')
363 if cmd == 'rev' and 'mercurial' in ua:
363 if cmd == 'rev' and 'mercurial' in ua:
364 req.qsparams['style'] = 'raw'
364 req.qsparams['style'] = 'raw'
365
365
366 if cmd == 'archive':
366 if cmd == 'archive':
367 fn = req.qsparams['node']
367 fn = req.qsparams['node']
368 for type_, spec in rctx.archivespecs.iteritems():
368 for type_, spec in rctx.archivespecs.iteritems():
369 ext = spec[2]
369 ext = spec[2]
370 if fn.endswith(ext):
370 if fn.endswith(ext):
371 req.qsparams['node'] = fn[:-len(ext)]
371 req.qsparams['node'] = fn[:-len(ext)]
372 req.qsparams['type'] = type_
372 req.qsparams['type'] = type_
373 else:
373 else:
374 cmd = req.qsparams.get('cmd', '')
374 cmd = req.qsparams.get('cmd', '')
375
375
376 # process the web interface request
376 # process the web interface request
377
377
378 try:
378 try:
379 rctx.tmpl = rctx.templater(req)
379 rctx.tmpl = rctx.templater(req)
380 ctype = rctx.tmpl('mimetype', encoding=encoding.encoding)
380 ctype = rctx.tmpl('mimetype', encoding=encoding.encoding)
381 ctype = templater.stringify(ctype)
381 ctype = templater.stringify(ctype)
382
382
383 # check read permissions non-static content
383 # check read permissions non-static content
384 if cmd != 'static':
384 if cmd != 'static':
385 self.check_perm(rctx, req, None)
385 self.check_perm(rctx, req, None)
386
386
387 if cmd == '':
387 if cmd == '':
388 req.qsparams['cmd'] = rctx.tmpl.cache['default']
388 req.qsparams['cmd'] = rctx.tmpl.cache['default']
389 cmd = req.qsparams['cmd']
389 cmd = req.qsparams['cmd']
390
390
391 # 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
392 # be a nonce.
392 # be a nonce.
393 if rctx.configbool('web', 'cache') and not rctx.nonce:
393 if rctx.configbool('web', 'cache') and not rctx.nonce:
394 tag = 'W/"%d"' % self.mtime
394 tag = 'W/"%d"' % self.mtime
395 if req.headers.get('If-None-Match') == tag:
395 if req.headers.get('If-None-Match') == tag:
396 res.status = '304 Not Modified'
396 res.status = '304 Not Modified'
397 # Response body not allowed on 304.
397 # Response body not allowed on 304.
398 res.setbodybytes('')
398 res.setbodybytes('')
399 return res.sendresponse()
399 return res.sendresponse()
400
400
401 res.headers['ETag'] = tag
401 res.headers['ETag'] = tag
402
402
403 if cmd not in webcommands.__all__:
403 if cmd not in webcommands.__all__:
404 msg = 'no such method: %s' % cmd
404 msg = 'no such method: %s' % cmd
405 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
405 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
406 else:
406 else:
407 # Set some globals appropriate for web handlers. Commands can
407 # Set some globals appropriate for web handlers. Commands can
408 # override easily enough.
408 # override easily enough.
409 res.status = '200 Script output follows'
409 res.status = '200 Script output follows'
410 res.headers['Content-Type'] = ctype
410 res.headers['Content-Type'] = ctype
411 return getattr(webcommands, cmd)(rctx)
411 return getattr(webcommands, cmd)(rctx)
412
412
413 except (error.LookupError, error.RepoLookupError) as err:
413 except (error.LookupError, error.RepoLookupError) as err:
414 msg = pycompat.bytestr(err)
414 msg = pycompat.bytestr(err)
415 if (util.safehasattr(err, 'name') and
415 if (util.safehasattr(err, 'name') and
416 not isinstance(err, error.ManifestLookupError)):
416 not isinstance(err, error.ManifestLookupError)):
417 msg = 'revision not found: %s' % err.name
417 msg = 'revision not found: %s' % err.name
418
418
419 res.status = '404 Not Found'
419 res.status = '404 Not Found'
420 res.headers['Content-Type'] = ctype
420 res.headers['Content-Type'] = ctype
421 return rctx.sendtemplate('error', error=msg)
421 return rctx.sendtemplate('error', error=msg)
422 except (error.RepoError, error.RevlogError) as e:
422 except (error.RepoError, error.RevlogError) as e:
423 res.status = '500 Internal Server Error'
423 res.status = '500 Internal Server Error'
424 res.headers['Content-Type'] = ctype
424 res.headers['Content-Type'] = ctype
425 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
425 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
426 except ErrorResponse as e:
426 except ErrorResponse as e:
427 res.status = statusmessage(e.code, pycompat.bytestr(e))
427 res.status = statusmessage(e.code, pycompat.bytestr(e))
428 res.headers['Content-Type'] = ctype
428 res.headers['Content-Type'] = ctype
429 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
429 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
430
430
431 def check_perm(self, rctx, req, op):
431 def check_perm(self, rctx, req, op):
432 for permhook in permhooks:
432 for permhook in permhooks:
433 permhook(rctx, req, op)
433 permhook(rctx, req, op)
434
434
435 def getwebview(repo):
435 def getwebview(repo):
436 """The 'web.view' config controls changeset filter to hgweb. Possible
436 """The 'web.view' config controls changeset filter to hgweb. Possible
437 values are ``served``, ``visible`` and ``all``. Default is ``served``.
437 values are ``served``, ``visible`` and ``all``. Default is ``served``.
438 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
439 hgweb instance. The``visible`` filter includes secret changesets but
439 hgweb instance. The``visible`` filter includes secret changesets but
440 still excludes "hidden" one.
440 still excludes "hidden" one.
441
441
442 See the repoview module for details.
442 See the repoview module for details.
443
443
444 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
445 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."""
446 # experimental config: web.view
446 # experimental config: web.view
447 viewconfig = repo.ui.config('web', 'view', untrusted=True)
447 viewconfig = repo.ui.config('web', 'view', untrusted=True)
448 if viewconfig == 'all':
448 if viewconfig == 'all':
449 return repo.unfiltered()
449 return repo.unfiltered()
450 elif viewconfig in repoview.filtertable:
450 elif viewconfig in repoview.filtertable:
451 return repo.filtered(viewconfig)
451 return repo.filtered(viewconfig)
452 else:
452 else:
453 return repo.filtered('served')
453 return repo.filtered('served')
@@ -1,527 +1,526 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 os
11 import os
12 import time
12 import time
13
13
14 from ..i18n import _
14 from ..i18n import _
15
15
16 from .common import (
16 from .common import (
17 ErrorResponse,
17 ErrorResponse,
18 HTTP_SERVER_ERROR,
18 HTTP_SERVER_ERROR,
19 cspvalues,
19 cspvalues,
20 get_contact,
20 get_contact,
21 get_mtime,
21 get_mtime,
22 ismember,
22 ismember,
23 paritygen,
23 paritygen,
24 staticfile,
24 staticfile,
25 statusmessage,
25 statusmessage,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 configitems,
29 configitems,
30 encoding,
30 encoding,
31 error,
31 error,
32 hg,
32 hg,
33 profiling,
33 profiling,
34 pycompat,
34 pycompat,
35 scmutil,
35 scmutil,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 hgweb_mod,
42 hgweb_mod,
43 request as requestmod,
43 request as requestmod,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47 from ..utils import dateutil
47 from ..utils import dateutil
48
48
49 def cleannames(items):
49 def cleannames(items):
50 return [(util.pconvert(name).strip('/'), path) for name, path in items]
50 return [(util.pconvert(name).strip('/'), path) for name, path in items]
51
51
52 def findrepos(paths):
52 def findrepos(paths):
53 repos = []
53 repos = []
54 for prefix, root in cleannames(paths):
54 for prefix, root in cleannames(paths):
55 roothead, roottail = os.path.split(root)
55 roothead, roottail = os.path.split(root)
56 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
56 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
57 # /bar/ be served as as foo/N .
57 # /bar/ be served as as foo/N .
58 # '*' will not search inside dirs with .hg (except .hg/patches),
58 # '*' will not search inside dirs with .hg (except .hg/patches),
59 # '**' will search inside dirs with .hg (and thus also find subrepos).
59 # '**' will search inside dirs with .hg (and thus also find subrepos).
60 try:
60 try:
61 recurse = {'*': False, '**': True}[roottail]
61 recurse = {'*': False, '**': True}[roottail]
62 except KeyError:
62 except KeyError:
63 repos.append((prefix, root))
63 repos.append((prefix, root))
64 continue
64 continue
65 roothead = os.path.normpath(os.path.abspath(roothead))
65 roothead = os.path.normpath(os.path.abspath(roothead))
66 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
66 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
67 repos.extend(urlrepos(prefix, roothead, paths))
67 repos.extend(urlrepos(prefix, roothead, paths))
68 return repos
68 return repos
69
69
70 def urlrepos(prefix, roothead, paths):
70 def urlrepos(prefix, roothead, paths):
71 """yield url paths and filesystem paths from a list of repo paths
71 """yield url paths and filesystem paths from a list of repo paths
72
72
73 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
73 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
74 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
74 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
75 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
75 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
76 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
77 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
78 """
78 """
79 for path in paths:
79 for path in paths:
80 path = os.path.normpath(path)
80 path = os.path.normpath(path)
81 yield (prefix + '/' +
81 yield (prefix + '/' +
82 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
82 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
83
83
84 def readallowed(ui, req):
84 def readallowed(ui, req):
85 """Check allow_read and deny_read config options of a repo's ui object
85 """Check allow_read and deny_read config options of a repo's ui object
86 to determine user permissions. By default, with neither option set (or
86 to determine user permissions. By default, with neither option set (or
87 both empty), allow all users to read the repo. There are two ways a
87 both empty), allow all users to read the repo. There are two ways a
88 user can be denied read access: (1) deny_read is not empty, and the
88 user can be denied read access: (1) deny_read is not empty, and the
89 user is unauthenticated or deny_read contains user (or *), and (2)
89 user is unauthenticated or deny_read contains user (or *), and (2)
90 allow_read is not empty and the user is not in allow_read. Return True
90 allow_read is not empty and the user is not in allow_read. Return True
91 if user is allowed to read the repo, else return False."""
91 if user is allowed to read the repo, else return False."""
92
92
93 user = req.remoteuser
93 user = req.remoteuser
94
94
95 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
95 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
96 if deny_read and (not user or ismember(ui, user, deny_read)):
96 if deny_read and (not user or ismember(ui, user, deny_read)):
97 return False
97 return False
98
98
99 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
99 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
100 # by default, allow reading if no allow_read option has been set
100 # by default, allow reading if no allow_read option has been set
101 if not allow_read or ismember(ui, user, allow_read):
101 if not allow_read or ismember(ui, user, allow_read):
102 return True
102 return True
103
103
104 return False
104 return False
105
105
106 def archivelist(ui, nodeid, url):
106 def archivelist(ui, nodeid, url):
107 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
107 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
108 archives = []
108 archives = []
109
109
110 for typ, spec in hgweb_mod.archivespecs.iteritems():
110 for typ, spec in hgweb_mod.archivespecs.iteritems():
111 if typ in allowed or ui.configbool('web', 'allow' + typ,
111 if typ in allowed or ui.configbool('web', 'allow' + typ,
112 untrusted=True):
112 untrusted=True):
113 archives.append({
113 archives.append({
114 'type': typ,
114 'type': typ,
115 'extension': spec[2],
115 'extension': spec[2],
116 'node': nodeid,
116 'node': nodeid,
117 'url': url,
117 'url': url,
118 })
118 })
119
119
120 return archives
120 return archives
121
121
122 def rawindexentries(ui, repos, req, subdir=''):
122 def rawindexentries(ui, repos, req, subdir=''):
123 descend = ui.configbool('web', 'descend')
123 descend = ui.configbool('web', 'descend')
124 collapse = ui.configbool('web', 'collapse')
124 collapse = ui.configbool('web', 'collapse')
125 seenrepos = set()
125 seenrepos = set()
126 seendirs = set()
126 seendirs = set()
127 for name, path in repos:
127 for name, path in repos:
128
128
129 if not name.startswith(subdir):
129 if not name.startswith(subdir):
130 continue
130 continue
131 name = name[len(subdir):]
131 name = name[len(subdir):]
132 directory = False
132 directory = False
133
133
134 if '/' in name:
134 if '/' in name:
135 if not descend:
135 if not descend:
136 continue
136 continue
137
137
138 nameparts = name.split('/')
138 nameparts = name.split('/')
139 rootname = nameparts[0]
139 rootname = nameparts[0]
140
140
141 if not collapse:
141 if not collapse:
142 pass
142 pass
143 elif rootname in seendirs:
143 elif rootname in seendirs:
144 continue
144 continue
145 elif rootname in seenrepos:
145 elif rootname in seenrepos:
146 pass
146 pass
147 else:
147 else:
148 directory = True
148 directory = True
149 name = rootname
149 name = rootname
150
150
151 # redefine the path to refer to the directory
151 # redefine the path to refer to the directory
152 discarded = '/'.join(nameparts[1:])
152 discarded = '/'.join(nameparts[1:])
153
153
154 # remove name parts plus accompanying slash
154 # remove name parts plus accompanying slash
155 path = path[:-len(discarded) - 1]
155 path = path[:-len(discarded) - 1]
156
156
157 try:
157 try:
158 r = hg.repository(ui, path)
158 r = hg.repository(ui, path)
159 directory = False
159 directory = False
160 except (IOError, error.RepoError):
160 except (IOError, error.RepoError):
161 pass
161 pass
162
162
163 parts = [
163 parts = [
164 req.apppath.strip('/'),
164 req.apppath.strip('/'),
165 subdir.strip('/'),
165 subdir.strip('/'),
166 name.strip('/'),
166 name.strip('/'),
167 ]
167 ]
168 url = '/' + '/'.join(p for p in parts if p) + '/'
168 url = '/' + '/'.join(p for p in parts if p) + '/'
169
169
170 # show either a directory entry or a repository
170 # show either a directory entry or a repository
171 if directory:
171 if directory:
172 # get the directory's time information
172 # get the directory's time information
173 try:
173 try:
174 d = (get_mtime(path), dateutil.makedate()[1])
174 d = (get_mtime(path), dateutil.makedate()[1])
175 except OSError:
175 except OSError:
176 continue
176 continue
177
177
178 # add '/' to the name to make it obvious that
178 # add '/' to the name to make it obvious that
179 # the entry is a directory, not a regular repository
179 # the entry is a directory, not a regular repository
180 row = {'contact': "",
180 row = {'contact': "",
181 'contact_sort': "",
181 'contact_sort': "",
182 'name': name + '/',
182 'name': name + '/',
183 'name_sort': name,
183 'name_sort': name,
184 'url': url,
184 'url': url,
185 'description': "",
185 'description': "",
186 'description_sort': "",
186 'description_sort': "",
187 'lastchange': d,
187 'lastchange': d,
188 'lastchange_sort': d[1] - d[0],
188 'lastchange_sort': d[1] - d[0],
189 'archives': [],
189 'archives': [],
190 'isdirectory': True,
190 'isdirectory': True,
191 'labels': [],
191 'labels': [],
192 }
192 }
193
193
194 seendirs.add(name)
194 seendirs.add(name)
195 yield row
195 yield row
196 continue
196 continue
197
197
198 u = ui.copy()
198 u = ui.copy()
199 try:
199 try:
200 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
200 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
201 except Exception as e:
201 except Exception as e:
202 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
202 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
203 continue
203 continue
204
204
205 def get(section, name, default=uimod._unset):
205 def get(section, name, default=uimod._unset):
206 return u.config(section, name, default, untrusted=True)
206 return u.config(section, name, default, untrusted=True)
207
207
208 if u.configbool("web", "hidden", untrusted=True):
208 if u.configbool("web", "hidden", untrusted=True):
209 continue
209 continue
210
210
211 if not readallowed(u, req):
211 if not readallowed(u, req):
212 continue
212 continue
213
213
214 # update time with local timezone
214 # update time with local timezone
215 try:
215 try:
216 r = hg.repository(ui, path)
216 r = hg.repository(ui, path)
217 except IOError:
217 except IOError:
218 u.warn(_('error accessing repository at %s\n') % path)
218 u.warn(_('error accessing repository at %s\n') % path)
219 continue
219 continue
220 except error.RepoError:
220 except error.RepoError:
221 u.warn(_('error accessing repository at %s\n') % path)
221 u.warn(_('error accessing repository at %s\n') % path)
222 continue
222 continue
223 try:
223 try:
224 d = (get_mtime(r.spath), dateutil.makedate()[1])
224 d = (get_mtime(r.spath), dateutil.makedate()[1])
225 except OSError:
225 except OSError:
226 continue
226 continue
227
227
228 contact = get_contact(get)
228 contact = get_contact(get)
229 description = get("web", "description")
229 description = get("web", "description")
230 seenrepos.add(name)
230 seenrepos.add(name)
231 name = get("web", "name", name)
231 name = get("web", "name", name)
232 row = {'contact': contact or "unknown",
232 row = {'contact': contact or "unknown",
233 'contact_sort': contact.upper() or "unknown",
233 'contact_sort': contact.upper() or "unknown",
234 'name': name,
234 'name': name,
235 'name_sort': name,
235 'name_sort': name,
236 'url': url,
236 'url': url,
237 'description': description or "unknown",
237 'description': description or "unknown",
238 'description_sort': description.upper() or "unknown",
238 'description_sort': description.upper() or "unknown",
239 'lastchange': d,
239 'lastchange': d,
240 'lastchange_sort': d[1] - d[0],
240 'lastchange_sort': d[1] - d[0],
241 'archives': archivelist(u, "tip", url),
241 'archives': archivelist(u, "tip", url),
242 'isdirectory': None,
242 'isdirectory': None,
243 'labels': u.configlist('web', 'labels', untrusted=True),
243 'labels': u.configlist('web', 'labels', untrusted=True),
244 }
244 }
245
245
246 yield row
246 yield row
247
247
248 def indexentries(ui, repos, req, stripecount, sortcolumn='',
248 def indexentries(ui, repos, req, stripecount, sortcolumn='',
249 descending=False, subdir=''):
249 descending=False, subdir=''):
250
250
251 rows = rawindexentries(ui, repos, req, subdir=subdir)
251 rows = rawindexentries(ui, repos, req, subdir=subdir)
252
252
253 sortdefault = None, False
253 sortdefault = None, False
254
254
255 if sortcolumn and sortdefault != (sortcolumn, descending):
255 if sortcolumn and sortdefault != (sortcolumn, descending):
256 sortkey = '%s_sort' % sortcolumn
256 sortkey = '%s_sort' % sortcolumn
257 rows = sorted(rows, key=lambda x: x[sortkey],
257 rows = sorted(rows, key=lambda x: x[sortkey],
258 reverse=descending)
258 reverse=descending)
259
259
260 for row, parity in zip(rows, paritygen(stripecount)):
260 for row, parity in zip(rows, paritygen(stripecount)):
261 row['parity'] = parity
261 row['parity'] = parity
262 yield row
262 yield row
263
263
264 class hgwebdir(object):
264 class hgwebdir(object):
265 """HTTP server for multiple repositories.
265 """HTTP server for multiple repositories.
266
266
267 Given a configuration, different repositories will be served depending
267 Given a configuration, different repositories will be served depending
268 on the request path.
268 on the request path.
269
269
270 Instances are typically used as WSGI applications.
270 Instances are typically used as WSGI applications.
271 """
271 """
272 def __init__(self, conf, baseui=None):
272 def __init__(self, conf, baseui=None):
273 self.conf = conf
273 self.conf = conf
274 self.baseui = baseui
274 self.baseui = baseui
275 self.ui = None
275 self.ui = None
276 self.lastrefresh = 0
276 self.lastrefresh = 0
277 self.motd = None
277 self.motd = None
278 self.refresh()
278 self.refresh()
279
279
280 def refresh(self):
280 def refresh(self):
281 if self.ui:
281 if self.ui:
282 refreshinterval = self.ui.configint('web', 'refreshinterval')
282 refreshinterval = self.ui.configint('web', 'refreshinterval')
283 else:
283 else:
284 item = configitems.coreitems['web']['refreshinterval']
284 item = configitems.coreitems['web']['refreshinterval']
285 refreshinterval = item.default
285 refreshinterval = item.default
286
286
287 # refreshinterval <= 0 means to always refresh.
287 # refreshinterval <= 0 means to always refresh.
288 if (refreshinterval > 0 and
288 if (refreshinterval > 0 and
289 self.lastrefresh + refreshinterval > time.time()):
289 self.lastrefresh + refreshinterval > time.time()):
290 return
290 return
291
291
292 if self.baseui:
292 if self.baseui:
293 u = self.baseui.copy()
293 u = self.baseui.copy()
294 else:
294 else:
295 u = uimod.ui.load()
295 u = uimod.ui.load()
296 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
296 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
297 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
297 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
298 # displaying bundling progress bar while serving feels wrong and may
298 # displaying bundling progress bar while serving feels wrong and may
299 # break some wsgi implementations.
299 # break some wsgi implementations.
300 u.setconfig('progress', 'disable', 'true', 'hgweb')
300 u.setconfig('progress', 'disable', 'true', 'hgweb')
301
301
302 if not isinstance(self.conf, (dict, list, tuple)):
302 if not isinstance(self.conf, (dict, list, tuple)):
303 map = {'paths': 'hgweb-paths'}
303 map = {'paths': 'hgweb-paths'}
304 if not os.path.exists(self.conf):
304 if not os.path.exists(self.conf):
305 raise error.Abort(_('config file %s not found!') % self.conf)
305 raise error.Abort(_('config file %s not found!') % self.conf)
306 u.readconfig(self.conf, remap=map, trust=True)
306 u.readconfig(self.conf, remap=map, trust=True)
307 paths = []
307 paths = []
308 for name, ignored in u.configitems('hgweb-paths'):
308 for name, ignored in u.configitems('hgweb-paths'):
309 for path in u.configlist('hgweb-paths', name):
309 for path in u.configlist('hgweb-paths', name):
310 paths.append((name, path))
310 paths.append((name, path))
311 elif isinstance(self.conf, (list, tuple)):
311 elif isinstance(self.conf, (list, tuple)):
312 paths = self.conf
312 paths = self.conf
313 elif isinstance(self.conf, dict):
313 elif isinstance(self.conf, dict):
314 paths = self.conf.items()
314 paths = self.conf.items()
315
315
316 repos = findrepos(paths)
316 repos = findrepos(paths)
317 for prefix, root in u.configitems('collections'):
317 for prefix, root in u.configitems('collections'):
318 prefix = util.pconvert(prefix)
318 prefix = util.pconvert(prefix)
319 for path in scmutil.walkrepos(root, followsym=True):
319 for path in scmutil.walkrepos(root, followsym=True):
320 repo = os.path.normpath(path)
320 repo = os.path.normpath(path)
321 name = util.pconvert(repo)
321 name = util.pconvert(repo)
322 if name.startswith(prefix):
322 if name.startswith(prefix):
323 name = name[len(prefix):]
323 name = name[len(prefix):]
324 repos.append((name.lstrip('/'), repo))
324 repos.append((name.lstrip('/'), repo))
325
325
326 self.repos = repos
326 self.repos = repos
327 self.ui = u
327 self.ui = u
328 encoding.encoding = self.ui.config('web', 'encoding')
328 encoding.encoding = self.ui.config('web', 'encoding')
329 self.style = self.ui.config('web', 'style')
329 self.style = self.ui.config('web', 'style')
330 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
330 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
331 self.stripecount = self.ui.config('web', 'stripes')
331 self.stripecount = self.ui.config('web', 'stripes')
332 if self.stripecount:
332 if self.stripecount:
333 self.stripecount = int(self.stripecount)
333 self.stripecount = int(self.stripecount)
334 prefix = self.ui.config('web', 'prefix')
334 prefix = self.ui.config('web', 'prefix')
335 if prefix.startswith('/'):
335 if prefix.startswith('/'):
336 prefix = prefix[1:]
336 prefix = prefix[1:]
337 if prefix.endswith('/'):
337 if prefix.endswith('/'):
338 prefix = prefix[:-1]
338 prefix = prefix[:-1]
339 self.prefix = prefix
339 self.prefix = prefix
340 self.lastrefresh = time.time()
340 self.lastrefresh = time.time()
341
341
342 def run(self):
342 def run(self):
343 if not encoding.environ.get('GATEWAY_INTERFACE',
343 if not encoding.environ.get('GATEWAY_INTERFACE',
344 '').startswith("CGI/1."):
344 '').startswith("CGI/1."):
345 raise RuntimeError("This function is only intended to be "
345 raise RuntimeError("This function is only intended to be "
346 "called while running as a CGI script.")
346 "called while running as a CGI script.")
347 wsgicgi.launch(self)
347 wsgicgi.launch(self)
348
348
349 def __call__(self, env, respond):
349 def __call__(self, env, respond):
350 baseurl = self.ui.config('web', 'baseurl')
350 baseurl = self.ui.config('web', 'baseurl')
351 wsgireq = requestmod.wsgirequest(env, respond, altbaseurl=baseurl)
351 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
352 return self.run_wsgi(wsgireq)
352 res = requestmod.wsgiresponse(req, respond)
353
353
354 def run_wsgi(self, wsgireq):
354 return self.run_wsgi(req, res)
355
356 def run_wsgi(self, req, res):
355 profile = self.ui.configbool('profiling', 'enabled')
357 profile = self.ui.configbool('profiling', 'enabled')
356 with profiling.profile(self.ui, enabled=profile):
358 with profiling.profile(self.ui, enabled=profile):
357 for r in self._runwsgi(wsgireq):
359 for r in self._runwsgi(req, res):
358 yield r
360 yield r
359
361
360 def _runwsgi(self, wsgireq):
362 def _runwsgi(self, req, res):
361 req = wsgireq.req
362 res = wsgireq.res
363
364 try:
363 try:
365 self.refresh()
364 self.refresh()
366
365
367 csp, nonce = cspvalues(self.ui)
366 csp, nonce = cspvalues(self.ui)
368 if csp:
367 if csp:
369 res.headers['Content-Security-Policy'] = csp
368 res.headers['Content-Security-Policy'] = csp
370
369
371 virtual = req.dispatchpath.strip('/')
370 virtual = req.dispatchpath.strip('/')
372 tmpl = self.templater(req, nonce)
371 tmpl = self.templater(req, nonce)
373 ctype = tmpl('mimetype', encoding=encoding.encoding)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
374 ctype = templater.stringify(ctype)
373 ctype = templater.stringify(ctype)
375
374
376 # Global defaults. These can be overridden by any handler.
375 # Global defaults. These can be overridden by any handler.
377 res.status = '200 Script output follows'
376 res.status = '200 Script output follows'
378 res.headers['Content-Type'] = ctype
377 res.headers['Content-Type'] = ctype
379
378
380 # a static file
379 # a static file
381 if virtual.startswith('static/') or 'static' in req.qsparams:
380 if virtual.startswith('static/') or 'static' in req.qsparams:
382 if virtual.startswith('static/'):
381 if virtual.startswith('static/'):
383 fname = virtual[7:]
382 fname = virtual[7:]
384 else:
383 else:
385 fname = req.qsparams['static']
384 fname = req.qsparams['static']
386 static = self.ui.config("web", "static", None,
385 static = self.ui.config("web", "static", None,
387 untrusted=False)
386 untrusted=False)
388 if not static:
387 if not static:
389 tp = self.templatepath or templater.templatepaths()
388 tp = self.templatepath or templater.templatepaths()
390 if isinstance(tp, str):
389 if isinstance(tp, str):
391 tp = [tp]
390 tp = [tp]
392 static = [os.path.join(p, 'static') for p in tp]
391 static = [os.path.join(p, 'static') for p in tp]
393
392
394 staticfile(static, fname, res)
393 staticfile(static, fname, res)
395 return res.sendresponse()
394 return res.sendresponse()
396
395
397 # top-level index
396 # top-level index
398
397
399 repos = dict(self.repos)
398 repos = dict(self.repos)
400
399
401 if (not virtual or virtual == 'index') and virtual not in repos:
400 if (not virtual or virtual == 'index') and virtual not in repos:
402 return self.makeindex(req, res, tmpl)
401 return self.makeindex(req, res, tmpl)
403
402
404 # nested indexes and hgwebs
403 # nested indexes and hgwebs
405
404
406 if virtual.endswith('/index') and virtual not in repos:
405 if virtual.endswith('/index') and virtual not in repos:
407 subdir = virtual[:-len('index')]
406 subdir = virtual[:-len('index')]
408 if any(r.startswith(subdir) for r in repos):
407 if any(r.startswith(subdir) for r in repos):
409 return self.makeindex(req, res, tmpl, subdir)
408 return self.makeindex(req, res, tmpl, subdir)
410
409
411 def _virtualdirs():
410 def _virtualdirs():
412 # Check the full virtual path, each parent, and the root ('')
411 # Check the full virtual path, each parent, and the root ('')
413 if virtual != '':
412 if virtual != '':
414 yield virtual
413 yield virtual
415
414
416 for p in util.finddirs(virtual):
415 for p in util.finddirs(virtual):
417 yield p
416 yield p
418
417
419 yield ''
418 yield ''
420
419
421 for virtualrepo in _virtualdirs():
420 for virtualrepo in _virtualdirs():
422 real = repos.get(virtualrepo)
421 real = repos.get(virtualrepo)
423 if real:
422 if real:
424 # Re-parse the WSGI environment to take into account our
423 # Re-parse the WSGI environment to take into account our
425 # repository path component.
424 # repository path component.
426 wsgireq.req = requestmod.parserequestfromenv(
425 req = requestmod.parserequestfromenv(
427 wsgireq.env, wsgireq.req.bodyfh, reponame=virtualrepo,
426 req.rawenv, reponame=virtualrepo,
428 altbaseurl=self.ui.config('web', 'baseurl'))
427 altbaseurl=self.ui.config('web', 'baseurl'))
429 try:
428 try:
430 # ensure caller gets private copy of ui
429 # ensure caller gets private copy of ui
431 repo = hg.repository(self.ui.copy(), real)
430 repo = hg.repository(self.ui.copy(), real)
432 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
431 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
433 except IOError as inst:
432 except IOError as inst:
434 msg = encoding.strtolocal(inst.strerror)
433 msg = encoding.strtolocal(inst.strerror)
435 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
434 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
436 except error.RepoError as inst:
435 except error.RepoError as inst:
437 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
436 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
438
437
439 # browse subdirectories
438 # browse subdirectories
440 subdir = virtual + '/'
439 subdir = virtual + '/'
441 if [r for r in repos if r.startswith(subdir)]:
440 if [r for r in repos if r.startswith(subdir)]:
442 return self.makeindex(req, res, tmpl, subdir)
441 return self.makeindex(req, res, tmpl, subdir)
443
442
444 # prefixes not found
443 # prefixes not found
445 res.status = '404 Not Found'
444 res.status = '404 Not Found'
446 res.setbodygen(tmpl('notfound', repo=virtual))
445 res.setbodygen(tmpl('notfound', repo=virtual))
447 return res.sendresponse()
446 return res.sendresponse()
448
447
449 except ErrorResponse as e:
448 except ErrorResponse as e:
450 res.status = statusmessage(e.code, pycompat.bytestr(e))
449 res.status = statusmessage(e.code, pycompat.bytestr(e))
451 res.setbodygen(tmpl('error', error=e.message or ''))
450 res.setbodygen(tmpl('error', error=e.message or ''))
452 return res.sendresponse()
451 return res.sendresponse()
453 finally:
452 finally:
454 tmpl = None
453 tmpl = None
455
454
456 def makeindex(self, req, res, tmpl, subdir=""):
455 def makeindex(self, req, res, tmpl, subdir=""):
457 self.refresh()
456 self.refresh()
458 sortable = ["name", "description", "contact", "lastchange"]
457 sortable = ["name", "description", "contact", "lastchange"]
459 sortcolumn, descending = None, False
458 sortcolumn, descending = None, False
460 if 'sort' in req.qsparams:
459 if 'sort' in req.qsparams:
461 sortcolumn = req.qsparams['sort']
460 sortcolumn = req.qsparams['sort']
462 descending = sortcolumn.startswith('-')
461 descending = sortcolumn.startswith('-')
463 if descending:
462 if descending:
464 sortcolumn = sortcolumn[1:]
463 sortcolumn = sortcolumn[1:]
465 if sortcolumn not in sortable:
464 if sortcolumn not in sortable:
466 sortcolumn = ""
465 sortcolumn = ""
467
466
468 sort = [("sort_%s" % column,
467 sort = [("sort_%s" % column,
469 "%s%s" % ((not descending and column == sortcolumn)
468 "%s%s" % ((not descending and column == sortcolumn)
470 and "-" or "", column))
469 and "-" or "", column))
471 for column in sortable]
470 for column in sortable]
472
471
473 self.refresh()
472 self.refresh()
474
473
475 entries = indexentries(self.ui, self.repos, req,
474 entries = indexentries(self.ui, self.repos, req,
476 self.stripecount, sortcolumn=sortcolumn,
475 self.stripecount, sortcolumn=sortcolumn,
477 descending=descending, subdir=subdir)
476 descending=descending, subdir=subdir)
478
477
479 res.setbodygen(tmpl(
478 res.setbodygen(tmpl(
480 'index',
479 'index',
481 entries=entries,
480 entries=entries,
482 subdir=subdir,
481 subdir=subdir,
483 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
484 sortcolumn=sortcolumn,
483 sortcolumn=sortcolumn,
485 descending=descending,
484 descending=descending,
486 **dict(sort)))
485 **dict(sort)))
487
486
488 return res.sendresponse()
487 return res.sendresponse()
489
488
490 def templater(self, req, nonce):
489 def templater(self, req, nonce):
491
490
492 def motd(**map):
491 def motd(**map):
493 if self.motd is not None:
492 if self.motd is not None:
494 yield self.motd
493 yield self.motd
495 else:
494 else:
496 yield config('web', 'motd')
495 yield config('web', 'motd')
497
496
498 def config(section, name, default=uimod._unset, untrusted=True):
497 def config(section, name, default=uimod._unset, untrusted=True):
499 return self.ui.config(section, name, default, untrusted)
498 return self.ui.config(section, name, default, untrusted)
500
499
501 vars = {}
500 vars = {}
502 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
501 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
503 self.templatepath)
502 self.templatepath)
504 if style == styles[0]:
503 if style == styles[0]:
505 vars['style'] = style
504 vars['style'] = style
506
505
507 sessionvars = webutil.sessionvars(vars, r'?')
506 sessionvars = webutil.sessionvars(vars, r'?')
508 logourl = config('web', 'logourl')
507 logourl = config('web', 'logourl')
509 logoimg = config('web', 'logoimg')
508 logoimg = config('web', 'logoimg')
510 staticurl = (config('web', 'staticurl')
509 staticurl = (config('web', 'staticurl')
511 or req.apppath + '/static/')
510 or req.apppath + '/static/')
512 if not staticurl.endswith('/'):
511 if not staticurl.endswith('/'):
513 staticurl += '/'
512 staticurl += '/'
514
513
515 defaults = {
514 defaults = {
516 "encoding": encoding.encoding,
515 "encoding": encoding.encoding,
517 "motd": motd,
516 "motd": motd,
518 "url": req.apppath + '/',
517 "url": req.apppath + '/',
519 "logourl": logourl,
518 "logourl": logourl,
520 "logoimg": logoimg,
519 "logoimg": logoimg,
521 "staticurl": staticurl,
520 "staticurl": staticurl,
522 "sessionvars": sessionvars,
521 "sessionvars": sessionvars,
523 "style": style,
522 "style": style,
524 "nonce": nonce,
523 "nonce": nonce,
525 }
524 }
526 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
525 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
527 return tmpl
526 return tmpl
@@ -1,615 +1,585 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 wsgiref.headers as wsgiheaders
11 import wsgiref.headers as wsgiheaders
12 #import wsgiref.validate
12 #import wsgiref.validate
13
13
14 from ..thirdparty import (
14 from ..thirdparty import (
15 attr,
15 attr,
16 )
16 )
17 from .. import (
17 from .. import (
18 error,
18 error,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 )
21 )
22
22
23 class multidict(object):
23 class multidict(object):
24 """A dict like object that can store multiple values for a key.
24 """A dict like object that can store multiple values for a key.
25
25
26 Used to store parsed request parameters.
26 Used to store parsed request parameters.
27
27
28 This is inspired by WebOb's class of the same name.
28 This is inspired by WebOb's class of the same name.
29 """
29 """
30 def __init__(self):
30 def __init__(self):
31 # Stores (key, value) 2-tuples. This isn't the most efficient. But we
31 # Stores (key, value) 2-tuples. This isn't the most efficient. But we
32 # don't rely on parameters that much, so it shouldn't be a perf issue.
32 # don't rely on parameters that much, so it shouldn't be a perf issue.
33 # we can always add dict for fast lookups.
33 # we can always add dict for fast lookups.
34 self._items = []
34 self._items = []
35
35
36 def __getitem__(self, key):
36 def __getitem__(self, key):
37 """Returns the last set value for a key."""
37 """Returns the last set value for a key."""
38 for k, v in reversed(self._items):
38 for k, v in reversed(self._items):
39 if k == key:
39 if k == key:
40 return v
40 return v
41
41
42 raise KeyError(key)
42 raise KeyError(key)
43
43
44 def __setitem__(self, key, value):
44 def __setitem__(self, key, value):
45 """Replace a values for a key with a new value."""
45 """Replace a values for a key with a new value."""
46 try:
46 try:
47 del self[key]
47 del self[key]
48 except KeyError:
48 except KeyError:
49 pass
49 pass
50
50
51 self._items.append((key, value))
51 self._items.append((key, value))
52
52
53 def __delitem__(self, key):
53 def __delitem__(self, key):
54 """Delete all values for a key."""
54 """Delete all values for a key."""
55 oldlen = len(self._items)
55 oldlen = len(self._items)
56
56
57 self._items[:] = [(k, v) for k, v in self._items if k != key]
57 self._items[:] = [(k, v) for k, v in self._items if k != key]
58
58
59 if oldlen == len(self._items):
59 if oldlen == len(self._items):
60 raise KeyError(key)
60 raise KeyError(key)
61
61
62 def __contains__(self, key):
62 def __contains__(self, key):
63 return any(k == key for k, v in self._items)
63 return any(k == key for k, v in self._items)
64
64
65 def __len__(self):
65 def __len__(self):
66 return len(self._items)
66 return len(self._items)
67
67
68 def get(self, key, default=None):
68 def get(self, key, default=None):
69 try:
69 try:
70 return self.__getitem__(key)
70 return self.__getitem__(key)
71 except KeyError:
71 except KeyError:
72 return default
72 return default
73
73
74 def add(self, key, value):
74 def add(self, key, value):
75 """Add a new value for a key. Does not replace existing values."""
75 """Add a new value for a key. Does not replace existing values."""
76 self._items.append((key, value))
76 self._items.append((key, value))
77
77
78 def getall(self, key):
78 def getall(self, key):
79 """Obtains all values for a key."""
79 """Obtains all values for a key."""
80 return [v for k, v in self._items if k == key]
80 return [v for k, v in self._items if k == key]
81
81
82 def getone(self, key):
82 def getone(self, key):
83 """Obtain a single value for a key.
83 """Obtain a single value for a key.
84
84
85 Raises KeyError if key not defined or it has multiple values set.
85 Raises KeyError if key not defined or it has multiple values set.
86 """
86 """
87 vals = self.getall(key)
87 vals = self.getall(key)
88
88
89 if not vals:
89 if not vals:
90 raise KeyError(key)
90 raise KeyError(key)
91
91
92 if len(vals) > 1:
92 if len(vals) > 1:
93 raise KeyError('multiple values for %r' % key)
93 raise KeyError('multiple values for %r' % key)
94
94
95 return vals[0]
95 return vals[0]
96
96
97 def asdictoflists(self):
97 def asdictoflists(self):
98 d = {}
98 d = {}
99 for k, v in self._items:
99 for k, v in self._items:
100 if k in d:
100 if k in d:
101 d[k].append(v)
101 d[k].append(v)
102 else:
102 else:
103 d[k] = [v]
103 d[k] = [v]
104
104
105 return d
105 return d
106
106
107 @attr.s(frozen=True)
107 @attr.s(frozen=True)
108 class parsedrequest(object):
108 class parsedrequest(object):
109 """Represents a parsed WSGI request.
109 """Represents a parsed WSGI request.
110
110
111 Contains both parsed parameters as well as a handle on the input stream.
111 Contains both parsed parameters as well as a handle on the input stream.
112 """
112 """
113
113
114 # Request method.
114 # Request method.
115 method = attr.ib()
115 method = attr.ib()
116 # Full URL for this request.
116 # Full URL for this request.
117 url = attr.ib()
117 url = attr.ib()
118 # URL without any path components. Just <proto>://<host><port>.
118 # URL without any path components. Just <proto>://<host><port>.
119 baseurl = attr.ib()
119 baseurl = attr.ib()
120 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
120 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
121 # of HTTP: Host header for hostname. This is likely what clients used.
121 # of HTTP: Host header for hostname. This is likely what clients used.
122 advertisedurl = attr.ib()
122 advertisedurl = attr.ib()
123 advertisedbaseurl = attr.ib()
123 advertisedbaseurl = attr.ib()
124 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
124 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
125 urlscheme = attr.ib()
125 urlscheme = attr.ib()
126 # Value of REMOTE_USER, if set, or None.
126 # Value of REMOTE_USER, if set, or None.
127 remoteuser = attr.ib()
127 remoteuser = attr.ib()
128 # Value of REMOTE_HOST, if set, or None.
128 # Value of REMOTE_HOST, if set, or None.
129 remotehost = attr.ib()
129 remotehost = attr.ib()
130 # Relative WSGI application path. If defined, will begin with a
130 # Relative WSGI application path. If defined, will begin with a
131 # ``/``.
131 # ``/``.
132 apppath = attr.ib()
132 apppath = attr.ib()
133 # List of path parts to be used for dispatch.
133 # List of path parts to be used for dispatch.
134 dispatchparts = attr.ib()
134 dispatchparts = attr.ib()
135 # URL path component (no query string) used for dispatch. Can be
135 # URL path component (no query string) used for dispatch. Can be
136 # ``None`` to signal no path component given to the request, an
136 # ``None`` to signal no path component given to the request, an
137 # empty string to signal a request to the application's root URL,
137 # empty string to signal a request to the application's root URL,
138 # or a string not beginning with ``/`` containing the requested
138 # or a string not beginning with ``/`` containing the requested
139 # path under the application.
139 # path under the application.
140 dispatchpath = attr.ib()
140 dispatchpath = attr.ib()
141 # The name of the repository being accessed.
141 # The name of the repository being accessed.
142 reponame = attr.ib()
142 reponame = attr.ib()
143 # Raw query string (part after "?" in URL).
143 # Raw query string (part after "?" in URL).
144 querystring = attr.ib()
144 querystring = attr.ib()
145 # multidict of query string parameters.
145 # multidict of query string parameters.
146 qsparams = attr.ib()
146 qsparams = attr.ib()
147 # wsgiref.headers.Headers instance. Operates like a dict with case
147 # wsgiref.headers.Headers instance. Operates like a dict with case
148 # insensitive keys.
148 # insensitive keys.
149 headers = attr.ib()
149 headers = attr.ib()
150 # Request body input stream.
150 # Request body input stream.
151 bodyfh = attr.ib()
151 bodyfh = attr.ib()
152 # WSGI environment dict, unmodified.
152 # WSGI environment dict, unmodified.
153 rawenv = attr.ib()
153 rawenv = attr.ib()
154
154
155 def parserequestfromenv(env, bodyfh, reponame=None, altbaseurl=None):
155 def parserequestfromenv(env, reponame=None, altbaseurl=None):
156 """Parse URL components from environment variables.
156 """Parse URL components from environment variables.
157
157
158 WSGI defines request attributes via environment variables. This function
158 WSGI defines request attributes via environment variables. This function
159 parses the environment variables into a data structure.
159 parses the environment variables into a data structure.
160
160
161 If ``reponame`` is defined, the leading path components matching that
161 If ``reponame`` is defined, the leading path components matching that
162 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
162 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
163 This simulates the world view of a WSGI application that processes
163 This simulates the world view of a WSGI application that processes
164 requests from the base URL of a repo.
164 requests from the base URL of a repo.
165
165
166 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
166 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
167 is defined, it is used - instead of the WSGI environment variables - for
167 is defined, it is used - instead of the WSGI environment variables - for
168 constructing URL components up to and including the WSGI application path.
168 constructing URL components up to and including the WSGI application path.
169 For example, if the current WSGI application is at ``/repo`` and a request
169 For example, if the current WSGI application is at ``/repo`` and a request
170 is made to ``/rev/@`` with this argument set to
170 is made to ``/rev/@`` with this argument set to
171 ``http://myserver:9000/prefix``, the URL and path components will resolve as
171 ``http://myserver:9000/prefix``, the URL and path components will resolve as
172 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
172 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
173 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
173 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
174 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
174 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
175 """
175 """
176 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
176 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
177
177
178 # We first validate that the incoming object conforms with the WSGI spec.
178 # We first validate that the incoming object conforms with the WSGI spec.
179 # We only want to be dealing with spec-conforming WSGI implementations.
179 # We only want to be dealing with spec-conforming WSGI implementations.
180 # TODO enable this once we fix internal violations.
180 # TODO enable this once we fix internal violations.
181 #wsgiref.validate.check_environ(env)
181 #wsgiref.validate.check_environ(env)
182
182
183 # PEP-0333 states that environment keys and values are native strings
183 # PEP-0333 states that environment keys and values are native strings
184 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
184 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
185 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
185 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
186 # in Mercurial, so mass convert string keys and values to bytes.
186 # in Mercurial, so mass convert string keys and values to bytes.
187 if pycompat.ispy3:
187 if pycompat.ispy3:
188 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
188 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
189 env = {k: v.encode('latin-1') if isinstance(v, str) else v
189 env = {k: v.encode('latin-1') if isinstance(v, str) else v
190 for k, v in env.iteritems()}
190 for k, v in env.iteritems()}
191
191
192 if altbaseurl:
192 if altbaseurl:
193 altbaseurl = util.url(altbaseurl)
193 altbaseurl = util.url(altbaseurl)
194
194
195 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
195 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
196 # the environment variables.
196 # the environment variables.
197 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
197 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
198 # how URLs are reconstructed.
198 # how URLs are reconstructed.
199 fullurl = env['wsgi.url_scheme'] + '://'
199 fullurl = env['wsgi.url_scheme'] + '://'
200
200
201 if altbaseurl and altbaseurl.scheme:
201 if altbaseurl and altbaseurl.scheme:
202 advertisedfullurl = altbaseurl.scheme + '://'
202 advertisedfullurl = altbaseurl.scheme + '://'
203 else:
203 else:
204 advertisedfullurl = fullurl
204 advertisedfullurl = fullurl
205
205
206 def addport(s, port):
206 def addport(s, port):
207 if s.startswith('https://'):
207 if s.startswith('https://'):
208 if port != '443':
208 if port != '443':
209 s += ':' + port
209 s += ':' + port
210 else:
210 else:
211 if port != '80':
211 if port != '80':
212 s += ':' + port
212 s += ':' + port
213
213
214 return s
214 return s
215
215
216 if env.get('HTTP_HOST'):
216 if env.get('HTTP_HOST'):
217 fullurl += env['HTTP_HOST']
217 fullurl += env['HTTP_HOST']
218 else:
218 else:
219 fullurl += env['SERVER_NAME']
219 fullurl += env['SERVER_NAME']
220 fullurl = addport(fullurl, env['SERVER_PORT'])
220 fullurl = addport(fullurl, env['SERVER_PORT'])
221
221
222 if altbaseurl and altbaseurl.host:
222 if altbaseurl and altbaseurl.host:
223 advertisedfullurl += altbaseurl.host
223 advertisedfullurl += altbaseurl.host
224
224
225 if altbaseurl.port:
225 if altbaseurl.port:
226 port = altbaseurl.port
226 port = altbaseurl.port
227 elif altbaseurl.scheme == 'http' and not altbaseurl.port:
227 elif altbaseurl.scheme == 'http' and not altbaseurl.port:
228 port = '80'
228 port = '80'
229 elif altbaseurl.scheme == 'https' and not altbaseurl.port:
229 elif altbaseurl.scheme == 'https' and not altbaseurl.port:
230 port = '443'
230 port = '443'
231 else:
231 else:
232 port = env['SERVER_PORT']
232 port = env['SERVER_PORT']
233
233
234 advertisedfullurl = addport(advertisedfullurl, port)
234 advertisedfullurl = addport(advertisedfullurl, port)
235 else:
235 else:
236 advertisedfullurl += env['SERVER_NAME']
236 advertisedfullurl += env['SERVER_NAME']
237 advertisedfullurl = addport(advertisedfullurl, env['SERVER_PORT'])
237 advertisedfullurl = addport(advertisedfullurl, env['SERVER_PORT'])
238
238
239 baseurl = fullurl
239 baseurl = fullurl
240 advertisedbaseurl = advertisedfullurl
240 advertisedbaseurl = advertisedfullurl
241
241
242 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
242 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
243 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
243 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
244
244
245 if altbaseurl:
245 if altbaseurl:
246 path = altbaseurl.path or ''
246 path = altbaseurl.path or ''
247 if path and not path.startswith('/'):
247 if path and not path.startswith('/'):
248 path = '/' + path
248 path = '/' + path
249 advertisedfullurl += util.urlreq.quote(path)
249 advertisedfullurl += util.urlreq.quote(path)
250 else:
250 else:
251 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
251 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
252
252
253 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
253 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
254
254
255 if env.get('QUERY_STRING'):
255 if env.get('QUERY_STRING'):
256 fullurl += '?' + env['QUERY_STRING']
256 fullurl += '?' + env['QUERY_STRING']
257 advertisedfullurl += '?' + env['QUERY_STRING']
257 advertisedfullurl += '?' + env['QUERY_STRING']
258
258
259 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
259 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
260 # that represents the repository being dispatched to. When computing
260 # that represents the repository being dispatched to. When computing
261 # the dispatch info, we ignore these leading path components.
261 # the dispatch info, we ignore these leading path components.
262
262
263 if altbaseurl:
263 if altbaseurl:
264 apppath = altbaseurl.path or ''
264 apppath = altbaseurl.path or ''
265 if apppath and not apppath.startswith('/'):
265 if apppath and not apppath.startswith('/'):
266 apppath = '/' + apppath
266 apppath = '/' + apppath
267 else:
267 else:
268 apppath = env.get('SCRIPT_NAME', '')
268 apppath = env.get('SCRIPT_NAME', '')
269
269
270 if reponame:
270 if reponame:
271 repoprefix = '/' + reponame.strip('/')
271 repoprefix = '/' + reponame.strip('/')
272
272
273 if not env.get('PATH_INFO'):
273 if not env.get('PATH_INFO'):
274 raise error.ProgrammingError('reponame requires PATH_INFO')
274 raise error.ProgrammingError('reponame requires PATH_INFO')
275
275
276 if not env['PATH_INFO'].startswith(repoprefix):
276 if not env['PATH_INFO'].startswith(repoprefix):
277 raise error.ProgrammingError('PATH_INFO does not begin with repo '
277 raise error.ProgrammingError('PATH_INFO does not begin with repo '
278 'name: %s (%s)' % (env['PATH_INFO'],
278 'name: %s (%s)' % (env['PATH_INFO'],
279 reponame))
279 reponame))
280
280
281 dispatchpath = env['PATH_INFO'][len(repoprefix):]
281 dispatchpath = env['PATH_INFO'][len(repoprefix):]
282
282
283 if dispatchpath and not dispatchpath.startswith('/'):
283 if dispatchpath and not dispatchpath.startswith('/'):
284 raise error.ProgrammingError('reponame prefix of PATH_INFO does '
284 raise error.ProgrammingError('reponame prefix of PATH_INFO does '
285 'not end at path delimiter: %s (%s)' %
285 'not end at path delimiter: %s (%s)' %
286 (env['PATH_INFO'], reponame))
286 (env['PATH_INFO'], reponame))
287
287
288 apppath = apppath.rstrip('/') + repoprefix
288 apppath = apppath.rstrip('/') + repoprefix
289 dispatchparts = dispatchpath.strip('/').split('/')
289 dispatchparts = dispatchpath.strip('/').split('/')
290 dispatchpath = '/'.join(dispatchparts)
290 dispatchpath = '/'.join(dispatchparts)
291
291
292 elif 'PATH_INFO' in env:
292 elif 'PATH_INFO' in env:
293 if env['PATH_INFO'].strip('/'):
293 if env['PATH_INFO'].strip('/'):
294 dispatchparts = env['PATH_INFO'].strip('/').split('/')
294 dispatchparts = env['PATH_INFO'].strip('/').split('/')
295 dispatchpath = '/'.join(dispatchparts)
295 dispatchpath = '/'.join(dispatchparts)
296 else:
296 else:
297 dispatchparts = []
297 dispatchparts = []
298 dispatchpath = ''
298 dispatchpath = ''
299 else:
299 else:
300 dispatchparts = []
300 dispatchparts = []
301 dispatchpath = None
301 dispatchpath = None
302
302
303 querystring = env.get('QUERY_STRING', '')
303 querystring = env.get('QUERY_STRING', '')
304
304
305 # We store as a list so we have ordering information. We also store as
305 # We store as a list so we have ordering information. We also store as
306 # a dict to facilitate fast lookup.
306 # a dict to facilitate fast lookup.
307 qsparams = multidict()
307 qsparams = multidict()
308 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
308 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
309 qsparams.add(k, v)
309 qsparams.add(k, v)
310
310
311 # HTTP_* keys contain HTTP request headers. The Headers structure should
311 # HTTP_* keys contain HTTP request headers. The Headers structure should
312 # perform case normalization for us. We just rewrite underscore to dash
312 # perform case normalization for us. We just rewrite underscore to dash
313 # so keys match what likely went over the wire.
313 # so keys match what likely went over the wire.
314 headers = []
314 headers = []
315 for k, v in env.iteritems():
315 for k, v in env.iteritems():
316 if k.startswith('HTTP_'):
316 if k.startswith('HTTP_'):
317 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
317 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
318
318
319 headers = wsgiheaders.Headers(headers)
319 headers = wsgiheaders.Headers(headers)
320
320
321 # This is kind of a lie because the HTTP header wasn't explicitly
321 # This is kind of a lie because the HTTP header wasn't explicitly
322 # sent. But for all intents and purposes it should be OK to lie about
322 # sent. But for all intents and purposes it should be OK to lie about
323 # this, since a consumer will either either value to determine how many
323 # this, since a consumer will either either value to determine how many
324 # bytes are available to read.
324 # bytes are available to read.
325 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
325 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
326 headers['Content-Length'] = env['CONTENT_LENGTH']
326 headers['Content-Length'] = env['CONTENT_LENGTH']
327
327
328 # TODO do this once we remove wsgirequest.inp, otherwise we could have
328 bodyfh = env['wsgi.input']
329 # multiple readers from the underlying input stream.
329 if 'Content-Length' in headers:
330 #bodyfh = env['wsgi.input']
330 bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
331 #if 'Content-Length' in headers:
332 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
333
331
334 return parsedrequest(method=env['REQUEST_METHOD'],
332 return parsedrequest(method=env['REQUEST_METHOD'],
335 url=fullurl, baseurl=baseurl,
333 url=fullurl, baseurl=baseurl,
336 advertisedurl=advertisedfullurl,
334 advertisedurl=advertisedfullurl,
337 advertisedbaseurl=advertisedbaseurl,
335 advertisedbaseurl=advertisedbaseurl,
338 urlscheme=env['wsgi.url_scheme'],
336 urlscheme=env['wsgi.url_scheme'],
339 remoteuser=env.get('REMOTE_USER'),
337 remoteuser=env.get('REMOTE_USER'),
340 remotehost=env.get('REMOTE_HOST'),
338 remotehost=env.get('REMOTE_HOST'),
341 apppath=apppath,
339 apppath=apppath,
342 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
340 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
343 reponame=reponame,
341 reponame=reponame,
344 querystring=querystring,
342 querystring=querystring,
345 qsparams=qsparams,
343 qsparams=qsparams,
346 headers=headers,
344 headers=headers,
347 bodyfh=bodyfh,
345 bodyfh=bodyfh,
348 rawenv=env)
346 rawenv=env)
349
347
350 class offsettrackingwriter(object):
348 class offsettrackingwriter(object):
351 """A file object like object that is append only and tracks write count.
349 """A file object like object that is append only and tracks write count.
352
350
353 Instances are bound to a callable. This callable is called with data
351 Instances are bound to a callable. This callable is called with data
354 whenever a ``write()`` is attempted.
352 whenever a ``write()`` is attempted.
355
353
356 Instances track the amount of written data so they can answer ``tell()``
354 Instances track the amount of written data so they can answer ``tell()``
357 requests.
355 requests.
358
356
359 The intent of this class is to wrap the ``write()`` function returned by
357 The intent of this class is to wrap the ``write()`` function returned by
360 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
358 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
361 not a file object, it doesn't implement other file object methods.
359 not a file object, it doesn't implement other file object methods.
362 """
360 """
363 def __init__(self, writefn):
361 def __init__(self, writefn):
364 self._write = writefn
362 self._write = writefn
365 self._offset = 0
363 self._offset = 0
366
364
367 def write(self, s):
365 def write(self, s):
368 res = self._write(s)
366 res = self._write(s)
369 # Some Python objects don't report the number of bytes written.
367 # Some Python objects don't report the number of bytes written.
370 if res is None:
368 if res is None:
371 self._offset += len(s)
369 self._offset += len(s)
372 else:
370 else:
373 self._offset += res
371 self._offset += res
374
372
375 def flush(self):
373 def flush(self):
376 pass
374 pass
377
375
378 def tell(self):
376 def tell(self):
379 return self._offset
377 return self._offset
380
378
381 class wsgiresponse(object):
379 class wsgiresponse(object):
382 """Represents a response to a WSGI request.
380 """Represents a response to a WSGI request.
383
381
384 A response consists of a status line, headers, and a body.
382 A response consists of a status line, headers, and a body.
385
383
386 Consumers must populate the ``status`` and ``headers`` fields and
384 Consumers must populate the ``status`` and ``headers`` fields and
387 make a call to a ``setbody*()`` method before the response can be
385 make a call to a ``setbody*()`` method before the response can be
388 issued.
386 issued.
389
387
390 When it is time to start sending the response over the wire,
388 When it is time to start sending the response over the wire,
391 ``sendresponse()`` is called. It handles emitting the header portion
389 ``sendresponse()`` is called. It handles emitting the header portion
392 of the response message. It then yields chunks of body data to be
390 of the response message. It then yields chunks of body data to be
393 written to the peer. Typically, the WSGI application itself calls
391 written to the peer. Typically, the WSGI application itself calls
394 and returns the value from ``sendresponse()``.
392 and returns the value from ``sendresponse()``.
395 """
393 """
396
394
397 def __init__(self, req, startresponse):
395 def __init__(self, req, startresponse):
398 """Create an empty response tied to a specific request.
396 """Create an empty response tied to a specific request.
399
397
400 ``req`` is a ``parsedrequest``. ``startresponse`` is the
398 ``req`` is a ``parsedrequest``. ``startresponse`` is the
401 ``start_response`` function passed to the WSGI application.
399 ``start_response`` function passed to the WSGI application.
402 """
400 """
403 self._req = req
401 self._req = req
404 self._startresponse = startresponse
402 self._startresponse = startresponse
405
403
406 self.status = None
404 self.status = None
407 self.headers = wsgiheaders.Headers([])
405 self.headers = wsgiheaders.Headers([])
408
406
409 self._bodybytes = None
407 self._bodybytes = None
410 self._bodygen = None
408 self._bodygen = None
411 self._bodywillwrite = False
409 self._bodywillwrite = False
412 self._started = False
410 self._started = False
413 self._bodywritefn = None
411 self._bodywritefn = None
414
412
415 def _verifybody(self):
413 def _verifybody(self):
416 if (self._bodybytes is not None or self._bodygen is not None
414 if (self._bodybytes is not None or self._bodygen is not None
417 or self._bodywillwrite):
415 or self._bodywillwrite):
418 raise error.ProgrammingError('cannot define body multiple times')
416 raise error.ProgrammingError('cannot define body multiple times')
419
417
420 def setbodybytes(self, b):
418 def setbodybytes(self, b):
421 """Define the response body as static bytes.
419 """Define the response body as static bytes.
422
420
423 The empty string signals that there is no response body.
421 The empty string signals that there is no response body.
424 """
422 """
425 self._verifybody()
423 self._verifybody()
426 self._bodybytes = b
424 self._bodybytes = b
427 self.headers['Content-Length'] = '%d' % len(b)
425 self.headers['Content-Length'] = '%d' % len(b)
428
426
429 def setbodygen(self, gen):
427 def setbodygen(self, gen):
430 """Define the response body as a generator of bytes."""
428 """Define the response body as a generator of bytes."""
431 self._verifybody()
429 self._verifybody()
432 self._bodygen = gen
430 self._bodygen = gen
433
431
434 def setbodywillwrite(self):
432 def setbodywillwrite(self):
435 """Signal an intent to use write() to emit the response body.
433 """Signal an intent to use write() to emit the response body.
436
434
437 **This is the least preferred way to send a body.**
435 **This is the least preferred way to send a body.**
438
436
439 It is preferred for WSGI applications to emit a generator of chunks
437 It is preferred for WSGI applications to emit a generator of chunks
440 constituting the response body. However, some consumers can't emit
438 constituting the response body. However, some consumers can't emit
441 data this way. So, WSGI provides a way to obtain a ``write(data)``
439 data this way. So, WSGI provides a way to obtain a ``write(data)``
442 function that can be used to synchronously perform an unbuffered
440 function that can be used to synchronously perform an unbuffered
443 write.
441 write.
444
442
445 Calling this function signals an intent to produce the body in this
443 Calling this function signals an intent to produce the body in this
446 manner.
444 manner.
447 """
445 """
448 self._verifybody()
446 self._verifybody()
449 self._bodywillwrite = True
447 self._bodywillwrite = True
450
448
451 def sendresponse(self):
449 def sendresponse(self):
452 """Send the generated response to the client.
450 """Send the generated response to the client.
453
451
454 Before this is called, ``status`` must be set and one of
452 Before this is called, ``status`` must be set and one of
455 ``setbodybytes()`` or ``setbodygen()`` must be called.
453 ``setbodybytes()`` or ``setbodygen()`` must be called.
456
454
457 Calling this method multiple times is not allowed.
455 Calling this method multiple times is not allowed.
458 """
456 """
459 if self._started:
457 if self._started:
460 raise error.ProgrammingError('sendresponse() called multiple times')
458 raise error.ProgrammingError('sendresponse() called multiple times')
461
459
462 self._started = True
460 self._started = True
463
461
464 if not self.status:
462 if not self.status:
465 raise error.ProgrammingError('status line not defined')
463 raise error.ProgrammingError('status line not defined')
466
464
467 if (self._bodybytes is None and self._bodygen is None
465 if (self._bodybytes is None and self._bodygen is None
468 and not self._bodywillwrite):
466 and not self._bodywillwrite):
469 raise error.ProgrammingError('response body not defined')
467 raise error.ProgrammingError('response body not defined')
470
468
471 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
469 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
472 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
470 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
473 # and SHOULD NOT generate other headers unless they could be used
471 # and SHOULD NOT generate other headers unless they could be used
474 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
472 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
475 # states that no response body can be issued. Content-Length can
473 # states that no response body can be issued. Content-Length can
476 # be sent. But if it is present, it should be the size of the response
474 # be sent. But if it is present, it should be the size of the response
477 # that wasn't transferred.
475 # that wasn't transferred.
478 if self.status.startswith('304 '):
476 if self.status.startswith('304 '):
479 # setbodybytes('') will set C-L to 0. This doesn't conform with the
477 # setbodybytes('') will set C-L to 0. This doesn't conform with the
480 # spec. So remove it.
478 # spec. So remove it.
481 if self.headers.get('Content-Length') == '0':
479 if self.headers.get('Content-Length') == '0':
482 del self.headers['Content-Length']
480 del self.headers['Content-Length']
483
481
484 # Strictly speaking, this is too strict. But until it causes
482 # Strictly speaking, this is too strict. But until it causes
485 # problems, let's be strict.
483 # problems, let's be strict.
486 badheaders = {k for k in self.headers.keys()
484 badheaders = {k for k in self.headers.keys()
487 if k.lower() not in ('date', 'etag', 'expires',
485 if k.lower() not in ('date', 'etag', 'expires',
488 'cache-control',
486 'cache-control',
489 'content-location',
487 'content-location',
490 'vary')}
488 'vary')}
491 if badheaders:
489 if badheaders:
492 raise error.ProgrammingError(
490 raise error.ProgrammingError(
493 'illegal header on 304 response: %s' %
491 'illegal header on 304 response: %s' %
494 ', '.join(sorted(badheaders)))
492 ', '.join(sorted(badheaders)))
495
493
496 if self._bodygen is not None or self._bodywillwrite:
494 if self._bodygen is not None or self._bodywillwrite:
497 raise error.ProgrammingError("must use setbodybytes('') with "
495 raise error.ProgrammingError("must use setbodybytes('') with "
498 "304 responses")
496 "304 responses")
499
497
500 # Various HTTP clients (notably httplib) won't read the HTTP response
498 # Various HTTP clients (notably httplib) won't read the HTTP response
501 # until the HTTP request has been sent in full. If servers (us) send a
499 # until the HTTP request has been sent in full. If servers (us) send a
502 # response before the HTTP request has been fully sent, the connection
500 # response before the HTTP request has been fully sent, the connection
503 # may deadlock because neither end is reading.
501 # may deadlock because neither end is reading.
504 #
502 #
505 # We work around this by "draining" the request data before
503 # We work around this by "draining" the request data before
506 # sending any response in some conditions.
504 # sending any response in some conditions.
507 drain = False
505 drain = False
508 close = False
506 close = False
509
507
510 # If the client sent Expect: 100-continue, we assume it is smart enough
508 # If the client sent Expect: 100-continue, we assume it is smart enough
511 # to deal with the server sending a response before reading the request.
509 # to deal with the server sending a response before reading the request.
512 # (httplib doesn't do this.)
510 # (httplib doesn't do this.)
513 if self._req.headers.get('Expect', '').lower() == '100-continue':
511 if self._req.headers.get('Expect', '').lower() == '100-continue':
514 pass
512 pass
515 # Only tend to request methods that have bodies. Strictly speaking,
513 # Only tend to request methods that have bodies. Strictly speaking,
516 # we should sniff for a body. But this is fine for our existing
514 # we should sniff for a body. But this is fine for our existing
517 # WSGI applications.
515 # WSGI applications.
518 elif self._req.method not in ('POST', 'PUT'):
516 elif self._req.method not in ('POST', 'PUT'):
519 pass
517 pass
520 else:
518 else:
521 # If we don't know how much data to read, there's no guarantee
519 # If we don't know how much data to read, there's no guarantee
522 # that we can drain the request responsibly. The WSGI
520 # that we can drain the request responsibly. The WSGI
523 # specification only says that servers *should* ensure the
521 # specification only says that servers *should* ensure the
524 # input stream doesn't overrun the actual request. So there's
522 # input stream doesn't overrun the actual request. So there's
525 # no guarantee that reading until EOF won't corrupt the stream
523 # no guarantee that reading until EOF won't corrupt the stream
526 # state.
524 # state.
527 if not isinstance(self._req.bodyfh, util.cappedreader):
525 if not isinstance(self._req.bodyfh, util.cappedreader):
528 close = True
526 close = True
529 else:
527 else:
530 # We /could/ only drain certain HTTP response codes. But 200 and
528 # We /could/ only drain certain HTTP response codes. But 200 and
531 # non-200 wire protocol responses both require draining. Since
529 # non-200 wire protocol responses both require draining. Since
532 # we have a capped reader in place for all situations where we
530 # we have a capped reader in place for all situations where we
533 # drain, it is safe to read from that stream. We'll either do
531 # drain, it is safe to read from that stream. We'll either do
534 # a drain or no-op if we're already at EOF.
532 # a drain or no-op if we're already at EOF.
535 drain = True
533 drain = True
536
534
537 if close:
535 if close:
538 self.headers['Connection'] = 'Close'
536 self.headers['Connection'] = 'Close'
539
537
540 if drain:
538 if drain:
541 assert isinstance(self._req.bodyfh, util.cappedreader)
539 assert isinstance(self._req.bodyfh, util.cappedreader)
542 while True:
540 while True:
543 chunk = self._req.bodyfh.read(32768)
541 chunk = self._req.bodyfh.read(32768)
544 if not chunk:
542 if not chunk:
545 break
543 break
546
544
547 write = self._startresponse(pycompat.sysstr(self.status),
545 write = self._startresponse(pycompat.sysstr(self.status),
548 self.headers.items())
546 self.headers.items())
549
547
550 if self._bodybytes:
548 if self._bodybytes:
551 yield self._bodybytes
549 yield self._bodybytes
552 elif self._bodygen:
550 elif self._bodygen:
553 for chunk in self._bodygen:
551 for chunk in self._bodygen:
554 yield chunk
552 yield chunk
555 elif self._bodywillwrite:
553 elif self._bodywillwrite:
556 self._bodywritefn = write
554 self._bodywritefn = write
557 else:
555 else:
558 error.ProgrammingError('do not know how to send body')
556 error.ProgrammingError('do not know how to send body')
559
557
560 def getbodyfile(self):
558 def getbodyfile(self):
561 """Obtain a file object like object representing the response body.
559 """Obtain a file object like object representing the response body.
562
560
563 For this to work, you must call ``setbodywillwrite()`` and then
561 For this to work, you must call ``setbodywillwrite()`` and then
564 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
562 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
565 function won't run to completion unless the generator is advanced. The
563 function won't run to completion unless the generator is advanced. The
566 generator yields not items. The easiest way to consume it is with
564 generator yields not items. The easiest way to consume it is with
567 ``list(res.sendresponse())``, which should resolve to an empty list -
565 ``list(res.sendresponse())``, which should resolve to an empty list -
568 ``[]``.
566 ``[]``.
569 """
567 """
570 if not self._bodywillwrite:
568 if not self._bodywillwrite:
571 raise error.ProgrammingError('must call setbodywillwrite() first')
569 raise error.ProgrammingError('must call setbodywillwrite() first')
572
570
573 if not self._started:
571 if not self._started:
574 raise error.ProgrammingError('must call sendresponse() first; did '
572 raise error.ProgrammingError('must call sendresponse() first; did '
575 'you remember to consume it since it '
573 'you remember to consume it since it '
576 'is a generator?')
574 'is a generator?')
577
575
578 assert self._bodywritefn
576 assert self._bodywritefn
579 return offsettrackingwriter(self._bodywritefn)
577 return offsettrackingwriter(self._bodywritefn)
580
578
581 class wsgirequest(object):
582 """Higher-level API for a WSGI request.
583
584 WSGI applications are invoked with 2 arguments. They are used to
585 instantiate instances of this class, which provides higher-level APIs
586 for obtaining request parameters, writing HTTP output, etc.
587 """
588 def __init__(self, wsgienv, start_response, altbaseurl=None):
589 version = wsgienv[r'wsgi.version']
590 if (version < (1, 0)) or (version >= (2, 0)):
591 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
592 % version)
593
594 inp = wsgienv[r'wsgi.input']
595
596 if r'HTTP_CONTENT_LENGTH' in wsgienv:
597 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
598 elif r'CONTENT_LENGTH' in wsgienv:
599 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
600
601 self.err = wsgienv[r'wsgi.errors']
602 self.threaded = wsgienv[r'wsgi.multithread']
603 self.multiprocess = wsgienv[r'wsgi.multiprocess']
604 self.run_once = wsgienv[r'wsgi.run_once']
605 self.env = wsgienv
606 self.req = parserequestfromenv(wsgienv, inp, altbaseurl=altbaseurl)
607 self.res = wsgiresponse(self.req, start_response)
608
609 def wsgiapplication(app_maker):
579 def wsgiapplication(app_maker):
610 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
580 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
611 can and should now be used as a WSGI application.'''
581 can and should now be used as a WSGI application.'''
612 application = app_maker()
582 application = app_maker()
613 def run_wsgi(env, respond):
583 def run_wsgi(env, respond):
614 return application(env, respond)
584 return application(env, respond)
615 return run_wsgi
585 return run_wsgi
@@ -1,416 +1,416 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, altbaseurl=None, extra=None):
26 def parse(env, reponame=None, altbaseurl=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, reponame=reponame,
31 altbaseurl=altbaseurl)
31 altbaseurl=altbaseurl)
32
32
33 class ParseRequestTests(unittest.TestCase):
33 class ParseRequestTests(unittest.TestCase):
34 def testdefault(self):
34 def testdefault(self):
35 r = parse(DEFAULT_ENV)
35 r = parse(DEFAULT_ENV)
36 self.assertEqual(r.url, b'http://testserver')
36 self.assertEqual(r.url, b'http://testserver')
37 self.assertEqual(r.baseurl, b'http://testserver')
37 self.assertEqual(r.baseurl, b'http://testserver')
38 self.assertEqual(r.advertisedurl, b'http://testserver')
38 self.assertEqual(r.advertisedurl, b'http://testserver')
39 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
39 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
40 self.assertEqual(r.urlscheme, b'http')
40 self.assertEqual(r.urlscheme, b'http')
41 self.assertEqual(r.method, b'GET')
41 self.assertEqual(r.method, b'GET')
42 self.assertIsNone(r.remoteuser)
42 self.assertIsNone(r.remoteuser)
43 self.assertIsNone(r.remotehost)
43 self.assertIsNone(r.remotehost)
44 self.assertEqual(r.apppath, b'')
44 self.assertEqual(r.apppath, b'')
45 self.assertEqual(r.dispatchparts, [])
45 self.assertEqual(r.dispatchparts, [])
46 self.assertIsNone(r.dispatchpath)
46 self.assertIsNone(r.dispatchpath)
47 self.assertIsNone(r.reponame)
47 self.assertIsNone(r.reponame)
48 self.assertEqual(r.querystring, b'')
48 self.assertEqual(r.querystring, b'')
49 self.assertEqual(len(r.qsparams), 0)
49 self.assertEqual(len(r.qsparams), 0)
50 self.assertEqual(len(r.headers), 0)
50 self.assertEqual(len(r.headers), 0)
51
51
52 def testcustomport(self):
52 def testcustomport(self):
53 r = parse(DEFAULT_ENV, extra={
53 r = parse(DEFAULT_ENV, extra={
54 r'SERVER_PORT': r'8000',
54 r'SERVER_PORT': r'8000',
55 })
55 })
56
56
57 self.assertEqual(r.url, b'http://testserver:8000')
57 self.assertEqual(r.url, b'http://testserver:8000')
58 self.assertEqual(r.baseurl, b'http://testserver:8000')
58 self.assertEqual(r.baseurl, b'http://testserver:8000')
59 self.assertEqual(r.advertisedurl, b'http://testserver:8000')
59 self.assertEqual(r.advertisedurl, b'http://testserver:8000')
60 self.assertEqual(r.advertisedbaseurl, b'http://testserver:8000')
60 self.assertEqual(r.advertisedbaseurl, b'http://testserver:8000')
61
61
62 r = parse(DEFAULT_ENV, extra={
62 r = parse(DEFAULT_ENV, extra={
63 r'SERVER_PORT': r'4000',
63 r'SERVER_PORT': r'4000',
64 r'wsgi.url_scheme': r'https',
64 r'wsgi.url_scheme': r'https',
65 })
65 })
66
66
67 self.assertEqual(r.url, b'https://testserver:4000')
67 self.assertEqual(r.url, b'https://testserver:4000')
68 self.assertEqual(r.baseurl, b'https://testserver:4000')
68 self.assertEqual(r.baseurl, b'https://testserver:4000')
69 self.assertEqual(r.advertisedurl, b'https://testserver:4000')
69 self.assertEqual(r.advertisedurl, b'https://testserver:4000')
70 self.assertEqual(r.advertisedbaseurl, b'https://testserver:4000')
70 self.assertEqual(r.advertisedbaseurl, b'https://testserver:4000')
71
71
72 def testhttphost(self):
72 def testhttphost(self):
73 r = parse(DEFAULT_ENV, extra={
73 r = parse(DEFAULT_ENV, extra={
74 r'HTTP_HOST': r'altserver',
74 r'HTTP_HOST': r'altserver',
75 })
75 })
76
76
77 self.assertEqual(r.url, b'http://altserver')
77 self.assertEqual(r.url, b'http://altserver')
78 self.assertEqual(r.baseurl, b'http://altserver')
78 self.assertEqual(r.baseurl, b'http://altserver')
79 self.assertEqual(r.advertisedurl, b'http://testserver')
79 self.assertEqual(r.advertisedurl, b'http://testserver')
80 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
80 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
81
81
82 def testscriptname(self):
82 def testscriptname(self):
83 r = parse(DEFAULT_ENV, extra={
83 r = parse(DEFAULT_ENV, extra={
84 r'SCRIPT_NAME': r'',
84 r'SCRIPT_NAME': r'',
85 })
85 })
86
86
87 self.assertEqual(r.url, b'http://testserver')
87 self.assertEqual(r.url, b'http://testserver')
88 self.assertEqual(r.baseurl, b'http://testserver')
88 self.assertEqual(r.baseurl, b'http://testserver')
89 self.assertEqual(r.advertisedurl, b'http://testserver')
89 self.assertEqual(r.advertisedurl, b'http://testserver')
90 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
90 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
91 self.assertEqual(r.apppath, b'')
91 self.assertEqual(r.apppath, b'')
92 self.assertEqual(r.dispatchparts, [])
92 self.assertEqual(r.dispatchparts, [])
93 self.assertIsNone(r.dispatchpath)
93 self.assertIsNone(r.dispatchpath)
94
94
95 r = parse(DEFAULT_ENV, extra={
95 r = parse(DEFAULT_ENV, extra={
96 r'SCRIPT_NAME': r'/script',
96 r'SCRIPT_NAME': r'/script',
97 })
97 })
98
98
99 self.assertEqual(r.url, b'http://testserver/script')
99 self.assertEqual(r.url, b'http://testserver/script')
100 self.assertEqual(r.baseurl, b'http://testserver')
100 self.assertEqual(r.baseurl, b'http://testserver')
101 self.assertEqual(r.advertisedurl, b'http://testserver/script')
101 self.assertEqual(r.advertisedurl, b'http://testserver/script')
102 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
102 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
103 self.assertEqual(r.apppath, b'/script')
103 self.assertEqual(r.apppath, b'/script')
104 self.assertEqual(r.dispatchparts, [])
104 self.assertEqual(r.dispatchparts, [])
105 self.assertIsNone(r.dispatchpath)
105 self.assertIsNone(r.dispatchpath)
106
106
107 r = parse(DEFAULT_ENV, extra={
107 r = parse(DEFAULT_ENV, extra={
108 r'SCRIPT_NAME': r'/multiple words',
108 r'SCRIPT_NAME': r'/multiple words',
109 })
109 })
110
110
111 self.assertEqual(r.url, b'http://testserver/multiple%20words')
111 self.assertEqual(r.url, b'http://testserver/multiple%20words')
112 self.assertEqual(r.baseurl, b'http://testserver')
112 self.assertEqual(r.baseurl, b'http://testserver')
113 self.assertEqual(r.advertisedurl, b'http://testserver/multiple%20words')
113 self.assertEqual(r.advertisedurl, b'http://testserver/multiple%20words')
114 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
114 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
115 self.assertEqual(r.apppath, b'/multiple words')
115 self.assertEqual(r.apppath, b'/multiple words')
116 self.assertEqual(r.dispatchparts, [])
116 self.assertEqual(r.dispatchparts, [])
117 self.assertIsNone(r.dispatchpath)
117 self.assertIsNone(r.dispatchpath)
118
118
119 def testpathinfo(self):
119 def testpathinfo(self):
120 r = parse(DEFAULT_ENV, extra={
120 r = parse(DEFAULT_ENV, extra={
121 r'PATH_INFO': r'',
121 r'PATH_INFO': r'',
122 })
122 })
123
123
124 self.assertEqual(r.url, b'http://testserver')
124 self.assertEqual(r.url, b'http://testserver')
125 self.assertEqual(r.baseurl, b'http://testserver')
125 self.assertEqual(r.baseurl, b'http://testserver')
126 self.assertEqual(r.advertisedurl, b'http://testserver')
126 self.assertEqual(r.advertisedurl, b'http://testserver')
127 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
127 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
128 self.assertEqual(r.apppath, b'')
128 self.assertEqual(r.apppath, b'')
129 self.assertEqual(r.dispatchparts, [])
129 self.assertEqual(r.dispatchparts, [])
130 self.assertEqual(r.dispatchpath, b'')
130 self.assertEqual(r.dispatchpath, b'')
131
131
132 r = parse(DEFAULT_ENV, extra={
132 r = parse(DEFAULT_ENV, extra={
133 r'PATH_INFO': r'/pathinfo',
133 r'PATH_INFO': r'/pathinfo',
134 })
134 })
135
135
136 self.assertEqual(r.url, b'http://testserver/pathinfo')
136 self.assertEqual(r.url, b'http://testserver/pathinfo')
137 self.assertEqual(r.baseurl, b'http://testserver')
137 self.assertEqual(r.baseurl, b'http://testserver')
138 self.assertEqual(r.advertisedurl, b'http://testserver/pathinfo')
138 self.assertEqual(r.advertisedurl, b'http://testserver/pathinfo')
139 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
139 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
140 self.assertEqual(r.apppath, b'')
140 self.assertEqual(r.apppath, b'')
141 self.assertEqual(r.dispatchparts, [b'pathinfo'])
141 self.assertEqual(r.dispatchparts, [b'pathinfo'])
142 self.assertEqual(r.dispatchpath, b'pathinfo')
142 self.assertEqual(r.dispatchpath, b'pathinfo')
143
143
144 r = parse(DEFAULT_ENV, extra={
144 r = parse(DEFAULT_ENV, extra={
145 r'PATH_INFO': r'/one/two/',
145 r'PATH_INFO': r'/one/two/',
146 })
146 })
147
147
148 self.assertEqual(r.url, b'http://testserver/one/two/')
148 self.assertEqual(r.url, b'http://testserver/one/two/')
149 self.assertEqual(r.baseurl, b'http://testserver')
149 self.assertEqual(r.baseurl, b'http://testserver')
150 self.assertEqual(r.advertisedurl, b'http://testserver/one/two/')
150 self.assertEqual(r.advertisedurl, b'http://testserver/one/two/')
151 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
151 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
152 self.assertEqual(r.apppath, b'')
152 self.assertEqual(r.apppath, b'')
153 self.assertEqual(r.dispatchparts, [b'one', b'two'])
153 self.assertEqual(r.dispatchparts, [b'one', b'two'])
154 self.assertEqual(r.dispatchpath, b'one/two')
154 self.assertEqual(r.dispatchpath, b'one/two')
155
155
156 def testscriptandpathinfo(self):
156 def testscriptandpathinfo(self):
157 r = parse(DEFAULT_ENV, extra={
157 r = parse(DEFAULT_ENV, extra={
158 r'SCRIPT_NAME': r'/script',
158 r'SCRIPT_NAME': r'/script',
159 r'PATH_INFO': r'/pathinfo',
159 r'PATH_INFO': r'/pathinfo',
160 })
160 })
161
161
162 self.assertEqual(r.url, b'http://testserver/script/pathinfo')
162 self.assertEqual(r.url, b'http://testserver/script/pathinfo')
163 self.assertEqual(r.baseurl, b'http://testserver')
163 self.assertEqual(r.baseurl, b'http://testserver')
164 self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo')
164 self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo')
165 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
165 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
166 self.assertEqual(r.apppath, b'/script')
166 self.assertEqual(r.apppath, b'/script')
167 self.assertEqual(r.dispatchparts, [b'pathinfo'])
167 self.assertEqual(r.dispatchparts, [b'pathinfo'])
168 self.assertEqual(r.dispatchpath, b'pathinfo')
168 self.assertEqual(r.dispatchpath, b'pathinfo')
169
169
170 r = parse(DEFAULT_ENV, extra={
170 r = parse(DEFAULT_ENV, extra={
171 r'SCRIPT_NAME': r'/script1/script2',
171 r'SCRIPT_NAME': r'/script1/script2',
172 r'PATH_INFO': r'/path1/path2',
172 r'PATH_INFO': r'/path1/path2',
173 })
173 })
174
174
175 self.assertEqual(r.url,
175 self.assertEqual(r.url,
176 b'http://testserver/script1/script2/path1/path2')
176 b'http://testserver/script1/script2/path1/path2')
177 self.assertEqual(r.baseurl, b'http://testserver')
177 self.assertEqual(r.baseurl, b'http://testserver')
178 self.assertEqual(r.advertisedurl,
178 self.assertEqual(r.advertisedurl,
179 b'http://testserver/script1/script2/path1/path2')
179 b'http://testserver/script1/script2/path1/path2')
180 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
180 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
181 self.assertEqual(r.apppath, b'/script1/script2')
181 self.assertEqual(r.apppath, b'/script1/script2')
182 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
182 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
183 self.assertEqual(r.dispatchpath, b'path1/path2')
183 self.assertEqual(r.dispatchpath, b'path1/path2')
184
184
185 r = parse(DEFAULT_ENV, extra={
185 r = parse(DEFAULT_ENV, extra={
186 r'HTTP_HOST': r'hostserver',
186 r'HTTP_HOST': r'hostserver',
187 r'SCRIPT_NAME': r'/script',
187 r'SCRIPT_NAME': r'/script',
188 r'PATH_INFO': r'/pathinfo',
188 r'PATH_INFO': r'/pathinfo',
189 })
189 })
190
190
191 self.assertEqual(r.url, b'http://hostserver/script/pathinfo')
191 self.assertEqual(r.url, b'http://hostserver/script/pathinfo')
192 self.assertEqual(r.baseurl, b'http://hostserver')
192 self.assertEqual(r.baseurl, b'http://hostserver')
193 self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo')
193 self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo')
194 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
194 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
195 self.assertEqual(r.apppath, b'/script')
195 self.assertEqual(r.apppath, b'/script')
196 self.assertEqual(r.dispatchparts, [b'pathinfo'])
196 self.assertEqual(r.dispatchparts, [b'pathinfo'])
197 self.assertEqual(r.dispatchpath, b'pathinfo')
197 self.assertEqual(r.dispatchpath, b'pathinfo')
198
198
199 def testreponame(self):
199 def testreponame(self):
200 """repository path components get stripped from URL."""
200 """repository path components get stripped from URL."""
201
201
202 with self.assertRaisesRegexp(error.ProgrammingError,
202 with self.assertRaisesRegexp(error.ProgrammingError,
203 b'reponame requires PATH_INFO'):
203 b'reponame requires PATH_INFO'):
204 parse(DEFAULT_ENV, reponame=b'repo')
204 parse(DEFAULT_ENV, reponame=b'repo')
205
205
206 with self.assertRaisesRegexp(error.ProgrammingError,
206 with self.assertRaisesRegexp(error.ProgrammingError,
207 b'PATH_INFO does not begin with repo '
207 b'PATH_INFO does not begin with repo '
208 b'name'):
208 b'name'):
209 parse(DEFAULT_ENV, reponame=b'repo', extra={
209 parse(DEFAULT_ENV, reponame=b'repo', extra={
210 r'PATH_INFO': r'/pathinfo',
210 r'PATH_INFO': r'/pathinfo',
211 })
211 })
212
212
213 with self.assertRaisesRegexp(error.ProgrammingError,
213 with self.assertRaisesRegexp(error.ProgrammingError,
214 b'reponame prefix of PATH_INFO'):
214 b'reponame prefix of PATH_INFO'):
215 parse(DEFAULT_ENV, reponame=b'repo', extra={
215 parse(DEFAULT_ENV, reponame=b'repo', extra={
216 r'PATH_INFO': r'/repoextra/path',
216 r'PATH_INFO': r'/repoextra/path',
217 })
217 })
218
218
219 r = parse(DEFAULT_ENV, reponame=b'repo', extra={
219 r = parse(DEFAULT_ENV, reponame=b'repo', extra={
220 r'PATH_INFO': r'/repo/path1/path2',
220 r'PATH_INFO': r'/repo/path1/path2',
221 })
221 })
222
222
223 self.assertEqual(r.url, b'http://testserver/repo/path1/path2')
223 self.assertEqual(r.url, b'http://testserver/repo/path1/path2')
224 self.assertEqual(r.baseurl, b'http://testserver')
224 self.assertEqual(r.baseurl, b'http://testserver')
225 self.assertEqual(r.advertisedurl, b'http://testserver/repo/path1/path2')
225 self.assertEqual(r.advertisedurl, b'http://testserver/repo/path1/path2')
226 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
226 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
227 self.assertEqual(r.apppath, b'/repo')
227 self.assertEqual(r.apppath, b'/repo')
228 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
228 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
229 self.assertEqual(r.dispatchpath, b'path1/path2')
229 self.assertEqual(r.dispatchpath, b'path1/path2')
230 self.assertEqual(r.reponame, b'repo')
230 self.assertEqual(r.reponame, b'repo')
231
231
232 r = parse(DEFAULT_ENV, reponame=b'prefix/repo', extra={
232 r = parse(DEFAULT_ENV, reponame=b'prefix/repo', extra={
233 r'PATH_INFO': r'/prefix/repo/path1/path2',
233 r'PATH_INFO': r'/prefix/repo/path1/path2',
234 })
234 })
235
235
236 self.assertEqual(r.url, b'http://testserver/prefix/repo/path1/path2')
236 self.assertEqual(r.url, b'http://testserver/prefix/repo/path1/path2')
237 self.assertEqual(r.baseurl, b'http://testserver')
237 self.assertEqual(r.baseurl, b'http://testserver')
238 self.assertEqual(r.advertisedurl,
238 self.assertEqual(r.advertisedurl,
239 b'http://testserver/prefix/repo/path1/path2')
239 b'http://testserver/prefix/repo/path1/path2')
240 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
240 self.assertEqual(r.advertisedbaseurl, b'http://testserver')
241 self.assertEqual(r.apppath, b'/prefix/repo')
241 self.assertEqual(r.apppath, b'/prefix/repo')
242 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
242 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
243 self.assertEqual(r.dispatchpath, b'path1/path2')
243 self.assertEqual(r.dispatchpath, b'path1/path2')
244 self.assertEqual(r.reponame, b'prefix/repo')
244 self.assertEqual(r.reponame, b'prefix/repo')
245
245
246 def testaltbaseurl(self):
246 def testaltbaseurl(self):
247 # Simple hostname remap.
247 # Simple hostname remap.
248 r = parse(DEFAULT_ENV, altbaseurl='http://altserver')
248 r = parse(DEFAULT_ENV, altbaseurl='http://altserver')
249
249
250 self.assertEqual(r.url, b'http://testserver')
250 self.assertEqual(r.url, b'http://testserver')
251 self.assertEqual(r.baseurl, b'http://testserver')
251 self.assertEqual(r.baseurl, b'http://testserver')
252 self.assertEqual(r.advertisedurl, b'http://altserver')
252 self.assertEqual(r.advertisedurl, b'http://altserver')
253 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
253 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
254 self.assertEqual(r.urlscheme, b'http')
254 self.assertEqual(r.urlscheme, b'http')
255 self.assertEqual(r.apppath, b'')
255 self.assertEqual(r.apppath, b'')
256 self.assertEqual(r.dispatchparts, [])
256 self.assertEqual(r.dispatchparts, [])
257 self.assertIsNone(r.dispatchpath)
257 self.assertIsNone(r.dispatchpath)
258 self.assertIsNone(r.reponame)
258 self.assertIsNone(r.reponame)
259
259
260 # With a custom port.
260 # With a custom port.
261 r = parse(DEFAULT_ENV, altbaseurl='http://altserver:8000')
261 r = parse(DEFAULT_ENV, altbaseurl='http://altserver:8000')
262 self.assertEqual(r.url, b'http://testserver')
262 self.assertEqual(r.url, b'http://testserver')
263 self.assertEqual(r.baseurl, b'http://testserver')
263 self.assertEqual(r.baseurl, b'http://testserver')
264 self.assertEqual(r.advertisedurl, b'http://altserver:8000')
264 self.assertEqual(r.advertisedurl, b'http://altserver:8000')
265 self.assertEqual(r.advertisedbaseurl, b'http://altserver:8000')
265 self.assertEqual(r.advertisedbaseurl, b'http://altserver:8000')
266 self.assertEqual(r.urlscheme, b'http')
266 self.assertEqual(r.urlscheme, b'http')
267 self.assertEqual(r.apppath, b'')
267 self.assertEqual(r.apppath, b'')
268 self.assertEqual(r.dispatchparts, [])
268 self.assertEqual(r.dispatchparts, [])
269 self.assertIsNone(r.dispatchpath)
269 self.assertIsNone(r.dispatchpath)
270 self.assertIsNone(r.reponame)
270 self.assertIsNone(r.reponame)
271
271
272 # With a changed protocol.
272 # With a changed protocol.
273 r = parse(DEFAULT_ENV, altbaseurl='https://altserver')
273 r = parse(DEFAULT_ENV, altbaseurl='https://altserver')
274 self.assertEqual(r.url, b'http://testserver')
274 self.assertEqual(r.url, b'http://testserver')
275 self.assertEqual(r.baseurl, b'http://testserver')
275 self.assertEqual(r.baseurl, b'http://testserver')
276 self.assertEqual(r.advertisedurl, b'https://altserver')
276 self.assertEqual(r.advertisedurl, b'https://altserver')
277 self.assertEqual(r.advertisedbaseurl, b'https://altserver')
277 self.assertEqual(r.advertisedbaseurl, b'https://altserver')
278 # URL scheme is defined as the actual scheme, not advertised.
278 # URL scheme is defined as the actual scheme, not advertised.
279 self.assertEqual(r.urlscheme, b'http')
279 self.assertEqual(r.urlscheme, b'http')
280 self.assertEqual(r.apppath, b'')
280 self.assertEqual(r.apppath, b'')
281 self.assertEqual(r.dispatchparts, [])
281 self.assertEqual(r.dispatchparts, [])
282 self.assertIsNone(r.dispatchpath)
282 self.assertIsNone(r.dispatchpath)
283 self.assertIsNone(r.reponame)
283 self.assertIsNone(r.reponame)
284
284
285 # Need to specify explicit port number for proper https:// alt URLs.
285 # Need to specify explicit port number for proper https:// alt URLs.
286 r = parse(DEFAULT_ENV, altbaseurl='https://altserver:443')
286 r = parse(DEFAULT_ENV, altbaseurl='https://altserver:443')
287 self.assertEqual(r.url, b'http://testserver')
287 self.assertEqual(r.url, b'http://testserver')
288 self.assertEqual(r.baseurl, b'http://testserver')
288 self.assertEqual(r.baseurl, b'http://testserver')
289 self.assertEqual(r.advertisedurl, b'https://altserver')
289 self.assertEqual(r.advertisedurl, b'https://altserver')
290 self.assertEqual(r.advertisedbaseurl, b'https://altserver')
290 self.assertEqual(r.advertisedbaseurl, b'https://altserver')
291 self.assertEqual(r.urlscheme, b'http')
291 self.assertEqual(r.urlscheme, b'http')
292 self.assertEqual(r.apppath, b'')
292 self.assertEqual(r.apppath, b'')
293 self.assertEqual(r.dispatchparts, [])
293 self.assertEqual(r.dispatchparts, [])
294 self.assertIsNone(r.dispatchpath)
294 self.assertIsNone(r.dispatchpath)
295 self.assertIsNone(r.reponame)
295 self.assertIsNone(r.reponame)
296
296
297 # With only PATH_INFO defined.
297 # With only PATH_INFO defined.
298 r = parse(DEFAULT_ENV, altbaseurl='http://altserver', extra={
298 r = parse(DEFAULT_ENV, altbaseurl='http://altserver', extra={
299 r'PATH_INFO': r'/path1/path2',
299 r'PATH_INFO': r'/path1/path2',
300 })
300 })
301 self.assertEqual(r.url, b'http://testserver/path1/path2')
301 self.assertEqual(r.url, b'http://testserver/path1/path2')
302 self.assertEqual(r.baseurl, b'http://testserver')
302 self.assertEqual(r.baseurl, b'http://testserver')
303 self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2')
303 self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2')
304 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
304 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
305 self.assertEqual(r.urlscheme, b'http')
305 self.assertEqual(r.urlscheme, b'http')
306 self.assertEqual(r.apppath, b'')
306 self.assertEqual(r.apppath, b'')
307 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
307 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
308 self.assertEqual(r.dispatchpath, b'path1/path2')
308 self.assertEqual(r.dispatchpath, b'path1/path2')
309 self.assertIsNone(r.reponame)
309 self.assertIsNone(r.reponame)
310
310
311 # Path on alt URL.
311 # Path on alt URL.
312 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath')
312 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath')
313 self.assertEqual(r.url, b'http://testserver')
313 self.assertEqual(r.url, b'http://testserver')
314 self.assertEqual(r.baseurl, b'http://testserver')
314 self.assertEqual(r.baseurl, b'http://testserver')
315 self.assertEqual(r.advertisedurl, b'http://altserver/altpath')
315 self.assertEqual(r.advertisedurl, b'http://altserver/altpath')
316 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
316 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
317 self.assertEqual(r.urlscheme, b'http')
317 self.assertEqual(r.urlscheme, b'http')
318 self.assertEqual(r.apppath, b'/altpath')
318 self.assertEqual(r.apppath, b'/altpath')
319 self.assertEqual(r.dispatchparts, [])
319 self.assertEqual(r.dispatchparts, [])
320 self.assertIsNone(r.dispatchpath)
320 self.assertIsNone(r.dispatchpath)
321 self.assertIsNone(r.reponame)
321 self.assertIsNone(r.reponame)
322
322
323 # With a trailing slash.
323 # With a trailing slash.
324 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath/')
324 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath/')
325 self.assertEqual(r.url, b'http://testserver')
325 self.assertEqual(r.url, b'http://testserver')
326 self.assertEqual(r.baseurl, b'http://testserver')
326 self.assertEqual(r.baseurl, b'http://testserver')
327 self.assertEqual(r.advertisedurl, b'http://altserver/altpath/')
327 self.assertEqual(r.advertisedurl, b'http://altserver/altpath/')
328 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
328 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
329 self.assertEqual(r.urlscheme, b'http')
329 self.assertEqual(r.urlscheme, b'http')
330 self.assertEqual(r.apppath, b'/altpath/')
330 self.assertEqual(r.apppath, b'/altpath/')
331 self.assertEqual(r.dispatchparts, [])
331 self.assertEqual(r.dispatchparts, [])
332 self.assertIsNone(r.dispatchpath)
332 self.assertIsNone(r.dispatchpath)
333 self.assertIsNone(r.reponame)
333 self.assertIsNone(r.reponame)
334
334
335 # PATH_INFO + path on alt URL.
335 # PATH_INFO + path on alt URL.
336 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath', extra={
336 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath', extra={
337 r'PATH_INFO': r'/path1/path2',
337 r'PATH_INFO': r'/path1/path2',
338 })
338 })
339 self.assertEqual(r.url, b'http://testserver/path1/path2')
339 self.assertEqual(r.url, b'http://testserver/path1/path2')
340 self.assertEqual(r.baseurl, b'http://testserver')
340 self.assertEqual(r.baseurl, b'http://testserver')
341 self.assertEqual(r.advertisedurl,
341 self.assertEqual(r.advertisedurl,
342 b'http://altserver/altpath/path1/path2')
342 b'http://altserver/altpath/path1/path2')
343 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
343 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
344 self.assertEqual(r.urlscheme, b'http')
344 self.assertEqual(r.urlscheme, b'http')
345 self.assertEqual(r.apppath, b'/altpath')
345 self.assertEqual(r.apppath, b'/altpath')
346 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
346 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
347 self.assertEqual(r.dispatchpath, b'path1/path2')
347 self.assertEqual(r.dispatchpath, b'path1/path2')
348 self.assertIsNone(r.reponame)
348 self.assertIsNone(r.reponame)
349
349
350 # PATH_INFO + path on alt URL with trailing slash.
350 # PATH_INFO + path on alt URL with trailing slash.
351 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath/', extra={
351 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altpath/', extra={
352 r'PATH_INFO': r'/path1/path2',
352 r'PATH_INFO': r'/path1/path2',
353 })
353 })
354 self.assertEqual(r.url, b'http://testserver/path1/path2')
354 self.assertEqual(r.url, b'http://testserver/path1/path2')
355 self.assertEqual(r.baseurl, b'http://testserver')
355 self.assertEqual(r.baseurl, b'http://testserver')
356 self.assertEqual(r.advertisedurl,
356 self.assertEqual(r.advertisedurl,
357 b'http://altserver/altpath//path1/path2')
357 b'http://altserver/altpath//path1/path2')
358 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
358 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
359 self.assertEqual(r.urlscheme, b'http')
359 self.assertEqual(r.urlscheme, b'http')
360 self.assertEqual(r.apppath, b'/altpath/')
360 self.assertEqual(r.apppath, b'/altpath/')
361 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
361 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
362 self.assertEqual(r.dispatchpath, b'path1/path2')
362 self.assertEqual(r.dispatchpath, b'path1/path2')
363 self.assertIsNone(r.reponame)
363 self.assertIsNone(r.reponame)
364
364
365 # Local SCRIPT_NAME is ignored.
365 # Local SCRIPT_NAME is ignored.
366 r = parse(DEFAULT_ENV, altbaseurl='http://altserver', extra={
366 r = parse(DEFAULT_ENV, altbaseurl='http://altserver', extra={
367 r'SCRIPT_NAME': r'/script',
367 r'SCRIPT_NAME': r'/script',
368 r'PATH_INFO': r'/path1/path2',
368 r'PATH_INFO': r'/path1/path2',
369 })
369 })
370 self.assertEqual(r.url, b'http://testserver/script/path1/path2')
370 self.assertEqual(r.url, b'http://testserver/script/path1/path2')
371 self.assertEqual(r.baseurl, b'http://testserver')
371 self.assertEqual(r.baseurl, b'http://testserver')
372 self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2')
372 self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2')
373 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
373 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
374 self.assertEqual(r.urlscheme, b'http')
374 self.assertEqual(r.urlscheme, b'http')
375 self.assertEqual(r.apppath, b'')
375 self.assertEqual(r.apppath, b'')
376 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
376 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
377 self.assertEqual(r.dispatchpath, b'path1/path2')
377 self.assertEqual(r.dispatchpath, b'path1/path2')
378 self.assertIsNone(r.reponame)
378 self.assertIsNone(r.reponame)
379
379
380 # Use remote's path for script name, app path
380 # Use remote's path for script name, app path
381 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altroot', extra={
381 r = parse(DEFAULT_ENV, altbaseurl='http://altserver/altroot', extra={
382 r'SCRIPT_NAME': r'/script',
382 r'SCRIPT_NAME': r'/script',
383 r'PATH_INFO': r'/path1/path2',
383 r'PATH_INFO': r'/path1/path2',
384 })
384 })
385 self.assertEqual(r.url, b'http://testserver/script/path1/path2')
385 self.assertEqual(r.url, b'http://testserver/script/path1/path2')
386 self.assertEqual(r.baseurl, b'http://testserver')
386 self.assertEqual(r.baseurl, b'http://testserver')
387 self.assertEqual(r.advertisedurl,
387 self.assertEqual(r.advertisedurl,
388 b'http://altserver/altroot/path1/path2')
388 b'http://altserver/altroot/path1/path2')
389 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
389 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
390 self.assertEqual(r.urlscheme, b'http')
390 self.assertEqual(r.urlscheme, b'http')
391 self.assertEqual(r.apppath, b'/altroot')
391 self.assertEqual(r.apppath, b'/altroot')
392 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
392 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
393 self.assertEqual(r.dispatchpath, b'path1/path2')
393 self.assertEqual(r.dispatchpath, b'path1/path2')
394 self.assertIsNone(r.reponame)
394 self.assertIsNone(r.reponame)
395
395
396 # reponame is factored in properly.
396 # reponame is factored in properly.
397 r = parse(DEFAULT_ENV, reponame=b'repo',
397 r = parse(DEFAULT_ENV, reponame=b'repo',
398 altbaseurl='http://altserver/altroot',
398 altbaseurl='http://altserver/altroot',
399 extra={
399 extra={
400 r'SCRIPT_NAME': r'/script',
400 r'SCRIPT_NAME': r'/script',
401 r'PATH_INFO': r'/repo/path1/path2',
401 r'PATH_INFO': r'/repo/path1/path2',
402 })
402 })
403
403
404 self.assertEqual(r.url, b'http://testserver/script/repo/path1/path2')
404 self.assertEqual(r.url, b'http://testserver/script/repo/path1/path2')
405 self.assertEqual(r.baseurl, b'http://testserver')
405 self.assertEqual(r.baseurl, b'http://testserver')
406 self.assertEqual(r.advertisedurl,
406 self.assertEqual(r.advertisedurl,
407 b'http://altserver/altroot/repo/path1/path2')
407 b'http://altserver/altroot/repo/path1/path2')
408 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
408 self.assertEqual(r.advertisedbaseurl, b'http://altserver')
409 self.assertEqual(r.apppath, b'/altroot/repo')
409 self.assertEqual(r.apppath, b'/altroot/repo')
410 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
410 self.assertEqual(r.dispatchparts, [b'path1', b'path2'])
411 self.assertEqual(r.dispatchpath, b'path1/path2')
411 self.assertEqual(r.dispatchpath, b'path1/path2')
412 self.assertEqual(r.reponame, b'repo')
412 self.assertEqual(r.reponame, b'repo')
413
413
414 if __name__ == '__main__':
414 if __name__ == '__main__':
415 import silenttestrunner
415 import silenttestrunner
416 silenttestrunner.main(__name__)
416 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now