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