##// END OF EJS Templates
hgweb: allow static content when deny_read denies access
Mark Edgington -
r7562:b663b556 default
parent child Browse files
Show More
@@ -1,314 +1,315 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes
9 import os, mimetypes
10 from mercurial.node import hex, nullid
10 from mercurial.node import hex, nullid
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import ui, hg, util, hook
12 from mercurial import ui, hg, util, hook
13 from mercurial import revlog, templater, templatefilters
13 from mercurial import revlog, templater, templatefilters
14 from common import get_mtime, style_map, ErrorResponse
14 from common import get_mtime, style_map, ErrorResponse
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
16 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
17 from request import wsgirequest
17 from request import wsgirequest
18 import webcommands, protocol, webutil
18 import webcommands, protocol, webutil
19
19
20 perms = {
20 perms = {
21 'changegroup': 'pull',
21 'changegroup': 'pull',
22 'changegroupsubset': 'pull',
22 'changegroupsubset': 'pull',
23 'unbundle': 'push',
23 'unbundle': 'push',
24 'stream_out': 'pull',
24 'stream_out': 'pull',
25 }
25 }
26
26
27 class hgweb(object):
27 class hgweb(object):
28 def __init__(self, repo, name=None):
28 def __init__(self, repo, name=None):
29 if isinstance(repo, str):
29 if isinstance(repo, str):
30 parentui = ui.ui(report_untrusted=False, interactive=False)
30 parentui = ui.ui(report_untrusted=False, interactive=False)
31 self.repo = hg.repository(parentui, repo)
31 self.repo = hg.repository(parentui, repo)
32 else:
32 else:
33 self.repo = repo
33 self.repo = repo
34
34
35 hook.redirect(True)
35 hook.redirect(True)
36 self.mtime = -1
36 self.mtime = -1
37 self.reponame = name
37 self.reponame = name
38 self.archives = 'zip', 'gz', 'bz2'
38 self.archives = 'zip', 'gz', 'bz2'
39 self.stripecount = 1
39 self.stripecount = 1
40 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # a repo owner may set web.templates in .hg/hgrc to get any file
41 # readable by the user running the CGI script
41 # readable by the user running the CGI script
42 self.templatepath = self.config("web", "templates",
42 self.templatepath = self.config("web", "templates",
43 templater.templatepath(),
43 templater.templatepath(),
44 untrusted=False)
44 untrusted=False)
45
45
46 # The CGI scripts are often run by a user different from the repo owner.
46 # The CGI scripts are often run by a user different from the repo owner.
47 # Trust the settings from the .hg/hgrc files by default.
47 # Trust the settings from the .hg/hgrc files by default.
48 def config(self, section, name, default=None, untrusted=True):
48 def config(self, section, name, default=None, untrusted=True):
49 return self.repo.ui.config(section, name, default,
49 return self.repo.ui.config(section, name, default,
50 untrusted=untrusted)
50 untrusted=untrusted)
51
51
52 def configbool(self, section, name, default=False, untrusted=True):
52 def configbool(self, section, name, default=False, untrusted=True):
53 return self.repo.ui.configbool(section, name, default,
53 return self.repo.ui.configbool(section, name, default,
54 untrusted=untrusted)
54 untrusted=untrusted)
55
55
56 def configlist(self, section, name, default=None, untrusted=True):
56 def configlist(self, section, name, default=None, untrusted=True):
57 return self.repo.ui.configlist(section, name, default,
57 return self.repo.ui.configlist(section, name, default,
58 untrusted=untrusted)
58 untrusted=untrusted)
59
59
60 def refresh(self):
60 def refresh(self):
61 mtime = get_mtime(self.repo.root)
61 mtime = get_mtime(self.repo.root)
62 if mtime != self.mtime:
62 if mtime != self.mtime:
63 self.mtime = mtime
63 self.mtime = mtime
64 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.repo = hg.repository(self.repo.ui, self.repo.root)
65 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.maxchanges = int(self.config("web", "maxchanges", 10))
66 self.stripecount = int(self.config("web", "stripes", 1))
66 self.stripecount = int(self.config("web", "stripes", 1))
67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
68 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.maxfiles = int(self.config("web", "maxfiles", 10))
69 self.allowpull = self.configbool("web", "allowpull", True)
69 self.allowpull = self.configbool("web", "allowpull", True)
70 self.encoding = self.config("web", "encoding", util._encoding)
70 self.encoding = self.config("web", "encoding", util._encoding)
71
71
72 def run(self):
72 def run(self):
73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
74 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
74 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
75 import mercurial.hgweb.wsgicgi as wsgicgi
75 import mercurial.hgweb.wsgicgi as wsgicgi
76 wsgicgi.launch(self)
76 wsgicgi.launch(self)
77
77
78 def __call__(self, env, respond):
78 def __call__(self, env, respond):
79 req = wsgirequest(env, respond)
79 req = wsgirequest(env, respond)
80 return self.run_wsgi(req)
80 return self.run_wsgi(req)
81
81
82 def run_wsgi(self, req):
82 def run_wsgi(self, req):
83
83
84 self.refresh()
84 self.refresh()
85
85
86 # process this if it's a protocol request
86 # process this if it's a protocol request
87 # protocol bits don't need to create any URLs
87 # protocol bits don't need to create any URLs
88 # and the clients always use the old URL structure
88 # and the clients always use the old URL structure
89
89
90 cmd = req.form.get('cmd', [''])[0]
90 cmd = req.form.get('cmd', [''])[0]
91 if cmd and cmd in protocol.__all__:
91 if cmd and cmd in protocol.__all__:
92 try:
92 try:
93 if cmd in perms:
93 if cmd in perms:
94 try:
94 try:
95 self.check_perm(req, perms[cmd])
95 self.check_perm(req, perms[cmd])
96 except ErrorResponse, inst:
96 except ErrorResponse, inst:
97 if cmd == 'unbundle':
97 if cmd == 'unbundle':
98 req.drain()
98 req.drain()
99 raise
99 raise
100 method = getattr(protocol, cmd)
100 method = getattr(protocol, cmd)
101 return method(self.repo, req)
101 return method(self.repo, req)
102 except ErrorResponse, inst:
102 except ErrorResponse, inst:
103 req.respond(inst.code, protocol.HGTYPE)
103 req.respond(inst.code, protocol.HGTYPE)
104 if not inst.message:
104 if not inst.message:
105 return []
105 return []
106 return '0\n%s\n' % inst.message,
106 return '0\n%s\n' % inst.message,
107
107
108 # work with CGI variables to create coherent structure
108 # work with CGI variables to create coherent structure
109 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
109 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
110
110
111 req.url = req.env['SCRIPT_NAME']
111 req.url = req.env['SCRIPT_NAME']
112 if not req.url.endswith('/'):
112 if not req.url.endswith('/'):
113 req.url += '/'
113 req.url += '/'
114 if 'REPO_NAME' in req.env:
114 if 'REPO_NAME' in req.env:
115 req.url += req.env['REPO_NAME'] + '/'
115 req.url += req.env['REPO_NAME'] + '/'
116
116
117 if 'PATH_INFO' in req.env:
117 if 'PATH_INFO' in req.env:
118 parts = req.env['PATH_INFO'].strip('/').split('/')
118 parts = req.env['PATH_INFO'].strip('/').split('/')
119 repo_parts = req.env.get('REPO_NAME', '').split('/')
119 repo_parts = req.env.get('REPO_NAME', '').split('/')
120 if parts[:len(repo_parts)] == repo_parts:
120 if parts[:len(repo_parts)] == repo_parts:
121 parts = parts[len(repo_parts):]
121 parts = parts[len(repo_parts):]
122 query = '/'.join(parts)
122 query = '/'.join(parts)
123 else:
123 else:
124 query = req.env['QUERY_STRING'].split('&', 1)[0]
124 query = req.env['QUERY_STRING'].split('&', 1)[0]
125 query = query.split(';', 1)[0]
125 query = query.split(';', 1)[0]
126
126
127 # translate user-visible url structure to internal structure
127 # translate user-visible url structure to internal structure
128
128
129 args = query.split('/', 2)
129 args = query.split('/', 2)
130 if 'cmd' not in req.form and args and args[0]:
130 if 'cmd' not in req.form and args and args[0]:
131
131
132 cmd = args.pop(0)
132 cmd = args.pop(0)
133 style = cmd.rfind('-')
133 style = cmd.rfind('-')
134 if style != -1:
134 if style != -1:
135 req.form['style'] = [cmd[:style]]
135 req.form['style'] = [cmd[:style]]
136 cmd = cmd[style+1:]
136 cmd = cmd[style+1:]
137
137
138 # avoid accepting e.g. style parameter as command
138 # avoid accepting e.g. style parameter as command
139 if hasattr(webcommands, cmd):
139 if hasattr(webcommands, cmd):
140 req.form['cmd'] = [cmd]
140 req.form['cmd'] = [cmd]
141 else:
141 else:
142 cmd = ''
142 cmd = ''
143
143
144 if cmd == 'static':
144 if cmd == 'static':
145 req.form['file'] = ['/'.join(args)]
145 req.form['file'] = ['/'.join(args)]
146 else:
146 else:
147 if args and args[0]:
147 if args and args[0]:
148 node = args.pop(0)
148 node = args.pop(0)
149 req.form['node'] = [node]
149 req.form['node'] = [node]
150 if args:
150 if args:
151 req.form['file'] = args
151 req.form['file'] = args
152
152
153 if cmd == 'archive':
153 if cmd == 'archive':
154 fn = req.form['node'][0]
154 fn = req.form['node'][0]
155 for type_, spec in self.archive_specs.iteritems():
155 for type_, spec in self.archive_specs.iteritems():
156 ext = spec[2]
156 ext = spec[2]
157 if fn.endswith(ext):
157 if fn.endswith(ext):
158 req.form['node'] = [fn[:-len(ext)]]
158 req.form['node'] = [fn[:-len(ext)]]
159 req.form['type'] = [type_]
159 req.form['type'] = [type_]
160
160
161 # process the web interface request
161 # process the web interface request
162
162
163 try:
163 try:
164 tmpl = self.templater(req)
164 tmpl = self.templater(req)
165 ctype = tmpl('mimetype', encoding=self.encoding)
165 ctype = tmpl('mimetype', encoding=self.encoding)
166 ctype = templater.stringify(ctype)
166 ctype = templater.stringify(ctype)
167
167
168 # check allow_read / deny_read config options
168 # check read permissions non-static content
169 self.check_perm(req, None)
169 if cmd != 'static':
170 self.check_perm(req, None)
170
171
171 if cmd == '':
172 if cmd == '':
172 req.form['cmd'] = [tmpl.cache['default']]
173 req.form['cmd'] = [tmpl.cache['default']]
173 cmd = req.form['cmd'][0]
174 cmd = req.form['cmd'][0]
174
175
175 if cmd not in webcommands.__all__:
176 if cmd not in webcommands.__all__:
176 msg = 'no such method: %s' % cmd
177 msg = 'no such method: %s' % cmd
177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 self.ctype = ctype
180 self.ctype = ctype
180 content = webcommands.rawfile(self, req, tmpl)
181 content = webcommands.rawfile(self, req, tmpl)
181 else:
182 else:
182 content = getattr(webcommands, cmd)(self, req, tmpl)
183 content = getattr(webcommands, cmd)(self, req, tmpl)
183 req.respond(HTTP_OK, ctype)
184 req.respond(HTTP_OK, ctype)
184
185
185 return content
186 return content
186
187
187 except revlog.LookupError, err:
188 except revlog.LookupError, err:
188 req.respond(HTTP_NOT_FOUND, ctype)
189 req.respond(HTTP_NOT_FOUND, ctype)
189 msg = str(err)
190 msg = str(err)
190 if 'manifest' not in msg:
191 if 'manifest' not in msg:
191 msg = 'revision not found: %s' % err.name
192 msg = 'revision not found: %s' % err.name
192 return tmpl('error', error=msg)
193 return tmpl('error', error=msg)
193 except (RepoError, revlog.RevlogError), inst:
194 except (RepoError, revlog.RevlogError), inst:
194 req.respond(HTTP_SERVER_ERROR, ctype)
195 req.respond(HTTP_SERVER_ERROR, ctype)
195 return tmpl('error', error=str(inst))
196 return tmpl('error', error=str(inst))
196 except ErrorResponse, inst:
197 except ErrorResponse, inst:
197 req.respond(inst.code, ctype)
198 req.respond(inst.code, ctype)
198 return tmpl('error', error=inst.message)
199 return tmpl('error', error=inst.message)
199
200
200 def templater(self, req):
201 def templater(self, req):
201
202
202 # determine scheme, port and server name
203 # determine scheme, port and server name
203 # this is needed to create absolute urls
204 # this is needed to create absolute urls
204
205
205 proto = req.env.get('wsgi.url_scheme')
206 proto = req.env.get('wsgi.url_scheme')
206 if proto == 'https':
207 if proto == 'https':
207 proto = 'https'
208 proto = 'https'
208 default_port = "443"
209 default_port = "443"
209 else:
210 else:
210 proto = 'http'
211 proto = 'http'
211 default_port = "80"
212 default_port = "80"
212
213
213 port = req.env["SERVER_PORT"]
214 port = req.env["SERVER_PORT"]
214 port = port != default_port and (":" + port) or ""
215 port = port != default_port and (":" + port) or ""
215 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 if not staticurl.endswith('/'):
218 if not staticurl.endswith('/'):
218 staticurl += '/'
219 staticurl += '/'
219
220
220 # some functions for the templater
221 # some functions for the templater
221
222
222 def header(**map):
223 def header(**map):
223 yield tmpl('header', encoding=self.encoding, **map)
224 yield tmpl('header', encoding=self.encoding, **map)
224
225
225 def footer(**map):
226 def footer(**map):
226 yield tmpl("footer", **map)
227 yield tmpl("footer", **map)
227
228
228 def motd(**map):
229 def motd(**map):
229 yield self.config("web", "motd", "")
230 yield self.config("web", "motd", "")
230
231
231 # figure out which style to use
232 # figure out which style to use
232
233
233 vars = {}
234 vars = {}
234 style = self.config("web", "style", "paper")
235 style = self.config("web", "style", "paper")
235 if 'style' in req.form:
236 if 'style' in req.form:
236 style = req.form['style'][0]
237 style = req.form['style'][0]
237 vars['style'] = style
238 vars['style'] = style
238
239
239 start = req.url[-1] == '?' and '&' or '?'
240 start = req.url[-1] == '?' and '&' or '?'
240 sessionvars = webutil.sessionvars(vars, start)
241 sessionvars = webutil.sessionvars(vars, start)
241 mapfile = style_map(self.templatepath, style)
242 mapfile = style_map(self.templatepath, style)
242
243
243 if not self.reponame:
244 if not self.reponame:
244 self.reponame = (self.config("web", "name")
245 self.reponame = (self.config("web", "name")
245 or req.env.get('REPO_NAME')
246 or req.env.get('REPO_NAME')
246 or req.url.strip('/') or self.repo.root)
247 or req.url.strip('/') or self.repo.root)
247
248
248 # create the templater
249 # create the templater
249
250
250 tmpl = templater.templater(mapfile, templatefilters.filters,
251 tmpl = templater.templater(mapfile, templatefilters.filters,
251 defaults={"url": req.url,
252 defaults={"url": req.url,
252 "staticurl": staticurl,
253 "staticurl": staticurl,
253 "urlbase": urlbase,
254 "urlbase": urlbase,
254 "repo": self.reponame,
255 "repo": self.reponame,
255 "header": header,
256 "header": header,
256 "footer": footer,
257 "footer": footer,
257 "motd": motd,
258 "motd": motd,
258 "sessionvars": sessionvars
259 "sessionvars": sessionvars
259 })
260 })
260 return tmpl
261 return tmpl
261
262
262 def archivelist(self, nodeid):
263 def archivelist(self, nodeid):
263 allowed = self.configlist("web", "allow_archive")
264 allowed = self.configlist("web", "allow_archive")
264 for i, spec in self.archive_specs.iteritems():
265 for i, spec in self.archive_specs.iteritems():
265 if i in allowed or self.configbool("web", "allow" + i):
266 if i in allowed or self.configbool("web", "allow" + i):
266 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
267 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
267
268
268 archive_specs = {
269 archive_specs = {
269 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
270 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
270 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
271 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
271 'zip': ('application/zip', 'zip', '.zip', None),
272 'zip': ('application/zip', 'zip', '.zip', None),
272 }
273 }
273
274
274 def check_perm(self, req, op):
275 def check_perm(self, req, op):
275 '''Check permission for operation based on request data (including
276 '''Check permission for operation based on request data (including
276 authentication info). Return if op allowed, else raise an ErrorResponse
277 authentication info). Return if op allowed, else raise an ErrorResponse
277 exception.'''
278 exception.'''
278
279
279 user = req.env.get('REMOTE_USER')
280 user = req.env.get('REMOTE_USER')
280
281
281 deny_read = self.configlist('web', 'deny_read')
282 deny_read = self.configlist('web', 'deny_read')
282 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
283 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
283 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
284 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
284
285
285 allow_read = self.configlist('web', 'allow_read')
286 allow_read = self.configlist('web', 'allow_read')
286 result = (not allow_read) or (allow_read == ['*']) or (user in allow_read)
287 result = (not allow_read) or (allow_read == ['*']) or (user in allow_read)
287 if not result:
288 if not result:
288 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
289 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
289
290
290 if op == 'pull' and not self.allowpull:
291 if op == 'pull' and not self.allowpull:
291 raise ErrorResponse(HTTP_OK, '')
292 raise ErrorResponse(HTTP_OK, '')
292 # op is None when checking allow/deny_read permissions for a web-browser request
293 # op is None when checking allow/deny_read permissions for a web-browser request
293 elif op == 'pull' or op is None:
294 elif op == 'pull' or op is None:
294 return
295 return
295
296
296 # enforce that you can only push using POST requests
297 # enforce that you can only push using POST requests
297 if req.env['REQUEST_METHOD'] != 'POST':
298 if req.env['REQUEST_METHOD'] != 'POST':
298 msg = 'push requires POST request'
299 msg = 'push requires POST request'
299 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
300 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
300
301
301 # require ssl by default for pushing, auth info cannot be sniffed
302 # require ssl by default for pushing, auth info cannot be sniffed
302 # and replayed
303 # and replayed
303 scheme = req.env.get('wsgi.url_scheme')
304 scheme = req.env.get('wsgi.url_scheme')
304 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
305 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
305 raise ErrorResponse(HTTP_OK, 'ssl required')
306 raise ErrorResponse(HTTP_OK, 'ssl required')
306
307
307 deny = self.configlist('web', 'deny_push')
308 deny = self.configlist('web', 'deny_push')
308 if deny and (not user or deny == ['*'] or user in deny):
309 if deny and (not user or deny == ['*'] or user in deny):
309 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
310 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
310
311
311 allow = self.configlist('web', 'allow_push')
312 allow = self.configlist('web', 'allow_push')
312 result = allow and (allow == ['*'] or user in allow)
313 result = allow and (allow == ['*'] or user in allow)
313 if not result:
314 if not result:
314 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
315 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
General Comments 0
You need to be logged in to leave comments. Login now