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