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