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