##// END OF EJS Templates
hgweb: send HTTP unauthorized error when denying pull
Mark Edgington -
r7563:bbcd2dea default
parent child Browse files
Show More
@@ -1,315 +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 read permissions non-static content
168 # check read permissions non-static content
169 if cmd != 'static':
169 if cmd != 'static':
170 self.check_perm(req, None)
170 self.check_perm(req, None)
171
171
172 if cmd == '':
172 if cmd == '':
173 req.form['cmd'] = [tmpl.cache['default']]
173 req.form['cmd'] = [tmpl.cache['default']]
174 cmd = req.form['cmd'][0]
174 cmd = req.form['cmd'][0]
175
175
176 if cmd not in webcommands.__all__:
176 if cmd not in webcommands.__all__:
177 msg = 'no such method: %s' % cmd
177 msg = 'no such method: %s' % cmd
178 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
179 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 elif cmd == 'file' and 'raw' in req.form.get('style', []):
180 self.ctype = ctype
180 self.ctype = ctype
181 content = webcommands.rawfile(self, req, tmpl)
181 content = webcommands.rawfile(self, req, tmpl)
182 else:
182 else:
183 content = getattr(webcommands, cmd)(self, req, tmpl)
183 content = getattr(webcommands, cmd)(self, req, tmpl)
184 req.respond(HTTP_OK, ctype)
184 req.respond(HTTP_OK, ctype)
185
185
186 return content
186 return content
187
187
188 except revlog.LookupError, err:
188 except revlog.LookupError, err:
189 req.respond(HTTP_NOT_FOUND, ctype)
189 req.respond(HTTP_NOT_FOUND, ctype)
190 msg = str(err)
190 msg = str(err)
191 if 'manifest' not in msg:
191 if 'manifest' not in msg:
192 msg = 'revision not found: %s' % err.name
192 msg = 'revision not found: %s' % err.name
193 return tmpl('error', error=msg)
193 return tmpl('error', error=msg)
194 except (RepoError, revlog.RevlogError), inst:
194 except (RepoError, revlog.RevlogError), inst:
195 req.respond(HTTP_SERVER_ERROR, ctype)
195 req.respond(HTTP_SERVER_ERROR, ctype)
196 return tmpl('error', error=str(inst))
196 return tmpl('error', error=str(inst))
197 except ErrorResponse, inst:
197 except ErrorResponse, inst:
198 req.respond(inst.code, ctype)
198 req.respond(inst.code, ctype)
199 return tmpl('error', error=inst.message)
199 return tmpl('error', error=inst.message)
200
200
201 def templater(self, req):
201 def templater(self, req):
202
202
203 # determine scheme, port and server name
203 # determine scheme, port and server name
204 # this is needed to create absolute urls
204 # this is needed to create absolute urls
205
205
206 proto = req.env.get('wsgi.url_scheme')
206 proto = req.env.get('wsgi.url_scheme')
207 if proto == 'https':
207 if proto == 'https':
208 proto = 'https'
208 proto = 'https'
209 default_port = "443"
209 default_port = "443"
210 else:
210 else:
211 proto = 'http'
211 proto = 'http'
212 default_port = "80"
212 default_port = "80"
213
213
214 port = req.env["SERVER_PORT"]
214 port = req.env["SERVER_PORT"]
215 port = port != default_port and (":" + port) or ""
215 port = port != default_port and (":" + port) or ""
216 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
217 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 staticurl = self.config("web", "staticurl") or req.url + 'static/'
218 if not staticurl.endswith('/'):
218 if not staticurl.endswith('/'):
219 staticurl += '/'
219 staticurl += '/'
220
220
221 # some functions for the templater
221 # some functions for the templater
222
222
223 def header(**map):
223 def header(**map):
224 yield tmpl('header', encoding=self.encoding, **map)
224 yield tmpl('header', encoding=self.encoding, **map)
225
225
226 def footer(**map):
226 def footer(**map):
227 yield tmpl("footer", **map)
227 yield tmpl("footer", **map)
228
228
229 def motd(**map):
229 def motd(**map):
230 yield self.config("web", "motd", "")
230 yield self.config("web", "motd", "")
231
231
232 # figure out which style to use
232 # figure out which style to use
233
233
234 vars = {}
234 vars = {}
235 style = self.config("web", "style", "paper")
235 style = self.config("web", "style", "paper")
236 if 'style' in req.form:
236 if 'style' in req.form:
237 style = req.form['style'][0]
237 style = req.form['style'][0]
238 vars['style'] = style
238 vars['style'] = style
239
239
240 start = req.url[-1] == '?' and '&' or '?'
240 start = req.url[-1] == '?' and '&' or '?'
241 sessionvars = webutil.sessionvars(vars, start)
241 sessionvars = webutil.sessionvars(vars, start)
242 mapfile = style_map(self.templatepath, style)
242 mapfile = style_map(self.templatepath, style)
243
243
244 if not self.reponame:
244 if not self.reponame:
245 self.reponame = (self.config("web", "name")
245 self.reponame = (self.config("web", "name")
246 or req.env.get('REPO_NAME')
246 or req.env.get('REPO_NAME')
247 or req.url.strip('/') or self.repo.root)
247 or req.url.strip('/') or self.repo.root)
248
248
249 # create the templater
249 # create the templater
250
250
251 tmpl = templater.templater(mapfile, templatefilters.filters,
251 tmpl = templater.templater(mapfile, templatefilters.filters,
252 defaults={"url": req.url,
252 defaults={"url": req.url,
253 "staticurl": staticurl,
253 "staticurl": staticurl,
254 "urlbase": urlbase,
254 "urlbase": urlbase,
255 "repo": self.reponame,
255 "repo": self.reponame,
256 "header": header,
256 "header": header,
257 "footer": footer,
257 "footer": footer,
258 "motd": motd,
258 "motd": motd,
259 "sessionvars": sessionvars
259 "sessionvars": sessionvars
260 })
260 })
261 return tmpl
261 return tmpl
262
262
263 def archivelist(self, nodeid):
263 def archivelist(self, nodeid):
264 allowed = self.configlist("web", "allow_archive")
264 allowed = self.configlist("web", "allow_archive")
265 for i, spec in self.archive_specs.iteritems():
265 for i, spec in self.archive_specs.iteritems():
266 if i in allowed or self.configbool("web", "allow" + i):
266 if i in allowed or self.configbool("web", "allow" + i):
267 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
267 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
268
268
269 archive_specs = {
269 archive_specs = {
270 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
270 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
271 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
271 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
272 'zip': ('application/zip', 'zip', '.zip', None),
272 'zip': ('application/zip', 'zip', '.zip', None),
273 }
273 }
274
274
275 def check_perm(self, req, op):
275 def check_perm(self, req, op):
276 '''Check permission for operation based on request data (including
276 '''Check permission for operation based on request data (including
277 authentication info). Return if op allowed, else raise an ErrorResponse
277 authentication info). Return if op allowed, else raise an ErrorResponse
278 exception.'''
278 exception.'''
279
279
280 user = req.env.get('REMOTE_USER')
280 user = req.env.get('REMOTE_USER')
281
281
282 deny_read = self.configlist('web', 'deny_read')
282 deny_read = self.configlist('web', 'deny_read')
283 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):
284 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
284 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
285
285
286 allow_read = self.configlist('web', 'allow_read')
286 allow_read = self.configlist('web', 'allow_read')
287 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)
288 if not result:
288 if not result:
289 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
289 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
290
290
291 if op == 'pull' and not self.allowpull:
291 if op == 'pull' and not self.allowpull:
292 raise ErrorResponse(HTTP_OK, '')
292 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
293 # 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
294 elif op == 'pull' or op is None:
294 elif op == 'pull' or op is None:
295 return
295 return
296
296
297 # enforce that you can only push using POST requests
297 # enforce that you can only push using POST requests
298 if req.env['REQUEST_METHOD'] != 'POST':
298 if req.env['REQUEST_METHOD'] != 'POST':
299 msg = 'push requires POST request'
299 msg = 'push requires POST request'
300 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
300 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
301
301
302 # require ssl by default for pushing, auth info cannot be sniffed
302 # require ssl by default for pushing, auth info cannot be sniffed
303 # and replayed
303 # and replayed
304 scheme = req.env.get('wsgi.url_scheme')
304 scheme = req.env.get('wsgi.url_scheme')
305 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
305 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
306 raise ErrorResponse(HTTP_OK, 'ssl required')
306 raise ErrorResponse(HTTP_OK, 'ssl required')
307
307
308 deny = self.configlist('web', 'deny_push')
308 deny = self.configlist('web', 'deny_push')
309 if deny and (not user or deny == ['*'] or user in deny):
309 if deny and (not user or deny == ['*'] or user in deny):
310 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
310 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
311
311
312 allow = self.configlist('web', 'allow_push')
312 allow = self.configlist('web', 'allow_push')
313 result = allow and (allow == ['*'] or user in allow)
313 result = allow and (allow == ['*'] or user in allow)
314 if not result:
314 if not result:
315 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
315 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
@@ -1,12 +1,12 b''
1 adding a
1 adding a
2 updating working directory
2 updating working directory
3 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
3 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
4 % expect error, cloning not allowed
4 % expect error, cloning not allowed
5 abort: error:
5 abort: authorization failed
6 requesting all changes
6 requesting all changes
7 % serve errors
7 % serve errors
8 % expect error, pulling not allowed
8 % expect error, pulling not allowed
9 abort: error:
9 abort: authorization failed
10 pulling from http://localhost/
10 pulling from http://localhost/
11 searching for changes
11 searching for changes
12 % serve errors
12 % serve errors
General Comments 0
You need to be logged in to leave comments. Login now