##// END OF EJS Templates
hgweb: inline caching() and port to modern mechanisms...
Gregory Szorc -
r36885:7ad6a275 default
parent child Browse files
Show More
@@ -1,257 +1,251 b''
1 1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 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 base64
12 12 import errno
13 13 import mimetypes
14 14 import os
15 15 import stat
16 16
17 17 from .. import (
18 18 encoding,
19 19 pycompat,
20 20 util,
21 21 )
22 22
23 23 httpserver = util.httpserver
24 24
25 25 HTTP_OK = 200
26 26 HTTP_NOT_MODIFIED = 304
27 27 HTTP_BAD_REQUEST = 400
28 28 HTTP_UNAUTHORIZED = 401
29 29 HTTP_FORBIDDEN = 403
30 30 HTTP_NOT_FOUND = 404
31 31 HTTP_METHOD_NOT_ALLOWED = 405
32 32 HTTP_SERVER_ERROR = 500
33 33
34 34
35 35 def ismember(ui, username, userlist):
36 36 """Check if username is a member of userlist.
37 37
38 38 If userlist has a single '*' member, all users are considered members.
39 39 Can be overridden by extensions to provide more complex authorization
40 40 schemes.
41 41 """
42 42 return userlist == ['*'] or username in userlist
43 43
44 44 def checkauthz(hgweb, req, op):
45 45 '''Check permission for operation based on request data (including
46 46 authentication info). Return if op allowed, else raise an ErrorResponse
47 47 exception.'''
48 48
49 49 user = req.env.get(r'REMOTE_USER')
50 50
51 51 deny_read = hgweb.configlist('web', 'deny_read')
52 52 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
53 53 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
54 54
55 55 allow_read = hgweb.configlist('web', 'allow_read')
56 56 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
57 57 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
58 58
59 59 if op == 'pull' and not hgweb.allowpull:
60 60 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
61 61 elif op == 'pull' or op is None: # op is None for interface requests
62 62 return
63 63
64 64 # enforce that you can only push using POST requests
65 65 if req.env[r'REQUEST_METHOD'] != r'POST':
66 66 msg = 'push requires POST request'
67 67 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
68 68
69 69 # require ssl by default for pushing, auth info cannot be sniffed
70 70 # and replayed
71 71 scheme = req.env.get('wsgi.url_scheme')
72 72 if hgweb.configbool('web', 'push_ssl') and scheme != 'https':
73 73 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
74 74
75 75 deny = hgweb.configlist('web', 'deny_push')
76 76 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
77 77 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
78 78
79 79 allow = hgweb.configlist('web', 'allow-push')
80 80 if not (allow and ismember(hgweb.repo.ui, user, allow)):
81 81 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
82 82
83 83 # Hooks for hgweb permission checks; extensions can add hooks here.
84 84 # Each hook is invoked like this: hook(hgweb, request, operation),
85 85 # where operation is either read, pull or push. Hooks should either
86 86 # raise an ErrorResponse exception, or just return.
87 87 #
88 88 # It is possible to do both authentication and authorization through
89 89 # this.
90 90 permhooks = [checkauthz]
91 91
92 92
93 93 class ErrorResponse(Exception):
94 94 def __init__(self, code, message=None, headers=None):
95 95 if message is None:
96 96 message = _statusmessage(code)
97 97 Exception.__init__(self, pycompat.sysstr(message))
98 98 self.code = code
99 99 if headers is None:
100 100 headers = []
101 101 self.headers = headers
102 102
103 103 class continuereader(object):
104 104 """File object wrapper to handle HTTP 100-continue.
105 105
106 106 This is used by servers so they automatically handle Expect: 100-continue
107 107 request headers. On first read of the request body, the 100 Continue
108 108 response is sent. This should trigger the client into actually sending
109 109 the request body.
110 110 """
111 111 def __init__(self, f, write):
112 112 self.f = f
113 113 self._write = write
114 114 self.continued = False
115 115
116 116 def read(self, amt=-1):
117 117 if not self.continued:
118 118 self.continued = True
119 119 self._write('HTTP/1.1 100 Continue\r\n\r\n')
120 120 return self.f.read(amt)
121 121
122 122 def __getattr__(self, attr):
123 123 if attr in ('close', 'readline', 'readlines', '__iter__'):
124 124 return getattr(self.f, attr)
125 125 raise AttributeError
126 126
127 127 def _statusmessage(code):
128 128 responses = httpserver.basehttprequesthandler.responses
129 129 return responses.get(code, ('Error', 'Unknown error'))[0]
130 130
131 131 def statusmessage(code, message=None):
132 132 return '%d %s' % (code, message or _statusmessage(code))
133 133
134 134 def get_stat(spath, fn):
135 135 """stat fn if it exists, spath otherwise"""
136 136 cl_path = os.path.join(spath, fn)
137 137 if os.path.exists(cl_path):
138 138 return os.stat(cl_path)
139 139 else:
140 140 return os.stat(spath)
141 141
142 142 def get_mtime(spath):
143 143 return get_stat(spath, "00changelog.i")[stat.ST_MTIME]
144 144
145 145 def ispathsafe(path):
146 146 """Determine if a path is safe to use for filesystem access."""
147 147 parts = path.split('/')
148 148 for part in parts:
149 149 if (part in ('', pycompat.oscurdir, pycompat.ospardir) or
150 150 pycompat.ossep in part or
151 151 pycompat.osaltsep is not None and pycompat.osaltsep in part):
152 152 return False
153 153
154 154 return True
155 155
156 156 def staticfile(directory, fname, req):
157 157 """return a file inside directory with guessed Content-Type header
158 158
159 159 fname always uses '/' as directory separator and isn't allowed to
160 160 contain unusual path components.
161 161 Content-Type is guessed using the mimetypes module.
162 162 Return an empty string if fname is illegal or file not found.
163 163
164 164 """
165 165 if not ispathsafe(fname):
166 166 return
167 167
168 168 fpath = os.path.join(*fname.split('/'))
169 169 if isinstance(directory, str):
170 170 directory = [directory]
171 171 for d in directory:
172 172 path = os.path.join(d, fpath)
173 173 if os.path.exists(path):
174 174 break
175 175 try:
176 176 os.stat(path)
177 177 ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain"
178 178 with open(path, 'rb') as fh:
179 179 data = fh.read()
180 180
181 181 req.respond(HTTP_OK, ct, body=data)
182 182 except TypeError:
183 183 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
184 184 except OSError as err:
185 185 if err.errno == errno.ENOENT:
186 186 raise ErrorResponse(HTTP_NOT_FOUND)
187 187 else:
188 188 raise ErrorResponse(HTTP_SERVER_ERROR,
189 189 encoding.strtolocal(err.strerror))
190 190
191 191 def paritygen(stripecount, offset=0):
192 192 """count parity of horizontal stripes for easier reading"""
193 193 if stripecount and offset:
194 194 # account for offset, e.g. due to building the list in reverse
195 195 count = (stripecount + offset) % stripecount
196 196 parity = (stripecount + offset) // stripecount & 1
197 197 else:
198 198 count = 0
199 199 parity = 0
200 200 while True:
201 201 yield parity
202 202 count += 1
203 203 if stripecount and count >= stripecount:
204 204 parity = 1 - parity
205 205 count = 0
206 206
207 207 def get_contact(config):
208 208 """Return repo contact information or empty string.
209 209
210 210 web.contact is the primary source, but if that is not set, try
211 211 ui.username or $EMAIL as a fallback to display something useful.
212 212 """
213 213 return (config("web", "contact") or
214 214 config("ui", "username") or
215 215 encoding.environ.get("EMAIL") or "")
216 216
217 def caching(web, req):
218 tag = r'W/"%d"' % web.mtime
219 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
220 raise ErrorResponse(HTTP_NOT_MODIFIED)
221 req.headers.append(('ETag', tag))
222
223 217 def cspvalues(ui):
224 218 """Obtain the Content-Security-Policy header and nonce value.
225 219
226 220 Returns a 2-tuple of the CSP header value and the nonce value.
227 221
228 222 First value is ``None`` if CSP isn't enabled. Second value is ``None``
229 223 if CSP isn't enabled or if the CSP header doesn't need a nonce.
230 224 """
231 225 # Without demandimport, "import uuid" could have an immediate side-effect
232 226 # running "ldconfig" on Linux trying to find libuuid.
233 227 # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell
234 228 # may pollute the terminal with:
235 229 #
236 230 # shell-init: error retrieving current directory: getcwd: cannot access
237 231 # parent directories: No such file or directory
238 232 #
239 233 # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a
240 234 # shell (hg changeset a09ae70f3489).
241 235 #
242 236 # Moved "import uuid" from here so it's executed after we know we have
243 237 # a sane cwd (i.e. after dispatch.py cwd check).
244 238 #
245 239 # We can move it back once we no longer need Python <= 2.7.12 support.
246 240 import uuid
247 241
248 242 # Don't allow untrusted CSP setting since it be disable protections
249 243 # from a trusted/global source.
250 244 csp = ui.config('web', 'csp', untrusted=False)
251 245 nonce = None
252 246
253 247 if csp and '%nonce%' in csp:
254 248 nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=')
255 249 csp = csp.replace('%nonce%', nonce)
256 250
257 251 return csp, nonce
@@ -1,443 +1,448 b''
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 caching,
22 21 cspvalues,
23 22 permhooks,
24 23 )
25 24
26 25 from .. import (
27 26 encoding,
28 27 error,
29 28 formatter,
30 29 hg,
31 30 hook,
32 31 profiling,
33 32 pycompat,
34 33 repoview,
35 34 templatefilters,
36 35 templater,
37 36 ui as uimod,
38 37 util,
39 38 wireprotoserver,
40 39 )
41 40
42 41 from . import (
43 42 request as requestmod,
44 43 webcommands,
45 44 webutil,
46 45 wsgicgi,
47 46 )
48 47
49 48 archivespecs = util.sortdict((
50 49 ('zip', ('application/zip', 'zip', '.zip', None)),
51 50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 52 ))
54 53
55 54 def getstyle(req, configfn, templatepath):
56 55 styles = (
57 56 req.qsparams.get('style', None),
58 57 configfn('web', 'style'),
59 58 'paper',
60 59 )
61 60 return styles, templater.stylemap(styles, templatepath)
62 61
63 62 def makebreadcrumb(url, prefix=''):
64 63 '''Return a 'URL breadcrumb' list
65 64
66 65 A 'URL breadcrumb' is a list of URL-name pairs,
67 66 corresponding to each of the path items on a URL.
68 67 This can be used to create path navigation entries.
69 68 '''
70 69 if url.endswith('/'):
71 70 url = url[:-1]
72 71 if prefix:
73 72 url = '/' + prefix + url
74 73 relpath = url
75 74 if relpath.startswith('/'):
76 75 relpath = relpath[1:]
77 76
78 77 breadcrumb = []
79 78 urlel = url
80 79 pathitems = [''] + relpath.split('/')
81 80 for pathel in reversed(pathitems):
82 81 if not pathel or not urlel:
83 82 break
84 83 breadcrumb.append({'url': urlel, 'name': pathel})
85 84 urlel = os.path.dirname(urlel)
86 85 return reversed(breadcrumb)
87 86
88 87 class requestcontext(object):
89 88 """Holds state/context for an individual request.
90 89
91 90 Servers can be multi-threaded. Holding state on the WSGI application
92 91 is prone to race conditions. Instances of this class exist to hold
93 92 mutable and race-free state for requests.
94 93 """
95 94 def __init__(self, app, repo):
96 95 self.repo = repo
97 96 self.reponame = app.reponame
98 97
99 98 self.archivespecs = archivespecs
100 99
101 100 self.maxchanges = self.configint('web', 'maxchanges')
102 101 self.stripecount = self.configint('web', 'stripes')
103 102 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 103 self.maxfiles = self.configint('web', 'maxfiles')
105 104 self.allowpull = self.configbool('web', 'allow-pull')
106 105
107 106 # we use untrusted=False to prevent a repo owner from using
108 107 # web.templates in .hg/hgrc to get access to any file readable
109 108 # by the user running the CGI script
110 109 self.templatepath = self.config('web', 'templates', untrusted=False)
111 110
112 111 # This object is more expensive to build than simple config values.
113 112 # It is shared across requests. The app will replace the object
114 113 # if it is updated. Since this is a reference and nothing should
115 114 # modify the underlying object, it should be constant for the lifetime
116 115 # of the request.
117 116 self.websubtable = app.websubtable
118 117
119 118 self.csp, self.nonce = cspvalues(self.repo.ui)
120 119
121 120 # Trust the settings from the .hg/hgrc files by default.
122 121 def config(self, section, name, default=uimod._unset, untrusted=True):
123 122 return self.repo.ui.config(section, name, default,
124 123 untrusted=untrusted)
125 124
126 125 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 126 return self.repo.ui.configbool(section, name, default,
128 127 untrusted=untrusted)
129 128
130 129 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 130 return self.repo.ui.configint(section, name, default,
132 131 untrusted=untrusted)
133 132
134 133 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 134 return self.repo.ui.configlist(section, name, default,
136 135 untrusted=untrusted)
137 136
138 137 def archivelist(self, nodeid):
139 138 allowed = self.configlist('web', 'allow_archive')
140 139 for typ, spec in self.archivespecs.iteritems():
141 140 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 141 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 142
144 143 def templater(self, req):
145 144 # determine scheme, port and server name
146 145 # this is needed to create absolute urls
147 146 logourl = self.config('web', 'logourl')
148 147 logoimg = self.config('web', 'logoimg')
149 148 staticurl = (self.config('web', 'staticurl')
150 149 or req.apppath + '/static/')
151 150 if not staticurl.endswith('/'):
152 151 staticurl += '/'
153 152
154 153 # some functions for the templater
155 154
156 155 def motd(**map):
157 156 yield self.config('web', 'motd')
158 157
159 158 # figure out which style to use
160 159
161 160 vars = {}
162 161 styles, (style, mapfile) = getstyle(req, self.config,
163 162 self.templatepath)
164 163 if style == styles[0]:
165 164 vars['style'] = style
166 165
167 166 sessionvars = webutil.sessionvars(vars, '?')
168 167
169 168 if not self.reponame:
170 169 self.reponame = (self.config('web', 'name', '')
171 170 or req.reponame
172 171 or req.apppath
173 172 or self.repo.root)
174 173
175 174 def websubfilter(text):
176 175 return templatefilters.websub(text, self.websubtable)
177 176
178 177 # create the templater
179 178 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 179 defaults = {
181 180 'url': req.apppath + '/',
182 181 'logourl': logourl,
183 182 'logoimg': logoimg,
184 183 'staticurl': staticurl,
185 184 'urlbase': req.advertisedbaseurl,
186 185 'repo': self.reponame,
187 186 'encoding': encoding.encoding,
188 187 'motd': motd,
189 188 'sessionvars': sessionvars,
190 189 'pathdef': makebreadcrumb(req.apppath),
191 190 'style': style,
192 191 'nonce': self.nonce,
193 192 }
194 193 tres = formatter.templateresources(self.repo.ui, self.repo)
195 194 tmpl = templater.templater.frommapfile(mapfile,
196 195 filters={'websub': websubfilter},
197 196 defaults=defaults,
198 197 resources=tres)
199 198 return tmpl
200 199
201 200
202 201 class hgweb(object):
203 202 """HTTP server for individual repositories.
204 203
205 204 Instances of this class serve HTTP responses for a particular
206 205 repository.
207 206
208 207 Instances are typically used as WSGI applications.
209 208
210 209 Some servers are multi-threaded. On these servers, there may
211 210 be multiple active threads inside __call__.
212 211 """
213 212 def __init__(self, repo, name=None, baseui=None):
214 213 if isinstance(repo, str):
215 214 if baseui:
216 215 u = baseui.copy()
217 216 else:
218 217 u = uimod.ui.load()
219 218 r = hg.repository(u, repo)
220 219 else:
221 220 # we trust caller to give us a private copy
222 221 r = repo
223 222
224 223 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 224 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 225 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 226 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 227 # resolve file patterns relative to repo root
229 228 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 229 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 230 # displaying bundling progress bar while serving feel wrong and may
232 231 # break some wsgi implementation.
233 232 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 233 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 234 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 235 self._lastrepo = self._repos[0]
237 236 hook.redirect(True)
238 237 self.reponame = name
239 238
240 239 def _webifyrepo(self, repo):
241 240 repo = getwebview(repo)
242 241 self.websubtable = webutil.getwebsubs(repo)
243 242 return repo
244 243
245 244 @contextlib.contextmanager
246 245 def _obtainrepo(self):
247 246 """Obtain a repo unique to the caller.
248 247
249 248 Internally we maintain a stack of cachedlocalrepo instances
250 249 to be handed out. If one is available, we pop it and return it,
251 250 ensuring it is up to date in the process. If one is not available,
252 251 we clone the most recently used repo instance and return it.
253 252
254 253 It is currently possible for the stack to grow without bounds
255 254 if the server allows infinite threads. However, servers should
256 255 have a thread limit, thus establishing our limit.
257 256 """
258 257 if self._repos:
259 258 cached = self._repos.pop()
260 259 r, created = cached.fetch()
261 260 else:
262 261 cached = self._lastrepo.copy()
263 262 r, created = cached.fetch()
264 263 if created:
265 264 r = self._webifyrepo(r)
266 265
267 266 self._lastrepo = cached
268 267 self.mtime = cached.mtime
269 268 try:
270 269 yield r
271 270 finally:
272 271 self._repos.append(cached)
273 272
274 273 def run(self):
275 274 """Start a server from CGI environment.
276 275
277 276 Modern servers should be using WSGI and should avoid this
278 277 method, if possible.
279 278 """
280 279 if not encoding.environ.get('GATEWAY_INTERFACE',
281 280 '').startswith("CGI/1."):
282 281 raise RuntimeError("This function is only intended to be "
283 282 "called while running as a CGI script.")
284 283 wsgicgi.launch(self)
285 284
286 285 def __call__(self, env, respond):
287 286 """Run the WSGI application.
288 287
289 288 This may be called by multiple threads.
290 289 """
291 290 req = requestmod.wsgirequest(env, respond)
292 291 return self.run_wsgi(req)
293 292
294 293 def run_wsgi(self, wsgireq):
295 294 """Internal method to run the WSGI application.
296 295
297 296 This is typically only called by Mercurial. External consumers
298 297 should be using instances of this class as the WSGI application.
299 298 """
300 299 with self._obtainrepo() as repo:
301 300 profile = repo.ui.configbool('profiling', 'enabled')
302 301 with profiling.profile(repo.ui, enabled=profile):
303 302 for r in self._runwsgi(wsgireq, repo):
304 303 yield r
305 304
306 305 def _runwsgi(self, wsgireq, repo):
307 306 req = wsgireq.req
308 307 res = wsgireq.res
309 308 rctx = requestcontext(self, repo)
310 309
311 310 # This state is global across all threads.
312 311 encoding.encoding = rctx.config('web', 'encoding')
313 312 rctx.repo.ui.environ = wsgireq.env
314 313
315 314 if rctx.csp:
316 315 # hgwebdir may have added CSP header. Since we generate our own,
317 316 # replace it.
318 317 wsgireq.headers = [h for h in wsgireq.headers
319 318 if h[0] != 'Content-Security-Policy']
320 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
321 320 res.headers['Content-Security-Policy'] = rctx.csp
322 321
323 322 handled = wireprotoserver.handlewsgirequest(
324 323 rctx, wsgireq, req, res, self.check_perm)
325 324 if handled:
326 325 return res.sendresponse()
327 326
328 327 if req.havepathinfo:
329 328 query = req.dispatchpath
330 329 else:
331 330 query = req.querystring.partition('&')[0].partition(';')[0]
332 331
333 332 # translate user-visible url structure to internal structure
334 333
335 334 args = query.split('/', 2)
336 335 if 'cmd' not in req.qsparams and args and args[0]:
337 336 cmd = args.pop(0)
338 337 style = cmd.rfind('-')
339 338 if style != -1:
340 339 req.qsparams['style'] = cmd[:style]
341 340 cmd = cmd[style + 1:]
342 341
343 342 # avoid accepting e.g. style parameter as command
344 343 if util.safehasattr(webcommands, cmd):
345 344 req.qsparams['cmd'] = cmd
346 345
347 346 if cmd == 'static':
348 347 req.qsparams['file'] = '/'.join(args)
349 348 else:
350 349 if args and args[0]:
351 350 node = args.pop(0).replace('%2F', '/')
352 351 req.qsparams['node'] = node
353 352 if args:
354 353 if 'file' in req.qsparams:
355 354 del req.qsparams['file']
356 355 for a in args:
357 356 req.qsparams.add('file', a)
358 357
359 358 ua = req.headers.get('User-Agent', '')
360 359 if cmd == 'rev' and 'mercurial' in ua:
361 360 req.qsparams['style'] = 'raw'
362 361
363 362 if cmd == 'archive':
364 363 fn = req.qsparams['node']
365 364 for type_, spec in rctx.archivespecs.iteritems():
366 365 ext = spec[2]
367 366 if fn.endswith(ext):
368 367 req.qsparams['node'] = fn[:-len(ext)]
369 368 req.qsparams['type'] = type_
370 369 else:
371 370 cmd = req.qsparams.get('cmd', '')
372 371
373 372 # process the web interface request
374 373
375 374 try:
376 375 tmpl = rctx.templater(req)
377 376 ctype = tmpl('mimetype', encoding=encoding.encoding)
378 377 ctype = templater.stringify(ctype)
379 378
380 379 # check read permissions non-static content
381 380 if cmd != 'static':
382 381 self.check_perm(rctx, wsgireq, None)
383 382
384 383 if cmd == '':
385 384 req.qsparams['cmd'] = tmpl.cache['default']
386 385 cmd = req.qsparams['cmd']
387 386
388 387 # Don't enable caching if using a CSP nonce because then it wouldn't
389 388 # be a nonce.
390 389 if rctx.configbool('web', 'cache') and not rctx.nonce:
391 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
390 tag = 'W/"%d"' % self.mtime
391 if req.headers.get('If-None-Match') == tag:
392 raise ErrorResponse(HTTP_NOT_MODIFIED)
393
394 wsgireq.headers.append((r'ETag', pycompat.sysstr(tag)))
395 res.headers['ETag'] = tag
396
392 397 if cmd not in webcommands.__all__:
393 398 msg = 'no such method: %s' % cmd
394 399 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
395 400 elif cmd == 'file' and req.qsparams.get('style') == 'raw':
396 401 rctx.ctype = ctype
397 402 content = webcommands.rawfile(rctx, wsgireq, tmpl)
398 403 else:
399 404 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
400 405 wsgireq.respond(HTTP_OK, ctype)
401 406
402 407 return content
403 408
404 409 except (error.LookupError, error.RepoLookupError) as err:
405 410 wsgireq.respond(HTTP_NOT_FOUND, ctype)
406 411 msg = pycompat.bytestr(err)
407 412 if (util.safehasattr(err, 'name') and
408 413 not isinstance(err, error.ManifestLookupError)):
409 414 msg = 'revision not found: %s' % err.name
410 415 return tmpl('error', error=msg)
411 416 except (error.RepoError, error.RevlogError) as inst:
412 417 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
413 418 return tmpl('error', error=pycompat.bytestr(inst))
414 419 except ErrorResponse as inst:
415 420 wsgireq.respond(inst, ctype)
416 421 if inst.code == HTTP_NOT_MODIFIED:
417 422 # Not allowed to return a body on a 304
418 423 return ['']
419 424 return tmpl('error', error=pycompat.bytestr(inst))
420 425
421 426 def check_perm(self, rctx, req, op):
422 427 for permhook in permhooks:
423 428 permhook(rctx, req, op)
424 429
425 430 def getwebview(repo):
426 431 """The 'web.view' config controls changeset filter to hgweb. Possible
427 432 values are ``served``, ``visible`` and ``all``. Default is ``served``.
428 433 The ``served`` filter only shows changesets that can be pulled from the
429 434 hgweb instance. The``visible`` filter includes secret changesets but
430 435 still excludes "hidden" one.
431 436
432 437 See the repoview module for details.
433 438
434 439 The option has been around undocumented since Mercurial 2.5, but no
435 440 user ever asked about it. So we better keep it undocumented for now."""
436 441 # experimental config: web.view
437 442 viewconfig = repo.ui.config('web', 'view', untrusted=True)
438 443 if viewconfig == 'all':
439 444 return repo.unfiltered()
440 445 elif viewconfig in repoview.filtertable:
441 446 return repo.filtered(viewconfig)
442 447 else:
443 448 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now