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