##// END OF EJS Templates
hgweb: update _runwsgi try/except range to be valid...
marmoute -
r52189:3972d090 default
parent child Browse files
Show More
@@ -1,593 +1,593 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 Olivia Mackall <olivia@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
10 10 import gc
11 11 import os
12 12 import time
13 13
14 14 from ..i18n import _
15 15
16 16 from .common import (
17 17 ErrorResponse,
18 18 HTTP_SERVER_ERROR,
19 19 cspvalues,
20 20 get_contact,
21 21 get_mtime,
22 22 ismember,
23 23 paritygen,
24 24 staticfile,
25 25 statusmessage,
26 26 )
27 27
28 28 from .. import (
29 29 configitems,
30 30 encoding,
31 31 error,
32 32 extensions,
33 33 hg,
34 34 pathutil,
35 35 profiling,
36 36 pycompat,
37 37 rcutil,
38 38 registrar,
39 39 scmutil,
40 40 templater,
41 41 templateutil,
42 42 ui as uimod,
43 43 util,
44 44 )
45 45
46 46 from . import (
47 47 hgweb_mod,
48 48 request as requestmod,
49 49 webutil,
50 50 wsgicgi,
51 51 )
52 52 from ..utils import dateutil
53 53
54 54
55 55 def cleannames(items):
56 56 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
57 57
58 58
59 59 def findrepos(paths):
60 60 repos = []
61 61 for prefix, root in cleannames(paths):
62 62 roothead, roottail = os.path.split(root)
63 63 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
64 64 # /bar/ be served as as foo/N .
65 65 # '*' will not search inside dirs with .hg (except .hg/patches),
66 66 # '**' will search inside dirs with .hg (and thus also find subrepos).
67 67 try:
68 68 recurse = {b'*': False, b'**': True}[roottail]
69 69 except KeyError:
70 70 repos.append((prefix, root))
71 71 continue
72 72 roothead = os.path.normpath(util.abspath(roothead))
73 73 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
74 74 repos.extend(urlrepos(prefix, roothead, paths))
75 75 return repos
76 76
77 77
78 78 def urlrepos(prefix, roothead, paths):
79 79 """yield url paths and filesystem paths from a list of repo paths
80 80
81 81 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
82 82 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
83 83 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
84 84 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
85 85 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
86 86 """
87 87 for path in paths:
88 88 path = os.path.normpath(path)
89 89 yield (
90 90 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
91 91 ).strip(b'/'), path
92 92
93 93
94 94 def readallowed(ui, req):
95 95 """Check allow_read and deny_read config options of a repo's ui object
96 96 to determine user permissions. By default, with neither option set (or
97 97 both empty), allow all users to read the repo. There are two ways a
98 98 user can be denied read access: (1) deny_read is not empty, and the
99 99 user is unauthenticated or deny_read contains user (or *), and (2)
100 100 allow_read is not empty and the user is not in allow_read. Return True
101 101 if user is allowed to read the repo, else return False."""
102 102
103 103 user = req.remoteuser
104 104
105 105 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
106 106 if deny_read and (not user or ismember(ui, user, deny_read)):
107 107 return False
108 108
109 109 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
110 110 # by default, allow reading if no allow_read option has been set
111 111 if not allow_read or ismember(ui, user, allow_read):
112 112 return True
113 113
114 114 return False
115 115
116 116
117 117 def rawindexentries(ui, repos, req, subdir=b''):
118 118 descend = ui.configbool(b'web', b'descend')
119 119 collapse = ui.configbool(b'web', b'collapse')
120 120 seenrepos = set()
121 121 seendirs = set()
122 122 for name, path in repos:
123 123
124 124 if not name.startswith(subdir):
125 125 continue
126 126 name = name[len(subdir) :]
127 127 directory = False
128 128
129 129 if b'/' in name:
130 130 if not descend:
131 131 continue
132 132
133 133 nameparts = name.split(b'/')
134 134 rootname = nameparts[0]
135 135
136 136 if not collapse:
137 137 pass
138 138 elif rootname in seendirs:
139 139 continue
140 140 elif rootname in seenrepos:
141 141 pass
142 142 else:
143 143 directory = True
144 144 name = rootname
145 145
146 146 # redefine the path to refer to the directory
147 147 discarded = b'/'.join(nameparts[1:])
148 148
149 149 # remove name parts plus accompanying slash
150 150 path = path[: -len(discarded) - 1]
151 151
152 152 try:
153 153 hg.repository(ui, path)
154 154 directory = False
155 155 except (IOError, error.RepoError):
156 156 pass
157 157
158 158 parts = [
159 159 req.apppath.strip(b'/'),
160 160 subdir.strip(b'/'),
161 161 name.strip(b'/'),
162 162 ]
163 163 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
164 164
165 165 # show either a directory entry or a repository
166 166 if directory:
167 167 # get the directory's time information
168 168 try:
169 169 d = (get_mtime(path), dateutil.makedate()[1])
170 170 except OSError:
171 171 continue
172 172
173 173 # add '/' to the name to make it obvious that
174 174 # the entry is a directory, not a regular repository
175 175 row = {
176 176 b'contact': b"",
177 177 b'contact_sort': b"",
178 178 b'name': name + b'/',
179 179 b'name_sort': name,
180 180 b'url': url,
181 181 b'description': b"",
182 182 b'description_sort': b"",
183 183 b'lastchange': d,
184 184 b'lastchange_sort': d[1] - d[0],
185 185 b'archives': templateutil.mappinglist([]),
186 186 b'isdirectory': True,
187 187 b'labels': templateutil.hybridlist([], name=b'label'),
188 188 }
189 189
190 190 seendirs.add(name)
191 191 yield row
192 192 continue
193 193
194 194 u = ui.copy()
195 195 if rcutil.use_repo_hgrc():
196 196 try:
197 197 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
198 198 except Exception as e:
199 199 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
200 200 continue
201 201
202 202 def get(section, name, default=uimod._unset):
203 203 return u.config(section, name, default, untrusted=True)
204 204
205 205 if u.configbool(b"web", b"hidden", untrusted=True):
206 206 continue
207 207
208 208 if not readallowed(u, req):
209 209 continue
210 210
211 211 # update time with local timezone
212 212 try:
213 213 r = hg.repository(ui, path)
214 214 except IOError:
215 215 u.warn(_(b'error accessing repository at %s\n') % path)
216 216 continue
217 217 except error.RepoError:
218 218 u.warn(_(b'error accessing repository at %s\n') % path)
219 219 continue
220 220 try:
221 221 d = (get_mtime(r.spath), dateutil.makedate()[1])
222 222 except OSError:
223 223 continue
224 224
225 225 contact = get_contact(get)
226 226 description = get(b"web", b"description")
227 227 seenrepos.add(name)
228 228 name = get(b"web", b"name", name)
229 229 labels = u.configlist(b'web', b'labels', untrusted=True)
230 230 row = {
231 231 b'contact': contact or b"unknown",
232 232 b'contact_sort': contact.upper() or b"unknown",
233 233 b'name': name,
234 234 b'name_sort': name,
235 235 b'url': url,
236 236 b'description': description or b"unknown",
237 237 b'description_sort': description.upper() or b"unknown",
238 238 b'lastchange': d,
239 239 b'lastchange_sort': d[1] - d[0],
240 240 b'archives': webutil.archivelist(u, b"tip", url),
241 241 b'isdirectory': None,
242 242 b'labels': templateutil.hybridlist(labels, name=b'label'),
243 243 }
244 244
245 245 yield row
246 246
247 247
248 248 def _indexentriesgen(
249 249 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
250 250 ):
251 251 rows = rawindexentries(ui, repos, req, subdir=subdir)
252 252
253 253 sortdefault = None, False
254 254
255 255 if sortcolumn and sortdefault != (sortcolumn, descending):
256 256 sortkey = b'%s_sort' % sortcolumn
257 257 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
258 258
259 259 for row, parity in zip(rows, paritygen(stripecount)):
260 260 row[b'parity'] = parity
261 261 yield row
262 262
263 263
264 264 def indexentries(
265 265 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
266 266 ):
267 267 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
268 268 return templateutil.mappinggenerator(_indexentriesgen, args=args)
269 269
270 270
271 271 class hgwebdir:
272 272 """HTTP server for multiple repositories.
273 273
274 274 Given a configuration, different repositories will be served depending
275 275 on the request path.
276 276
277 277 Instances are typically used as WSGI applications.
278 278 """
279 279
280 280 def __init__(self, conf, baseui=None):
281 281 self.conf = conf
282 282 self.baseui = baseui
283 283 self.ui = None
284 284 self.lastrefresh = 0
285 285 self.motd = None
286 286 self.refresh()
287 287 self.requests_count = 0
288 288 if not baseui:
289 289 # set up environment for new ui
290 290 extensions.loadall(self.ui)
291 291 extensions.populateui(self.ui)
292 292
293 293 def refresh(self):
294 294 if self.ui:
295 295 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
296 296 else:
297 297 item = configitems.coreitems[b'web'][b'refreshinterval']
298 298 refreshinterval = item.default
299 299
300 300 # refreshinterval <= 0 means to always refresh.
301 301 if (
302 302 refreshinterval > 0
303 303 and self.lastrefresh + refreshinterval > time.time()
304 304 ):
305 305 return
306 306
307 307 if self.baseui:
308 308 u = self.baseui.copy()
309 309 else:
310 310 u = uimod.ui.load()
311 311 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
312 312 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
313 313 # displaying bundling progress bar while serving feels wrong and may
314 314 # break some wsgi implementations.
315 315 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
316 316
317 317 if not isinstance(self.conf, (dict, list, tuple)):
318 318 map = {b'paths': b'hgweb-paths'}
319 319 if not os.path.exists(self.conf):
320 320 raise error.Abort(_(b'config file %s not found!') % self.conf)
321 321 u.readconfig(self.conf, remap=map, trust=True)
322 322 paths = []
323 323 for name, ignored in u.configitems(b'hgweb-paths'):
324 324 for path in u.configlist(b'hgweb-paths', name):
325 325 paths.append((name, path))
326 326 elif isinstance(self.conf, (list, tuple)):
327 327 paths = self.conf
328 328 elif isinstance(self.conf, dict):
329 329 paths = self.conf.items()
330 330 extensions.populateui(u)
331 331
332 332 repos = findrepos(paths)
333 333 for prefix, root in u.configitems(b'collections'):
334 334 prefix = util.pconvert(prefix)
335 335 for path in scmutil.walkrepos(root, followsym=True):
336 336 repo = os.path.normpath(path)
337 337 name = util.pconvert(repo)
338 338 if name.startswith(prefix):
339 339 name = name[len(prefix) :]
340 340 repos.append((name.lstrip(b'/'), repo))
341 341
342 342 self.repos = repos
343 343 self.ui = u
344 344 self.gc_full_collect_rate = self.ui.configint(
345 345 b'experimental', b'web.full-garbage-collection-rate'
346 346 )
347 347 self.gc_full_collections_done = 0
348 348 encoding.encoding = self.ui.config(b'web', b'encoding')
349 349 self.style = self.ui.config(b'web', b'style')
350 350 self.templatepath = self.ui.config(
351 351 b'web', b'templates', untrusted=False
352 352 )
353 353 self.stripecount = self.ui.config(b'web', b'stripes')
354 354 if self.stripecount:
355 355 self.stripecount = int(self.stripecount)
356 356 prefix = self.ui.config(b'web', b'prefix')
357 357 if prefix.startswith(b'/'):
358 358 prefix = prefix[1:]
359 359 if prefix.endswith(b'/'):
360 360 prefix = prefix[:-1]
361 361 self.prefix = prefix
362 362 self.lastrefresh = time.time()
363 363
364 364 def run(self):
365 365 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
366 366 b"CGI/1."
367 367 ):
368 368 raise RuntimeError(
369 369 b"This function is only intended to be "
370 370 b"called while running as a CGI script."
371 371 )
372 372 wsgicgi.launch(self)
373 373
374 374 def __call__(self, env, respond):
375 375 baseurl = self.ui.config(b'web', b'baseurl')
376 376 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
377 377 res = requestmod.wsgiresponse(req, respond)
378 378
379 379 return self.run_wsgi(req, res)
380 380
381 381 def run_wsgi(self, req, res):
382 382 profile = self.ui.configbool(b'profiling', b'enabled')
383 383 with profiling.profile(self.ui, enabled=profile):
384 384 try:
385 385 for r in self._runwsgi(req, res):
386 386 yield r
387 387 finally:
388 388 # There are known cycles in localrepository that prevent
389 389 # those objects (and tons of held references) from being
390 390 # collected through normal refcounting.
391 391 # In some cases, the resulting memory consumption can
392 392 # be tamed by performing explicit garbage collections.
393 393 # In presence of actual leaks or big long-lived caches, the
394 394 # impact on performance of such collections can become a
395 395 # problem, hence the rate shouldn't be set too low.
396 396 # See "Collecting the oldest generation" in
397 397 # https://devguide.python.org/garbage_collector
398 398 # for more about such trade-offs.
399 399 rate = self.gc_full_collect_rate
400 400
401 401 # this is not thread safe, but the consequence (skipping
402 402 # a garbage collection) is arguably better than risking
403 403 # to have several threads perform a collection in parallel
404 404 # (long useless wait on all threads).
405 405 self.requests_count += 1
406 406 if rate > 0 and self.requests_count % rate == 0:
407 407 gc.collect()
408 408 self.gc_full_collections_done += 1
409 409 else:
410 410 gc.collect(generation=1)
411 411
412 412 def _runwsgi(self, req, res):
413 try:
414 self.refresh()
413 self.refresh()
415 414
416 csp, nonce = cspvalues(self.ui)
417 if csp:
418 res.headers[b'Content-Security-Policy'] = csp
415 csp, nonce = cspvalues(self.ui)
416 if csp:
417 res.headers[b'Content-Security-Policy'] = csp
419 418
420 virtual = req.dispatchpath.strip(b'/')
421 tmpl = self.templater(req, nonce)
419 virtual = req.dispatchpath.strip(b'/')
420 tmpl = self.templater(req, nonce)
421 try:
422 422 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
423 423
424 424 # Global defaults. These can be overridden by any handler.
425 425 res.status = b'200 Script output follows'
426 426 res.headers[b'Content-Type'] = ctype
427 427
428 428 # a static file
429 429 if virtual.startswith(b'static/') or b'static' in req.qsparams:
430 430 if virtual.startswith(b'static/'):
431 431 fname = virtual[7:]
432 432 else:
433 433 fname = req.qsparams[b'static']
434 434 static = self.ui.config(b"web", b"static", untrusted=False)
435 435 staticfile(self.templatepath, static, fname, res)
436 436 return res.sendresponse()
437 437
438 438 # top-level index
439 439
440 440 repos = dict(self.repos)
441 441
442 442 if (not virtual or virtual == b'index') and virtual not in repos:
443 443 return self.makeindex(req, res, tmpl)
444 444
445 445 # nested indexes and hgwebs
446 446
447 447 if virtual.endswith(b'/index') and virtual not in repos:
448 448 subdir = virtual[: -len(b'index')]
449 449 if any(r.startswith(subdir) for r in repos):
450 450 return self.makeindex(req, res, tmpl, subdir)
451 451
452 452 def _virtualdirs():
453 453 # Check the full virtual path, and each parent
454 454 yield virtual
455 455 for p in pathutil.finddirs(virtual):
456 456 yield p
457 457
458 458 for virtualrepo in _virtualdirs():
459 459 real = repos.get(virtualrepo)
460 460 if real:
461 461 # Re-parse the WSGI environment to take into account our
462 462 # repository path component.
463 463 uenv = {
464 464 k.decode('latin1'): v for k, v in req.rawenv.items()
465 465 }
466 466 req = requestmod.parserequestfromenv(
467 467 uenv,
468 468 reponame=virtualrepo,
469 469 altbaseurl=self.ui.config(b'web', b'baseurl'),
470 470 # Reuse wrapped body file object otherwise state
471 471 # tracking can get confused.
472 472 bodyfh=req.bodyfh,
473 473 )
474 474 try:
475 475 # ensure caller gets private copy of ui
476 476 repo = hg.repository(self.ui.copy(), real)
477 477 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
478 478 except IOError as inst:
479 479 msg = encoding.strtolocal(inst.strerror)
480 480 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
481 481 except error.RepoError as inst:
482 482 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
483 483
484 484 # browse subdirectories
485 485 subdir = virtual + b'/'
486 486 if [r for r in repos if r.startswith(subdir)]:
487 487 return self.makeindex(req, res, tmpl, subdir)
488 488
489 489 # prefixes not found
490 490 res.status = b'404 Not Found'
491 491 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
492 492 return res.sendresponse()
493 493
494 494 except ErrorResponse as e:
495 495 res.status = statusmessage(e.code, pycompat.bytestr(e))
496 496 res.setbodygen(
497 497 tmpl.generate(b'error', {b'error': e.message or b''})
498 498 )
499 499 return res.sendresponse()
500 500 finally:
501 501 del tmpl
502 502
503 503 def makeindex(self, req, res, tmpl, subdir=b""):
504 504 self.refresh()
505 505 sortable = [b"name", b"description", b"contact", b"lastchange"]
506 506 sortcolumn, descending = None, False
507 507 if b'sort' in req.qsparams:
508 508 sortcolumn = req.qsparams[b'sort']
509 509 descending = sortcolumn.startswith(b'-')
510 510 if descending:
511 511 sortcolumn = sortcolumn[1:]
512 512 if sortcolumn not in sortable:
513 513 sortcolumn = b""
514 514
515 515 sort = [
516 516 (
517 517 b"sort_%s" % column,
518 518 b"%s%s"
519 519 % (
520 520 (not descending and column == sortcolumn) and b"-" or b"",
521 521 column,
522 522 ),
523 523 )
524 524 for column in sortable
525 525 ]
526 526
527 527 self.refresh()
528 528
529 529 entries = indexentries(
530 530 self.ui,
531 531 self.repos,
532 532 req,
533 533 self.stripecount,
534 534 sortcolumn=sortcolumn,
535 535 descending=descending,
536 536 subdir=subdir,
537 537 )
538 538
539 539 mapping = {
540 540 b'entries': entries,
541 541 b'subdir': subdir,
542 542 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
543 543 b'sortcolumn': sortcolumn,
544 544 b'descending': descending,
545 545 }
546 546 mapping.update(sort)
547 547 res.setbodygen(tmpl.generate(b'index', mapping))
548 548 return res.sendresponse()
549 549
550 550 def templater(self, req, nonce):
551 551 def config(*args, **kwargs):
552 552 kwargs.setdefault('untrusted', True)
553 553 return self.ui.config(*args, **kwargs)
554 554
555 555 vars = {}
556 556 styles, (style, mapfile, fp) = hgweb_mod.getstyle(
557 557 req, config, self.templatepath
558 558 )
559 559 if style == styles[0]:
560 560 vars[b'style'] = style
561 561
562 562 sessionvars = webutil.sessionvars(vars, b'?')
563 563 logourl = config(b'web', b'logourl')
564 564 logoimg = config(b'web', b'logoimg')
565 565 staticurl = (
566 566 config(b'web', b'staticurl')
567 567 or req.apppath.rstrip(b'/') + b'/static/'
568 568 )
569 569 if not staticurl.endswith(b'/'):
570 570 staticurl += b'/'
571 571
572 572 defaults = {
573 573 b"encoding": encoding.encoding,
574 574 b"url": req.apppath + b'/',
575 575 b"logourl": logourl,
576 576 b"logoimg": logoimg,
577 577 b"staticurl": staticurl,
578 578 b"sessionvars": sessionvars,
579 579 b"style": style,
580 580 b"nonce": nonce,
581 581 }
582 582 templatekeyword = registrar.templatekeyword(defaults)
583 583
584 584 @templatekeyword(b'motd', requires=())
585 585 def motd(context, mapping):
586 586 if self.motd is not None:
587 587 yield self.motd
588 588 else:
589 589 yield config(b'web', b'motd')
590 590
591 591 return templater.templater.frommapfile(
592 592 mapfile, fp=fp, defaults=defaults
593 593 )
General Comments 0
You need to be logged in to leave comments. Login now