comparison mercurial/hgweb/hgweb_mod.py @ 36804:b9b968e21f78

hgweb: rename req to wsgireq We will soon introduce a parsed WSGI request object so we don't have to concern ourselves with low-level WSGI matters. Prepare for multiple request objects by renaming the existing one so it is clear it deals with WSGI. We also remove a symbol import to avoid even more naming confusion. # no-check-commit because of some new foo_bar naming that's required Differential Revision: https://phab.mercurial-scm.org/D2732
author Gregory Szorc <gregory.szorc@gmail.com>
date Thu, 08 Mar 2018 15:15:59 -0800
parents 7fc80c982656
children ec46415ed826
comparison
equal deleted inserted replaced
36803:8e1556ac01bb 36804:b9b968e21f78
20 HTTP_SERVER_ERROR, 20 HTTP_SERVER_ERROR,
21 caching, 21 caching,
22 cspvalues, 22 cspvalues,
23 permhooks, 23 permhooks,
24 ) 24 )
25 from .request import wsgirequest
26 25
27 from .. import ( 26 from .. import (
28 encoding, 27 encoding,
29 error, 28 error,
30 formatter, 29 formatter,
39 util, 38 util,
40 wireprotoserver, 39 wireprotoserver,
41 ) 40 )
42 41
43 from . import ( 42 from . import (
43 request as requestmod,
44 webcommands, 44 webcommands,
45 webutil, 45 webutil,
46 wsgicgi, 46 wsgicgi,
47 ) 47 )
48 48
140 allowed = self.configlist('web', 'allow_archive') 140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems(): 141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ): 142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid} 143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144 144
145 def templater(self, req): 145 def templater(self, wsgireq):
146 # determine scheme, port and server name 146 # determine scheme, port and server name
147 # this is needed to create absolute urls 147 # this is needed to create absolute urls
148 148
149 proto = req.env.get('wsgi.url_scheme') 149 proto = wsgireq.env.get('wsgi.url_scheme')
150 if proto == 'https': 150 if proto == 'https':
151 proto = 'https' 151 proto = 'https'
152 default_port = '443' 152 default_port = '443'
153 else: 153 else:
154 proto = 'http' 154 proto = 'http'
155 default_port = '80' 155 default_port = '80'
156 156
157 port = req.env[r'SERVER_PORT'] 157 port = wsgireq.env[r'SERVER_PORT']
158 port = port != default_port and (r':' + port) or r'' 158 port = port != default_port and (r':' + port) or r''
159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port) 159 urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port)
160 logourl = self.config('web', 'logourl') 160 logourl = self.config('web', 'logourl')
161 logoimg = self.config('web', 'logoimg') 161 logoimg = self.config('web', 'logoimg')
162 staticurl = (self.config('web', 'staticurl') 162 staticurl = (self.config('web', 'staticurl')
163 or pycompat.sysbytes(req.url) + 'static/') 163 or pycompat.sysbytes(wsgireq.url) + 'static/')
164 if not staticurl.endswith('/'): 164 if not staticurl.endswith('/'):
165 staticurl += '/' 165 staticurl += '/'
166 166
167 # some functions for the templater 167 # some functions for the templater
168 168
170 yield self.config('web', 'motd') 170 yield self.config('web', 'motd')
171 171
172 # figure out which style to use 172 # figure out which style to use
173 173
174 vars = {} 174 vars = {}
175 styles, (style, mapfile) = getstyle(req, self.config, 175 styles, (style, mapfile) = getstyle(wsgireq, self.config,
176 self.templatepath) 176 self.templatepath)
177 if style == styles[0]: 177 if style == styles[0]:
178 vars['style'] = style 178 vars['style'] = style
179 179
180 start = '&' if req.url[-1] == r'?' else '?' 180 start = '&' if wsgireq.url[-1] == r'?' else '?'
181 sessionvars = webutil.sessionvars(vars, start) 181 sessionvars = webutil.sessionvars(vars, start)
182 182
183 if not self.reponame: 183 if not self.reponame:
184 self.reponame = (self.config('web', 'name', '') 184 self.reponame = (self.config('web', 'name', '')
185 or req.env.get('REPO_NAME') 185 or wsgireq.env.get('REPO_NAME')
186 or req.url.strip(r'/') or self.repo.root) 186 or wsgireq.url.strip(r'/') or self.repo.root)
187 187
188 def websubfilter(text): 188 def websubfilter(text):
189 return templatefilters.websub(text, self.websubtable) 189 return templatefilters.websub(text, self.websubtable)
190 190
191 # create the templater 191 # create the templater
192 # TODO: export all keywords: defaults = templatekw.keywords.copy() 192 # TODO: export all keywords: defaults = templatekw.keywords.copy()
193 defaults = { 193 defaults = {
194 'url': pycompat.sysbytes(req.url), 194 'url': pycompat.sysbytes(wsgireq.url),
195 'logourl': logourl, 195 'logourl': logourl,
196 'logoimg': logoimg, 196 'logoimg': logoimg,
197 'staticurl': staticurl, 197 'staticurl': staticurl,
198 'urlbase': urlbase, 198 'urlbase': urlbase,
199 'repo': self.reponame, 199 'repo': self.reponame,
200 'encoding': encoding.encoding, 200 'encoding': encoding.encoding,
201 'motd': motd, 201 'motd': motd,
202 'sessionvars': sessionvars, 202 'sessionvars': sessionvars,
203 'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)), 203 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
204 'style': style, 204 'style': style,
205 'nonce': self.nonce, 205 'nonce': self.nonce,
206 } 206 }
207 tres = formatter.templateresources(self.repo.ui, self.repo) 207 tres = formatter.templateresources(self.repo.ui, self.repo)
208 tmpl = templater.templater.frommapfile(mapfile, 208 tmpl = templater.templater.frommapfile(mapfile,
299 def __call__(self, env, respond): 299 def __call__(self, env, respond):
300 """Run the WSGI application. 300 """Run the WSGI application.
301 301
302 This may be called by multiple threads. 302 This may be called by multiple threads.
303 """ 303 """
304 req = wsgirequest(env, respond) 304 req = requestmod.wsgirequest(env, respond)
305 return self.run_wsgi(req) 305 return self.run_wsgi(req)
306 306
307 def run_wsgi(self, req): 307 def run_wsgi(self, wsgireq):
308 """Internal method to run the WSGI application. 308 """Internal method to run the WSGI application.
309 309
310 This is typically only called by Mercurial. External consumers 310 This is typically only called by Mercurial. External consumers
311 should be using instances of this class as the WSGI application. 311 should be using instances of this class as the WSGI application.
312 """ 312 """
313 with self._obtainrepo() as repo: 313 with self._obtainrepo() as repo:
314 profile = repo.ui.configbool('profiling', 'enabled') 314 profile = repo.ui.configbool('profiling', 'enabled')
315 with profiling.profile(repo.ui, enabled=profile): 315 with profiling.profile(repo.ui, enabled=profile):
316 for r in self._runwsgi(req, repo): 316 for r in self._runwsgi(wsgireq, repo):
317 yield r 317 yield r
318 318
319 def _runwsgi(self, req, repo): 319 def _runwsgi(self, wsgireq, repo):
320 rctx = requestcontext(self, repo) 320 rctx = requestcontext(self, repo)
321 321
322 # This state is global across all threads. 322 # This state is global across all threads.
323 encoding.encoding = rctx.config('web', 'encoding') 323 encoding.encoding = rctx.config('web', 'encoding')
324 rctx.repo.ui.environ = req.env 324 rctx.repo.ui.environ = wsgireq.env
325 325
326 if rctx.csp: 326 if rctx.csp:
327 # hgwebdir may have added CSP header. Since we generate our own, 327 # hgwebdir may have added CSP header. Since we generate our own,
328 # replace it. 328 # replace it.
329 req.headers = [h for h in req.headers 329 wsgireq.headers = [h for h in wsgireq.headers
330 if h[0] != 'Content-Security-Policy'] 330 if h[0] != 'Content-Security-Policy']
331 req.headers.append(('Content-Security-Policy', rctx.csp)) 331 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
332 332
333 # work with CGI variables to create coherent structure 333 # work with CGI variables to create coherent structure
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME 334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
335 335
336 req.url = req.env[r'SCRIPT_NAME'] 336 wsgireq.url = wsgireq.env[r'SCRIPT_NAME']
337 if not req.url.endswith(r'/'): 337 if not wsgireq.url.endswith(r'/'):
338 req.url += r'/' 338 wsgireq.url += r'/'
339 if req.env.get('REPO_NAME'): 339 if wsgireq.env.get('REPO_NAME'):
340 req.url += req.env[r'REPO_NAME'] + r'/' 340 wsgireq.url += wsgireq.env[r'REPO_NAME'] + r'/'
341 341
342 if r'PATH_INFO' in req.env: 342 if r'PATH_INFO' in wsgireq.env:
343 parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/') 343 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
344 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/') 344 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
345 if parts[:len(repo_parts)] == repo_parts: 345 if parts[:len(repo_parts)] == repo_parts:
346 parts = parts[len(repo_parts):] 346 parts = parts[len(repo_parts):]
347 query = r'/'.join(parts) 347 query = r'/'.join(parts)
348 else: 348 else:
349 query = req.env[r'QUERY_STRING'].partition(r'&')[0] 349 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
350 query = query.partition(r';')[0] 350 query = query.partition(r';')[0]
351 351
352 # Route it to a wire protocol handler if it looks like a wire protocol 352 # Route it to a wire protocol handler if it looks like a wire protocol
353 # request. 353 # request.
354 protohandler = wireprotoserver.parsehttprequest(rctx, req, query, 354 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
355 self.check_perm) 355 self.check_perm)
356 356
357 if protohandler: 357 if protohandler:
358 try: 358 try:
359 if query: 359 if query:
364 return protohandler['handleerror'](inst) 364 return protohandler['handleerror'](inst)
365 365
366 # translate user-visible url structure to internal structure 366 # translate user-visible url structure to internal structure
367 367
368 args = query.split(r'/', 2) 368 args = query.split(r'/', 2)
369 if 'cmd' not in req.form and args and args[0]: 369 if 'cmd' not in wsgireq.form and args and args[0]:
370 cmd = args.pop(0) 370 cmd = args.pop(0)
371 style = cmd.rfind('-') 371 style = cmd.rfind('-')
372 if style != -1: 372 if style != -1:
373 req.form['style'] = [cmd[:style]] 373 wsgireq.form['style'] = [cmd[:style]]
374 cmd = cmd[style + 1:] 374 cmd = cmd[style + 1:]
375 375
376 # avoid accepting e.g. style parameter as command 376 # avoid accepting e.g. style parameter as command
377 if util.safehasattr(webcommands, cmd): 377 if util.safehasattr(webcommands, cmd):
378 req.form['cmd'] = [cmd] 378 wsgireq.form['cmd'] = [cmd]
379 379
380 if cmd == 'static': 380 if cmd == 'static':
381 req.form['file'] = ['/'.join(args)] 381 wsgireq.form['file'] = ['/'.join(args)]
382 else: 382 else:
383 if args and args[0]: 383 if args and args[0]:
384 node = args.pop(0).replace('%2F', '/') 384 node = args.pop(0).replace('%2F', '/')
385 req.form['node'] = [node] 385 wsgireq.form['node'] = [node]
386 if args: 386 if args:
387 req.form['file'] = args 387 wsgireq.form['file'] = args
388 388
389 ua = req.env.get('HTTP_USER_AGENT', '') 389 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
390 if cmd == 'rev' and 'mercurial' in ua: 390 if cmd == 'rev' and 'mercurial' in ua:
391 req.form['style'] = ['raw'] 391 wsgireq.form['style'] = ['raw']
392 392
393 if cmd == 'archive': 393 if cmd == 'archive':
394 fn = req.form['node'][0] 394 fn = wsgireq.form['node'][0]
395 for type_, spec in rctx.archivespecs.iteritems(): 395 for type_, spec in rctx.archivespecs.iteritems():
396 ext = spec[2] 396 ext = spec[2]
397 if fn.endswith(ext): 397 if fn.endswith(ext):
398 req.form['node'] = [fn[:-len(ext)]] 398 wsgireq.form['node'] = [fn[:-len(ext)]]
399 req.form['type'] = [type_] 399 wsgireq.form['type'] = [type_]
400 else: 400 else:
401 cmd = req.form.get('cmd', [''])[0] 401 cmd = wsgireq.form.get('cmd', [''])[0]
402 402
403 # process the web interface request 403 # process the web interface request
404 404
405 try: 405 try:
406 tmpl = rctx.templater(req) 406 tmpl = rctx.templater(wsgireq)
407 ctype = tmpl('mimetype', encoding=encoding.encoding) 407 ctype = tmpl('mimetype', encoding=encoding.encoding)
408 ctype = templater.stringify(ctype) 408 ctype = templater.stringify(ctype)
409 409
410 # check read permissions non-static content 410 # check read permissions non-static content
411 if cmd != 'static': 411 if cmd != 'static':
412 self.check_perm(rctx, req, None) 412 self.check_perm(rctx, wsgireq, None)
413 413
414 if cmd == '': 414 if cmd == '':
415 req.form['cmd'] = [tmpl.cache['default']] 415 wsgireq.form['cmd'] = [tmpl.cache['default']]
416 cmd = req.form['cmd'][0] 416 cmd = wsgireq.form['cmd'][0]
417 417
418 # Don't enable caching if using a CSP nonce because then it wouldn't 418 # Don't enable caching if using a CSP nonce because then it wouldn't
419 # be a nonce. 419 # be a nonce.
420 if rctx.configbool('web', 'cache') and not rctx.nonce: 420 if rctx.configbool('web', 'cache') and not rctx.nonce:
421 caching(self, req) # sets ETag header or raises NOT_MODIFIED 421 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
422 if cmd not in webcommands.__all__: 422 if cmd not in webcommands.__all__:
423 msg = 'no such method: %s' % cmd 423 msg = 'no such method: %s' % cmd
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg) 424 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
425 elif cmd == 'file' and 'raw' in req.form.get('style', []): 425 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
426 rctx.ctype = ctype 426 rctx.ctype = ctype
427 content = webcommands.rawfile(rctx, req, tmpl) 427 content = webcommands.rawfile(rctx, wsgireq, tmpl)
428 else: 428 else:
429 content = getattr(webcommands, cmd)(rctx, req, tmpl) 429 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
430 req.respond(HTTP_OK, ctype) 430 wsgireq.respond(HTTP_OK, ctype)
431 431
432 return content 432 return content
433 433
434 except (error.LookupError, error.RepoLookupError) as err: 434 except (error.LookupError, error.RepoLookupError) as err:
435 req.respond(HTTP_NOT_FOUND, ctype) 435 wsgireq.respond(HTTP_NOT_FOUND, ctype)
436 msg = pycompat.bytestr(err) 436 msg = pycompat.bytestr(err)
437 if (util.safehasattr(err, 'name') and 437 if (util.safehasattr(err, 'name') and
438 not isinstance(err, error.ManifestLookupError)): 438 not isinstance(err, error.ManifestLookupError)):
439 msg = 'revision not found: %s' % err.name 439 msg = 'revision not found: %s' % err.name
440 return tmpl('error', error=msg) 440 return tmpl('error', error=msg)
441 except (error.RepoError, error.RevlogError) as inst: 441 except (error.RepoError, error.RevlogError) as inst:
442 req.respond(HTTP_SERVER_ERROR, ctype) 442 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
443 return tmpl('error', error=pycompat.bytestr(inst)) 443 return tmpl('error', error=pycompat.bytestr(inst))
444 except ErrorResponse as inst: 444 except ErrorResponse as inst:
445 req.respond(inst, ctype) 445 wsgireq.respond(inst, ctype)
446 if inst.code == HTTP_NOT_MODIFIED: 446 if inst.code == HTTP_NOT_MODIFIED:
447 # Not allowed to return a body on a 304 447 # Not allowed to return a body on a 304
448 return [''] 448 return ['']
449 return tmpl('error', error=pycompat.bytestr(inst)) 449 return tmpl('error', error=pycompat.bytestr(inst))
450 450