##// END OF EJS Templates
hgweb: use computed base URL from parsed request...
Gregory Szorc -
r36825:1e2194e0 default
parent child Browse files
Show More
@@ -1,466 +1,454 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 def templater(self, wsgireq):
145 def templater(self, wsgireq, req):
146 146 # determine scheme, port and server name
147 147 # this is needed to create absolute urls
148
149 proto = wsgireq.env.get('wsgi.url_scheme')
150 if proto == 'https':
151 proto = 'https'
152 default_port = '443'
153 else:
154 proto = 'http'
155 default_port = '80'
156
157 port = wsgireq.env[r'SERVER_PORT']
158 port = port != default_port and (r':' + port) or r''
159 urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port)
160 148 logourl = self.config('web', 'logourl')
161 149 logoimg = self.config('web', 'logoimg')
162 150 staticurl = (self.config('web', 'staticurl')
163 151 or pycompat.sysbytes(wsgireq.url) + 'static/')
164 152 if not staticurl.endswith('/'):
165 153 staticurl += '/'
166 154
167 155 # some functions for the templater
168 156
169 157 def motd(**map):
170 158 yield self.config('web', 'motd')
171 159
172 160 # figure out which style to use
173 161
174 162 vars = {}
175 163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
176 164 self.templatepath)
177 165 if style == styles[0]:
178 166 vars['style'] = style
179 167
180 168 sessionvars = webutil.sessionvars(vars, '?')
181 169
182 170 if not self.reponame:
183 171 self.reponame = (self.config('web', 'name', '')
184 172 or wsgireq.env.get('REPO_NAME')
185 173 or wsgireq.url.strip(r'/') or self.repo.root)
186 174
187 175 def websubfilter(text):
188 176 return templatefilters.websub(text, self.websubtable)
189 177
190 178 # create the templater
191 179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 180 defaults = {
193 181 'url': pycompat.sysbytes(wsgireq.url),
194 182 'logourl': logourl,
195 183 'logoimg': logoimg,
196 184 'staticurl': staticurl,
197 'urlbase': urlbase,
185 'urlbase': req.advertisedbaseurl,
198 186 'repo': self.reponame,
199 187 'encoding': encoding.encoding,
200 188 'motd': motd,
201 189 'sessionvars': sessionvars,
202 190 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
203 191 'style': style,
204 192 'nonce': self.nonce,
205 193 }
206 194 tres = formatter.templateresources(self.repo.ui, self.repo)
207 195 tmpl = templater.templater.frommapfile(mapfile,
208 196 filters={'websub': websubfilter},
209 197 defaults=defaults,
210 198 resources=tres)
211 199 return tmpl
212 200
213 201
214 202 class hgweb(object):
215 203 """HTTP server for individual repositories.
216 204
217 205 Instances of this class serve HTTP responses for a particular
218 206 repository.
219 207
220 208 Instances are typically used as WSGI applications.
221 209
222 210 Some servers are multi-threaded. On these servers, there may
223 211 be multiple active threads inside __call__.
224 212 """
225 213 def __init__(self, repo, name=None, baseui=None):
226 214 if isinstance(repo, str):
227 215 if baseui:
228 216 u = baseui.copy()
229 217 else:
230 218 u = uimod.ui.load()
231 219 r = hg.repository(u, repo)
232 220 else:
233 221 # we trust caller to give us a private copy
234 222 r = repo
235 223
236 224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 228 # resolve file patterns relative to repo root
241 229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 231 # displaying bundling progress bar while serving feel wrong and may
244 232 # break some wsgi implementation.
245 233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 236 self._lastrepo = self._repos[0]
249 237 hook.redirect(True)
250 238 self.reponame = name
251 239
252 240 def _webifyrepo(self, repo):
253 241 repo = getwebview(repo)
254 242 self.websubtable = webutil.getwebsubs(repo)
255 243 return repo
256 244
257 245 @contextlib.contextmanager
258 246 def _obtainrepo(self):
259 247 """Obtain a repo unique to the caller.
260 248
261 249 Internally we maintain a stack of cachedlocalrepo instances
262 250 to be handed out. If one is available, we pop it and return it,
263 251 ensuring it is up to date in the process. If one is not available,
264 252 we clone the most recently used repo instance and return it.
265 253
266 254 It is currently possible for the stack to grow without bounds
267 255 if the server allows infinite threads. However, servers should
268 256 have a thread limit, thus establishing our limit.
269 257 """
270 258 if self._repos:
271 259 cached = self._repos.pop()
272 260 r, created = cached.fetch()
273 261 else:
274 262 cached = self._lastrepo.copy()
275 263 r, created = cached.fetch()
276 264 if created:
277 265 r = self._webifyrepo(r)
278 266
279 267 self._lastrepo = cached
280 268 self.mtime = cached.mtime
281 269 try:
282 270 yield r
283 271 finally:
284 272 self._repos.append(cached)
285 273
286 274 def run(self):
287 275 """Start a server from CGI environment.
288 276
289 277 Modern servers should be using WSGI and should avoid this
290 278 method, if possible.
291 279 """
292 280 if not encoding.environ.get('GATEWAY_INTERFACE',
293 281 '').startswith("CGI/1."):
294 282 raise RuntimeError("This function is only intended to be "
295 283 "called while running as a CGI script.")
296 284 wsgicgi.launch(self)
297 285
298 286 def __call__(self, env, respond):
299 287 """Run the WSGI application.
300 288
301 289 This may be called by multiple threads.
302 290 """
303 291 req = requestmod.wsgirequest(env, respond)
304 292 return self.run_wsgi(req)
305 293
306 294 def run_wsgi(self, wsgireq):
307 295 """Internal method to run the WSGI application.
308 296
309 297 This is typically only called by Mercurial. External consumers
310 298 should be using instances of this class as the WSGI application.
311 299 """
312 300 with self._obtainrepo() as repo:
313 301 profile = repo.ui.configbool('profiling', 'enabled')
314 302 with profiling.profile(repo.ui, enabled=profile):
315 303 for r in self._runwsgi(wsgireq, repo):
316 304 yield r
317 305
318 306 def _runwsgi(self, wsgireq, repo):
319 307 req = requestmod.parserequestfromenv(wsgireq.env)
320 308 rctx = requestcontext(self, repo)
321 309
322 310 # This state is global across all threads.
323 311 encoding.encoding = rctx.config('web', 'encoding')
324 312 rctx.repo.ui.environ = wsgireq.env
325 313
326 314 if rctx.csp:
327 315 # hgwebdir may have added CSP header. Since we generate our own,
328 316 # replace it.
329 317 wsgireq.headers = [h for h in wsgireq.headers
330 318 if h[0] != 'Content-Security-Policy']
331 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
332 320
333 321 wsgireq.url = pycompat.sysstr(req.apppath)
334 322
335 323 if r'PATH_INFO' in wsgireq.env:
336 324 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
337 325 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
338 326 if parts[:len(repo_parts)] == repo_parts:
339 327 parts = parts[len(repo_parts):]
340 328 query = r'/'.join(parts)
341 329 else:
342 330 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
343 331 query = query.partition(r';')[0]
344 332
345 333 # Route it to a wire protocol handler if it looks like a wire protocol
346 334 # request.
347 335 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
348 336 self.check_perm)
349 337
350 338 if protohandler:
351 339 try:
352 340 if query:
353 341 raise ErrorResponse(HTTP_NOT_FOUND)
354 342
355 343 return protohandler['dispatch']()
356 344 except ErrorResponse as inst:
357 345 return protohandler['handleerror'](inst)
358 346
359 347 # translate user-visible url structure to internal structure
360 348
361 349 args = query.split(r'/', 2)
362 350 if 'cmd' not in wsgireq.form and args and args[0]:
363 351 cmd = args.pop(0)
364 352 style = cmd.rfind('-')
365 353 if style != -1:
366 354 wsgireq.form['style'] = [cmd[:style]]
367 355 cmd = cmd[style + 1:]
368 356
369 357 # avoid accepting e.g. style parameter as command
370 358 if util.safehasattr(webcommands, cmd):
371 359 wsgireq.form['cmd'] = [cmd]
372 360
373 361 if cmd == 'static':
374 362 wsgireq.form['file'] = ['/'.join(args)]
375 363 else:
376 364 if args and args[0]:
377 365 node = args.pop(0).replace('%2F', '/')
378 366 wsgireq.form['node'] = [node]
379 367 if args:
380 368 wsgireq.form['file'] = args
381 369
382 370 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
383 371 if cmd == 'rev' and 'mercurial' in ua:
384 372 wsgireq.form['style'] = ['raw']
385 373
386 374 if cmd == 'archive':
387 375 fn = wsgireq.form['node'][0]
388 376 for type_, spec in rctx.archivespecs.iteritems():
389 377 ext = spec[2]
390 378 if fn.endswith(ext):
391 379 wsgireq.form['node'] = [fn[:-len(ext)]]
392 380 wsgireq.form['type'] = [type_]
393 381 else:
394 382 cmd = wsgireq.form.get('cmd', [''])[0]
395 383
396 384 # process the web interface request
397 385
398 386 try:
399 tmpl = rctx.templater(wsgireq)
387 tmpl = rctx.templater(wsgireq, req)
400 388 ctype = tmpl('mimetype', encoding=encoding.encoding)
401 389 ctype = templater.stringify(ctype)
402 390
403 391 # check read permissions non-static content
404 392 if cmd != 'static':
405 393 self.check_perm(rctx, wsgireq, None)
406 394
407 395 if cmd == '':
408 396 wsgireq.form['cmd'] = [tmpl.cache['default']]
409 397 cmd = wsgireq.form['cmd'][0]
410 398
411 399 # Don't enable caching if using a CSP nonce because then it wouldn't
412 400 # be a nonce.
413 401 if rctx.configbool('web', 'cache') and not rctx.nonce:
414 402 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
415 403 if cmd not in webcommands.__all__:
416 404 msg = 'no such method: %s' % cmd
417 405 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
418 406 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
419 407 rctx.ctype = ctype
420 408 content = webcommands.rawfile(rctx, wsgireq, tmpl)
421 409 else:
422 410 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
423 411 wsgireq.respond(HTTP_OK, ctype)
424 412
425 413 return content
426 414
427 415 except (error.LookupError, error.RepoLookupError) as err:
428 416 wsgireq.respond(HTTP_NOT_FOUND, ctype)
429 417 msg = pycompat.bytestr(err)
430 418 if (util.safehasattr(err, 'name') and
431 419 not isinstance(err, error.ManifestLookupError)):
432 420 msg = 'revision not found: %s' % err.name
433 421 return tmpl('error', error=msg)
434 422 except (error.RepoError, error.RevlogError) as inst:
435 423 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
436 424 return tmpl('error', error=pycompat.bytestr(inst))
437 425 except ErrorResponse as inst:
438 426 wsgireq.respond(inst, ctype)
439 427 if inst.code == HTTP_NOT_MODIFIED:
440 428 # Not allowed to return a body on a 304
441 429 return ['']
442 430 return tmpl('error', error=pycompat.bytestr(inst))
443 431
444 432 def check_perm(self, rctx, req, op):
445 433 for permhook in permhooks:
446 434 permhook(rctx, req, op)
447 435
448 436 def getwebview(repo):
449 437 """The 'web.view' config controls changeset filter to hgweb. Possible
450 438 values are ``served``, ``visible`` and ``all``. Default is ``served``.
451 439 The ``served`` filter only shows changesets that can be pulled from the
452 440 hgweb instance. The``visible`` filter includes secret changesets but
453 441 still excludes "hidden" one.
454 442
455 443 See the repoview module for details.
456 444
457 445 The option has been around undocumented since Mercurial 2.5, but no
458 446 user ever asked about it. So we better keep it undocumented for now."""
459 447 # experimental config: web.view
460 448 viewconfig = repo.ui.config('web', 'view', untrusted=True)
461 449 if viewconfig == 'all':
462 450 return repo.unfiltered()
463 451 elif viewconfig in repoview.filtertable:
464 452 return repo.filtered(viewconfig)
465 453 else:
466 454 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now