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