##// END OF EJS Templates
protocol: move hgweb protocol support back into protocol.py...
Matt Mackall -
r11595:368cd532 default
parent child Browse files
Show More
@@ -1,350 +1,285
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 or any later version.
8 8
9 import os, zlib, sys, cStringIO, urllib
10 from mercurial import ui, hg, hook, error, encoding, templater, wireproto, util
9 import os, sys, urllib
10 from mercurial import ui, hg, hook, error, encoding, templater, util
11 11 from common import get_mtime, ErrorResponse, permhooks
12 12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 13 from request import wsgirequest
14 14 import webcommands, protocol, webutil
15 15
16 16 perms = {
17 17 'changegroup': 'pull',
18 18 'changegroupsubset': 'pull',
19 19 'stream_out': 'pull',
20 20 'listkeys': 'pull',
21 21 'unbundle': 'push',
22 22 'pushkey': 'push',
23 23 }
24 24
25 HGTYPE = 'application/mercurial-0.1'
26 class webproto(object):
27 def __init__(self, req):
28 self.req = req
29 self.response = ''
30 def getargs(self, args):
31 data = {}
32 keys = args.split()
33 for k in keys:
34 if k == '*':
35 star = {}
36 for key in self.req.form.keys():
37 if key not in keys:
38 star[key] = self.req.form[key][0]
39 data['*'] = star
40 else:
41 data[k] = self.req.form[k][0]
42 return [data[k] for k in keys]
43 def sendchangegroup(self, cg):
44 self.req.respond(HTTP_OK, HGTYPE)
45 z = zlib.compressobj()
46 while 1:
47 chunk = cg.read(4096)
48 if not chunk:
49 break
50 self.req.write(z.compress(chunk))
51 self.req.write(z.flush())
52 def sendstream(self, source):
53 self.req.respond(HTTP_OK, HGTYPE)
54 for chunk in source:
55 self.req.write(chunk)
56 def respond(self, s):
57 self.req.respond(HTTP_OK, HGTYPE, length=len(s))
58 self.response = s
59 def getfile(self, fp):
60 length = int(self.req.env['CONTENT_LENGTH'])
61 for s in util.filechunkiter(self.req, limit=length):
62 fp.write(s)
63 def redirect(self):
64 self.oldio = sys.stdout, sys.stderr
65 sys.stderr = sys.stdout = cStringIO.StringIO()
66 def respondpush(self, ret):
67 val = sys.stdout.getvalue()
68 sys.stdout, sys.stderr = self.oldio
69 self.req.respond(HTTP_OK, HGTYPE)
70 self.response = '%d\n%s' % (ret, val)
71 def _client(self):
72 return 'remote:%s:%s:%s' % (
73 self.req.env.get('wsgi.url_scheme') or 'http',
74 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
75 urllib.quote(self.req.env.get('REMOTE_USER', '')))
76
77 def callproto(repo, req, cmd):
78 p = webproto(req)
79 r = wireproto.dispatch(repo, p, cmd)
80 yield p.response
81
82 25 class hgweb(object):
83 26 def __init__(self, repo, name=None, baseui=None):
84 27 if isinstance(repo, str):
85 28 if baseui:
86 29 u = baseui.copy()
87 30 else:
88 31 u = ui.ui()
89 32 self.repo = hg.repository(u, repo)
90 33 else:
91 34 self.repo = repo
92 35
93 36 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
94 37 self.repo.ui.setconfig('ui', 'interactive', 'off')
95 38 hook.redirect(True)
96 39 self.mtime = -1
97 40 self.reponame = name
98 41 self.archives = 'zip', 'gz', 'bz2'
99 42 self.stripecount = 1
100 43 # a repo owner may set web.templates in .hg/hgrc to get any file
101 44 # readable by the user running the CGI script
102 45 self.templatepath = self.config('web', 'templates')
103 46
104 47 # The CGI scripts are often run by a user different from the repo owner.
105 48 # Trust the settings from the .hg/hgrc files by default.
106 49 def config(self, section, name, default=None, untrusted=True):
107 50 return self.repo.ui.config(section, name, default,
108 51 untrusted=untrusted)
109 52
110 53 def configbool(self, section, name, default=False, untrusted=True):
111 54 return self.repo.ui.configbool(section, name, default,
112 55 untrusted=untrusted)
113 56
114 57 def configlist(self, section, name, default=None, untrusted=True):
115 58 return self.repo.ui.configlist(section, name, default,
116 59 untrusted=untrusted)
117 60
118 61 def refresh(self, request=None):
119 62 if request:
120 63 self.repo.ui.environ = request.env
121 64 mtime = get_mtime(self.repo.spath)
122 65 if mtime != self.mtime:
123 66 self.mtime = mtime
124 67 self.repo = hg.repository(self.repo.ui, self.repo.root)
125 68 self.maxchanges = int(self.config("web", "maxchanges", 10))
126 69 self.stripecount = int(self.config("web", "stripes", 1))
127 70 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
128 71 self.maxfiles = int(self.config("web", "maxfiles", 10))
129 72 self.allowpull = self.configbool("web", "allowpull", True)
130 73 encoding.encoding = self.config("web", "encoding",
131 74 encoding.encoding)
132 75
133 76 def run(self):
134 77 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
135 78 raise RuntimeError("This function is only intended to be "
136 79 "called while running as a CGI script.")
137 80 import mercurial.hgweb.wsgicgi as wsgicgi
138 81 wsgicgi.launch(self)
139 82
140 83 def __call__(self, env, respond):
141 84 req = wsgirequest(env, respond)
142 85 return self.run_wsgi(req)
143 86
144 87 def run_wsgi(self, req):
145 88
146 89 self.refresh(req)
147 90
148 91 # work with CGI variables to create coherent structure
149 92 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
150 93
151 94 req.url = req.env['SCRIPT_NAME']
152 95 if not req.url.endswith('/'):
153 96 req.url += '/'
154 97 if 'REPO_NAME' in req.env:
155 98 req.url += req.env['REPO_NAME'] + '/'
156 99
157 100 if 'PATH_INFO' in req.env:
158 101 parts = req.env['PATH_INFO'].strip('/').split('/')
159 102 repo_parts = req.env.get('REPO_NAME', '').split('/')
160 103 if parts[:len(repo_parts)] == repo_parts:
161 104 parts = parts[len(repo_parts):]
162 105 query = '/'.join(parts)
163 106 else:
164 107 query = req.env['QUERY_STRING'].split('&', 1)[0]
165 108 query = query.split(';', 1)[0]
166 109
167 110 # process this if it's a protocol request
168 111 # protocol bits don't need to create any URLs
169 112 # and the clients always use the old URL structure
170 113
171 114 cmd = req.form.get('cmd', [''])[0]
172 if cmd and cmd in protocol.__all__:
115 if protocol.iscmd(cmd):
173 116 if query:
174 117 raise ErrorResponse(HTTP_NOT_FOUND)
175 try:
176 if cmd in perms:
177 try:
178 self.check_perm(req, perms[cmd])
179 except ErrorResponse, inst:
180 if cmd == 'unbundle':
181 req.drain()
182 raise
183 if cmd in wireproto.commands:
184 return callproto(self.repo, req, cmd)
185 method = getattr(protocol, cmd)
186 return method(self.repo, req)
187 except ErrorResponse, inst:
188 req.respond(inst, protocol.HGTYPE)
189 if not inst.message:
190 return []
191 return '0\n%s\n' % inst.message,
118 if cmd in perms:
119 try:
120 self.check_perm(req, perms[cmd])
121 except ErrorResponse, inst:
122 if cmd == 'unbundle':
123 req.drain()
124 req.respond(inst, protocol.HGTYPE)
125 return '0\n%s\n' % inst.message
126 return protocol.call(self.repo, req, cmd)
192 127
193 128 # translate user-visible url structure to internal structure
194 129
195 130 args = query.split('/', 2)
196 131 if 'cmd' not in req.form and args and args[0]:
197 132
198 133 cmd = args.pop(0)
199 134 style = cmd.rfind('-')
200 135 if style != -1:
201 136 req.form['style'] = [cmd[:style]]
202 137 cmd = cmd[style + 1:]
203 138
204 139 # avoid accepting e.g. style parameter as command
205 140 if hasattr(webcommands, cmd):
206 141 req.form['cmd'] = [cmd]
207 142 else:
208 143 cmd = ''
209 144
210 145 if cmd == 'static':
211 146 req.form['file'] = ['/'.join(args)]
212 147 else:
213 148 if args and args[0]:
214 149 node = args.pop(0)
215 150 req.form['node'] = [node]
216 151 if args:
217 152 req.form['file'] = args
218 153
219 154 ua = req.env.get('HTTP_USER_AGENT', '')
220 155 if cmd == 'rev' and 'mercurial' in ua:
221 156 req.form['style'] = ['raw']
222 157
223 158 if cmd == 'archive':
224 159 fn = req.form['node'][0]
225 160 for type_, spec in self.archive_specs.iteritems():
226 161 ext = spec[2]
227 162 if fn.endswith(ext):
228 163 req.form['node'] = [fn[:-len(ext)]]
229 164 req.form['type'] = [type_]
230 165
231 166 # process the web interface request
232 167
233 168 try:
234 169 tmpl = self.templater(req)
235 170 ctype = tmpl('mimetype', encoding=encoding.encoding)
236 171 ctype = templater.stringify(ctype)
237 172
238 173 # check read permissions non-static content
239 174 if cmd != 'static':
240 175 self.check_perm(req, None)
241 176
242 177 if cmd == '':
243 178 req.form['cmd'] = [tmpl.cache['default']]
244 179 cmd = req.form['cmd'][0]
245 180
246 181 if cmd not in webcommands.__all__:
247 182 msg = 'no such method: %s' % cmd
248 183 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
249 184 elif cmd == 'file' and 'raw' in req.form.get('style', []):
250 185 self.ctype = ctype
251 186 content = webcommands.rawfile(self, req, tmpl)
252 187 else:
253 188 content = getattr(webcommands, cmd)(self, req, tmpl)
254 189 req.respond(HTTP_OK, ctype)
255 190
256 191 return content
257 192
258 193 except error.LookupError, err:
259 194 req.respond(HTTP_NOT_FOUND, ctype)
260 195 msg = str(err)
261 196 if 'manifest' not in msg:
262 197 msg = 'revision not found: %s' % err.name
263 198 return tmpl('error', error=msg)
264 199 except (error.RepoError, error.RevlogError), inst:
265 200 req.respond(HTTP_SERVER_ERROR, ctype)
266 201 return tmpl('error', error=str(inst))
267 202 except ErrorResponse, inst:
268 203 req.respond(inst, ctype)
269 204 return tmpl('error', error=inst.message)
270 205
271 206 def templater(self, req):
272 207
273 208 # determine scheme, port and server name
274 209 # this is needed to create absolute urls
275 210
276 211 proto = req.env.get('wsgi.url_scheme')
277 212 if proto == 'https':
278 213 proto = 'https'
279 214 default_port = "443"
280 215 else:
281 216 proto = 'http'
282 217 default_port = "80"
283 218
284 219 port = req.env["SERVER_PORT"]
285 220 port = port != default_port and (":" + port) or ""
286 221 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
287 222 staticurl = self.config("web", "staticurl") or req.url + 'static/'
288 223 if not staticurl.endswith('/'):
289 224 staticurl += '/'
290 225
291 226 # some functions for the templater
292 227
293 228 def header(**map):
294 229 yield tmpl('header', encoding=encoding.encoding, **map)
295 230
296 231 def footer(**map):
297 232 yield tmpl("footer", **map)
298 233
299 234 def motd(**map):
300 235 yield self.config("web", "motd", "")
301 236
302 237 # figure out which style to use
303 238
304 239 vars = {}
305 240 styles = (
306 241 req.form.get('style', [None])[0],
307 242 self.config('web', 'style'),
308 243 'paper',
309 244 )
310 245 style, mapfile = templater.stylemap(styles, self.templatepath)
311 246 if style == styles[0]:
312 247 vars['style'] = style
313 248
314 249 start = req.url[-1] == '?' and '&' or '?'
315 250 sessionvars = webutil.sessionvars(vars, start)
316 251
317 252 if not self.reponame:
318 253 self.reponame = (self.config("web", "name")
319 254 or req.env.get('REPO_NAME')
320 255 or req.url.strip('/') or self.repo.root)
321 256
322 257 # create the templater
323 258
324 259 tmpl = templater.templater(mapfile,
325 260 defaults={"url": req.url,
326 261 "staticurl": staticurl,
327 262 "urlbase": urlbase,
328 263 "repo": self.reponame,
329 264 "header": header,
330 265 "footer": footer,
331 266 "motd": motd,
332 267 "sessionvars": sessionvars
333 268 })
334 269 return tmpl
335 270
336 271 def archivelist(self, nodeid):
337 272 allowed = self.configlist("web", "allow_archive")
338 273 for i, spec in self.archive_specs.iteritems():
339 274 if i in allowed or self.configbool("web", "allow" + i):
340 275 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
341 276
342 277 archive_specs = {
343 278 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
344 279 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
345 280 'zip': ('application/zip', 'zip', '.zip', None),
346 281 }
347 282
348 283 def check_perm(self, req, op):
349 284 for hook in permhooks:
350 285 hook(self, req, op)
@@ -1,23 +1,71
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 import cStringIO, zlib, tempfile, errno, os, sys, urllib, copy
9 from mercurial import util, streamclone, pushkey
10 from mercurial.node import bin, hex
11 from mercurial import changegroup as changegroupmod
12 from common import ErrorResponse, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13
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.
16
17 __all__ = [
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
20 'branchmap', 'pushkey', 'listkeys'
21 ]
8 import cStringIO, zlib, sys, urllib
9 from mercurial import util, wireproto
10 from common import HTTP_OK
22 11
23 12 HGTYPE = 'application/mercurial-0.1'
13
14 class webproto(object):
15 def __init__(self, req):
16 self.req = req
17 self.response = ''
18 def getargs(self, args):
19 data = {}
20 keys = args.split()
21 for k in keys:
22 if k == '*':
23 star = {}
24 for key in self.req.form.keys():
25 if key not in keys:
26 star[key] = self.req.form[key][0]
27 data['*'] = star
28 else:
29 data[k] = self.req.form[k][0]
30 return [data[k] for k in keys]
31 def sendchangegroup(self, cg):
32 self.req.respond(HTTP_OK, HGTYPE)
33 z = zlib.compressobj()
34 while 1:
35 chunk = cg.read(4096)
36 if not chunk:
37 break
38 self.req.write(z.compress(chunk))
39 self.req.write(z.flush())
40 def sendstream(self, source):
41 self.req.respond(HTTP_OK, HGTYPE)
42 for chunk in source:
43 self.req.write(chunk)
44 def respond(self, s):
45 self.req.respond(HTTP_OK, HGTYPE, length=len(s))
46 self.response = s
47 def getfile(self, fp):
48 length = int(self.req.env['CONTENT_LENGTH'])
49 for s in util.filechunkiter(self.req, limit=length):
50 fp.write(s)
51 def redirect(self):
52 self.oldio = sys.stdout, sys.stderr
53 sys.stderr = sys.stdout = cStringIO.StringIO()
54 def respondpush(self, ret):
55 val = sys.stdout.getvalue()
56 sys.stdout, sys.stderr = self.oldio
57 self.req.respond(HTTP_OK, HGTYPE)
58 self.response = '%d\n%s' % (ret, val)
59 def _client(self):
60 return 'remote:%s:%s:%s' % (
61 self.req.env.get('wsgi.url_scheme') or 'http',
62 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
63 urllib.quote(self.req.env.get('REMOTE_USER', '')))
64
65 def iscmd(cmd):
66 return cmd in wireproto.commands
67
68 def call(repo, req, cmd):
69 p = webproto(req)
70 r = wireproto.dispatch(repo, p, cmd)
71 yield p.response
General Comments 0
You need to be logged in to leave comments. Login now