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