##// END OF EJS Templates
hgweb: propagate http headers from ErrorResponse for web interface commands...
Sune Foldager -
r38326:519b46a8 default
parent child Browse files
Show More
@@ -1,470 +1,472 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 cspvalues,
18 18 permhooks,
19 19 statusmessage,
20 20 )
21 21
22 22 from .. import (
23 23 encoding,
24 24 error,
25 25 formatter,
26 26 hg,
27 27 hook,
28 28 profiling,
29 29 pycompat,
30 30 registrar,
31 31 repoview,
32 32 templatefilters,
33 33 templater,
34 34 templateutil,
35 35 ui as uimod,
36 36 util,
37 37 wireprotoserver,
38 38 )
39 39
40 40 from . import (
41 41 request as requestmod,
42 42 webcommands,
43 43 webutil,
44 44 wsgicgi,
45 45 )
46 46
47 47 def getstyle(req, configfn, templatepath):
48 48 styles = (
49 49 req.qsparams.get('style', None),
50 50 configfn('web', 'style'),
51 51 'paper',
52 52 )
53 53 return styles, templater.stylemap(styles, templatepath)
54 54
55 55 def makebreadcrumb(url, prefix=''):
56 56 '''Return a 'URL breadcrumb' list
57 57
58 58 A 'URL breadcrumb' is a list of URL-name pairs,
59 59 corresponding to each of the path items on a URL.
60 60 This can be used to create path navigation entries.
61 61 '''
62 62 if url.endswith('/'):
63 63 url = url[:-1]
64 64 if prefix:
65 65 url = '/' + prefix + url
66 66 relpath = url
67 67 if relpath.startswith('/'):
68 68 relpath = relpath[1:]
69 69
70 70 breadcrumb = []
71 71 urlel = url
72 72 pathitems = [''] + relpath.split('/')
73 73 for pathel in reversed(pathitems):
74 74 if not pathel or not urlel:
75 75 break
76 76 breadcrumb.append({'url': urlel, 'name': pathel})
77 77 urlel = os.path.dirname(urlel)
78 78 return templateutil.mappinglist(reversed(breadcrumb))
79 79
80 80 class requestcontext(object):
81 81 """Holds state/context for an individual request.
82 82
83 83 Servers can be multi-threaded. Holding state on the WSGI application
84 84 is prone to race conditions. Instances of this class exist to hold
85 85 mutable and race-free state for requests.
86 86 """
87 87 def __init__(self, app, repo, req, res):
88 88 self.repo = repo
89 89 self.reponame = app.reponame
90 90 self.req = req
91 91 self.res = res
92 92
93 93 self.maxchanges = self.configint('web', 'maxchanges')
94 94 self.stripecount = self.configint('web', 'stripes')
95 95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
96 96 self.maxfiles = self.configint('web', 'maxfiles')
97 97 self.allowpull = self.configbool('web', 'allow-pull')
98 98
99 99 # we use untrusted=False to prevent a repo owner from using
100 100 # web.templates in .hg/hgrc to get access to any file readable
101 101 # by the user running the CGI script
102 102 self.templatepath = self.config('web', 'templates', untrusted=False)
103 103
104 104 # This object is more expensive to build than simple config values.
105 105 # It is shared across requests. The app will replace the object
106 106 # if it is updated. Since this is a reference and nothing should
107 107 # modify the underlying object, it should be constant for the lifetime
108 108 # of the request.
109 109 self.websubtable = app.websubtable
110 110
111 111 self.csp, self.nonce = cspvalues(self.repo.ui)
112 112
113 113 # Trust the settings from the .hg/hgrc files by default.
114 114 def config(self, section, name, default=uimod._unset, untrusted=True):
115 115 return self.repo.ui.config(section, name, default,
116 116 untrusted=untrusted)
117 117
118 118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
119 119 return self.repo.ui.configbool(section, name, default,
120 120 untrusted=untrusted)
121 121
122 122 def configint(self, section, name, default=uimod._unset, untrusted=True):
123 123 return self.repo.ui.configint(section, name, default,
124 124 untrusted=untrusted)
125 125
126 126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
127 127 return self.repo.ui.configlist(section, name, default,
128 128 untrusted=untrusted)
129 129
130 130 def archivelist(self, nodeid):
131 131 return webutil.archivelist(self.repo.ui, nodeid)
132 132
133 133 def templater(self, req):
134 134 # determine scheme, port and server name
135 135 # this is needed to create absolute urls
136 136 logourl = self.config('web', 'logourl')
137 137 logoimg = self.config('web', 'logoimg')
138 138 staticurl = (self.config('web', 'staticurl')
139 139 or req.apppath + '/static/')
140 140 if not staticurl.endswith('/'):
141 141 staticurl += '/'
142 142
143 143 # some functions for the templater
144 144
145 145 def motd(**map):
146 146 yield self.config('web', 'motd')
147 147
148 148 # figure out which style to use
149 149
150 150 vars = {}
151 151 styles, (style, mapfile) = getstyle(req, self.config,
152 152 self.templatepath)
153 153 if style == styles[0]:
154 154 vars['style'] = style
155 155
156 156 sessionvars = webutil.sessionvars(vars, '?')
157 157
158 158 if not self.reponame:
159 159 self.reponame = (self.config('web', 'name', '')
160 160 or req.reponame
161 161 or req.apppath
162 162 or self.repo.root)
163 163
164 164 filters = {}
165 165 templatefilter = registrar.templatefilter(filters)
166 166 @templatefilter('websub', intype=bytes)
167 167 def websubfilter(text):
168 168 return templatefilters.websub(text, self.websubtable)
169 169
170 170 # create the templater
171 171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
172 172 defaults = {
173 173 'url': req.apppath + '/',
174 174 'logourl': logourl,
175 175 'logoimg': logoimg,
176 176 'staticurl': staticurl,
177 177 'urlbase': req.advertisedbaseurl,
178 178 'repo': self.reponame,
179 179 'encoding': encoding.encoding,
180 180 'motd': motd,
181 181 'sessionvars': sessionvars,
182 182 'pathdef': makebreadcrumb(req.apppath),
183 183 'style': style,
184 184 'nonce': self.nonce,
185 185 }
186 186 tres = formatter.templateresources(self.repo.ui, self.repo)
187 187 tmpl = templater.templater.frommapfile(mapfile,
188 188 filters=filters,
189 189 defaults=defaults,
190 190 resources=tres)
191 191 return tmpl
192 192
193 193 def sendtemplate(self, name, **kwargs):
194 194 """Helper function to send a response generated from a template."""
195 195 kwargs = pycompat.byteskwargs(kwargs)
196 196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
197 197 return self.res.sendresponse()
198 198
199 199 class hgweb(object):
200 200 """HTTP server for individual repositories.
201 201
202 202 Instances of this class serve HTTP responses for a particular
203 203 repository.
204 204
205 205 Instances are typically used as WSGI applications.
206 206
207 207 Some servers are multi-threaded. On these servers, there may
208 208 be multiple active threads inside __call__.
209 209 """
210 210 def __init__(self, repo, name=None, baseui=None):
211 211 if isinstance(repo, bytes):
212 212 if baseui:
213 213 u = baseui.copy()
214 214 else:
215 215 u = uimod.ui.load()
216 216 r = hg.repository(u, repo)
217 217 else:
218 218 # we trust caller to give us a private copy
219 219 r = repo
220 220
221 221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
223 223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
225 225 # resolve file patterns relative to repo root
226 226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
228 228 # it's unlikely that we can replace signal handlers in WSGI server,
229 229 # and mod_wsgi issues a big warning. a plain hgweb process (with no
230 230 # threading) could replace signal handlers, but we don't bother
231 231 # conditionally enabling it.
232 232 r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
233 233 r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
234 234 # displaying bundling progress bar while serving feel wrong and may
235 235 # break some wsgi implementation.
236 236 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
237 237 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
238 238 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
239 239 self._lastrepo = self._repos[0]
240 240 hook.redirect(True)
241 241 self.reponame = name
242 242
243 243 def _webifyrepo(self, repo):
244 244 repo = getwebview(repo)
245 245 self.websubtable = webutil.getwebsubs(repo)
246 246 return repo
247 247
248 248 @contextlib.contextmanager
249 249 def _obtainrepo(self):
250 250 """Obtain a repo unique to the caller.
251 251
252 252 Internally we maintain a stack of cachedlocalrepo instances
253 253 to be handed out. If one is available, we pop it and return it,
254 254 ensuring it is up to date in the process. If one is not available,
255 255 we clone the most recently used repo instance and return it.
256 256
257 257 It is currently possible for the stack to grow without bounds
258 258 if the server allows infinite threads. However, servers should
259 259 have a thread limit, thus establishing our limit.
260 260 """
261 261 if self._repos:
262 262 cached = self._repos.pop()
263 263 r, created = cached.fetch()
264 264 else:
265 265 cached = self._lastrepo.copy()
266 266 r, created = cached.fetch()
267 267 if created:
268 268 r = self._webifyrepo(r)
269 269
270 270 self._lastrepo = cached
271 271 self.mtime = cached.mtime
272 272 try:
273 273 yield r
274 274 finally:
275 275 self._repos.append(cached)
276 276
277 277 def run(self):
278 278 """Start a server from CGI environment.
279 279
280 280 Modern servers should be using WSGI and should avoid this
281 281 method, if possible.
282 282 """
283 283 if not encoding.environ.get('GATEWAY_INTERFACE',
284 284 '').startswith("CGI/1."):
285 285 raise RuntimeError("This function is only intended to be "
286 286 "called while running as a CGI script.")
287 287 wsgicgi.launch(self)
288 288
289 289 def __call__(self, env, respond):
290 290 """Run the WSGI application.
291 291
292 292 This may be called by multiple threads.
293 293 """
294 294 req = requestmod.parserequestfromenv(env)
295 295 res = requestmod.wsgiresponse(req, respond)
296 296
297 297 return self.run_wsgi(req, res)
298 298
299 299 def run_wsgi(self, req, res):
300 300 """Internal method to run the WSGI application.
301 301
302 302 This is typically only called by Mercurial. External consumers
303 303 should be using instances of this class as the WSGI application.
304 304 """
305 305 with self._obtainrepo() as repo:
306 306 profile = repo.ui.configbool('profiling', 'enabled')
307 307 with profiling.profile(repo.ui, enabled=profile):
308 308 for r in self._runwsgi(req, res, repo):
309 309 yield r
310 310
311 311 def _runwsgi(self, req, res, repo):
312 312 rctx = requestcontext(self, repo, req, res)
313 313
314 314 # This state is global across all threads.
315 315 encoding.encoding = rctx.config('web', 'encoding')
316 316 rctx.repo.ui.environ = req.rawenv
317 317
318 318 if rctx.csp:
319 319 # hgwebdir may have added CSP header. Since we generate our own,
320 320 # replace it.
321 321 res.headers['Content-Security-Policy'] = rctx.csp
322 322
323 323 # /api/* is reserved for various API implementations. Dispatch
324 324 # accordingly. But URL paths can conflict with subrepos and virtual
325 325 # repos in hgwebdir. So until we have a workaround for this, only
326 326 # expose the URLs if the feature is enabled.
327 327 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
328 328 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
329 329 wireprotoserver.handlewsgiapirequest(rctx, req, res,
330 330 self.check_perm)
331 331 return res.sendresponse()
332 332
333 333 handled = wireprotoserver.handlewsgirequest(
334 334 rctx, req, res, self.check_perm)
335 335 if handled:
336 336 return res.sendresponse()
337 337
338 338 # Old implementations of hgweb supported dispatching the request via
339 339 # the initial query string parameter instead of using PATH_INFO.
340 340 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
341 341 # a value), we use it. Otherwise fall back to the query string.
342 342 if req.dispatchpath is not None:
343 343 query = req.dispatchpath
344 344 else:
345 345 query = req.querystring.partition('&')[0].partition(';')[0]
346 346
347 347 # translate user-visible url structure to internal structure
348 348
349 349 args = query.split('/', 2)
350 350 if 'cmd' not in req.qsparams and args and args[0]:
351 351 cmd = args.pop(0)
352 352 style = cmd.rfind('-')
353 353 if style != -1:
354 354 req.qsparams['style'] = cmd[:style]
355 355 cmd = cmd[style + 1:]
356 356
357 357 # avoid accepting e.g. style parameter as command
358 358 if util.safehasattr(webcommands, cmd):
359 359 req.qsparams['cmd'] = cmd
360 360
361 361 if cmd == 'static':
362 362 req.qsparams['file'] = '/'.join(args)
363 363 else:
364 364 if args and args[0]:
365 365 node = args.pop(0).replace('%2F', '/')
366 366 req.qsparams['node'] = node
367 367 if args:
368 368 if 'file' in req.qsparams:
369 369 del req.qsparams['file']
370 370 for a in args:
371 371 req.qsparams.add('file', a)
372 372
373 373 ua = req.headers.get('User-Agent', '')
374 374 if cmd == 'rev' and 'mercurial' in ua:
375 375 req.qsparams['style'] = 'raw'
376 376
377 377 if cmd == 'archive':
378 378 fn = req.qsparams['node']
379 379 for type_, spec in webutil.archivespecs.iteritems():
380 380 ext = spec[2]
381 381 if fn.endswith(ext):
382 382 req.qsparams['node'] = fn[:-len(ext)]
383 383 req.qsparams['type'] = type_
384 384 else:
385 385 cmd = req.qsparams.get('cmd', '')
386 386
387 387 # process the web interface request
388 388
389 389 try:
390 390 rctx.tmpl = rctx.templater(req)
391 391 ctype = rctx.tmpl.render('mimetype',
392 392 {'encoding': encoding.encoding})
393 393
394 394 # check read permissions non-static content
395 395 if cmd != 'static':
396 396 self.check_perm(rctx, req, None)
397 397
398 398 if cmd == '':
399 399 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
400 400 cmd = req.qsparams['cmd']
401 401
402 402 # Don't enable caching if using a CSP nonce because then it wouldn't
403 403 # be a nonce.
404 404 if rctx.configbool('web', 'cache') and not rctx.nonce:
405 405 tag = 'W/"%d"' % self.mtime
406 406 if req.headers.get('If-None-Match') == tag:
407 407 res.status = '304 Not Modified'
408 408 # Content-Type may be defined globally. It isn't valid on a
409 409 # 304, so discard it.
410 410 try:
411 411 del res.headers[b'Content-Type']
412 412 except KeyError:
413 413 pass
414 414 # Response body not allowed on 304.
415 415 res.setbodybytes('')
416 416 return res.sendresponse()
417 417
418 418 res.headers['ETag'] = tag
419 419
420 420 if cmd not in webcommands.__all__:
421 421 msg = 'no such method: %s' % cmd
422 422 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
423 423 else:
424 424 # Set some globals appropriate for web handlers. Commands can
425 425 # override easily enough.
426 426 res.status = '200 Script output follows'
427 427 res.headers['Content-Type'] = ctype
428 428 return getattr(webcommands, cmd)(rctx)
429 429
430 430 except (error.LookupError, error.RepoLookupError) as err:
431 431 msg = pycompat.bytestr(err)
432 432 if (util.safehasattr(err, 'name') and
433 433 not isinstance(err, error.ManifestLookupError)):
434 434 msg = 'revision not found: %s' % err.name
435 435
436 436 res.status = '404 Not Found'
437 437 res.headers['Content-Type'] = ctype
438 438 return rctx.sendtemplate('error', error=msg)
439 439 except (error.RepoError, error.RevlogError) as e:
440 440 res.status = '500 Internal Server Error'
441 441 res.headers['Content-Type'] = ctype
442 442 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
443 443 except ErrorResponse as e:
444 for k, v in e.headers:
445 res.headers[k] = v
444 446 res.status = statusmessage(e.code, pycompat.bytestr(e))
445 447 res.headers['Content-Type'] = ctype
446 448 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
447 449
448 450 def check_perm(self, rctx, req, op):
449 451 for permhook in permhooks:
450 452 permhook(rctx, req, op)
451 453
452 454 def getwebview(repo):
453 455 """The 'web.view' config controls changeset filter to hgweb. Possible
454 456 values are ``served``, ``visible`` and ``all``. Default is ``served``.
455 457 The ``served`` filter only shows changesets that can be pulled from the
456 458 hgweb instance. The``visible`` filter includes secret changesets but
457 459 still excludes "hidden" one.
458 460
459 461 See the repoview module for details.
460 462
461 463 The option has been around undocumented since Mercurial 2.5, but no
462 464 user ever asked about it. So we better keep it undocumented for now."""
463 465 # experimental config: web.view
464 466 viewconfig = repo.ui.config('web', 'view', untrusted=True)
465 467 if viewconfig == 'all':
466 468 return repo.unfiltered()
467 469 elif viewconfig in repoview.filtertable:
468 470 return repo.filtered(viewconfig)
469 471 else:
470 472 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now