##// END OF EJS Templates
hgweb: make parsedrequest part of wsgirequest...
Gregory Szorc -
r36872:1f7d9024 default
parent child Browse files
Show More
@@ -1,438 +1,438 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 cspvalues,
23 23 permhooks,
24 24 )
25 25
26 26 from .. import (
27 27 encoding,
28 28 error,
29 29 formatter,
30 30 hg,
31 31 hook,
32 32 profiling,
33 33 pycompat,
34 34 repoview,
35 35 templatefilters,
36 36 templater,
37 37 ui as uimod,
38 38 util,
39 39 wireprotoserver,
40 40 )
41 41
42 42 from . import (
43 43 request as requestmod,
44 44 webcommands,
45 45 webutil,
46 46 wsgicgi,
47 47 )
48 48
49 49 archivespecs = util.sortdict((
50 50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 53 ))
54 54
55 55 def getstyle(req, configfn, templatepath):
56 56 fromreq = req.form.get('style', [None])[0]
57 57 styles = (
58 58 fromreq,
59 59 configfn('web', 'style'),
60 60 'paper',
61 61 )
62 62 return styles, templater.stylemap(styles, templatepath)
63 63
64 64 def makebreadcrumb(url, prefix=''):
65 65 '''Return a 'URL breadcrumb' list
66 66
67 67 A 'URL breadcrumb' is a list of URL-name pairs,
68 68 corresponding to each of the path items on a URL.
69 69 This can be used to create path navigation entries.
70 70 '''
71 71 if url.endswith('/'):
72 72 url = url[:-1]
73 73 if prefix:
74 74 url = '/' + prefix + url
75 75 relpath = url
76 76 if relpath.startswith('/'):
77 77 relpath = relpath[1:]
78 78
79 79 breadcrumb = []
80 80 urlel = url
81 81 pathitems = [''] + relpath.split('/')
82 82 for pathel in reversed(pathitems):
83 83 if not pathel or not urlel:
84 84 break
85 85 breadcrumb.append({'url': urlel, 'name': pathel})
86 86 urlel = os.path.dirname(urlel)
87 87 return reversed(breadcrumb)
88 88
89 89 class requestcontext(object):
90 90 """Holds state/context for an individual request.
91 91
92 92 Servers can be multi-threaded. Holding state on the WSGI application
93 93 is prone to race conditions. Instances of this class exist to hold
94 94 mutable and race-free state for requests.
95 95 """
96 96 def __init__(self, app, repo):
97 97 self.repo = repo
98 98 self.reponame = app.reponame
99 99
100 100 self.archivespecs = archivespecs
101 101
102 102 self.maxchanges = self.configint('web', 'maxchanges')
103 103 self.stripecount = self.configint('web', 'stripes')
104 104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 105 self.maxfiles = self.configint('web', 'maxfiles')
106 106 self.allowpull = self.configbool('web', 'allow-pull')
107 107
108 108 # we use untrusted=False to prevent a repo owner from using
109 109 # web.templates in .hg/hgrc to get access to any file readable
110 110 # by the user running the CGI script
111 111 self.templatepath = self.config('web', 'templates', untrusted=False)
112 112
113 113 # This object is more expensive to build than simple config values.
114 114 # It is shared across requests. The app will replace the object
115 115 # if it is updated. Since this is a reference and nothing should
116 116 # modify the underlying object, it should be constant for the lifetime
117 117 # of the request.
118 118 self.websubtable = app.websubtable
119 119
120 120 self.csp, self.nonce = cspvalues(self.repo.ui)
121 121
122 122 # Trust the settings from the .hg/hgrc files by default.
123 123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 124 return self.repo.ui.config(section, name, default,
125 125 untrusted=untrusted)
126 126
127 127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 128 return self.repo.ui.configbool(section, name, default,
129 129 untrusted=untrusted)
130 130
131 131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 132 return self.repo.ui.configint(section, name, default,
133 133 untrusted=untrusted)
134 134
135 135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 136 return self.repo.ui.configlist(section, name, default,
137 137 untrusted=untrusted)
138 138
139 139 def archivelist(self, nodeid):
140 140 allowed = self.configlist('web', 'allow_archive')
141 141 for typ, spec in self.archivespecs.iteritems():
142 142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144 144
145 145 def templater(self, wsgireq, req):
146 146 # determine scheme, port and server name
147 147 # this is needed to create absolute urls
148 148 logourl = self.config('web', 'logourl')
149 149 logoimg = self.config('web', 'logoimg')
150 150 staticurl = (self.config('web', 'staticurl')
151 151 or req.apppath + '/static/')
152 152 if not staticurl.endswith('/'):
153 153 staticurl += '/'
154 154
155 155 # some functions for the templater
156 156
157 157 def motd(**map):
158 158 yield self.config('web', 'motd')
159 159
160 160 # figure out which style to use
161 161
162 162 vars = {}
163 163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
164 164 self.templatepath)
165 165 if style == styles[0]:
166 166 vars['style'] = style
167 167
168 168 sessionvars = webutil.sessionvars(vars, '?')
169 169
170 170 if not self.reponame:
171 171 self.reponame = (self.config('web', 'name', '')
172 172 or wsgireq.env.get('REPO_NAME')
173 173 or req.apppath or self.repo.root)
174 174
175 175 def websubfilter(text):
176 176 return templatefilters.websub(text, self.websubtable)
177 177
178 178 # create the templater
179 179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 180 defaults = {
181 181 'url': req.apppath + '/',
182 182 'logourl': logourl,
183 183 'logoimg': logoimg,
184 184 'staticurl': staticurl,
185 185 'urlbase': req.advertisedbaseurl,
186 186 'repo': self.reponame,
187 187 'encoding': encoding.encoding,
188 188 'motd': motd,
189 189 'sessionvars': sessionvars,
190 190 'pathdef': makebreadcrumb(req.apppath),
191 191 'style': style,
192 192 'nonce': self.nonce,
193 193 }
194 194 tres = formatter.templateresources(self.repo.ui, self.repo)
195 195 tmpl = templater.templater.frommapfile(mapfile,
196 196 filters={'websub': websubfilter},
197 197 defaults=defaults,
198 198 resources=tres)
199 199 return tmpl
200 200
201 201
202 202 class hgweb(object):
203 203 """HTTP server for individual repositories.
204 204
205 205 Instances of this class serve HTTP responses for a particular
206 206 repository.
207 207
208 208 Instances are typically used as WSGI applications.
209 209
210 210 Some servers are multi-threaded. On these servers, there may
211 211 be multiple active threads inside __call__.
212 212 """
213 213 def __init__(self, repo, name=None, baseui=None):
214 214 if isinstance(repo, str):
215 215 if baseui:
216 216 u = baseui.copy()
217 217 else:
218 218 u = uimod.ui.load()
219 219 r = hg.repository(u, repo)
220 220 else:
221 221 # we trust caller to give us a private copy
222 222 r = repo
223 223
224 224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 228 # resolve file patterns relative to repo root
229 229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 231 # displaying bundling progress bar while serving feel wrong and may
232 232 # break some wsgi implementation.
233 233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 236 self._lastrepo = self._repos[0]
237 237 hook.redirect(True)
238 238 self.reponame = name
239 239
240 240 def _webifyrepo(self, repo):
241 241 repo = getwebview(repo)
242 242 self.websubtable = webutil.getwebsubs(repo)
243 243 return repo
244 244
245 245 @contextlib.contextmanager
246 246 def _obtainrepo(self):
247 247 """Obtain a repo unique to the caller.
248 248
249 249 Internally we maintain a stack of cachedlocalrepo instances
250 250 to be handed out. If one is available, we pop it and return it,
251 251 ensuring it is up to date in the process. If one is not available,
252 252 we clone the most recently used repo instance and return it.
253 253
254 254 It is currently possible for the stack to grow without bounds
255 255 if the server allows infinite threads. However, servers should
256 256 have a thread limit, thus establishing our limit.
257 257 """
258 258 if self._repos:
259 259 cached = self._repos.pop()
260 260 r, created = cached.fetch()
261 261 else:
262 262 cached = self._lastrepo.copy()
263 263 r, created = cached.fetch()
264 264 if created:
265 265 r = self._webifyrepo(r)
266 266
267 267 self._lastrepo = cached
268 268 self.mtime = cached.mtime
269 269 try:
270 270 yield r
271 271 finally:
272 272 self._repos.append(cached)
273 273
274 274 def run(self):
275 275 """Start a server from CGI environment.
276 276
277 277 Modern servers should be using WSGI and should avoid this
278 278 method, if possible.
279 279 """
280 280 if not encoding.environ.get('GATEWAY_INTERFACE',
281 281 '').startswith("CGI/1."):
282 282 raise RuntimeError("This function is only intended to be "
283 283 "called while running as a CGI script.")
284 284 wsgicgi.launch(self)
285 285
286 286 def __call__(self, env, respond):
287 287 """Run the WSGI application.
288 288
289 289 This may be called by multiple threads.
290 290 """
291 291 req = requestmod.wsgirequest(env, respond)
292 292 return self.run_wsgi(req)
293 293
294 294 def run_wsgi(self, wsgireq):
295 295 """Internal method to run the WSGI application.
296 296
297 297 This is typically only called by Mercurial. External consumers
298 298 should be using instances of this class as the WSGI application.
299 299 """
300 300 with self._obtainrepo() as repo:
301 301 profile = repo.ui.configbool('profiling', 'enabled')
302 302 with profiling.profile(repo.ui, enabled=profile):
303 303 for r in self._runwsgi(wsgireq, repo):
304 304 yield r
305 305
306 306 def _runwsgi(self, wsgireq, repo):
307 req = requestmod.parserequestfromenv(wsgireq.env)
307 req = wsgireq.req
308 308 rctx = requestcontext(self, repo)
309 309
310 310 # This state is global across all threads.
311 311 encoding.encoding = rctx.config('web', 'encoding')
312 312 rctx.repo.ui.environ = wsgireq.env
313 313
314 314 if rctx.csp:
315 315 # hgwebdir may have added CSP header. Since we generate our own,
316 316 # replace it.
317 317 wsgireq.headers = [h for h in wsgireq.headers
318 318 if h[0] != 'Content-Security-Policy']
319 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
320 320
321 321 handled, res = wireprotoserver.handlewsgirequest(
322 322 rctx, wsgireq, req, self.check_perm)
323 323 if handled:
324 324 return res
325 325
326 326 if req.havepathinfo:
327 327 query = req.dispatchpath
328 328 else:
329 329 query = req.querystring.partition('&')[0].partition(';')[0]
330 330
331 331 # translate user-visible url structure to internal structure
332 332
333 333 args = query.split('/', 2)
334 334 if 'cmd' not in wsgireq.form and args and args[0]:
335 335 cmd = args.pop(0)
336 336 style = cmd.rfind('-')
337 337 if style != -1:
338 338 wsgireq.form['style'] = [cmd[:style]]
339 339 cmd = cmd[style + 1:]
340 340
341 341 # avoid accepting e.g. style parameter as command
342 342 if util.safehasattr(webcommands, cmd):
343 343 wsgireq.form['cmd'] = [cmd]
344 344
345 345 if cmd == 'static':
346 346 wsgireq.form['file'] = ['/'.join(args)]
347 347 else:
348 348 if args and args[0]:
349 349 node = args.pop(0).replace('%2F', '/')
350 350 wsgireq.form['node'] = [node]
351 351 if args:
352 352 wsgireq.form['file'] = args
353 353
354 354 ua = req.headers.get('User-Agent', '')
355 355 if cmd == 'rev' and 'mercurial' in ua:
356 356 wsgireq.form['style'] = ['raw']
357 357
358 358 if cmd == 'archive':
359 359 fn = wsgireq.form['node'][0]
360 360 for type_, spec in rctx.archivespecs.iteritems():
361 361 ext = spec[2]
362 362 if fn.endswith(ext):
363 363 wsgireq.form['node'] = [fn[:-len(ext)]]
364 364 wsgireq.form['type'] = [type_]
365 365 else:
366 366 cmd = wsgireq.form.get('cmd', [''])[0]
367 367
368 368 # process the web interface request
369 369
370 370 try:
371 371 tmpl = rctx.templater(wsgireq, req)
372 372 ctype = tmpl('mimetype', encoding=encoding.encoding)
373 373 ctype = templater.stringify(ctype)
374 374
375 375 # check read permissions non-static content
376 376 if cmd != 'static':
377 377 self.check_perm(rctx, wsgireq, None)
378 378
379 379 if cmd == '':
380 380 wsgireq.form['cmd'] = [tmpl.cache['default']]
381 381 cmd = wsgireq.form['cmd'][0]
382 382
383 383 # Don't enable caching if using a CSP nonce because then it wouldn't
384 384 # be a nonce.
385 385 if rctx.configbool('web', 'cache') and not rctx.nonce:
386 386 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
387 387 if cmd not in webcommands.__all__:
388 388 msg = 'no such method: %s' % cmd
389 389 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
390 390 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
391 391 rctx.ctype = ctype
392 392 content = webcommands.rawfile(rctx, wsgireq, tmpl)
393 393 else:
394 394 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
395 395 wsgireq.respond(HTTP_OK, ctype)
396 396
397 397 return content
398 398
399 399 except (error.LookupError, error.RepoLookupError) as err:
400 400 wsgireq.respond(HTTP_NOT_FOUND, ctype)
401 401 msg = pycompat.bytestr(err)
402 402 if (util.safehasattr(err, 'name') and
403 403 not isinstance(err, error.ManifestLookupError)):
404 404 msg = 'revision not found: %s' % err.name
405 405 return tmpl('error', error=msg)
406 406 except (error.RepoError, error.RevlogError) as inst:
407 407 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
408 408 return tmpl('error', error=pycompat.bytestr(inst))
409 409 except ErrorResponse as inst:
410 410 wsgireq.respond(inst, ctype)
411 411 if inst.code == HTTP_NOT_MODIFIED:
412 412 # Not allowed to return a body on a 304
413 413 return ['']
414 414 return tmpl('error', error=pycompat.bytestr(inst))
415 415
416 416 def check_perm(self, rctx, req, op):
417 417 for permhook in permhooks:
418 418 permhook(rctx, req, op)
419 419
420 420 def getwebview(repo):
421 421 """The 'web.view' config controls changeset filter to hgweb. Possible
422 422 values are ``served``, ``visible`` and ``all``. Default is ``served``.
423 423 The ``served`` filter only shows changesets that can be pulled from the
424 424 hgweb instance. The``visible`` filter includes secret changesets but
425 425 still excludes "hidden" one.
426 426
427 427 See the repoview module for details.
428 428
429 429 The option has been around undocumented since Mercurial 2.5, but no
430 430 user ever asked about it. So we better keep it undocumented for now."""
431 431 # experimental config: web.view
432 432 viewconfig = repo.ui.config('web', 'view', untrusted=True)
433 433 if viewconfig == 'all':
434 434 return repo.unfiltered()
435 435 elif viewconfig in repoview.filtertable:
436 436 return repo.filtered(viewconfig)
437 437 else:
438 438 return repo.filtered('served')
@@ -1,538 +1,543 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 cspvalues,
23 23 get_contact,
24 24 get_mtime,
25 25 ismember,
26 26 paritygen,
27 27 staticfile,
28 28 )
29 29
30 30 from .. import (
31 31 configitems,
32 32 encoding,
33 33 error,
34 34 hg,
35 35 profiling,
36 36 pycompat,
37 37 scmutil,
38 38 templater,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def geturlcgivars(baseurl, port):
87 87 """
88 88 Extract CGI variables from baseurl
89 89
90 90 >>> geturlcgivars(b"http://host.org/base", b"80")
91 91 ('host.org', '80', '/base')
92 92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
93 93 ('host.org', '8000', '/base')
94 94 >>> geturlcgivars(b'/base', 8000)
95 95 ('', '8000', '/base')
96 96 >>> geturlcgivars(b"base", b'8000')
97 97 ('', '8000', '/base')
98 98 >>> geturlcgivars(b"http://host", b'8000')
99 99 ('host', '8000', '/')
100 100 >>> geturlcgivars(b"http://host/", b'8000')
101 101 ('host', '8000', '/')
102 102 """
103 103 u = util.url(baseurl)
104 104 name = u.host or ''
105 105 if u.port:
106 106 port = u.port
107 107 path = u.path or ""
108 108 if not path.startswith('/'):
109 109 path = '/' + path
110 110
111 111 return name, pycompat.bytestr(port), path
112 112
113 113 class hgwebdir(object):
114 114 """HTTP server for multiple repositories.
115 115
116 116 Given a configuration, different repositories will be served depending
117 117 on the request path.
118 118
119 119 Instances are typically used as WSGI applications.
120 120 """
121 121 def __init__(self, conf, baseui=None):
122 122 self.conf = conf
123 123 self.baseui = baseui
124 124 self.ui = None
125 125 self.lastrefresh = 0
126 126 self.motd = None
127 127 self.refresh()
128 128
129 129 def refresh(self):
130 130 if self.ui:
131 131 refreshinterval = self.ui.configint('web', 'refreshinterval')
132 132 else:
133 133 item = configitems.coreitems['web']['refreshinterval']
134 134 refreshinterval = item.default
135 135
136 136 # refreshinterval <= 0 means to always refresh.
137 137 if (refreshinterval > 0 and
138 138 self.lastrefresh + refreshinterval > time.time()):
139 139 return
140 140
141 141 if self.baseui:
142 142 u = self.baseui.copy()
143 143 else:
144 144 u = uimod.ui.load()
145 145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
146 146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
147 147 # displaying bundling progress bar while serving feels wrong and may
148 148 # break some wsgi implementations.
149 149 u.setconfig('progress', 'disable', 'true', 'hgweb')
150 150
151 151 if not isinstance(self.conf, (dict, list, tuple)):
152 152 map = {'paths': 'hgweb-paths'}
153 153 if not os.path.exists(self.conf):
154 154 raise error.Abort(_('config file %s not found!') % self.conf)
155 155 u.readconfig(self.conf, remap=map, trust=True)
156 156 paths = []
157 157 for name, ignored in u.configitems('hgweb-paths'):
158 158 for path in u.configlist('hgweb-paths', name):
159 159 paths.append((name, path))
160 160 elif isinstance(self.conf, (list, tuple)):
161 161 paths = self.conf
162 162 elif isinstance(self.conf, dict):
163 163 paths = self.conf.items()
164 164
165 165 repos = findrepos(paths)
166 166 for prefix, root in u.configitems('collections'):
167 167 prefix = util.pconvert(prefix)
168 168 for path in scmutil.walkrepos(root, followsym=True):
169 169 repo = os.path.normpath(path)
170 170 name = util.pconvert(repo)
171 171 if name.startswith(prefix):
172 172 name = name[len(prefix):]
173 173 repos.append((name.lstrip('/'), repo))
174 174
175 175 self.repos = repos
176 176 self.ui = u
177 177 encoding.encoding = self.ui.config('web', 'encoding')
178 178 self.style = self.ui.config('web', 'style')
179 179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
180 180 self.stripecount = self.ui.config('web', 'stripes')
181 181 if self.stripecount:
182 182 self.stripecount = int(self.stripecount)
183 183 self._baseurl = self.ui.config('web', 'baseurl')
184 184 prefix = self.ui.config('web', 'prefix')
185 185 if prefix.startswith('/'):
186 186 prefix = prefix[1:]
187 187 if prefix.endswith('/'):
188 188 prefix = prefix[:-1]
189 189 self.prefix = prefix
190 190 self.lastrefresh = time.time()
191 191
192 192 def run(self):
193 193 if not encoding.environ.get('GATEWAY_INTERFACE',
194 194 '').startswith("CGI/1."):
195 195 raise RuntimeError("This function is only intended to be "
196 196 "called while running as a CGI script.")
197 197 wsgicgi.launch(self)
198 198
199 199 def __call__(self, env, respond):
200 200 wsgireq = requestmod.wsgirequest(env, respond)
201 201 return self.run_wsgi(wsgireq)
202 202
203 203 def read_allowed(self, ui, wsgireq):
204 204 """Check allow_read and deny_read config options of a repo's ui object
205 205 to determine user permissions. By default, with neither option set (or
206 206 both empty), allow all users to read the repo. There are two ways a
207 207 user can be denied read access: (1) deny_read is not empty, and the
208 208 user is unauthenticated or deny_read contains user (or *), and (2)
209 209 allow_read is not empty and the user is not in allow_read. Return True
210 210 if user is allowed to read the repo, else return False."""
211 211
212 212 user = wsgireq.env.get('REMOTE_USER')
213 213
214 214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
215 215 if deny_read and (not user or ismember(ui, user, deny_read)):
216 216 return False
217 217
218 218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
219 219 # by default, allow reading if no allow_read option has been set
220 220 if (not allow_read) or ismember(ui, user, allow_read):
221 221 return True
222 222
223 223 return False
224 224
225 225 def run_wsgi(self, wsgireq):
226 226 profile = self.ui.configbool('profiling', 'enabled')
227 227 with profiling.profile(self.ui, enabled=profile):
228 228 for r in self._runwsgi(wsgireq):
229 229 yield r
230 230
231 231 def _runwsgi(self, wsgireq):
232 232 try:
233 233 self.refresh()
234 234
235 235 csp, nonce = cspvalues(self.ui)
236 236 if csp:
237 237 wsgireq.headers.append(('Content-Security-Policy', csp))
238 238
239 239 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
240 240 tmpl = self.templater(wsgireq, nonce)
241 241 ctype = tmpl('mimetype', encoding=encoding.encoding)
242 242 ctype = templater.stringify(ctype)
243 243
244 244 # a static file
245 245 if virtual.startswith('static/') or 'static' in wsgireq.form:
246 246 if virtual.startswith('static/'):
247 247 fname = virtual[7:]
248 248 else:
249 249 fname = wsgireq.form['static'][0]
250 250 static = self.ui.config("web", "static", None,
251 251 untrusted=False)
252 252 if not static:
253 253 tp = self.templatepath or templater.templatepaths()
254 254 if isinstance(tp, str):
255 255 tp = [tp]
256 256 static = [os.path.join(p, 'static') for p in tp]
257 257 staticfile(static, fname, wsgireq)
258 258 return []
259 259
260 260 # top-level index
261 261
262 262 repos = dict(self.repos)
263 263
264 264 if (not virtual or virtual == 'index') and virtual not in repos:
265 265 wsgireq.respond(HTTP_OK, ctype)
266 266 return self.makeindex(wsgireq, tmpl)
267 267
268 268 # nested indexes and hgwebs
269 269
270 270 if virtual.endswith('/index') and virtual not in repos:
271 271 subdir = virtual[:-len('index')]
272 272 if any(r.startswith(subdir) for r in repos):
273 273 wsgireq.respond(HTTP_OK, ctype)
274 274 return self.makeindex(wsgireq, tmpl, subdir)
275 275
276 276 def _virtualdirs():
277 277 # Check the full virtual path, each parent, and the root ('')
278 278 if virtual != '':
279 279 yield virtual
280 280
281 281 for p in util.finddirs(virtual):
282 282 yield p
283 283
284 284 yield ''
285 285
286 286 for virtualrepo in _virtualdirs():
287 287 real = repos.get(virtualrepo)
288 288 if real:
289 289 wsgireq.env['REPO_NAME'] = virtualrepo
290 # We have to re-parse because of updated environment
291 # variable.
292 # TODO this is kind of hacky and we should have a better
293 # way of doing this than with REPO_NAME side-effects.
294 wsgireq.req = requestmod.parserequestfromenv(wsgireq.env)
290 295 try:
291 296 # ensure caller gets private copy of ui
292 297 repo = hg.repository(self.ui.copy(), real)
293 298 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
294 299 except IOError as inst:
295 300 msg = encoding.strtolocal(inst.strerror)
296 301 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
297 302 except error.RepoError as inst:
298 303 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
299 304
300 305 # browse subdirectories
301 306 subdir = virtual + '/'
302 307 if [r for r in repos if r.startswith(subdir)]:
303 308 wsgireq.respond(HTTP_OK, ctype)
304 309 return self.makeindex(wsgireq, tmpl, subdir)
305 310
306 311 # prefixes not found
307 312 wsgireq.respond(HTTP_NOT_FOUND, ctype)
308 313 return tmpl("notfound", repo=virtual)
309 314
310 315 except ErrorResponse as err:
311 316 wsgireq.respond(err, ctype)
312 317 return tmpl('error', error=err.message or '')
313 318 finally:
314 319 tmpl = None
315 320
316 321 def makeindex(self, wsgireq, tmpl, subdir=""):
317 322
318 323 def archivelist(ui, nodeid, url):
319 324 allowed = ui.configlist("web", "allow_archive", untrusted=True)
320 325 archives = []
321 326 for typ, spec in hgweb_mod.archivespecs.iteritems():
322 327 if typ in allowed or ui.configbool("web", "allow" + typ,
323 328 untrusted=True):
324 329 archives.append({"type": typ, "extension": spec[2],
325 330 "node": nodeid, "url": url})
326 331 return archives
327 332
328 333 def rawentries(subdir="", **map):
329 334
330 335 descend = self.ui.configbool('web', 'descend')
331 336 collapse = self.ui.configbool('web', 'collapse')
332 337 seenrepos = set()
333 338 seendirs = set()
334 339 for name, path in self.repos:
335 340
336 341 if not name.startswith(subdir):
337 342 continue
338 343 name = name[len(subdir):]
339 344 directory = False
340 345
341 346 if '/' in name:
342 347 if not descend:
343 348 continue
344 349
345 350 nameparts = name.split('/')
346 351 rootname = nameparts[0]
347 352
348 353 if not collapse:
349 354 pass
350 355 elif rootname in seendirs:
351 356 continue
352 357 elif rootname in seenrepos:
353 358 pass
354 359 else:
355 360 directory = True
356 361 name = rootname
357 362
358 363 # redefine the path to refer to the directory
359 364 discarded = '/'.join(nameparts[1:])
360 365
361 366 # remove name parts plus accompanying slash
362 367 path = path[:-len(discarded) - 1]
363 368
364 369 try:
365 370 r = hg.repository(self.ui, path)
366 371 directory = False
367 372 except (IOError, error.RepoError):
368 373 pass
369 374
370 375 parts = [name]
371 376 parts.insert(0, '/' + subdir.rstrip('/'))
372 377 if wsgireq.env['SCRIPT_NAME']:
373 378 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
374 379 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
375 380
376 381 # show either a directory entry or a repository
377 382 if directory:
378 383 # get the directory's time information
379 384 try:
380 385 d = (get_mtime(path), dateutil.makedate()[1])
381 386 except OSError:
382 387 continue
383 388
384 389 # add '/' to the name to make it obvious that
385 390 # the entry is a directory, not a regular repository
386 391 row = {'contact': "",
387 392 'contact_sort': "",
388 393 'name': name + '/',
389 394 'name_sort': name,
390 395 'url': url,
391 396 'description': "",
392 397 'description_sort': "",
393 398 'lastchange': d,
394 399 'lastchange_sort': d[1]-d[0],
395 400 'archives': [],
396 401 'isdirectory': True,
397 402 'labels': [],
398 403 }
399 404
400 405 seendirs.add(name)
401 406 yield row
402 407 continue
403 408
404 409 u = self.ui.copy()
405 410 try:
406 411 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
407 412 except Exception as e:
408 413 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
409 414 continue
410 415 def get(section, name, default=uimod._unset):
411 416 return u.config(section, name, default, untrusted=True)
412 417
413 418 if u.configbool("web", "hidden", untrusted=True):
414 419 continue
415 420
416 421 if not self.read_allowed(u, wsgireq):
417 422 continue
418 423
419 424 # update time with local timezone
420 425 try:
421 426 r = hg.repository(self.ui, path)
422 427 except IOError:
423 428 u.warn(_('error accessing repository at %s\n') % path)
424 429 continue
425 430 except error.RepoError:
426 431 u.warn(_('error accessing repository at %s\n') % path)
427 432 continue
428 433 try:
429 434 d = (get_mtime(r.spath), dateutil.makedate()[1])
430 435 except OSError:
431 436 continue
432 437
433 438 contact = get_contact(get)
434 439 description = get("web", "description")
435 440 seenrepos.add(name)
436 441 name = get("web", "name", name)
437 442 row = {'contact': contact or "unknown",
438 443 'contact_sort': contact.upper() or "unknown",
439 444 'name': name,
440 445 'name_sort': name,
441 446 'url': url,
442 447 'description': description or "unknown",
443 448 'description_sort': description.upper() or "unknown",
444 449 'lastchange': d,
445 450 'lastchange_sort': d[1]-d[0],
446 451 'archives': archivelist(u, "tip", url),
447 452 'isdirectory': None,
448 453 'labels': u.configlist('web', 'labels', untrusted=True),
449 454 }
450 455
451 456 yield row
452 457
453 458 sortdefault = None, False
454 459 def entries(sortcolumn="", descending=False, subdir="", **map):
455 460 rows = rawentries(subdir=subdir, **map)
456 461
457 462 if sortcolumn and sortdefault != (sortcolumn, descending):
458 463 sortkey = '%s_sort' % sortcolumn
459 464 rows = sorted(rows, key=lambda x: x[sortkey],
460 465 reverse=descending)
461 466 for row, parity in zip(rows, paritygen(self.stripecount)):
462 467 row['parity'] = parity
463 468 yield row
464 469
465 470 self.refresh()
466 471 sortable = ["name", "description", "contact", "lastchange"]
467 472 sortcolumn, descending = sortdefault
468 473 if 'sort' in wsgireq.form:
469 474 sortcolumn = wsgireq.form['sort'][0]
470 475 descending = sortcolumn.startswith('-')
471 476 if descending:
472 477 sortcolumn = sortcolumn[1:]
473 478 if sortcolumn not in sortable:
474 479 sortcolumn = ""
475 480
476 481 sort = [("sort_%s" % column,
477 482 "%s%s" % ((not descending and column == sortcolumn)
478 483 and "-" or "", column))
479 484 for column in sortable]
480 485
481 486 self.refresh()
482 487 self.updatereqenv(wsgireq.env)
483 488
484 489 return tmpl("index", entries=entries, subdir=subdir,
485 490 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
486 491 sortcolumn=sortcolumn, descending=descending,
487 492 **dict(sort))
488 493
489 494 def templater(self, wsgireq, nonce):
490 495
491 496 def motd(**map):
492 497 if self.motd is not None:
493 498 yield self.motd
494 499 else:
495 500 yield config('web', 'motd')
496 501
497 502 def config(section, name, default=uimod._unset, untrusted=True):
498 503 return self.ui.config(section, name, default, untrusted)
499 504
500 505 self.updatereqenv(wsgireq.env)
501 506
502 507 url = wsgireq.env.get('SCRIPT_NAME', '')
503 508 if not url.endswith('/'):
504 509 url += '/'
505 510
506 511 vars = {}
507 512 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq, config,
508 513 self.templatepath)
509 514 if style == styles[0]:
510 515 vars['style'] = style
511 516
512 517 sessionvars = webutil.sessionvars(vars, r'?')
513 518 logourl = config('web', 'logourl')
514 519 logoimg = config('web', 'logoimg')
515 520 staticurl = config('web', 'staticurl') or url + 'static/'
516 521 if not staticurl.endswith('/'):
517 522 staticurl += '/'
518 523
519 524 defaults = {
520 525 "encoding": encoding.encoding,
521 526 "motd": motd,
522 527 "url": url,
523 528 "logourl": logourl,
524 529 "logoimg": logoimg,
525 530 "staticurl": staticurl,
526 531 "sessionvars": sessionvars,
527 532 "style": style,
528 533 "nonce": nonce,
529 534 }
530 535 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
531 536 return tmpl
532 537
533 538 def updatereqenv(self, env):
534 539 if self._baseurl is not None:
535 540 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
536 541 env['SERVER_NAME'] = name
537 542 env['SERVER_PORT'] = port
538 543 env['SCRIPT_NAME'] = path
@@ -1,361 +1,363 b''
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import cgi
12 12 import errno
13 13 import socket
14 14 import wsgiref.headers as wsgiheaders
15 15 #import wsgiref.validate
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_MODIFIED,
20 20 statusmessage,
21 21 )
22 22
23 23 from ..thirdparty import (
24 24 attr,
25 25 )
26 26 from .. import (
27 27 pycompat,
28 28 util,
29 29 )
30 30
31 31 shortcuts = {
32 32 'cl': [('cmd', ['changelog']), ('rev', None)],
33 33 'sl': [('cmd', ['shortlog']), ('rev', None)],
34 34 'cs': [('cmd', ['changeset']), ('node', None)],
35 35 'f': [('cmd', ['file']), ('filenode', None)],
36 36 'fl': [('cmd', ['filelog']), ('filenode', None)],
37 37 'fd': [('cmd', ['filediff']), ('node', None)],
38 38 'fa': [('cmd', ['annotate']), ('filenode', None)],
39 39 'mf': [('cmd', ['manifest']), ('manifest', None)],
40 40 'ca': [('cmd', ['archive']), ('node', None)],
41 41 'tags': [('cmd', ['tags'])],
42 42 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
43 43 'static': [('cmd', ['static']), ('file', None)]
44 44 }
45 45
46 46 def normalize(form):
47 47 # first expand the shortcuts
48 48 for k in shortcuts:
49 49 if k in form:
50 50 for name, value in shortcuts[k]:
51 51 if value is None:
52 52 value = form[k]
53 53 form[name] = value
54 54 del form[k]
55 55 # And strip the values
56 56 bytesform = {}
57 57 for k, v in form.iteritems():
58 58 bytesform[pycompat.bytesurl(k)] = [
59 59 pycompat.bytesurl(i.strip()) for i in v]
60 60 return bytesform
61 61
62 62 @attr.s(frozen=True)
63 63 class parsedrequest(object):
64 64 """Represents a parsed WSGI request / static HTTP request parameters."""
65 65
66 66 # Request method.
67 67 method = attr.ib()
68 68 # Full URL for this request.
69 69 url = attr.ib()
70 70 # URL without any path components. Just <proto>://<host><port>.
71 71 baseurl = attr.ib()
72 72 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
73 73 # of HTTP: Host header for hostname. This is likely what clients used.
74 74 advertisedurl = attr.ib()
75 75 advertisedbaseurl = attr.ib()
76 76 # WSGI application path.
77 77 apppath = attr.ib()
78 78 # List of path parts to be used for dispatch.
79 79 dispatchparts = attr.ib()
80 80 # URL path component (no query string) used for dispatch.
81 81 dispatchpath = attr.ib()
82 82 # Whether there is a path component to this request. This can be true
83 83 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
84 84 havepathinfo = attr.ib()
85 85 # Raw query string (part after "?" in URL).
86 86 querystring = attr.ib()
87 87 # List of 2-tuples of query string arguments.
88 88 querystringlist = attr.ib()
89 89 # Dict of query string arguments. Values are lists with at least 1 item.
90 90 querystringdict = attr.ib()
91 91 # wsgiref.headers.Headers instance. Operates like a dict with case
92 92 # insensitive keys.
93 93 headers = attr.ib()
94 94
95 95 def parserequestfromenv(env):
96 96 """Parse URL components from environment variables.
97 97
98 98 WSGI defines request attributes via environment variables. This function
99 99 parses the environment variables into a data structure.
100 100 """
101 101 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
102 102
103 103 # We first validate that the incoming object conforms with the WSGI spec.
104 104 # We only want to be dealing with spec-conforming WSGI implementations.
105 105 # TODO enable this once we fix internal violations.
106 106 #wsgiref.validate.check_environ(env)
107 107
108 108 # PEP-0333 states that environment keys and values are native strings
109 109 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
110 110 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
111 111 # in Mercurial, so mass convert string keys and values to bytes.
112 112 if pycompat.ispy3:
113 113 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
114 114 env = {k: v.encode('latin-1') if isinstance(v, str) else v
115 115 for k, v in env.iteritems()}
116 116
117 117 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
118 118 # the environment variables.
119 119 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
120 120 # how URLs are reconstructed.
121 121 fullurl = env['wsgi.url_scheme'] + '://'
122 122 advertisedfullurl = fullurl
123 123
124 124 def addport(s):
125 125 if env['wsgi.url_scheme'] == 'https':
126 126 if env['SERVER_PORT'] != '443':
127 127 s += ':' + env['SERVER_PORT']
128 128 else:
129 129 if env['SERVER_PORT'] != '80':
130 130 s += ':' + env['SERVER_PORT']
131 131
132 132 return s
133 133
134 134 if env.get('HTTP_HOST'):
135 135 fullurl += env['HTTP_HOST']
136 136 else:
137 137 fullurl += env['SERVER_NAME']
138 138 fullurl = addport(fullurl)
139 139
140 140 advertisedfullurl += env['SERVER_NAME']
141 141 advertisedfullurl = addport(advertisedfullurl)
142 142
143 143 baseurl = fullurl
144 144 advertisedbaseurl = advertisedfullurl
145 145
146 146 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
147 147 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
148 148 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
149 149 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
150 150
151 151 if env.get('QUERY_STRING'):
152 152 fullurl += '?' + env['QUERY_STRING']
153 153 advertisedfullurl += '?' + env['QUERY_STRING']
154 154
155 155 # When dispatching requests, we look at the URL components (PATH_INFO
156 156 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
157 157 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
158 158 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
159 159 # root. We also exclude its path components from PATH_INFO when resolving
160 160 # the dispatch path.
161 161
162 162 apppath = env['SCRIPT_NAME']
163 163
164 164 if env.get('REPO_NAME'):
165 165 if not apppath.endswith('/'):
166 166 apppath += '/'
167 167
168 168 apppath += env.get('REPO_NAME')
169 169
170 170 if 'PATH_INFO' in env:
171 171 dispatchparts = env['PATH_INFO'].strip('/').split('/')
172 172
173 173 # Strip out repo parts.
174 174 repoparts = env.get('REPO_NAME', '').split('/')
175 175 if dispatchparts[:len(repoparts)] == repoparts:
176 176 dispatchparts = dispatchparts[len(repoparts):]
177 177 else:
178 178 dispatchparts = []
179 179
180 180 dispatchpath = '/'.join(dispatchparts)
181 181
182 182 querystring = env.get('QUERY_STRING', '')
183 183
184 184 # We store as a list so we have ordering information. We also store as
185 185 # a dict to facilitate fast lookup.
186 186 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
187 187
188 188 querystringdict = {}
189 189 for k, v in querystringlist:
190 190 if k in querystringdict:
191 191 querystringdict[k].append(v)
192 192 else:
193 193 querystringdict[k] = [v]
194 194
195 195 # HTTP_* keys contain HTTP request headers. The Headers structure should
196 196 # perform case normalization for us. We just rewrite underscore to dash
197 197 # so keys match what likely went over the wire.
198 198 headers = []
199 199 for k, v in env.iteritems():
200 200 if k.startswith('HTTP_'):
201 201 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
202 202
203 203 headers = wsgiheaders.Headers(headers)
204 204
205 205 # This is kind of a lie because the HTTP header wasn't explicitly
206 206 # sent. But for all intents and purposes it should be OK to lie about
207 207 # this, since a consumer will either either value to determine how many
208 208 # bytes are available to read.
209 209 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
210 210 headers['Content-Length'] = env['CONTENT_LENGTH']
211 211
212 212 return parsedrequest(method=env['REQUEST_METHOD'],
213 213 url=fullurl, baseurl=baseurl,
214 214 advertisedurl=advertisedfullurl,
215 215 advertisedbaseurl=advertisedbaseurl,
216 216 apppath=apppath,
217 217 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
218 218 havepathinfo='PATH_INFO' in env,
219 219 querystring=querystring,
220 220 querystringlist=querystringlist,
221 221 querystringdict=querystringdict,
222 222 headers=headers)
223 223
224 224 class wsgirequest(object):
225 225 """Higher-level API for a WSGI request.
226 226
227 227 WSGI applications are invoked with 2 arguments. They are used to
228 228 instantiate instances of this class, which provides higher-level APIs
229 229 for obtaining request parameters, writing HTTP output, etc.
230 230 """
231 231 def __init__(self, wsgienv, start_response):
232 232 version = wsgienv[r'wsgi.version']
233 233 if (version < (1, 0)) or (version >= (2, 0)):
234 234 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
235 235 % version)
236 236 self.inp = wsgienv[r'wsgi.input']
237 237
238 238 if r'HTTP_CONTENT_LENGTH' in wsgienv:
239 239 self.inp = util.cappedreader(self.inp,
240 240 int(wsgienv[r'HTTP_CONTENT_LENGTH']))
241 241 elif r'CONTENT_LENGTH' in wsgienv:
242 242 self.inp = util.cappedreader(self.inp,
243 243 int(wsgienv[r'CONTENT_LENGTH']))
244 244
245 245 self.err = wsgienv[r'wsgi.errors']
246 246 self.threaded = wsgienv[r'wsgi.multithread']
247 247 self.multiprocess = wsgienv[r'wsgi.multiprocess']
248 248 self.run_once = wsgienv[r'wsgi.run_once']
249 249 self.env = wsgienv
250 250 self.form = normalize(cgi.parse(self.inp,
251 251 self.env,
252 252 keep_blank_values=1))
253 253 self._start_response = start_response
254 254 self.server_write = None
255 255 self.headers = []
256 256
257 self.req = parserequestfromenv(wsgienv)
258
257 259 def respond(self, status, type, filename=None, body=None):
258 260 if not isinstance(type, str):
259 261 type = pycompat.sysstr(type)
260 262 if self._start_response is not None:
261 263 self.headers.append((r'Content-Type', type))
262 264 if filename:
263 265 filename = (filename.rpartition('/')[-1]
264 266 .replace('\\', '\\\\').replace('"', '\\"'))
265 267 self.headers.append(('Content-Disposition',
266 268 'inline; filename="%s"' % filename))
267 269 if body is not None:
268 270 self.headers.append((r'Content-Length', str(len(body))))
269 271
270 272 for k, v in self.headers:
271 273 if not isinstance(v, str):
272 274 raise TypeError('header value must be string: %r' % (v,))
273 275
274 276 if isinstance(status, ErrorResponse):
275 277 self.headers.extend(status.headers)
276 278 if status.code == HTTP_NOT_MODIFIED:
277 279 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
278 280 # it MUST NOT include any headers other than these and no
279 281 # body
280 282 self.headers = [(k, v) for (k, v) in self.headers if
281 283 k in ('Date', 'ETag', 'Expires',
282 284 'Cache-Control', 'Vary')]
283 285 status = statusmessage(status.code, pycompat.bytestr(status))
284 286 elif status == 200:
285 287 status = '200 Script output follows'
286 288 elif isinstance(status, int):
287 289 status = statusmessage(status)
288 290
289 291 # Various HTTP clients (notably httplib) won't read the HTTP
290 292 # response until the HTTP request has been sent in full. If servers
291 293 # (us) send a response before the HTTP request has been fully sent,
292 294 # the connection may deadlock because neither end is reading.
293 295 #
294 296 # We work around this by "draining" the request data before
295 297 # sending any response in some conditions.
296 298 drain = False
297 299 close = False
298 300
299 301 # If the client sent Expect: 100-continue, we assume it is smart
300 302 # enough to deal with the server sending a response before reading
301 303 # the request. (httplib doesn't do this.)
302 304 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
303 305 pass
304 306 # Only tend to request methods that have bodies. Strictly speaking,
305 307 # we should sniff for a body. But this is fine for our existing
306 308 # WSGI applications.
307 309 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
308 310 pass
309 311 else:
310 312 # If we don't know how much data to read, there's no guarantee
311 313 # that we can drain the request responsibly. The WSGI
312 314 # specification only says that servers *should* ensure the
313 315 # input stream doesn't overrun the actual request. So there's
314 316 # no guarantee that reading until EOF won't corrupt the stream
315 317 # state.
316 318 if not isinstance(self.inp, util.cappedreader):
317 319 close = True
318 320 else:
319 321 # We /could/ only drain certain HTTP response codes. But 200
320 322 # and non-200 wire protocol responses both require draining.
321 323 # Since we have a capped reader in place for all situations
322 324 # where we drain, it is safe to read from that stream. We'll
323 325 # either do a drain or no-op if we're already at EOF.
324 326 drain = True
325 327
326 328 if close:
327 329 self.headers.append((r'Connection', r'Close'))
328 330
329 331 if drain:
330 332 assert isinstance(self.inp, util.cappedreader)
331 333 while True:
332 334 chunk = self.inp.read(32768)
333 335 if not chunk:
334 336 break
335 337
336 338 self.server_write = self._start_response(
337 339 pycompat.sysstr(status), self.headers)
338 340 self._start_response = None
339 341 self.headers = []
340 342 if body is not None:
341 343 self.write(body)
342 344 self.server_write = None
343 345
344 346 def write(self, thing):
345 347 if thing:
346 348 try:
347 349 self.server_write(thing)
348 350 except socket.error as inst:
349 351 if inst[0] != errno.ECONNRESET:
350 352 raise
351 353
352 354 def flush(self):
353 355 return None
354 356
355 357 def wsgiapplication(app_maker):
356 358 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
357 359 can and should now be used as a WSGI application.'''
358 360 application = app_maker()
359 361 def run_wsgi(env, respond):
360 362 return application(env, respond)
361 363 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now