##// END OF EJS Templates
hgweb: centralize permission checks for protocol commands...
Dirkjan Ochtman -
r6779:d3147b4e default
parent child Browse files
Show More
@@ -1,358 +1,390 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 mdiff, ui, hg, util, patch, hook
12 from mercurial import mdiff, ui, hg, util, patch, hook
13 from mercurial import revlog, templater, templatefilters, changegroup
13 from mercurial import revlog, templater, templatefilters, changegroup
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
14 from common import get_mtime, style_map, paritygen, countgen, 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 request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
22 'unbundle': 'push',
23 'stream_out': 'pull',
24 }
25
19 class hgweb(object):
26 class hgweb(object):
20 def __init__(self, repo, name=None):
27 def __init__(self, repo, name=None):
21 if isinstance(repo, str):
28 if isinstance(repo, str):
22 parentui = ui.ui(report_untrusted=False, interactive=False)
29 parentui = ui.ui(report_untrusted=False, interactive=False)
23 self.repo = hg.repository(parentui, repo)
30 self.repo = hg.repository(parentui, repo)
24 else:
31 else:
25 self.repo = repo
32 self.repo = repo
26
33
27 hook.redirect(True)
34 hook.redirect(True)
28 self.mtime = -1
35 self.mtime = -1
29 self.reponame = name
36 self.reponame = name
30 self.archives = 'zip', 'gz', 'bz2'
37 self.archives = 'zip', 'gz', 'bz2'
31 self.stripecount = 1
38 self.stripecount = 1
32 self._capabilities = None
39 self._capabilities = None
33 # 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
34 # readable by the user running the CGI script
41 # readable by the user running the CGI script
35 self.templatepath = self.config("web", "templates",
42 self.templatepath = self.config("web", "templates",
36 templater.templatepath(),
43 templater.templatepath(),
37 untrusted=False)
44 untrusted=False)
38
45
39 # 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.
40 # Trust the settings from the .hg/hgrc files by default.
47 # Trust the settings from the .hg/hgrc files by default.
41 def config(self, section, name, default=None, untrusted=True):
48 def config(self, section, name, default=None, untrusted=True):
42 return self.repo.ui.config(section, name, default,
49 return self.repo.ui.config(section, name, default,
43 untrusted=untrusted)
50 untrusted=untrusted)
44
51
45 def configbool(self, section, name, default=False, untrusted=True):
52 def configbool(self, section, name, default=False, untrusted=True):
46 return self.repo.ui.configbool(section, name, default,
53 return self.repo.ui.configbool(section, name, default,
47 untrusted=untrusted)
54 untrusted=untrusted)
48
55
49 def configlist(self, section, name, default=None, untrusted=True):
56 def configlist(self, section, name, default=None, untrusted=True):
50 return self.repo.ui.configlist(section, name, default,
57 return self.repo.ui.configlist(section, name, default,
51 untrusted=untrusted)
58 untrusted=untrusted)
52
59
53 def refresh(self):
60 def refresh(self):
54 mtime = get_mtime(self.repo.root)
61 mtime = get_mtime(self.repo.root)
55 if mtime != self.mtime:
62 if mtime != self.mtime:
56 self.mtime = mtime
63 self.mtime = mtime
57 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.repo = hg.repository(self.repo.ui, self.repo.root)
58 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.maxchanges = int(self.config("web", "maxchanges", 10))
59 self.stripecount = int(self.config("web", "stripes", 1))
66 self.stripecount = int(self.config("web", "stripes", 1))
60 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
61 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.maxfiles = int(self.config("web", "maxfiles", 10))
62 self.allowpull = self.configbool("web", "allowpull", True)
69 self.allowpull = self.configbool("web", "allowpull", True)
63 self.encoding = self.config("web", "encoding", util._encoding)
70 self.encoding = self.config("web", "encoding", util._encoding)
64 self._capabilities = None
71 self._capabilities = None
65
72
66 def capabilities(self):
73 def capabilities(self):
67 if self._capabilities is not None:
74 if self._capabilities is not None:
68 return self._capabilities
75 return self._capabilities
69 caps = ['lookup', 'changegroupsubset']
76 caps = ['lookup', 'changegroupsubset']
70 if self.configbool('server', 'uncompressed'):
77 if self.configbool('server', 'uncompressed'):
71 caps.append('stream=%d' % self.repo.changelog.version)
78 caps.append('stream=%d' % self.repo.changelog.version)
72 if changegroup.bundlepriority:
79 if changegroup.bundlepriority:
73 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
80 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
74 self._capabilities = caps
81 self._capabilities = caps
75 return caps
82 return caps
76
83
77 def run(self):
84 def run(self):
78 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
85 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
79 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
86 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
80 import mercurial.hgweb.wsgicgi as wsgicgi
87 import mercurial.hgweb.wsgicgi as wsgicgi
81 wsgicgi.launch(self)
88 wsgicgi.launch(self)
82
89
83 def __call__(self, env, respond):
90 def __call__(self, env, respond):
84 req = wsgirequest(env, respond)
91 req = wsgirequest(env, respond)
85 self.run_wsgi(req)
92 self.run_wsgi(req)
86 return req
93 return req
87
94
88 def run_wsgi(self, req):
95 def run_wsgi(self, req):
89
96
90 self.refresh()
97 self.refresh()
91
98
92 # process this if it's a protocol request
99 # process this if it's a protocol request
93 # protocol bits don't need to create any URLs
100 # protocol bits don't need to create any URLs
94 # and the clients always use the old URL structure
101 # and the clients always use the old URL structure
95
102
96 cmd = req.form.get('cmd', [''])[0]
103 cmd = req.form.get('cmd', [''])[0]
97 if cmd and cmd in protocol.__all__:
104 if cmd and cmd in protocol.__all__:
105 if cmd in perms and not self.check_perm(req, perms[cmd]):
106 return
98 method = getattr(protocol, cmd)
107 method = getattr(protocol, cmd)
99 method(self, req)
108 method(self, req)
100 return
109 return
101
110
102 # work with CGI variables to create coherent structure
111 # work with CGI variables to create coherent structure
103 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
112 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
104
113
105 req.url = req.env['SCRIPT_NAME']
114 req.url = req.env['SCRIPT_NAME']
106 if not req.url.endswith('/'):
115 if not req.url.endswith('/'):
107 req.url += '/'
116 req.url += '/'
108 if 'REPO_NAME' in req.env:
117 if 'REPO_NAME' in req.env:
109 req.url += req.env['REPO_NAME'] + '/'
118 req.url += req.env['REPO_NAME'] + '/'
110
119
111 if 'PATH_INFO' in req.env:
120 if 'PATH_INFO' in req.env:
112 parts = req.env['PATH_INFO'].strip('/').split('/')
121 parts = req.env['PATH_INFO'].strip('/').split('/')
113 repo_parts = req.env.get('REPO_NAME', '').split('/')
122 repo_parts = req.env.get('REPO_NAME', '').split('/')
114 if parts[:len(repo_parts)] == repo_parts:
123 if parts[:len(repo_parts)] == repo_parts:
115 parts = parts[len(repo_parts):]
124 parts = parts[len(repo_parts):]
116 query = '/'.join(parts)
125 query = '/'.join(parts)
117 else:
126 else:
118 query = req.env['QUERY_STRING'].split('&', 1)[0]
127 query = req.env['QUERY_STRING'].split('&', 1)[0]
119 query = query.split(';', 1)[0]
128 query = query.split(';', 1)[0]
120
129
121 # translate user-visible url structure to internal structure
130 # translate user-visible url structure to internal structure
122
131
123 args = query.split('/', 2)
132 args = query.split('/', 2)
124 if 'cmd' not in req.form and args and args[0]:
133 if 'cmd' not in req.form and args and args[0]:
125
134
126 cmd = args.pop(0)
135 cmd = args.pop(0)
127 style = cmd.rfind('-')
136 style = cmd.rfind('-')
128 if style != -1:
137 if style != -1:
129 req.form['style'] = [cmd[:style]]
138 req.form['style'] = [cmd[:style]]
130 cmd = cmd[style+1:]
139 cmd = cmd[style+1:]
131
140
132 # avoid accepting e.g. style parameter as command
141 # avoid accepting e.g. style parameter as command
133 if hasattr(webcommands, cmd):
142 if hasattr(webcommands, cmd):
134 req.form['cmd'] = [cmd]
143 req.form['cmd'] = [cmd]
135 else:
144 else:
136 cmd = ''
145 cmd = ''
137
146
138 if args and args[0]:
147 if args and args[0]:
139 node = args.pop(0)
148 node = args.pop(0)
140 req.form['node'] = [node]
149 req.form['node'] = [node]
141 if args:
150 if args:
142 req.form['file'] = args
151 req.form['file'] = args
143
152
144 if cmd == 'static':
153 if cmd == 'static':
145 req.form['file'] = req.form['node']
154 req.form['file'] = req.form['node']
146 elif cmd == 'archive':
155 elif cmd == 'archive':
147 fn = req.form['node'][0]
156 fn = req.form['node'][0]
148 for type_, spec in self.archive_specs.iteritems():
157 for type_, spec in self.archive_specs.iteritems():
149 ext = spec[2]
158 ext = spec[2]
150 if fn.endswith(ext):
159 if fn.endswith(ext):
151 req.form['node'] = [fn[:-len(ext)]]
160 req.form['node'] = [fn[:-len(ext)]]
152 req.form['type'] = [type_]
161 req.form['type'] = [type_]
153
162
154 # process the web interface request
163 # process the web interface request
155
164
156 try:
165 try:
157
166
158 tmpl = self.templater(req)
167 tmpl = self.templater(req)
159 ctype = tmpl('mimetype', encoding=self.encoding)
168 ctype = tmpl('mimetype', encoding=self.encoding)
160 ctype = templater.stringify(ctype)
169 ctype = templater.stringify(ctype)
161
170
162 if cmd == '':
171 if cmd == '':
163 req.form['cmd'] = [tmpl.cache['default']]
172 req.form['cmd'] = [tmpl.cache['default']]
164 cmd = req.form['cmd'][0]
173 cmd = req.form['cmd'][0]
165
174
166 if cmd not in webcommands.__all__:
175 if cmd not in webcommands.__all__:
167 msg = 'no such method: %s' % cmd
176 msg = 'no such method: %s' % cmd
168 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
169 elif cmd == 'file' and 'raw' in req.form.get('style', []):
178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
170 self.ctype = ctype
179 self.ctype = ctype
171 content = webcommands.rawfile(self, req, tmpl)
180 content = webcommands.rawfile(self, req, tmpl)
172 else:
181 else:
173 content = getattr(webcommands, cmd)(self, req, tmpl)
182 content = getattr(webcommands, cmd)(self, req, tmpl)
174 req.respond(HTTP_OK, ctype)
183 req.respond(HTTP_OK, ctype)
175
184
176 req.write(content)
185 req.write(content)
177 del tmpl
186 del tmpl
178
187
179 except revlog.LookupError, err:
188 except revlog.LookupError, err:
180 req.respond(HTTP_NOT_FOUND, ctype)
189 req.respond(HTTP_NOT_FOUND, ctype)
181 msg = str(err)
190 msg = str(err)
182 if 'manifest' not in msg:
191 if 'manifest' not in msg:
183 msg = 'revision not found: %s' % err.name
192 msg = 'revision not found: %s' % err.name
184 req.write(tmpl('error', error=msg))
193 req.write(tmpl('error', error=msg))
185 except (RepoError, revlog.RevlogError), inst:
194 except (RepoError, revlog.RevlogError), inst:
186 req.respond(HTTP_SERVER_ERROR, ctype)
195 req.respond(HTTP_SERVER_ERROR, ctype)
187 req.write(tmpl('error', error=str(inst)))
196 req.write(tmpl('error', error=str(inst)))
188 except ErrorResponse, inst:
197 except ErrorResponse, inst:
189 req.respond(inst.code, ctype)
198 req.respond(inst.code, ctype)
190 req.write(tmpl('error', error=inst.message))
199 req.write(tmpl('error', error=inst.message))
191
200
192 def templater(self, req):
201 def templater(self, req):
193
202
194 # determine scheme, port and server name
203 # determine scheme, port and server name
195 # this is needed to create absolute urls
204 # this is needed to create absolute urls
196
205
197 proto = req.env.get('wsgi.url_scheme')
206 proto = req.env.get('wsgi.url_scheme')
198 if proto == 'https':
207 if proto == 'https':
199 proto = 'https'
208 proto = 'https'
200 default_port = "443"
209 default_port = "443"
201 else:
210 else:
202 proto = 'http'
211 proto = 'http'
203 default_port = "80"
212 default_port = "80"
204
213
205 port = req.env["SERVER_PORT"]
214 port = req.env["SERVER_PORT"]
206 port = port != default_port and (":" + port) or ""
215 port = port != default_port and (":" + port) or ""
207 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
208 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 staticurl = self.config("web", "staticurl") or req.url + 'static/'
209 if not staticurl.endswith('/'):
218 if not staticurl.endswith('/'):
210 staticurl += '/'
219 staticurl += '/'
211
220
212 # some functions for the templater
221 # some functions for the templater
213
222
214 def header(**map):
223 def header(**map):
215 yield tmpl('header', encoding=self.encoding, **map)
224 yield tmpl('header', encoding=self.encoding, **map)
216
225
217 def footer(**map):
226 def footer(**map):
218 yield tmpl("footer", **map)
227 yield tmpl("footer", **map)
219
228
220 def motd(**map):
229 def motd(**map):
221 yield self.config("web", "motd", "")
230 yield self.config("web", "motd", "")
222
231
223 def sessionvars(**map):
232 def sessionvars(**map):
224 fields = []
233 fields = []
225 if 'style' in req.form:
234 if 'style' in req.form:
226 style = req.form['style'][0]
235 style = req.form['style'][0]
227 if style != self.config('web', 'style', ''):
236 if style != self.config('web', 'style', ''):
228 fields.append(('style', style))
237 fields.append(('style', style))
229
238
230 separator = req.url[-1] == '?' and ';' or '?'
239 separator = req.url[-1] == '?' and ';' or '?'
231 for name, value in fields:
240 for name, value in fields:
232 yield dict(name=name, value=value, separator=separator)
241 yield dict(name=name, value=value, separator=separator)
233 separator = ';'
242 separator = ';'
234
243
235 # figure out which style to use
244 # figure out which style to use
236
245
237 style = self.config("web", "style", "")
246 style = self.config("web", "style", "")
238 if 'style' in req.form:
247 if 'style' in req.form:
239 style = req.form['style'][0]
248 style = req.form['style'][0]
240 mapfile = style_map(self.templatepath, style)
249 mapfile = style_map(self.templatepath, style)
241
250
242 if not self.reponame:
251 if not self.reponame:
243 self.reponame = (self.config("web", "name")
252 self.reponame = (self.config("web", "name")
244 or req.env.get('REPO_NAME')
253 or req.env.get('REPO_NAME')
245 or req.url.strip('/') or self.repo.root)
254 or req.url.strip('/') or self.repo.root)
246
255
247 # create the templater
256 # create the templater
248
257
249 tmpl = templater.templater(mapfile, templatefilters.filters,
258 tmpl = templater.templater(mapfile, templatefilters.filters,
250 defaults={"url": req.url,
259 defaults={"url": req.url,
251 "staticurl": staticurl,
260 "staticurl": staticurl,
252 "urlbase": urlbase,
261 "urlbase": urlbase,
253 "repo": self.reponame,
262 "repo": self.reponame,
254 "header": header,
263 "header": header,
255 "footer": footer,
264 "footer": footer,
256 "motd": motd,
265 "motd": motd,
257 "sessionvars": sessionvars
266 "sessionvars": sessionvars
258 })
267 })
259 return tmpl
268 return tmpl
260
269
261 def archivelist(self, nodeid):
270 def archivelist(self, nodeid):
262 allowed = self.configlist("web", "allow_archive")
271 allowed = self.configlist("web", "allow_archive")
263 for i, spec in self.archive_specs.iteritems():
272 for i, spec in self.archive_specs.iteritems():
264 if i in allowed or self.configbool("web", "allow" + i):
273 if i in allowed or self.configbool("web", "allow" + i):
265 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
274 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
266
275
267 def listfilediffs(self, tmpl, files, changeset):
276 def listfilediffs(self, tmpl, files, changeset):
268 for f in files[:self.maxfiles]:
277 for f in files[:self.maxfiles]:
269 yield tmpl("filedifflink", node=hex(changeset), file=f)
278 yield tmpl("filedifflink", node=hex(changeset), file=f)
270 if len(files) > self.maxfiles:
279 if len(files) > self.maxfiles:
271 yield tmpl("fileellipses")
280 yield tmpl("fileellipses")
272
281
273 def diff(self, tmpl, node1, node2, files):
282 def diff(self, tmpl, node1, node2, files):
274 def filterfiles(filters, files):
283 def filterfiles(filters, files):
275 l = [x for x in files if x in filters]
284 l = [x for x in files if x in filters]
276
285
277 for t in filters:
286 for t in filters:
278 if t and t[-1] != os.sep:
287 if t and t[-1] != os.sep:
279 t += os.sep
288 t += os.sep
280 l += [x for x in files if x.startswith(t)]
289 l += [x for x in files if x.startswith(t)]
281 return l
290 return l
282
291
283 parity = paritygen(self.stripecount)
292 parity = paritygen(self.stripecount)
284 def diffblock(diff, f, fn):
293 def diffblock(diff, f, fn):
285 yield tmpl("diffblock",
294 yield tmpl("diffblock",
286 lines=prettyprintlines(diff),
295 lines=prettyprintlines(diff),
287 parity=parity.next(),
296 parity=parity.next(),
288 file=f,
297 file=f,
289 filenode=hex(fn or nullid))
298 filenode=hex(fn or nullid))
290
299
291 blockcount = countgen()
300 blockcount = countgen()
292 def prettyprintlines(diff):
301 def prettyprintlines(diff):
293 blockno = blockcount.next()
302 blockno = blockcount.next()
294 for lineno, l in enumerate(diff.splitlines(1)):
303 for lineno, l in enumerate(diff.splitlines(1)):
295 if blockno == 0:
304 if blockno == 0:
296 lineno = lineno + 1
305 lineno = lineno + 1
297 else:
306 else:
298 lineno = "%d.%d" % (blockno, lineno + 1)
307 lineno = "%d.%d" % (blockno, lineno + 1)
299 if l.startswith('+'):
308 if l.startswith('+'):
300 ltype = "difflineplus"
309 ltype = "difflineplus"
301 elif l.startswith('-'):
310 elif l.startswith('-'):
302 ltype = "difflineminus"
311 ltype = "difflineminus"
303 elif l.startswith('@'):
312 elif l.startswith('@'):
304 ltype = "difflineat"
313 ltype = "difflineat"
305 else:
314 else:
306 ltype = "diffline"
315 ltype = "diffline"
307 yield tmpl(ltype,
316 yield tmpl(ltype,
308 line=l,
317 line=l,
309 lineid="l%s" % lineno,
318 lineid="l%s" % lineno,
310 linenumber="% 8s" % lineno)
319 linenumber="% 8s" % lineno)
311
320
312 r = self.repo
321 r = self.repo
313 c1 = r[node1]
322 c1 = r[node1]
314 c2 = r[node2]
323 c2 = r[node2]
315 date1 = util.datestr(c1.date())
324 date1 = util.datestr(c1.date())
316 date2 = util.datestr(c2.date())
325 date2 = util.datestr(c2.date())
317
326
318 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
327 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
319 if files:
328 if files:
320 modified, added, removed = map(lambda x: filterfiles(files, x),
329 modified, added, removed = map(lambda x: filterfiles(files, x),
321 (modified, added, removed))
330 (modified, added, removed))
322
331
323 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
332 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
324 for f in modified:
333 for f in modified:
325 to = c1.filectx(f).data()
334 to = c1.filectx(f).data()
326 tn = c2.filectx(f).data()
335 tn = c2.filectx(f).data()
327 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
336 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
328 opts=diffopts), f, tn)
337 opts=diffopts), f, tn)
329 for f in added:
338 for f in added:
330 to = None
339 to = None
331 tn = c2.filectx(f).data()
340 tn = c2.filectx(f).data()
332 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
341 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
333 opts=diffopts), f, tn)
342 opts=diffopts), f, tn)
334 for f in removed:
343 for f in removed:
335 to = c1.filectx(f).data()
344 to = c1.filectx(f).data()
336 tn = None
345 tn = None
337 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
346 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
338 opts=diffopts), f, tn)
347 opts=diffopts), f, tn)
339
348
340 archive_specs = {
349 archive_specs = {
341 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
350 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
342 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
351 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
343 'zip': ('application/zip', 'zip', '.zip', None),
352 'zip': ('application/zip', 'zip', '.zip', None),
344 }
353 }
345
354
346 def check_perm(self, req, op, default):
355 def check_perm(self, req, op):
347 '''check permission for operation based on user auth.
356 '''Check permission for operation based on request data (including
348 return true if op allowed, else false.
357 authentication info. Return true if op allowed, else false.'''
349 default is policy to use if no config given.'''
358
359 def error(status, message):
360 req.respond(status, protocol.HGTYPE)
361 req.write('0\n%s\n' % message)
362
363 if op == 'pull':
364 return self.allowpull
365
366 # enforce that you can only push using POST requests
367 if req.env['REQUEST_METHOD'] != 'POST':
368 error('405 Method Not Allowed', 'push requires POST request')
369 return False
370
371 # require ssl by default for pushing, auth info cannot be sniffed
372 # and replayed
373 scheme = req.env.get('wsgi.url_scheme')
374 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
375 error(HTTP_OK, 'ssl required')
376 return False
350
377
351 user = req.env.get('REMOTE_USER')
378 user = req.env.get('REMOTE_USER')
352
379
353 deny = self.configlist('web', 'deny_' + op)
380 deny = self.configlist('web', 'deny_push')
354 if deny and (not user or deny == ['*'] or user in deny):
381 if deny and (not user or deny == ['*'] or user in deny):
382 error('401 Unauthorized', 'push not authorized')
355 return False
383 return False
356
384
357 allow = self.configlist('web', 'allow_' + op)
385 allow = self.configlist('web', 'allow_push')
358 return (allow and (allow == ['*'] or user in allow)) or default
386 result = allow and (allow == ['*'] or user in allow)
387 if not result:
388 error('401 Unauthorized', 'push not authorized')
389
390 return result
@@ -1,230 +1,203 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 import cStringIO, zlib, tempfile, errno, os, sys
8 import cStringIO, zlib, tempfile, errno, os, sys
9 from mercurial import util, streamclone
9 from mercurial import util, streamclone
10 from mercurial.node import bin, hex
10 from mercurial.node import bin, hex
11 from mercurial import changegroup as changegroupmod
11 from mercurial import changegroup as changegroupmod
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13
13
14 # __all__ is populated with the allowed commands. Be sure to add to it if
14 # __all__ is populated with the allowed commands. Be sure to add to it if
15 # you're adding a new command, or the new command won't work.
15 # you're adding a new command, or the new command won't work.
16
16
17 __all__ = [
17 __all__ = [
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
20 ]
20 ]
21
21
22 HGTYPE = 'application/mercurial-0.1'
22 HGTYPE = 'application/mercurial-0.1'
23
23
24 def lookup(web, req):
24 def lookup(web, req):
25 try:
25 try:
26 r = hex(web.repo.lookup(req.form['key'][0]))
26 r = hex(web.repo.lookup(req.form['key'][0]))
27 success = 1
27 success = 1
28 except Exception,inst:
28 except Exception,inst:
29 r = str(inst)
29 r = str(inst)
30 success = 0
30 success = 0
31 resp = "%s %s\n" % (success, r)
31 resp = "%s %s\n" % (success, r)
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
33 req.write(resp)
33 req.write(resp)
34
34
35 def heads(web, req):
35 def heads(web, req):
36 resp = " ".join(map(hex, web.repo.heads())) + "\n"
36 resp = " ".join(map(hex, web.repo.heads())) + "\n"
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
38 req.write(resp)
38 req.write(resp)
39
39
40 def branches(web, req):
40 def branches(web, req):
41 nodes = []
41 nodes = []
42 if 'nodes' in req.form:
42 if 'nodes' in req.form:
43 nodes = map(bin, req.form['nodes'][0].split(" "))
43 nodes = map(bin, req.form['nodes'][0].split(" "))
44 resp = cStringIO.StringIO()
44 resp = cStringIO.StringIO()
45 for b in web.repo.branches(nodes):
45 for b in web.repo.branches(nodes):
46 resp.write(" ".join(map(hex, b)) + "\n")
46 resp.write(" ".join(map(hex, b)) + "\n")
47 resp = resp.getvalue()
47 resp = resp.getvalue()
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
49 req.write(resp)
49 req.write(resp)
50
50
51 def between(web, req):
51 def between(web, req):
52 if 'pairs' in req.form:
52 if 'pairs' in req.form:
53 pairs = [map(bin, p.split("-"))
53 pairs = [map(bin, p.split("-"))
54 for p in req.form['pairs'][0].split(" ")]
54 for p in req.form['pairs'][0].split(" ")]
55 resp = cStringIO.StringIO()
55 resp = cStringIO.StringIO()
56 for b in web.repo.between(pairs):
56 for b in web.repo.between(pairs):
57 resp.write(" ".join(map(hex, b)) + "\n")
57 resp.write(" ".join(map(hex, b)) + "\n")
58 resp = resp.getvalue()
58 resp = resp.getvalue()
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
60 req.write(resp)
60 req.write(resp)
61
61
62 def changegroup(web, req):
62 def changegroup(web, req):
63 req.respond(HTTP_OK, HGTYPE)
63 req.respond(HTTP_OK, HGTYPE)
64 nodes = []
64 nodes = []
65 if not web.allowpull:
66 return
67
65
68 if 'roots' in req.form:
66 if 'roots' in req.form:
69 nodes = map(bin, req.form['roots'][0].split(" "))
67 nodes = map(bin, req.form['roots'][0].split(" "))
70
68
71 z = zlib.compressobj()
69 z = zlib.compressobj()
72 f = web.repo.changegroup(nodes, 'serve')
70 f = web.repo.changegroup(nodes, 'serve')
73 while 1:
71 while 1:
74 chunk = f.read(4096)
72 chunk = f.read(4096)
75 if not chunk:
73 if not chunk:
76 break
74 break
77 req.write(z.compress(chunk))
75 req.write(z.compress(chunk))
78
76
79 req.write(z.flush())
77 req.write(z.flush())
80
78
81 def changegroupsubset(web, req):
79 def changegroupsubset(web, req):
82 req.respond(HTTP_OK, HGTYPE)
80 req.respond(HTTP_OK, HGTYPE)
83 bases = []
81 bases = []
84 heads = []
82 heads = []
85 if not web.allowpull:
86 return
87
83
88 if 'bases' in req.form:
84 if 'bases' in req.form:
89 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
85 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
90 if 'heads' in req.form:
86 if 'heads' in req.form:
91 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
87 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
92
88
93 z = zlib.compressobj()
89 z = zlib.compressobj()
94 f = web.repo.changegroupsubset(bases, heads, 'serve')
90 f = web.repo.changegroupsubset(bases, heads, 'serve')
95 while 1:
91 while 1:
96 chunk = f.read(4096)
92 chunk = f.read(4096)
97 if not chunk:
93 if not chunk:
98 break
94 break
99 req.write(z.compress(chunk))
95 req.write(z.compress(chunk))
100
96
101 req.write(z.flush())
97 req.write(z.flush())
102
98
103 def capabilities(web, req):
99 def capabilities(web, req):
104 resp = ' '.join(web.capabilities())
100 resp = ' '.join(web.capabilities())
105 req.respond(HTTP_OK, HGTYPE, length=len(resp))
101 req.respond(HTTP_OK, HGTYPE, length=len(resp))
106 req.write(resp)
102 req.write(resp)
107
103
108 def unbundle(web, req):
104 def unbundle(web, req):
109
105
110 def bail(response, headers={}):
106 def bail(response, headers={}):
111 length = int(req.env.get('CONTENT_LENGTH', 0))
107 length = int(req.env.get('CONTENT_LENGTH', 0))
112 for s in util.filechunkiter(req, limit=length):
108 for s in util.filechunkiter(req, limit=length):
113 # drain incoming bundle, else client will not see
109 # drain incoming bundle, else client will not see
114 # response when run outside cgi script
110 # response when run outside cgi script
115 pass
111 pass
116
112
117 status = headers.pop('status', HTTP_OK)
113 status = headers.pop('status', HTTP_OK)
118 req.header(headers.items())
114 req.header(headers.items())
119 req.respond(status, HGTYPE)
115 req.respond(status, HGTYPE)
120 req.write('0\n')
116 req.write('0\n')
121 req.write(response)
117 req.write(response)
122
118
123 # enforce that you can only unbundle with POST requests
119 proto = req.env.get('wsgi.url_scheme') or 'http'
124 if req.env['REQUEST_METHOD'] != 'POST':
125 headers = {'status': '405 Method Not Allowed'}
126 bail('unbundle requires POST request\n', headers)
127 return
128
129 # require ssl by default, auth info cannot be sniffed and
130 # replayed
131 ssl_req = web.configbool('web', 'push_ssl', True)
132 if ssl_req:
133 if req.env.get('wsgi.url_scheme') != 'https':
134 bail('ssl required\n')
135 return
136 proto = 'https'
137 else:
138 proto = 'http'
139
140 # do not allow push unless explicitly allowed
141 if not web.check_perm(req, 'push', False):
142 bail('push not authorized\n', headers={'status': '401 Unauthorized'})
143 return
144
145 their_heads = req.form['heads'][0].split(' ')
120 their_heads = req.form['heads'][0].split(' ')
146
121
147 def check_heads():
122 def check_heads():
148 heads = map(hex, web.repo.heads())
123 heads = map(hex, web.repo.heads())
149 return their_heads == [hex('force')] or their_heads == heads
124 return their_heads == [hex('force')] or their_heads == heads
150
125
151 # fail early if possible
126 # fail early if possible
152 if not check_heads():
127 if not check_heads():
153 bail('unsynced changes\n')
128 bail('unsynced changes\n')
154 return
129 return
155
130
156 req.respond(HTTP_OK, HGTYPE)
131 req.respond(HTTP_OK, HGTYPE)
157
132
158 # do not lock repo until all changegroup data is
133 # do not lock repo until all changegroup data is
159 # streamed. save to temporary file.
134 # streamed. save to temporary file.
160
135
161 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
136 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
162 fp = os.fdopen(fd, 'wb+')
137 fp = os.fdopen(fd, 'wb+')
163 try:
138 try:
164 length = int(req.env['CONTENT_LENGTH'])
139 length = int(req.env['CONTENT_LENGTH'])
165 for s in util.filechunkiter(req, limit=length):
140 for s in util.filechunkiter(req, limit=length):
166 fp.write(s)
141 fp.write(s)
167
142
168 try:
143 try:
169 lock = web.repo.lock()
144 lock = web.repo.lock()
170 try:
145 try:
171 if not check_heads():
146 if not check_heads():
172 req.write('0\n')
147 req.write('0\n')
173 req.write('unsynced changes\n')
148 req.write('unsynced changes\n')
174 return
149 return
175
150
176 fp.seek(0)
151 fp.seek(0)
177 header = fp.read(6)
152 header = fp.read(6)
178 if header.startswith('HG') and not header.startswith('HG10'):
153 if header.startswith('HG') and not header.startswith('HG10'):
179 raise ValueError('unknown bundle version')
154 raise ValueError('unknown bundle version')
180 elif header not in changegroupmod.bundletypes:
155 elif header not in changegroupmod.bundletypes:
181 raise ValueError('unknown bundle compression type')
156 raise ValueError('unknown bundle compression type')
182 gen = changegroupmod.unbundle(header, fp)
157 gen = changegroupmod.unbundle(header, fp)
183
158
184 # send addchangegroup output to client
159 # send addchangegroup output to client
185
160
186 oldio = sys.stdout, sys.stderr
161 oldio = sys.stdout, sys.stderr
187 sys.stderr = sys.stdout = cStringIO.StringIO()
162 sys.stderr = sys.stdout = cStringIO.StringIO()
188
163
189 try:
164 try:
190 url = 'remote:%s:%s' % (proto,
165 url = 'remote:%s:%s' % (proto,
191 req.env.get('REMOTE_HOST', ''))
166 req.env.get('REMOTE_HOST', ''))
192 try:
167 try:
193 ret = web.repo.addchangegroup(gen, 'serve', url)
168 ret = web.repo.addchangegroup(gen, 'serve', url)
194 except util.Abort, inst:
169 except util.Abort, inst:
195 sys.stdout.write("abort: %s\n" % inst)
170 sys.stdout.write("abort: %s\n" % inst)
196 ret = 0
171 ret = 0
197 finally:
172 finally:
198 val = sys.stdout.getvalue()
173 val = sys.stdout.getvalue()
199 sys.stdout, sys.stderr = oldio
174 sys.stdout, sys.stderr = oldio
200 req.write('%d\n' % ret)
175 req.write('%d\n' % ret)
201 req.write(val)
176 req.write(val)
202 finally:
177 finally:
203 del lock
178 del lock
204 except ValueError, inst:
179 except ValueError, inst:
205 req.write('0\n')
180 req.write('0\n')
206 req.write(str(inst) + '\n')
181 req.write(str(inst) + '\n')
207 except (OSError, IOError), inst:
182 except (OSError, IOError), inst:
208 req.write('0\n')
183 req.write('0\n')
209 filename = getattr(inst, 'filename', '')
184 filename = getattr(inst, 'filename', '')
210 # Don't send our filesystem layout to the client
185 # Don't send our filesystem layout to the client
211 if filename.startswith(web.repo.root):
186 if filename.startswith(web.repo.root):
212 filename = filename[len(web.repo.root)+1:]
187 filename = filename[len(web.repo.root)+1:]
213 else:
188 else:
214 filename = ''
189 filename = ''
215 error = getattr(inst, 'strerror', 'Unknown error')
190 error = getattr(inst, 'strerror', 'Unknown error')
216 if inst.errno == errno.ENOENT:
191 if inst.errno == errno.ENOENT:
217 code = HTTP_NOT_FOUND
192 code = HTTP_NOT_FOUND
218 else:
193 else:
219 code = HTTP_SERVER_ERROR
194 code = HTTP_SERVER_ERROR
220 req.respond(code)
195 req.respond(code)
221 req.write('%s: %s\n' % (error, filename))
196 req.write('%s: %s\n' % (error, filename))
222 finally:
197 finally:
223 fp.close()
198 fp.close()
224 os.unlink(tempname)
199 os.unlink(tempname)
225
200
226 def stream_out(web, req):
201 def stream_out(web, req):
227 if not web.allowpull:
228 return
229 req.respond(HTTP_OK, HGTYPE)
202 req.respond(HTTP_OK, HGTYPE)
230 streamclone.stream_out(web.repo, req, untrusted=True)
203 streamclone.stream_out(web.repo, req, untrusted=True)
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
General Comments 0
You need to be logged in to leave comments. Login now