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