##// END OF EJS Templates
hgweb: don't access self.repo during request processing...
Gregory Szorc -
r26209:7917746c default
parent child Browse files
Show More
@@ -1,447 +1,447
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 import os
10 10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 11 from mercurial.templatefilters import websub
12 12 from common import get_stat, ErrorResponse, permhooks, caching
13 13 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 14 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from request import wsgirequest
16 16 import webcommands, protocol, webutil
17 17
18 18 perms = {
19 19 'changegroup': 'pull',
20 20 'changegroupsubset': 'pull',
21 21 'getbundle': 'pull',
22 22 'stream_out': 'pull',
23 23 'listkeys': 'pull',
24 24 'unbundle': 'push',
25 25 'pushkey': 'push',
26 26 }
27 27
28 28 ## Files of interest
29 29 # Used to check if the repository has changed looking at mtime and size of
30 30 # theses files. This should probably be relocated a bit higher in core.
31 31 foi = [('spath', '00changelog.i'),
32 32 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 33 ('spath', 'obsstore'),
34 34 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 35 ]
36 36
37 37 def makebreadcrumb(url, prefix=''):
38 38 '''Return a 'URL breadcrumb' list
39 39
40 40 A 'URL breadcrumb' is a list of URL-name pairs,
41 41 corresponding to each of the path items on a URL.
42 42 This can be used to create path navigation entries.
43 43 '''
44 44 if url.endswith('/'):
45 45 url = url[:-1]
46 46 if prefix:
47 47 url = '/' + prefix + url
48 48 relpath = url
49 49 if relpath.startswith('/'):
50 50 relpath = relpath[1:]
51 51
52 52 breadcrumb = []
53 53 urlel = url
54 54 pathitems = [''] + relpath.split('/')
55 55 for pathel in reversed(pathitems):
56 56 if not pathel or not urlel:
57 57 break
58 58 breadcrumb.append({'url': urlel, 'name': pathel})
59 59 urlel = os.path.dirname(urlel)
60 60 return reversed(breadcrumb)
61 61
62 62 class requestcontext(object):
63 63 """Holds state/context for an individual request.
64 64
65 65 Servers can be multi-threaded. Holding state on the WSGI application
66 66 is prone to race conditions. Instances of this class exist to hold
67 67 mutable and race-free state for requests.
68 68 """
69 69 def __init__(self, app):
70 70 object.__setattr__(self, 'app', app)
71 71 object.__setattr__(self, 'repo', app.repo)
72 72
73 73 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
74 74
75 75 object.__setattr__(self, 'maxchanges',
76 76 self.configint('web', 'maxchanges', 10))
77 77 object.__setattr__(self, 'stripecount',
78 78 self.configint('web', 'stripes', 1))
79 79 object.__setattr__(self, 'maxshortchanges',
80 80 self.configint('web', 'maxshortchanges', 60))
81 81 object.__setattr__(self, 'maxfiles',
82 82 self.configint('web', 'maxfiles', 10))
83 83 object.__setattr__(self, 'allowpull',
84 84 self.configbool('web', 'allowpull', True))
85 85
86 86 # we use untrusted=False to prevent a repo owner from using
87 87 # web.templates in .hg/hgrc to get access to any file readable
88 88 # by the user running the CGI script
89 89 object.__setattr__(self, 'templatepath',
90 90 self.config('web', 'templates', untrusted=False))
91 91
92 92 # This object is more expensive to build than simple config values.
93 93 # It is shared across requests. The app will replace the object
94 94 # if it is updated. Since this is a reference and nothing should
95 95 # modify the underlying object, it should be constant for the lifetime
96 96 # of the request.
97 97 object.__setattr__(self, 'websubtable', app.websubtable)
98 98
99 99 # Proxy unknown reads and writes to the application instance
100 100 # until everything is moved to us.
101 101 def __getattr__(self, name):
102 102 return getattr(self.app, name)
103 103
104 104 def __setattr__(self, name, value):
105 105 return setattr(self.app, name, value)
106 106
107 107 # Servers are often run by a user different from the repo owner.
108 108 # Trust the settings from the .hg/hgrc files by default.
109 109 def config(self, section, name, default=None, untrusted=True):
110 110 return self.repo.ui.config(section, name, default,
111 111 untrusted=untrusted)
112 112
113 113 def configbool(self, section, name, default=False, untrusted=True):
114 114 return self.repo.ui.configbool(section, name, default,
115 115 untrusted=untrusted)
116 116
117 117 def configint(self, section, name, default=None, untrusted=True):
118 118 return self.repo.ui.configint(section, name, default,
119 119 untrusted=untrusted)
120 120
121 121 def configlist(self, section, name, default=None, untrusted=True):
122 122 return self.repo.ui.configlist(section, name, default,
123 123 untrusted=untrusted)
124 124
125 125 archivespecs = {
126 126 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
127 127 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
128 128 'zip': ('application/zip', 'zip', '.zip', None),
129 129 }
130 130
131 131 def archivelist(self, nodeid):
132 132 allowed = self.configlist('web', 'allow_archive')
133 133 for typ, spec in self.archivespecs.iteritems():
134 134 if typ in allowed or self.configbool('web', 'allow%s' % typ):
135 135 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
136 136
137 137 def templater(self, req):
138 138 # determine scheme, port and server name
139 139 # this is needed to create absolute urls
140 140
141 141 proto = req.env.get('wsgi.url_scheme')
142 142 if proto == 'https':
143 143 proto = 'https'
144 144 default_port = '443'
145 145 else:
146 146 proto = 'http'
147 147 default_port = '80'
148 148
149 149 port = req.env['SERVER_PORT']
150 150 port = port != default_port and (':' + port) or ''
151 151 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
152 152 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
153 153 logoimg = self.config('web', 'logoimg', 'hglogo.png')
154 154 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
155 155 if not staticurl.endswith('/'):
156 156 staticurl += '/'
157 157
158 158 # some functions for the templater
159 159
160 160 def motd(**map):
161 161 yield self.config('web', 'motd', '')
162 162
163 163 # figure out which style to use
164 164
165 165 vars = {}
166 166 styles = (
167 167 req.form.get('style', [None])[0],
168 168 self.config('web', 'style'),
169 169 'paper',
170 170 )
171 171 style, mapfile = templater.stylemap(styles, self.templatepath)
172 172 if style == styles[0]:
173 173 vars['style'] = style
174 174
175 175 start = req.url[-1] == '?' and '&' or '?'
176 176 sessionvars = webutil.sessionvars(vars, start)
177 177
178 178 if not self.reponame:
179 179 self.reponame = (self.config('web', 'name')
180 180 or req.env.get('REPO_NAME')
181 181 or req.url.strip('/') or self.repo.root)
182 182
183 183 def websubfilter(text):
184 184 return websub(text, self.websubtable)
185 185
186 186 # create the templater
187 187
188 188 tmpl = templater.templater(mapfile,
189 189 filters={'websub': websubfilter},
190 190 defaults={'url': req.url,
191 191 'logourl': logourl,
192 192 'logoimg': logoimg,
193 193 'staticurl': staticurl,
194 194 'urlbase': urlbase,
195 195 'repo': self.reponame,
196 196 'encoding': encoding.encoding,
197 197 'motd': motd,
198 198 'sessionvars': sessionvars,
199 199 'pathdef': makebreadcrumb(req.url),
200 200 'style': style,
201 201 })
202 202 return tmpl
203 203
204 204
205 205 class hgweb(object):
206 206 """HTTP server for individual repositories.
207 207
208 208 Instances of this class serve HTTP responses for a particular
209 209 repository.
210 210
211 211 Instances are typically used as WSGI applications.
212 212
213 213 Some servers are multi-threaded. On these servers, there may
214 214 be multiple active threads inside __call__.
215 215 """
216 216 def __init__(self, repo, name=None, baseui=None):
217 217 if isinstance(repo, str):
218 218 if baseui:
219 219 u = baseui.copy()
220 220 else:
221 221 u = ui.ui()
222 222 r = hg.repository(u, repo)
223 223 else:
224 224 # we trust caller to give us a private copy
225 225 r = repo
226 226
227 227 r = getwebview(r)
228 228 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
229 229 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
230 230 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
231 231 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
232 232 # displaying bundling progress bar while serving feel wrong and may
233 233 # break some wsgi implementation.
234 234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 236 self.repo = r
237 237 hook.redirect(True)
238 238 self.repostate = None
239 239 self.mtime = -1
240 240 self.reponame = name
241 241
242 242 def refresh(self):
243 243 repostate = []
244 244 mtime = 0
245 245 # file of interrests mtime and size
246 246 for meth, fname in foi:
247 247 prefix = getattr(self.repo, meth)
248 248 st = get_stat(prefix, fname)
249 249 repostate.append((st.st_mtime, st.st_size))
250 250 mtime = max(mtime, st.st_mtime)
251 251 repostate = tuple(repostate)
252 252 # we need to compare file size in addition to mtime to catch
253 253 # changes made less than a second ago
254 254 if repostate != self.repostate:
255 255 r = hg.repository(self.repo.baseui, self.repo.url())
256 256 self.repo = getwebview(r)
257 257 # update these last to avoid threads seeing empty settings
258 258 self.repostate = repostate
259 259 # mtime is needed for ETag
260 260 self.mtime = mtime
261 261
262 262 self.websubtable = webutil.getwebsubs(r)
263 263
264 264 def run(self):
265 265 """Start a server from CGI environment.
266 266
267 267 Modern servers should be using WSGI and should avoid this
268 268 method, if possible.
269 269 """
270 270 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
271 271 raise RuntimeError("This function is only intended to be "
272 272 "called while running as a CGI script.")
273 273 import mercurial.hgweb.wsgicgi as wsgicgi
274 274 wsgicgi.launch(self)
275 275
276 276 def __call__(self, env, respond):
277 277 """Run the WSGI application.
278 278
279 279 This may be called by multiple threads.
280 280 """
281 281 req = wsgirequest(env, respond)
282 282 return self.run_wsgi(req)
283 283
284 284 def run_wsgi(self, req):
285 285 """Internal method to run the WSGI application.
286 286
287 287 This is typically only called by Mercurial. External consumers
288 288 should be using instances of this class as the WSGI application.
289 289 """
290 290 self.refresh()
291 291 rctx = requestcontext(self)
292 292
293 293 # This state is global across all threads.
294 294 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
295 295 rctx.repo.ui.environ = req.env
296 296
297 297 # work with CGI variables to create coherent structure
298 298 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
299 299
300 300 req.url = req.env['SCRIPT_NAME']
301 301 if not req.url.endswith('/'):
302 302 req.url += '/'
303 303 if 'REPO_NAME' in req.env:
304 304 req.url += req.env['REPO_NAME'] + '/'
305 305
306 306 if 'PATH_INFO' in req.env:
307 307 parts = req.env['PATH_INFO'].strip('/').split('/')
308 308 repo_parts = req.env.get('REPO_NAME', '').split('/')
309 309 if parts[:len(repo_parts)] == repo_parts:
310 310 parts = parts[len(repo_parts):]
311 311 query = '/'.join(parts)
312 312 else:
313 313 query = req.env['QUERY_STRING'].split('&', 1)[0]
314 314 query = query.split(';', 1)[0]
315 315
316 316 # process this if it's a protocol request
317 317 # protocol bits don't need to create any URLs
318 318 # and the clients always use the old URL structure
319 319
320 320 cmd = req.form.get('cmd', [''])[0]
321 321 if protocol.iscmd(cmd):
322 322 try:
323 323 if query:
324 324 raise ErrorResponse(HTTP_NOT_FOUND)
325 325 if cmd in perms:
326 326 self.check_perm(rctx, req, perms[cmd])
327 return protocol.call(self.repo, req, cmd)
327 return protocol.call(rctx.repo, req, cmd)
328 328 except ErrorResponse as inst:
329 329 # A client that sends unbundle without 100-continue will
330 330 # break if we respond early.
331 331 if (cmd == 'unbundle' and
332 332 (req.env.get('HTTP_EXPECT',
333 333 '').lower() != '100-continue') or
334 334 req.env.get('X-HgHttp2', '')):
335 335 req.drain()
336 336 else:
337 337 req.headers.append(('Connection', 'Close'))
338 338 req.respond(inst, protocol.HGTYPE,
339 339 body='0\n%s\n' % inst)
340 340 return ''
341 341
342 342 # translate user-visible url structure to internal structure
343 343
344 344 args = query.split('/', 2)
345 345 if 'cmd' not in req.form and args and args[0]:
346 346
347 347 cmd = args.pop(0)
348 348 style = cmd.rfind('-')
349 349 if style != -1:
350 350 req.form['style'] = [cmd[:style]]
351 351 cmd = cmd[style + 1:]
352 352
353 353 # avoid accepting e.g. style parameter as command
354 354 if util.safehasattr(webcommands, cmd):
355 355 req.form['cmd'] = [cmd]
356 356
357 357 if cmd == 'static':
358 358 req.form['file'] = ['/'.join(args)]
359 359 else:
360 360 if args and args[0]:
361 361 node = args.pop(0).replace('%2F', '/')
362 362 req.form['node'] = [node]
363 363 if args:
364 364 req.form['file'] = args
365 365
366 366 ua = req.env.get('HTTP_USER_AGENT', '')
367 367 if cmd == 'rev' and 'mercurial' in ua:
368 368 req.form['style'] = ['raw']
369 369
370 370 if cmd == 'archive':
371 371 fn = req.form['node'][0]
372 372 for type_, spec in rctx.archivespecs.iteritems():
373 373 ext = spec[2]
374 374 if fn.endswith(ext):
375 375 req.form['node'] = [fn[:-len(ext)]]
376 376 req.form['type'] = [type_]
377 377
378 378 # process the web interface request
379 379
380 380 try:
381 381 tmpl = rctx.templater(req)
382 382 ctype = tmpl('mimetype', encoding=encoding.encoding)
383 383 ctype = templater.stringify(ctype)
384 384
385 385 # check read permissions non-static content
386 386 if cmd != 'static':
387 387 self.check_perm(rctx, req, None)
388 388
389 389 if cmd == '':
390 390 req.form['cmd'] = [tmpl.cache['default']]
391 391 cmd = req.form['cmd'][0]
392 392
393 393 if rctx.configbool('web', 'cache', True):
394 394 caching(self, req) # sets ETag header or raises NOT_MODIFIED
395 395 if cmd not in webcommands.__all__:
396 396 msg = 'no such method: %s' % cmd
397 397 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
398 398 elif cmd == 'file' and 'raw' in req.form.get('style', []):
399 399 self.ctype = ctype
400 400 content = webcommands.rawfile(rctx, req, tmpl)
401 401 else:
402 402 content = getattr(webcommands, cmd)(rctx, req, tmpl)
403 403 req.respond(HTTP_OK, ctype)
404 404
405 405 return content
406 406
407 407 except (error.LookupError, error.RepoLookupError) as err:
408 408 req.respond(HTTP_NOT_FOUND, ctype)
409 409 msg = str(err)
410 410 if (util.safehasattr(err, 'name') and
411 411 not isinstance(err, error.ManifestLookupError)):
412 412 msg = 'revision not found: %s' % err.name
413 413 return tmpl('error', error=msg)
414 414 except (error.RepoError, error.RevlogError) as inst:
415 415 req.respond(HTTP_SERVER_ERROR, ctype)
416 416 return tmpl('error', error=str(inst))
417 417 except ErrorResponse as inst:
418 418 req.respond(inst, ctype)
419 419 if inst.code == HTTP_NOT_MODIFIED:
420 420 # Not allowed to return a body on a 304
421 421 return ['']
422 422 return tmpl('error', error=str(inst))
423 423
424 424 def check_perm(self, rctx, req, op):
425 425 for permhook in permhooks:
426 426 permhook(rctx, req, op)
427 427
428 428 def getwebview(repo):
429 429 """The 'web.view' config controls changeset filter to hgweb. Possible
430 430 values are ``served``, ``visible`` and ``all``. Default is ``served``.
431 431 The ``served`` filter only shows changesets that can be pulled from the
432 432 hgweb instance. The``visible`` filter includes secret changesets but
433 433 still excludes "hidden" one.
434 434
435 435 See the repoview module for details.
436 436
437 437 The option has been around undocumented since Mercurial 2.5, but no
438 438 user ever asked about it. So we better keep it undocumented for now."""
439 439 viewconfig = repo.ui.config('web', 'view', 'served',
440 440 untrusted=True)
441 441 if viewconfig == 'all':
442 442 return repo.unfiltered()
443 443 elif viewconfig in repoview.filtertable:
444 444 return repo.filtered(viewconfig)
445 445 else:
446 446 return repo.filtered('served')
447 447
General Comments 0
You need to be logged in to leave comments. Login now