##// END OF EJS Templates
hgweb_mod: partially undo 8afc25e7effc to fix py3...
Augie Fackler -
r36292:a87093e2 default
parent child Browse files
Show More
@@ -1,483 +1,481
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 from .request import wsgirequest
26 26
27 27 from .. import (
28 28 encoding,
29 29 error,
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 webcommands,
44 44 webutil,
45 45 wsgicgi,
46 46 )
47 47
48 48 perms = {
49 49 'changegroup': 'pull',
50 50 'changegroupsubset': 'pull',
51 51 'getbundle': 'pull',
52 52 'stream_out': 'pull',
53 53 'listkeys': 'pull',
54 54 'unbundle': 'push',
55 55 'pushkey': 'push',
56 56 }
57 57
58 58 archivespecs = util.sortdict((
59 59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 62 ))
63 63
64 64 def getstyle(req, configfn, templatepath):
65 65 fromreq = req.form.get('style', [None])[0]
66 if fromreq is not None:
67 fromreq = pycompat.sysbytes(fromreq)
68 66 styles = (
69 67 fromreq,
70 68 configfn('web', 'style'),
71 69 'paper',
72 70 )
73 71 return styles, templater.stylemap(styles, templatepath)
74 72
75 73 def makebreadcrumb(url, prefix=''):
76 74 '''Return a 'URL breadcrumb' list
77 75
78 76 A 'URL breadcrumb' is a list of URL-name pairs,
79 77 corresponding to each of the path items on a URL.
80 78 This can be used to create path navigation entries.
81 79 '''
82 80 if url.endswith('/'):
83 81 url = url[:-1]
84 82 if prefix:
85 83 url = '/' + prefix + url
86 84 relpath = url
87 85 if relpath.startswith('/'):
88 86 relpath = relpath[1:]
89 87
90 88 breadcrumb = []
91 89 urlel = url
92 90 pathitems = [''] + relpath.split('/')
93 91 for pathel in reversed(pathitems):
94 92 if not pathel or not urlel:
95 93 break
96 94 breadcrumb.append({'url': urlel, 'name': pathel})
97 95 urlel = os.path.dirname(urlel)
98 96 return reversed(breadcrumb)
99 97
100 98 class requestcontext(object):
101 99 """Holds state/context for an individual request.
102 100
103 101 Servers can be multi-threaded. Holding state on the WSGI application
104 102 is prone to race conditions. Instances of this class exist to hold
105 103 mutable and race-free state for requests.
106 104 """
107 105 def __init__(self, app, repo):
108 106 self.repo = repo
109 107 self.reponame = app.reponame
110 108
111 109 self.archivespecs = archivespecs
112 110
113 111 self.maxchanges = self.configint('web', 'maxchanges')
114 112 self.stripecount = self.configint('web', 'stripes')
115 113 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 114 self.maxfiles = self.configint('web', 'maxfiles')
117 115 self.allowpull = self.configbool('web', 'allow-pull')
118 116
119 117 # we use untrusted=False to prevent a repo owner from using
120 118 # web.templates in .hg/hgrc to get access to any file readable
121 119 # by the user running the CGI script
122 120 self.templatepath = self.config('web', 'templates', untrusted=False)
123 121
124 122 # This object is more expensive to build than simple config values.
125 123 # It is shared across requests. The app will replace the object
126 124 # if it is updated. Since this is a reference and nothing should
127 125 # modify the underlying object, it should be constant for the lifetime
128 126 # of the request.
129 127 self.websubtable = app.websubtable
130 128
131 129 self.csp, self.nonce = cspvalues(self.repo.ui)
132 130
133 131 # Trust the settings from the .hg/hgrc files by default.
134 132 def config(self, section, name, default=uimod._unset, untrusted=True):
135 133 return self.repo.ui.config(section, name, default,
136 134 untrusted=untrusted)
137 135
138 136 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 137 return self.repo.ui.configbool(section, name, default,
140 138 untrusted=untrusted)
141 139
142 140 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 141 return self.repo.ui.configint(section, name, default,
144 142 untrusted=untrusted)
145 143
146 144 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 145 return self.repo.ui.configlist(section, name, default,
148 146 untrusted=untrusted)
149 147
150 148 def archivelist(self, nodeid):
151 149 allowed = self.configlist('web', 'allow_archive')
152 150 for typ, spec in self.archivespecs.iteritems():
153 151 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 152 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155 153
156 154 def templater(self, req):
157 155 # determine scheme, port and server name
158 156 # this is needed to create absolute urls
159 157
160 158 proto = req.env.get('wsgi.url_scheme')
161 159 if proto == 'https':
162 160 proto = 'https'
163 161 default_port = '443'
164 162 else:
165 163 proto = 'http'
166 164 default_port = '80'
167 165
168 166 port = req.env[r'SERVER_PORT']
169 167 port = port != default_port and (r':' + port) or r''
170 168 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
171 169 logourl = self.config('web', 'logourl')
172 170 logoimg = self.config('web', 'logoimg')
173 171 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 172 if not staticurl.endswith('/'):
175 173 staticurl += '/'
176 174
177 175 # some functions for the templater
178 176
179 177 def motd(**map):
180 178 yield self.config('web', 'motd')
181 179
182 180 # figure out which style to use
183 181
184 182 vars = {}
185 183 styles, (style, mapfile) = getstyle(req, self.config,
186 184 self.templatepath)
187 185 if style == styles[0]:
188 186 vars['style'] = style
189 187
190 188 start = '&' if req.url[-1] == r'?' else '?'
191 189 sessionvars = webutil.sessionvars(vars, start)
192 190
193 191 if not self.reponame:
194 192 self.reponame = (self.config('web', 'name', '')
195 193 or req.env.get('REPO_NAME')
196 194 or req.url.strip('/') or self.repo.root)
197 195
198 196 def websubfilter(text):
199 197 return templatefilters.websub(text, self.websubtable)
200 198
201 199 # create the templater
202 200
203 201 defaults = {
204 202 'url': req.url,
205 203 'logourl': logourl,
206 204 'logoimg': logoimg,
207 205 'staticurl': staticurl,
208 206 'urlbase': urlbase,
209 207 'repo': self.reponame,
210 208 'encoding': encoding.encoding,
211 209 'motd': motd,
212 210 'sessionvars': sessionvars,
213 211 'pathdef': makebreadcrumb(req.url),
214 212 'style': style,
215 213 'nonce': self.nonce,
216 214 }
217 215 tmpl = templater.templater.frommapfile(mapfile,
218 216 filters={'websub': websubfilter},
219 217 defaults=defaults)
220 218 return tmpl
221 219
222 220
223 221 class hgweb(object):
224 222 """HTTP server for individual repositories.
225 223
226 224 Instances of this class serve HTTP responses for a particular
227 225 repository.
228 226
229 227 Instances are typically used as WSGI applications.
230 228
231 229 Some servers are multi-threaded. On these servers, there may
232 230 be multiple active threads inside __call__.
233 231 """
234 232 def __init__(self, repo, name=None, baseui=None):
235 233 if isinstance(repo, str):
236 234 if baseui:
237 235 u = baseui.copy()
238 236 else:
239 237 u = uimod.ui.load()
240 238 r = hg.repository(u, repo)
241 239 else:
242 240 # we trust caller to give us a private copy
243 241 r = repo
244 242
245 243 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 244 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 245 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 246 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 247 # resolve file patterns relative to repo root
250 248 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 249 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 250 # displaying bundling progress bar while serving feel wrong and may
253 251 # break some wsgi implementation.
254 252 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 253 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 254 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 255 self._lastrepo = self._repos[0]
258 256 hook.redirect(True)
259 257 self.reponame = name
260 258
261 259 def _webifyrepo(self, repo):
262 260 repo = getwebview(repo)
263 261 self.websubtable = webutil.getwebsubs(repo)
264 262 return repo
265 263
266 264 @contextlib.contextmanager
267 265 def _obtainrepo(self):
268 266 """Obtain a repo unique to the caller.
269 267
270 268 Internally we maintain a stack of cachedlocalrepo instances
271 269 to be handed out. If one is available, we pop it and return it,
272 270 ensuring it is up to date in the process. If one is not available,
273 271 we clone the most recently used repo instance and return it.
274 272
275 273 It is currently possible for the stack to grow without bounds
276 274 if the server allows infinite threads. However, servers should
277 275 have a thread limit, thus establishing our limit.
278 276 """
279 277 if self._repos:
280 278 cached = self._repos.pop()
281 279 r, created = cached.fetch()
282 280 else:
283 281 cached = self._lastrepo.copy()
284 282 r, created = cached.fetch()
285 283 if created:
286 284 r = self._webifyrepo(r)
287 285
288 286 self._lastrepo = cached
289 287 self.mtime = cached.mtime
290 288 try:
291 289 yield r
292 290 finally:
293 291 self._repos.append(cached)
294 292
295 293 def run(self):
296 294 """Start a server from CGI environment.
297 295
298 296 Modern servers should be using WSGI and should avoid this
299 297 method, if possible.
300 298 """
301 299 if not encoding.environ.get('GATEWAY_INTERFACE',
302 300 '').startswith("CGI/1."):
303 301 raise RuntimeError("This function is only intended to be "
304 302 "called while running as a CGI script.")
305 303 wsgicgi.launch(self)
306 304
307 305 def __call__(self, env, respond):
308 306 """Run the WSGI application.
309 307
310 308 This may be called by multiple threads.
311 309 """
312 310 req = wsgirequest(env, respond)
313 311 return self.run_wsgi(req)
314 312
315 313 def run_wsgi(self, req):
316 314 """Internal method to run the WSGI application.
317 315
318 316 This is typically only called by Mercurial. External consumers
319 317 should be using instances of this class as the WSGI application.
320 318 """
321 319 with self._obtainrepo() as repo:
322 320 profile = repo.ui.configbool('profiling', 'enabled')
323 321 with profiling.profile(repo.ui, enabled=profile):
324 322 for r in self._runwsgi(req, repo):
325 323 yield r
326 324
327 325 def _runwsgi(self, req, repo):
328 326 rctx = requestcontext(self, repo)
329 327
330 328 # This state is global across all threads.
331 329 encoding.encoding = rctx.config('web', 'encoding')
332 330 rctx.repo.ui.environ = req.env
333 331
334 332 if rctx.csp:
335 333 # hgwebdir may have added CSP header. Since we generate our own,
336 334 # replace it.
337 335 req.headers = [h for h in req.headers
338 336 if h[0] != 'Content-Security-Policy']
339 337 req.headers.append(('Content-Security-Policy', rctx.csp))
340 338
341 339 # work with CGI variables to create coherent structure
342 340 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343 341
344 342 req.url = req.env[r'SCRIPT_NAME']
345 343 if not req.url.endswith('/'):
346 344 req.url += '/'
347 345 if req.env.get('REPO_NAME'):
348 346 req.url += req.env[r'REPO_NAME'] + r'/'
349 347
350 348 if r'PATH_INFO' in req.env:
351 349 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 350 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
353 351 if parts[:len(repo_parts)] == repo_parts:
354 352 parts = parts[len(repo_parts):]
355 353 query = '/'.join(parts)
356 354 else:
357 355 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 356 query = query.partition(r';')[0]
359 357
360 358 # Route it to a wire protocol handler if it looks like a wire protocol
361 359 # request.
362 360 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
363 361
364 362 if protohandler:
365 363 cmd = protohandler['cmd']
366 364 try:
367 365 if query:
368 366 raise ErrorResponse(HTTP_NOT_FOUND)
369 367 if cmd in perms:
370 368 self.check_perm(rctx, req, perms[cmd])
371 369 except ErrorResponse as inst:
372 370 return protohandler['handleerror'](inst)
373 371
374 372 return protohandler['dispatch']()
375 373
376 374 # translate user-visible url structure to internal structure
377 375
378 376 args = query.split('/', 2)
379 377 if r'cmd' not in req.form and args and args[0]:
380 378 cmd = args.pop(0)
381 379 style = cmd.rfind('-')
382 380 if style != -1:
383 381 req.form['style'] = [cmd[:style]]
384 382 cmd = cmd[style + 1:]
385 383
386 384 # avoid accepting e.g. style parameter as command
387 385 if util.safehasattr(webcommands, cmd):
388 386 req.form[r'cmd'] = [cmd]
389 387
390 388 if cmd == 'static':
391 389 req.form['file'] = ['/'.join(args)]
392 390 else:
393 391 if args and args[0]:
394 392 node = args.pop(0).replace('%2F', '/')
395 393 req.form['node'] = [node]
396 394 if args:
397 395 req.form['file'] = args
398 396
399 397 ua = req.env.get('HTTP_USER_AGENT', '')
400 398 if cmd == 'rev' and 'mercurial' in ua:
401 399 req.form['style'] = ['raw']
402 400
403 401 if cmd == 'archive':
404 402 fn = req.form['node'][0]
405 403 for type_, spec in rctx.archivespecs.iteritems():
406 404 ext = spec[2]
407 405 if fn.endswith(ext):
408 406 req.form['node'] = [fn[:-len(ext)]]
409 407 req.form['type'] = [type_]
410 408 else:
411 409 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
412 410
413 411 # process the web interface request
414 412
415 413 try:
416 414 tmpl = rctx.templater(req)
417 415 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 416 ctype = templater.stringify(ctype)
419 417
420 418 # check read permissions non-static content
421 419 if cmd != 'static':
422 420 self.check_perm(rctx, req, None)
423 421
424 422 if cmd == '':
425 423 req.form[r'cmd'] = [tmpl.cache['default']]
426 424 cmd = req.form[r'cmd'][0]
427 425
428 426 # Don't enable caching if using a CSP nonce because then it wouldn't
429 427 # be a nonce.
430 428 if rctx.configbool('web', 'cache') and not rctx.nonce:
431 429 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 430 if cmd not in webcommands.__all__:
433 431 msg = 'no such method: %s' % cmd
434 432 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 433 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
436 434 rctx.ctype = ctype
437 435 content = webcommands.rawfile(rctx, req, tmpl)
438 436 else:
439 437 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 438 req.respond(HTTP_OK, ctype)
441 439
442 440 return content
443 441
444 442 except (error.LookupError, error.RepoLookupError) as err:
445 443 req.respond(HTTP_NOT_FOUND, ctype)
446 444 msg = pycompat.bytestr(err)
447 445 if (util.safehasattr(err, 'name') and
448 446 not isinstance(err, error.ManifestLookupError)):
449 447 msg = 'revision not found: %s' % err.name
450 448 return tmpl('error', error=msg)
451 449 except (error.RepoError, error.RevlogError) as inst:
452 450 req.respond(HTTP_SERVER_ERROR, ctype)
453 451 return tmpl('error', error=pycompat.bytestr(inst))
454 452 except ErrorResponse as inst:
455 453 req.respond(inst, ctype)
456 454 if inst.code == HTTP_NOT_MODIFIED:
457 455 # Not allowed to return a body on a 304
458 456 return ['']
459 457 return tmpl('error', error=pycompat.bytestr(inst))
460 458
461 459 def check_perm(self, rctx, req, op):
462 460 for permhook in permhooks:
463 461 permhook(rctx, req, op)
464 462
465 463 def getwebview(repo):
466 464 """The 'web.view' config controls changeset filter to hgweb. Possible
467 465 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 466 The ``served`` filter only shows changesets that can be pulled from the
469 467 hgweb instance. The``visible`` filter includes secret changesets but
470 468 still excludes "hidden" one.
471 469
472 470 See the repoview module for details.
473 471
474 472 The option has been around undocumented since Mercurial 2.5, but no
475 473 user ever asked about it. So we better keep it undocumented for now."""
476 474 # experimental config: web.view
477 475 viewconfig = repo.ui.config('web', 'view', untrusted=True)
478 476 if viewconfig == 'all':
479 477 return repo.unfiltered()
480 478 elif viewconfig in repoview.filtertable:
481 479 return repo.filtered(viewconfig)
482 480 else:
483 481 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now