##// END OF EJS Templates
hgweb: expose input stream on parsed WSGI request object...
Gregory Szorc -
r36873:da4e2f87 default
parent child Browse files
Show More
@@ -1,543 +1,544 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29
29
30 from .. import (
30 from .. import (
31 configitems,
31 configitems,
32 encoding,
32 encoding,
33 error,
33 error,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 pycompat,
36 pycompat,
37 scmutil,
37 scmutil,
38 templater,
38 templater,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 hgweb_mod,
44 hgweb_mod,
45 request as requestmod,
45 request as requestmod,
46 webutil,
46 webutil,
47 wsgicgi,
47 wsgicgi,
48 )
48 )
49 from ..utils import dateutil
49 from ..utils import dateutil
50
50
51 def cleannames(items):
51 def cleannames(items):
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53
53
54 def findrepos(paths):
54 def findrepos(paths):
55 repos = []
55 repos = []
56 for prefix, root in cleannames(paths):
56 for prefix, root in cleannames(paths):
57 roothead, roottail = os.path.split(root)
57 roothead, roottail = os.path.split(root)
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # /bar/ be served as as foo/N .
59 # /bar/ be served as as foo/N .
60 # '*' will not search inside dirs with .hg (except .hg/patches),
60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 try:
62 try:
63 recurse = {'*': False, '**': True}[roottail]
63 recurse = {'*': False, '**': True}[roottail]
64 except KeyError:
64 except KeyError:
65 repos.append((prefix, root))
65 repos.append((prefix, root))
66 continue
66 continue
67 roothead = os.path.normpath(os.path.abspath(roothead))
67 roothead = os.path.normpath(os.path.abspath(roothead))
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 repos.extend(urlrepos(prefix, roothead, paths))
69 repos.extend(urlrepos(prefix, roothead, paths))
70 return repos
70 return repos
71
71
72 def urlrepos(prefix, roothead, paths):
72 def urlrepos(prefix, roothead, paths):
73 """yield url paths and filesystem paths from a list of repo paths
73 """yield url paths and filesystem paths from a list of repo paths
74
74
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 """
80 """
81 for path in paths:
81 for path in paths:
82 path = os.path.normpath(path)
82 path = os.path.normpath(path)
83 yield (prefix + '/' +
83 yield (prefix + '/' +
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85
85
86 def geturlcgivars(baseurl, port):
86 def geturlcgivars(baseurl, port):
87 """
87 """
88 Extract CGI variables from baseurl
88 Extract CGI variables from baseurl
89
89
90 >>> geturlcgivars(b"http://host.org/base", b"80")
90 >>> geturlcgivars(b"http://host.org/base", b"80")
91 ('host.org', '80', '/base')
91 ('host.org', '80', '/base')
92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
93 ('host.org', '8000', '/base')
93 ('host.org', '8000', '/base')
94 >>> geturlcgivars(b'/base', 8000)
94 >>> geturlcgivars(b'/base', 8000)
95 ('', '8000', '/base')
95 ('', '8000', '/base')
96 >>> geturlcgivars(b"base", b'8000')
96 >>> geturlcgivars(b"base", b'8000')
97 ('', '8000', '/base')
97 ('', '8000', '/base')
98 >>> geturlcgivars(b"http://host", b'8000')
98 >>> geturlcgivars(b"http://host", b'8000')
99 ('host', '8000', '/')
99 ('host', '8000', '/')
100 >>> geturlcgivars(b"http://host/", b'8000')
100 >>> geturlcgivars(b"http://host/", b'8000')
101 ('host', '8000', '/')
101 ('host', '8000', '/')
102 """
102 """
103 u = util.url(baseurl)
103 u = util.url(baseurl)
104 name = u.host or ''
104 name = u.host or ''
105 if u.port:
105 if u.port:
106 port = u.port
106 port = u.port
107 path = u.path or ""
107 path = u.path or ""
108 if not path.startswith('/'):
108 if not path.startswith('/'):
109 path = '/' + path
109 path = '/' + path
110
110
111 return name, pycompat.bytestr(port), path
111 return name, pycompat.bytestr(port), path
112
112
113 class hgwebdir(object):
113 class hgwebdir(object):
114 """HTTP server for multiple repositories.
114 """HTTP server for multiple repositories.
115
115
116 Given a configuration, different repositories will be served depending
116 Given a configuration, different repositories will be served depending
117 on the request path.
117 on the request path.
118
118
119 Instances are typically used as WSGI applications.
119 Instances are typically used as WSGI applications.
120 """
120 """
121 def __init__(self, conf, baseui=None):
121 def __init__(self, conf, baseui=None):
122 self.conf = conf
122 self.conf = conf
123 self.baseui = baseui
123 self.baseui = baseui
124 self.ui = None
124 self.ui = None
125 self.lastrefresh = 0
125 self.lastrefresh = 0
126 self.motd = None
126 self.motd = None
127 self.refresh()
127 self.refresh()
128
128
129 def refresh(self):
129 def refresh(self):
130 if self.ui:
130 if self.ui:
131 refreshinterval = self.ui.configint('web', 'refreshinterval')
131 refreshinterval = self.ui.configint('web', 'refreshinterval')
132 else:
132 else:
133 item = configitems.coreitems['web']['refreshinterval']
133 item = configitems.coreitems['web']['refreshinterval']
134 refreshinterval = item.default
134 refreshinterval = item.default
135
135
136 # refreshinterval <= 0 means to always refresh.
136 # refreshinterval <= 0 means to always refresh.
137 if (refreshinterval > 0 and
137 if (refreshinterval > 0 and
138 self.lastrefresh + refreshinterval > time.time()):
138 self.lastrefresh + refreshinterval > time.time()):
139 return
139 return
140
140
141 if self.baseui:
141 if self.baseui:
142 u = self.baseui.copy()
142 u = self.baseui.copy()
143 else:
143 else:
144 u = uimod.ui.load()
144 u = uimod.ui.load()
145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
147 # displaying bundling progress bar while serving feels wrong and may
147 # displaying bundling progress bar while serving feels wrong and may
148 # break some wsgi implementations.
148 # break some wsgi implementations.
149 u.setconfig('progress', 'disable', 'true', 'hgweb')
149 u.setconfig('progress', 'disable', 'true', 'hgweb')
150
150
151 if not isinstance(self.conf, (dict, list, tuple)):
151 if not isinstance(self.conf, (dict, list, tuple)):
152 map = {'paths': 'hgweb-paths'}
152 map = {'paths': 'hgweb-paths'}
153 if not os.path.exists(self.conf):
153 if not os.path.exists(self.conf):
154 raise error.Abort(_('config file %s not found!') % self.conf)
154 raise error.Abort(_('config file %s not found!') % self.conf)
155 u.readconfig(self.conf, remap=map, trust=True)
155 u.readconfig(self.conf, remap=map, trust=True)
156 paths = []
156 paths = []
157 for name, ignored in u.configitems('hgweb-paths'):
157 for name, ignored in u.configitems('hgweb-paths'):
158 for path in u.configlist('hgweb-paths', name):
158 for path in u.configlist('hgweb-paths', name):
159 paths.append((name, path))
159 paths.append((name, path))
160 elif isinstance(self.conf, (list, tuple)):
160 elif isinstance(self.conf, (list, tuple)):
161 paths = self.conf
161 paths = self.conf
162 elif isinstance(self.conf, dict):
162 elif isinstance(self.conf, dict):
163 paths = self.conf.items()
163 paths = self.conf.items()
164
164
165 repos = findrepos(paths)
165 repos = findrepos(paths)
166 for prefix, root in u.configitems('collections'):
166 for prefix, root in u.configitems('collections'):
167 prefix = util.pconvert(prefix)
167 prefix = util.pconvert(prefix)
168 for path in scmutil.walkrepos(root, followsym=True):
168 for path in scmutil.walkrepos(root, followsym=True):
169 repo = os.path.normpath(path)
169 repo = os.path.normpath(path)
170 name = util.pconvert(repo)
170 name = util.pconvert(repo)
171 if name.startswith(prefix):
171 if name.startswith(prefix):
172 name = name[len(prefix):]
172 name = name[len(prefix):]
173 repos.append((name.lstrip('/'), repo))
173 repos.append((name.lstrip('/'), repo))
174
174
175 self.repos = repos
175 self.repos = repos
176 self.ui = u
176 self.ui = u
177 encoding.encoding = self.ui.config('web', 'encoding')
177 encoding.encoding = self.ui.config('web', 'encoding')
178 self.style = self.ui.config('web', 'style')
178 self.style = self.ui.config('web', 'style')
179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
180 self.stripecount = self.ui.config('web', 'stripes')
180 self.stripecount = self.ui.config('web', 'stripes')
181 if self.stripecount:
181 if self.stripecount:
182 self.stripecount = int(self.stripecount)
182 self.stripecount = int(self.stripecount)
183 self._baseurl = self.ui.config('web', 'baseurl')
183 self._baseurl = self.ui.config('web', 'baseurl')
184 prefix = self.ui.config('web', 'prefix')
184 prefix = self.ui.config('web', 'prefix')
185 if prefix.startswith('/'):
185 if prefix.startswith('/'):
186 prefix = prefix[1:]
186 prefix = prefix[1:]
187 if prefix.endswith('/'):
187 if prefix.endswith('/'):
188 prefix = prefix[:-1]
188 prefix = prefix[:-1]
189 self.prefix = prefix
189 self.prefix = prefix
190 self.lastrefresh = time.time()
190 self.lastrefresh = time.time()
191
191
192 def run(self):
192 def run(self):
193 if not encoding.environ.get('GATEWAY_INTERFACE',
193 if not encoding.environ.get('GATEWAY_INTERFACE',
194 '').startswith("CGI/1."):
194 '').startswith("CGI/1."):
195 raise RuntimeError("This function is only intended to be "
195 raise RuntimeError("This function is only intended to be "
196 "called while running as a CGI script.")
196 "called while running as a CGI script.")
197 wsgicgi.launch(self)
197 wsgicgi.launch(self)
198
198
199 def __call__(self, env, respond):
199 def __call__(self, env, respond):
200 wsgireq = requestmod.wsgirequest(env, respond)
200 wsgireq = requestmod.wsgirequest(env, respond)
201 return self.run_wsgi(wsgireq)
201 return self.run_wsgi(wsgireq)
202
202
203 def read_allowed(self, ui, wsgireq):
203 def read_allowed(self, ui, wsgireq):
204 """Check allow_read and deny_read config options of a repo's ui object
204 """Check allow_read and deny_read config options of a repo's ui object
205 to determine user permissions. By default, with neither option set (or
205 to determine user permissions. By default, with neither option set (or
206 both empty), allow all users to read the repo. There are two ways a
206 both empty), allow all users to read the repo. There are two ways a
207 user can be denied read access: (1) deny_read is not empty, and the
207 user can be denied read access: (1) deny_read is not empty, and the
208 user is unauthenticated or deny_read contains user (or *), and (2)
208 user is unauthenticated or deny_read contains user (or *), and (2)
209 allow_read is not empty and the user is not in allow_read. Return True
209 allow_read is not empty and the user is not in allow_read. Return True
210 if user is allowed to read the repo, else return False."""
210 if user is allowed to read the repo, else return False."""
211
211
212 user = wsgireq.env.get('REMOTE_USER')
212 user = wsgireq.env.get('REMOTE_USER')
213
213
214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
215 if deny_read and (not user or ismember(ui, user, deny_read)):
215 if deny_read and (not user or ismember(ui, user, deny_read)):
216 return False
216 return False
217
217
218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
219 # by default, allow reading if no allow_read option has been set
219 # by default, allow reading if no allow_read option has been set
220 if (not allow_read) or ismember(ui, user, allow_read):
220 if (not allow_read) or ismember(ui, user, allow_read):
221 return True
221 return True
222
222
223 return False
223 return False
224
224
225 def run_wsgi(self, wsgireq):
225 def run_wsgi(self, wsgireq):
226 profile = self.ui.configbool('profiling', 'enabled')
226 profile = self.ui.configbool('profiling', 'enabled')
227 with profiling.profile(self.ui, enabled=profile):
227 with profiling.profile(self.ui, enabled=profile):
228 for r in self._runwsgi(wsgireq):
228 for r in self._runwsgi(wsgireq):
229 yield r
229 yield r
230
230
231 def _runwsgi(self, wsgireq):
231 def _runwsgi(self, wsgireq):
232 try:
232 try:
233 self.refresh()
233 self.refresh()
234
234
235 csp, nonce = cspvalues(self.ui)
235 csp, nonce = cspvalues(self.ui)
236 if csp:
236 if csp:
237 wsgireq.headers.append(('Content-Security-Policy', csp))
237 wsgireq.headers.append(('Content-Security-Policy', csp))
238
238
239 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
239 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
240 tmpl = self.templater(wsgireq, nonce)
240 tmpl = self.templater(wsgireq, nonce)
241 ctype = tmpl('mimetype', encoding=encoding.encoding)
241 ctype = tmpl('mimetype', encoding=encoding.encoding)
242 ctype = templater.stringify(ctype)
242 ctype = templater.stringify(ctype)
243
243
244 # a static file
244 # a static file
245 if virtual.startswith('static/') or 'static' in wsgireq.form:
245 if virtual.startswith('static/') or 'static' in wsgireq.form:
246 if virtual.startswith('static/'):
246 if virtual.startswith('static/'):
247 fname = virtual[7:]
247 fname = virtual[7:]
248 else:
248 else:
249 fname = wsgireq.form['static'][0]
249 fname = wsgireq.form['static'][0]
250 static = self.ui.config("web", "static", None,
250 static = self.ui.config("web", "static", None,
251 untrusted=False)
251 untrusted=False)
252 if not static:
252 if not static:
253 tp = self.templatepath or templater.templatepaths()
253 tp = self.templatepath or templater.templatepaths()
254 if isinstance(tp, str):
254 if isinstance(tp, str):
255 tp = [tp]
255 tp = [tp]
256 static = [os.path.join(p, 'static') for p in tp]
256 static = [os.path.join(p, 'static') for p in tp]
257 staticfile(static, fname, wsgireq)
257 staticfile(static, fname, wsgireq)
258 return []
258 return []
259
259
260 # top-level index
260 # top-level index
261
261
262 repos = dict(self.repos)
262 repos = dict(self.repos)
263
263
264 if (not virtual or virtual == 'index') and virtual not in repos:
264 if (not virtual or virtual == 'index') and virtual not in repos:
265 wsgireq.respond(HTTP_OK, ctype)
265 wsgireq.respond(HTTP_OK, ctype)
266 return self.makeindex(wsgireq, tmpl)
266 return self.makeindex(wsgireq, tmpl)
267
267
268 # nested indexes and hgwebs
268 # nested indexes and hgwebs
269
269
270 if virtual.endswith('/index') and virtual not in repos:
270 if virtual.endswith('/index') and virtual not in repos:
271 subdir = virtual[:-len('index')]
271 subdir = virtual[:-len('index')]
272 if any(r.startswith(subdir) for r in repos):
272 if any(r.startswith(subdir) for r in repos):
273 wsgireq.respond(HTTP_OK, ctype)
273 wsgireq.respond(HTTP_OK, ctype)
274 return self.makeindex(wsgireq, tmpl, subdir)
274 return self.makeindex(wsgireq, tmpl, subdir)
275
275
276 def _virtualdirs():
276 def _virtualdirs():
277 # Check the full virtual path, each parent, and the root ('')
277 # Check the full virtual path, each parent, and the root ('')
278 if virtual != '':
278 if virtual != '':
279 yield virtual
279 yield virtual
280
280
281 for p in util.finddirs(virtual):
281 for p in util.finddirs(virtual):
282 yield p
282 yield p
283
283
284 yield ''
284 yield ''
285
285
286 for virtualrepo in _virtualdirs():
286 for virtualrepo in _virtualdirs():
287 real = repos.get(virtualrepo)
287 real = repos.get(virtualrepo)
288 if real:
288 if real:
289 wsgireq.env['REPO_NAME'] = virtualrepo
289 wsgireq.env['REPO_NAME'] = virtualrepo
290 # We have to re-parse because of updated environment
290 # We have to re-parse because of updated environment
291 # variable.
291 # variable.
292 # TODO this is kind of hacky and we should have a better
292 # TODO this is kind of hacky and we should have a better
293 # way of doing this than with REPO_NAME side-effects.
293 # way of doing this than with REPO_NAME side-effects.
294 wsgireq.req = requestmod.parserequestfromenv(wsgireq.env)
294 wsgireq.req = requestmod.parserequestfromenv(
295 wsgireq.env, wsgireq.req.bodyfh)
295 try:
296 try:
296 # ensure caller gets private copy of ui
297 # ensure caller gets private copy of ui
297 repo = hg.repository(self.ui.copy(), real)
298 repo = hg.repository(self.ui.copy(), real)
298 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
299 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
299 except IOError as inst:
300 except IOError as inst:
300 msg = encoding.strtolocal(inst.strerror)
301 msg = encoding.strtolocal(inst.strerror)
301 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
302 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
302 except error.RepoError as inst:
303 except error.RepoError as inst:
303 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
304 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
304
305
305 # browse subdirectories
306 # browse subdirectories
306 subdir = virtual + '/'
307 subdir = virtual + '/'
307 if [r for r in repos if r.startswith(subdir)]:
308 if [r for r in repos if r.startswith(subdir)]:
308 wsgireq.respond(HTTP_OK, ctype)
309 wsgireq.respond(HTTP_OK, ctype)
309 return self.makeindex(wsgireq, tmpl, subdir)
310 return self.makeindex(wsgireq, tmpl, subdir)
310
311
311 # prefixes not found
312 # prefixes not found
312 wsgireq.respond(HTTP_NOT_FOUND, ctype)
313 wsgireq.respond(HTTP_NOT_FOUND, ctype)
313 return tmpl("notfound", repo=virtual)
314 return tmpl("notfound", repo=virtual)
314
315
315 except ErrorResponse as err:
316 except ErrorResponse as err:
316 wsgireq.respond(err, ctype)
317 wsgireq.respond(err, ctype)
317 return tmpl('error', error=err.message or '')
318 return tmpl('error', error=err.message or '')
318 finally:
319 finally:
319 tmpl = None
320 tmpl = None
320
321
321 def makeindex(self, wsgireq, tmpl, subdir=""):
322 def makeindex(self, wsgireq, tmpl, subdir=""):
322
323
323 def archivelist(ui, nodeid, url):
324 def archivelist(ui, nodeid, url):
324 allowed = ui.configlist("web", "allow_archive", untrusted=True)
325 allowed = ui.configlist("web", "allow_archive", untrusted=True)
325 archives = []
326 archives = []
326 for typ, spec in hgweb_mod.archivespecs.iteritems():
327 for typ, spec in hgweb_mod.archivespecs.iteritems():
327 if typ in allowed or ui.configbool("web", "allow" + typ,
328 if typ in allowed or ui.configbool("web", "allow" + typ,
328 untrusted=True):
329 untrusted=True):
329 archives.append({"type": typ, "extension": spec[2],
330 archives.append({"type": typ, "extension": spec[2],
330 "node": nodeid, "url": url})
331 "node": nodeid, "url": url})
331 return archives
332 return archives
332
333
333 def rawentries(subdir="", **map):
334 def rawentries(subdir="", **map):
334
335
335 descend = self.ui.configbool('web', 'descend')
336 descend = self.ui.configbool('web', 'descend')
336 collapse = self.ui.configbool('web', 'collapse')
337 collapse = self.ui.configbool('web', 'collapse')
337 seenrepos = set()
338 seenrepos = set()
338 seendirs = set()
339 seendirs = set()
339 for name, path in self.repos:
340 for name, path in self.repos:
340
341
341 if not name.startswith(subdir):
342 if not name.startswith(subdir):
342 continue
343 continue
343 name = name[len(subdir):]
344 name = name[len(subdir):]
344 directory = False
345 directory = False
345
346
346 if '/' in name:
347 if '/' in name:
347 if not descend:
348 if not descend:
348 continue
349 continue
349
350
350 nameparts = name.split('/')
351 nameparts = name.split('/')
351 rootname = nameparts[0]
352 rootname = nameparts[0]
352
353
353 if not collapse:
354 if not collapse:
354 pass
355 pass
355 elif rootname in seendirs:
356 elif rootname in seendirs:
356 continue
357 continue
357 elif rootname in seenrepos:
358 elif rootname in seenrepos:
358 pass
359 pass
359 else:
360 else:
360 directory = True
361 directory = True
361 name = rootname
362 name = rootname
362
363
363 # redefine the path to refer to the directory
364 # redefine the path to refer to the directory
364 discarded = '/'.join(nameparts[1:])
365 discarded = '/'.join(nameparts[1:])
365
366
366 # remove name parts plus accompanying slash
367 # remove name parts plus accompanying slash
367 path = path[:-len(discarded) - 1]
368 path = path[:-len(discarded) - 1]
368
369
369 try:
370 try:
370 r = hg.repository(self.ui, path)
371 r = hg.repository(self.ui, path)
371 directory = False
372 directory = False
372 except (IOError, error.RepoError):
373 except (IOError, error.RepoError):
373 pass
374 pass
374
375
375 parts = [name]
376 parts = [name]
376 parts.insert(0, '/' + subdir.rstrip('/'))
377 parts.insert(0, '/' + subdir.rstrip('/'))
377 if wsgireq.env['SCRIPT_NAME']:
378 if wsgireq.env['SCRIPT_NAME']:
378 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
379 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
379 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
380 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
380
381
381 # show either a directory entry or a repository
382 # show either a directory entry or a repository
382 if directory:
383 if directory:
383 # get the directory's time information
384 # get the directory's time information
384 try:
385 try:
385 d = (get_mtime(path), dateutil.makedate()[1])
386 d = (get_mtime(path), dateutil.makedate()[1])
386 except OSError:
387 except OSError:
387 continue
388 continue
388
389
389 # add '/' to the name to make it obvious that
390 # add '/' to the name to make it obvious that
390 # the entry is a directory, not a regular repository
391 # the entry is a directory, not a regular repository
391 row = {'contact': "",
392 row = {'contact': "",
392 'contact_sort': "",
393 'contact_sort': "",
393 'name': name + '/',
394 'name': name + '/',
394 'name_sort': name,
395 'name_sort': name,
395 'url': url,
396 'url': url,
396 'description': "",
397 'description': "",
397 'description_sort': "",
398 'description_sort': "",
398 'lastchange': d,
399 'lastchange': d,
399 'lastchange_sort': d[1]-d[0],
400 'lastchange_sort': d[1]-d[0],
400 'archives': [],
401 'archives': [],
401 'isdirectory': True,
402 'isdirectory': True,
402 'labels': [],
403 'labels': [],
403 }
404 }
404
405
405 seendirs.add(name)
406 seendirs.add(name)
406 yield row
407 yield row
407 continue
408 continue
408
409
409 u = self.ui.copy()
410 u = self.ui.copy()
410 try:
411 try:
411 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
412 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
412 except Exception as e:
413 except Exception as e:
413 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
414 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
414 continue
415 continue
415 def get(section, name, default=uimod._unset):
416 def get(section, name, default=uimod._unset):
416 return u.config(section, name, default, untrusted=True)
417 return u.config(section, name, default, untrusted=True)
417
418
418 if u.configbool("web", "hidden", untrusted=True):
419 if u.configbool("web", "hidden", untrusted=True):
419 continue
420 continue
420
421
421 if not self.read_allowed(u, wsgireq):
422 if not self.read_allowed(u, wsgireq):
422 continue
423 continue
423
424
424 # update time with local timezone
425 # update time with local timezone
425 try:
426 try:
426 r = hg.repository(self.ui, path)
427 r = hg.repository(self.ui, path)
427 except IOError:
428 except IOError:
428 u.warn(_('error accessing repository at %s\n') % path)
429 u.warn(_('error accessing repository at %s\n') % path)
429 continue
430 continue
430 except error.RepoError:
431 except error.RepoError:
431 u.warn(_('error accessing repository at %s\n') % path)
432 u.warn(_('error accessing repository at %s\n') % path)
432 continue
433 continue
433 try:
434 try:
434 d = (get_mtime(r.spath), dateutil.makedate()[1])
435 d = (get_mtime(r.spath), dateutil.makedate()[1])
435 except OSError:
436 except OSError:
436 continue
437 continue
437
438
438 contact = get_contact(get)
439 contact = get_contact(get)
439 description = get("web", "description")
440 description = get("web", "description")
440 seenrepos.add(name)
441 seenrepos.add(name)
441 name = get("web", "name", name)
442 name = get("web", "name", name)
442 row = {'contact': contact or "unknown",
443 row = {'contact': contact or "unknown",
443 'contact_sort': contact.upper() or "unknown",
444 'contact_sort': contact.upper() or "unknown",
444 'name': name,
445 'name': name,
445 'name_sort': name,
446 'name_sort': name,
446 'url': url,
447 'url': url,
447 'description': description or "unknown",
448 'description': description or "unknown",
448 'description_sort': description.upper() or "unknown",
449 'description_sort': description.upper() or "unknown",
449 'lastchange': d,
450 'lastchange': d,
450 'lastchange_sort': d[1]-d[0],
451 'lastchange_sort': d[1]-d[0],
451 'archives': archivelist(u, "tip", url),
452 'archives': archivelist(u, "tip", url),
452 'isdirectory': None,
453 'isdirectory': None,
453 'labels': u.configlist('web', 'labels', untrusted=True),
454 'labels': u.configlist('web', 'labels', untrusted=True),
454 }
455 }
455
456
456 yield row
457 yield row
457
458
458 sortdefault = None, False
459 sortdefault = None, False
459 def entries(sortcolumn="", descending=False, subdir="", **map):
460 def entries(sortcolumn="", descending=False, subdir="", **map):
460 rows = rawentries(subdir=subdir, **map)
461 rows = rawentries(subdir=subdir, **map)
461
462
462 if sortcolumn and sortdefault != (sortcolumn, descending):
463 if sortcolumn and sortdefault != (sortcolumn, descending):
463 sortkey = '%s_sort' % sortcolumn
464 sortkey = '%s_sort' % sortcolumn
464 rows = sorted(rows, key=lambda x: x[sortkey],
465 rows = sorted(rows, key=lambda x: x[sortkey],
465 reverse=descending)
466 reverse=descending)
466 for row, parity in zip(rows, paritygen(self.stripecount)):
467 for row, parity in zip(rows, paritygen(self.stripecount)):
467 row['parity'] = parity
468 row['parity'] = parity
468 yield row
469 yield row
469
470
470 self.refresh()
471 self.refresh()
471 sortable = ["name", "description", "contact", "lastchange"]
472 sortable = ["name", "description", "contact", "lastchange"]
472 sortcolumn, descending = sortdefault
473 sortcolumn, descending = sortdefault
473 if 'sort' in wsgireq.form:
474 if 'sort' in wsgireq.form:
474 sortcolumn = wsgireq.form['sort'][0]
475 sortcolumn = wsgireq.form['sort'][0]
475 descending = sortcolumn.startswith('-')
476 descending = sortcolumn.startswith('-')
476 if descending:
477 if descending:
477 sortcolumn = sortcolumn[1:]
478 sortcolumn = sortcolumn[1:]
478 if sortcolumn not in sortable:
479 if sortcolumn not in sortable:
479 sortcolumn = ""
480 sortcolumn = ""
480
481
481 sort = [("sort_%s" % column,
482 sort = [("sort_%s" % column,
482 "%s%s" % ((not descending and column == sortcolumn)
483 "%s%s" % ((not descending and column == sortcolumn)
483 and "-" or "", column))
484 and "-" or "", column))
484 for column in sortable]
485 for column in sortable]
485
486
486 self.refresh()
487 self.refresh()
487 self.updatereqenv(wsgireq.env)
488 self.updatereqenv(wsgireq.env)
488
489
489 return tmpl("index", entries=entries, subdir=subdir,
490 return tmpl("index", entries=entries, subdir=subdir,
490 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
491 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
491 sortcolumn=sortcolumn, descending=descending,
492 sortcolumn=sortcolumn, descending=descending,
492 **dict(sort))
493 **dict(sort))
493
494
494 def templater(self, wsgireq, nonce):
495 def templater(self, wsgireq, nonce):
495
496
496 def motd(**map):
497 def motd(**map):
497 if self.motd is not None:
498 if self.motd is not None:
498 yield self.motd
499 yield self.motd
499 else:
500 else:
500 yield config('web', 'motd')
501 yield config('web', 'motd')
501
502
502 def config(section, name, default=uimod._unset, untrusted=True):
503 def config(section, name, default=uimod._unset, untrusted=True):
503 return self.ui.config(section, name, default, untrusted)
504 return self.ui.config(section, name, default, untrusted)
504
505
505 self.updatereqenv(wsgireq.env)
506 self.updatereqenv(wsgireq.env)
506
507
507 url = wsgireq.env.get('SCRIPT_NAME', '')
508 url = wsgireq.env.get('SCRIPT_NAME', '')
508 if not url.endswith('/'):
509 if not url.endswith('/'):
509 url += '/'
510 url += '/'
510
511
511 vars = {}
512 vars = {}
512 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq, config,
513 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq, config,
513 self.templatepath)
514 self.templatepath)
514 if style == styles[0]:
515 if style == styles[0]:
515 vars['style'] = style
516 vars['style'] = style
516
517
517 sessionvars = webutil.sessionvars(vars, r'?')
518 sessionvars = webutil.sessionvars(vars, r'?')
518 logourl = config('web', 'logourl')
519 logourl = config('web', 'logourl')
519 logoimg = config('web', 'logoimg')
520 logoimg = config('web', 'logoimg')
520 staticurl = config('web', 'staticurl') or url + 'static/'
521 staticurl = config('web', 'staticurl') or url + 'static/'
521 if not staticurl.endswith('/'):
522 if not staticurl.endswith('/'):
522 staticurl += '/'
523 staticurl += '/'
523
524
524 defaults = {
525 defaults = {
525 "encoding": encoding.encoding,
526 "encoding": encoding.encoding,
526 "motd": motd,
527 "motd": motd,
527 "url": url,
528 "url": url,
528 "logourl": logourl,
529 "logourl": logourl,
529 "logoimg": logoimg,
530 "logoimg": logoimg,
530 "staticurl": staticurl,
531 "staticurl": staticurl,
531 "sessionvars": sessionvars,
532 "sessionvars": sessionvars,
532 "style": style,
533 "style": style,
533 "nonce": nonce,
534 "nonce": nonce,
534 }
535 }
535 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
536 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
536 return tmpl
537 return tmpl
537
538
538 def updatereqenv(self, env):
539 def updatereqenv(self, env):
539 if self._baseurl is not None:
540 if self._baseurl is not None:
540 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
541 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
541 env['SERVER_NAME'] = name
542 env['SERVER_NAME'] = name
542 env['SERVER_PORT'] = port
543 env['SERVER_PORT'] = port
543 env['SCRIPT_NAME'] = path
544 env['SCRIPT_NAME'] = path
@@ -1,363 +1,374 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 of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import cgi
11 import cgi
12 import errno
12 import errno
13 import socket
13 import socket
14 import wsgiref.headers as wsgiheaders
14 import wsgiref.headers as wsgiheaders
15 #import wsgiref.validate
15 #import wsgiref.validate
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_MODIFIED,
19 HTTP_NOT_MODIFIED,
20 statusmessage,
20 statusmessage,
21 )
21 )
22
22
23 from ..thirdparty import (
23 from ..thirdparty import (
24 attr,
24 attr,
25 )
25 )
26 from .. import (
26 from .. import (
27 pycompat,
27 pycompat,
28 util,
28 util,
29 )
29 )
30
30
31 shortcuts = {
31 shortcuts = {
32 'cl': [('cmd', ['changelog']), ('rev', None)],
32 'cl': [('cmd', ['changelog']), ('rev', None)],
33 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 'sl': [('cmd', ['shortlog']), ('rev', None)],
34 'cs': [('cmd', ['changeset']), ('node', None)],
34 'cs': [('cmd', ['changeset']), ('node', None)],
35 'f': [('cmd', ['file']), ('filenode', None)],
35 'f': [('cmd', ['file']), ('filenode', None)],
36 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 'fl': [('cmd', ['filelog']), ('filenode', None)],
37 'fd': [('cmd', ['filediff']), ('node', None)],
37 'fd': [('cmd', ['filediff']), ('node', None)],
38 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 'fa': [('cmd', ['annotate']), ('filenode', None)],
39 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 'mf': [('cmd', ['manifest']), ('manifest', None)],
40 'ca': [('cmd', ['archive']), ('node', None)],
40 'ca': [('cmd', ['archive']), ('node', None)],
41 'tags': [('cmd', ['tags'])],
41 'tags': [('cmd', ['tags'])],
42 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
43 'static': [('cmd', ['static']), ('file', None)]
43 'static': [('cmd', ['static']), ('file', None)]
44 }
44 }
45
45
46 def normalize(form):
46 def normalize(form):
47 # first expand the shortcuts
47 # first expand the shortcuts
48 for k in shortcuts:
48 for k in shortcuts:
49 if k in form:
49 if k in form:
50 for name, value in shortcuts[k]:
50 for name, value in shortcuts[k]:
51 if value is None:
51 if value is None:
52 value = form[k]
52 value = form[k]
53 form[name] = value
53 form[name] = value
54 del form[k]
54 del form[k]
55 # And strip the values
55 # And strip the values
56 bytesform = {}
56 bytesform = {}
57 for k, v in form.iteritems():
57 for k, v in form.iteritems():
58 bytesform[pycompat.bytesurl(k)] = [
58 bytesform[pycompat.bytesurl(k)] = [
59 pycompat.bytesurl(i.strip()) for i in v]
59 pycompat.bytesurl(i.strip()) for i in v]
60 return bytesform
60 return bytesform
61
61
62 @attr.s(frozen=True)
62 @attr.s(frozen=True)
63 class parsedrequest(object):
63 class parsedrequest(object):
64 """Represents a parsed WSGI request / static HTTP request parameters."""
64 """Represents a parsed WSGI request.
65
66 Contains both parsed parameters as well as a handle on the input stream.
67 """
65
68
66 # Request method.
69 # Request method.
67 method = attr.ib()
70 method = attr.ib()
68 # Full URL for this request.
71 # Full URL for this request.
69 url = attr.ib()
72 url = attr.ib()
70 # URL without any path components. Just <proto>://<host><port>.
73 # URL without any path components. Just <proto>://<host><port>.
71 baseurl = attr.ib()
74 baseurl = attr.ib()
72 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
75 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
73 # of HTTP: Host header for hostname. This is likely what clients used.
76 # of HTTP: Host header for hostname. This is likely what clients used.
74 advertisedurl = attr.ib()
77 advertisedurl = attr.ib()
75 advertisedbaseurl = attr.ib()
78 advertisedbaseurl = attr.ib()
76 # WSGI application path.
79 # WSGI application path.
77 apppath = attr.ib()
80 apppath = attr.ib()
78 # List of path parts to be used for dispatch.
81 # List of path parts to be used for dispatch.
79 dispatchparts = attr.ib()
82 dispatchparts = attr.ib()
80 # URL path component (no query string) used for dispatch.
83 # URL path component (no query string) used for dispatch.
81 dispatchpath = attr.ib()
84 dispatchpath = attr.ib()
82 # Whether there is a path component to this request. This can be true
85 # Whether there is a path component to this request. This can be true
83 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
86 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
84 havepathinfo = attr.ib()
87 havepathinfo = attr.ib()
85 # Raw query string (part after "?" in URL).
88 # Raw query string (part after "?" in URL).
86 querystring = attr.ib()
89 querystring = attr.ib()
87 # List of 2-tuples of query string arguments.
90 # List of 2-tuples of query string arguments.
88 querystringlist = attr.ib()
91 querystringlist = attr.ib()
89 # Dict of query string arguments. Values are lists with at least 1 item.
92 # Dict of query string arguments. Values are lists with at least 1 item.
90 querystringdict = attr.ib()
93 querystringdict = attr.ib()
91 # wsgiref.headers.Headers instance. Operates like a dict with case
94 # wsgiref.headers.Headers instance. Operates like a dict with case
92 # insensitive keys.
95 # insensitive keys.
93 headers = attr.ib()
96 headers = attr.ib()
97 # Request body input stream.
98 bodyfh = attr.ib()
94
99
95 def parserequestfromenv(env):
100 def parserequestfromenv(env, bodyfh):
96 """Parse URL components from environment variables.
101 """Parse URL components from environment variables.
97
102
98 WSGI defines request attributes via environment variables. This function
103 WSGI defines request attributes via environment variables. This function
99 parses the environment variables into a data structure.
104 parses the environment variables into a data structure.
100 """
105 """
101 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
106 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
102
107
103 # We first validate that the incoming object conforms with the WSGI spec.
108 # We first validate that the incoming object conforms with the WSGI spec.
104 # We only want to be dealing with spec-conforming WSGI implementations.
109 # We only want to be dealing with spec-conforming WSGI implementations.
105 # TODO enable this once we fix internal violations.
110 # TODO enable this once we fix internal violations.
106 #wsgiref.validate.check_environ(env)
111 #wsgiref.validate.check_environ(env)
107
112
108 # PEP-0333 states that environment keys and values are native strings
113 # PEP-0333 states that environment keys and values are native strings
109 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
114 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
110 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
115 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
111 # in Mercurial, so mass convert string keys and values to bytes.
116 # in Mercurial, so mass convert string keys and values to bytes.
112 if pycompat.ispy3:
117 if pycompat.ispy3:
113 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
118 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
114 env = {k: v.encode('latin-1') if isinstance(v, str) else v
119 env = {k: v.encode('latin-1') if isinstance(v, str) else v
115 for k, v in env.iteritems()}
120 for k, v in env.iteritems()}
116
121
117 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
122 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
118 # the environment variables.
123 # the environment variables.
119 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
124 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
120 # how URLs are reconstructed.
125 # how URLs are reconstructed.
121 fullurl = env['wsgi.url_scheme'] + '://'
126 fullurl = env['wsgi.url_scheme'] + '://'
122 advertisedfullurl = fullurl
127 advertisedfullurl = fullurl
123
128
124 def addport(s):
129 def addport(s):
125 if env['wsgi.url_scheme'] == 'https':
130 if env['wsgi.url_scheme'] == 'https':
126 if env['SERVER_PORT'] != '443':
131 if env['SERVER_PORT'] != '443':
127 s += ':' + env['SERVER_PORT']
132 s += ':' + env['SERVER_PORT']
128 else:
133 else:
129 if env['SERVER_PORT'] != '80':
134 if env['SERVER_PORT'] != '80':
130 s += ':' + env['SERVER_PORT']
135 s += ':' + env['SERVER_PORT']
131
136
132 return s
137 return s
133
138
134 if env.get('HTTP_HOST'):
139 if env.get('HTTP_HOST'):
135 fullurl += env['HTTP_HOST']
140 fullurl += env['HTTP_HOST']
136 else:
141 else:
137 fullurl += env['SERVER_NAME']
142 fullurl += env['SERVER_NAME']
138 fullurl = addport(fullurl)
143 fullurl = addport(fullurl)
139
144
140 advertisedfullurl += env['SERVER_NAME']
145 advertisedfullurl += env['SERVER_NAME']
141 advertisedfullurl = addport(advertisedfullurl)
146 advertisedfullurl = addport(advertisedfullurl)
142
147
143 baseurl = fullurl
148 baseurl = fullurl
144 advertisedbaseurl = advertisedfullurl
149 advertisedbaseurl = advertisedfullurl
145
150
146 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
151 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
147 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
152 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
148 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
153 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
149 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
154 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
150
155
151 if env.get('QUERY_STRING'):
156 if env.get('QUERY_STRING'):
152 fullurl += '?' + env['QUERY_STRING']
157 fullurl += '?' + env['QUERY_STRING']
153 advertisedfullurl += '?' + env['QUERY_STRING']
158 advertisedfullurl += '?' + env['QUERY_STRING']
154
159
155 # When dispatching requests, we look at the URL components (PATH_INFO
160 # When dispatching requests, we look at the URL components (PATH_INFO
156 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
161 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
157 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
162 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
158 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
163 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
159 # root. We also exclude its path components from PATH_INFO when resolving
164 # root. We also exclude its path components from PATH_INFO when resolving
160 # the dispatch path.
165 # the dispatch path.
161
166
162 apppath = env['SCRIPT_NAME']
167 apppath = env['SCRIPT_NAME']
163
168
164 if env.get('REPO_NAME'):
169 if env.get('REPO_NAME'):
165 if not apppath.endswith('/'):
170 if not apppath.endswith('/'):
166 apppath += '/'
171 apppath += '/'
167
172
168 apppath += env.get('REPO_NAME')
173 apppath += env.get('REPO_NAME')
169
174
170 if 'PATH_INFO' in env:
175 if 'PATH_INFO' in env:
171 dispatchparts = env['PATH_INFO'].strip('/').split('/')
176 dispatchparts = env['PATH_INFO'].strip('/').split('/')
172
177
173 # Strip out repo parts.
178 # Strip out repo parts.
174 repoparts = env.get('REPO_NAME', '').split('/')
179 repoparts = env.get('REPO_NAME', '').split('/')
175 if dispatchparts[:len(repoparts)] == repoparts:
180 if dispatchparts[:len(repoparts)] == repoparts:
176 dispatchparts = dispatchparts[len(repoparts):]
181 dispatchparts = dispatchparts[len(repoparts):]
177 else:
182 else:
178 dispatchparts = []
183 dispatchparts = []
179
184
180 dispatchpath = '/'.join(dispatchparts)
185 dispatchpath = '/'.join(dispatchparts)
181
186
182 querystring = env.get('QUERY_STRING', '')
187 querystring = env.get('QUERY_STRING', '')
183
188
184 # We store as a list so we have ordering information. We also store as
189 # We store as a list so we have ordering information. We also store as
185 # a dict to facilitate fast lookup.
190 # a dict to facilitate fast lookup.
186 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
191 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
187
192
188 querystringdict = {}
193 querystringdict = {}
189 for k, v in querystringlist:
194 for k, v in querystringlist:
190 if k in querystringdict:
195 if k in querystringdict:
191 querystringdict[k].append(v)
196 querystringdict[k].append(v)
192 else:
197 else:
193 querystringdict[k] = [v]
198 querystringdict[k] = [v]
194
199
195 # HTTP_* keys contain HTTP request headers. The Headers structure should
200 # HTTP_* keys contain HTTP request headers. The Headers structure should
196 # perform case normalization for us. We just rewrite underscore to dash
201 # perform case normalization for us. We just rewrite underscore to dash
197 # so keys match what likely went over the wire.
202 # so keys match what likely went over the wire.
198 headers = []
203 headers = []
199 for k, v in env.iteritems():
204 for k, v in env.iteritems():
200 if k.startswith('HTTP_'):
205 if k.startswith('HTTP_'):
201 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
206 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
202
207
203 headers = wsgiheaders.Headers(headers)
208 headers = wsgiheaders.Headers(headers)
204
209
205 # This is kind of a lie because the HTTP header wasn't explicitly
210 # This is kind of a lie because the HTTP header wasn't explicitly
206 # sent. But for all intents and purposes it should be OK to lie about
211 # sent. But for all intents and purposes it should be OK to lie about
207 # this, since a consumer will either either value to determine how many
212 # this, since a consumer will either either value to determine how many
208 # bytes are available to read.
213 # bytes are available to read.
209 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
214 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
210 headers['Content-Length'] = env['CONTENT_LENGTH']
215 headers['Content-Length'] = env['CONTENT_LENGTH']
211
216
217 # TODO do this once we remove wsgirequest.inp, otherwise we could have
218 # multiple readers from the underlying input stream.
219 #bodyfh = env['wsgi.input']
220 #if 'Content-Length' in headers:
221 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
222
212 return parsedrequest(method=env['REQUEST_METHOD'],
223 return parsedrequest(method=env['REQUEST_METHOD'],
213 url=fullurl, baseurl=baseurl,
224 url=fullurl, baseurl=baseurl,
214 advertisedurl=advertisedfullurl,
225 advertisedurl=advertisedfullurl,
215 advertisedbaseurl=advertisedbaseurl,
226 advertisedbaseurl=advertisedbaseurl,
216 apppath=apppath,
227 apppath=apppath,
217 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
228 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
218 havepathinfo='PATH_INFO' in env,
229 havepathinfo='PATH_INFO' in env,
219 querystring=querystring,
230 querystring=querystring,
220 querystringlist=querystringlist,
231 querystringlist=querystringlist,
221 querystringdict=querystringdict,
232 querystringdict=querystringdict,
222 headers=headers)
233 headers=headers,
234 bodyfh=bodyfh)
223
235
224 class wsgirequest(object):
236 class wsgirequest(object):
225 """Higher-level API for a WSGI request.
237 """Higher-level API for a WSGI request.
226
238
227 WSGI applications are invoked with 2 arguments. They are used to
239 WSGI applications are invoked with 2 arguments. They are used to
228 instantiate instances of this class, which provides higher-level APIs
240 instantiate instances of this class, which provides higher-level APIs
229 for obtaining request parameters, writing HTTP output, etc.
241 for obtaining request parameters, writing HTTP output, etc.
230 """
242 """
231 def __init__(self, wsgienv, start_response):
243 def __init__(self, wsgienv, start_response):
232 version = wsgienv[r'wsgi.version']
244 version = wsgienv[r'wsgi.version']
233 if (version < (1, 0)) or (version >= (2, 0)):
245 if (version < (1, 0)) or (version >= (2, 0)):
234 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
246 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
235 % version)
247 % version)
236 self.inp = wsgienv[r'wsgi.input']
248
249 inp = wsgienv[r'wsgi.input']
237
250
238 if r'HTTP_CONTENT_LENGTH' in wsgienv:
251 if r'HTTP_CONTENT_LENGTH' in wsgienv:
239 self.inp = util.cappedreader(self.inp,
252 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
240 int(wsgienv[r'HTTP_CONTENT_LENGTH']))
241 elif r'CONTENT_LENGTH' in wsgienv:
253 elif r'CONTENT_LENGTH' in wsgienv:
242 self.inp = util.cappedreader(self.inp,
254 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
243 int(wsgienv[r'CONTENT_LENGTH']))
244
255
245 self.err = wsgienv[r'wsgi.errors']
256 self.err = wsgienv[r'wsgi.errors']
246 self.threaded = wsgienv[r'wsgi.multithread']
257 self.threaded = wsgienv[r'wsgi.multithread']
247 self.multiprocess = wsgienv[r'wsgi.multiprocess']
258 self.multiprocess = wsgienv[r'wsgi.multiprocess']
248 self.run_once = wsgienv[r'wsgi.run_once']
259 self.run_once = wsgienv[r'wsgi.run_once']
249 self.env = wsgienv
260 self.env = wsgienv
250 self.form = normalize(cgi.parse(self.inp,
261 self.form = normalize(cgi.parse(inp,
251 self.env,
262 self.env,
252 keep_blank_values=1))
263 keep_blank_values=1))
253 self._start_response = start_response
264 self._start_response = start_response
254 self.server_write = None
265 self.server_write = None
255 self.headers = []
266 self.headers = []
256
267
257 self.req = parserequestfromenv(wsgienv)
268 self.req = parserequestfromenv(wsgienv, inp)
258
269
259 def respond(self, status, type, filename=None, body=None):
270 def respond(self, status, type, filename=None, body=None):
260 if not isinstance(type, str):
271 if not isinstance(type, str):
261 type = pycompat.sysstr(type)
272 type = pycompat.sysstr(type)
262 if self._start_response is not None:
273 if self._start_response is not None:
263 self.headers.append((r'Content-Type', type))
274 self.headers.append((r'Content-Type', type))
264 if filename:
275 if filename:
265 filename = (filename.rpartition('/')[-1]
276 filename = (filename.rpartition('/')[-1]
266 .replace('\\', '\\\\').replace('"', '\\"'))
277 .replace('\\', '\\\\').replace('"', '\\"'))
267 self.headers.append(('Content-Disposition',
278 self.headers.append(('Content-Disposition',
268 'inline; filename="%s"' % filename))
279 'inline; filename="%s"' % filename))
269 if body is not None:
280 if body is not None:
270 self.headers.append((r'Content-Length', str(len(body))))
281 self.headers.append((r'Content-Length', str(len(body))))
271
282
272 for k, v in self.headers:
283 for k, v in self.headers:
273 if not isinstance(v, str):
284 if not isinstance(v, str):
274 raise TypeError('header value must be string: %r' % (v,))
285 raise TypeError('header value must be string: %r' % (v,))
275
286
276 if isinstance(status, ErrorResponse):
287 if isinstance(status, ErrorResponse):
277 self.headers.extend(status.headers)
288 self.headers.extend(status.headers)
278 if status.code == HTTP_NOT_MODIFIED:
289 if status.code == HTTP_NOT_MODIFIED:
279 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
290 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
280 # it MUST NOT include any headers other than these and no
291 # it MUST NOT include any headers other than these and no
281 # body
292 # body
282 self.headers = [(k, v) for (k, v) in self.headers if
293 self.headers = [(k, v) for (k, v) in self.headers if
283 k in ('Date', 'ETag', 'Expires',
294 k in ('Date', 'ETag', 'Expires',
284 'Cache-Control', 'Vary')]
295 'Cache-Control', 'Vary')]
285 status = statusmessage(status.code, pycompat.bytestr(status))
296 status = statusmessage(status.code, pycompat.bytestr(status))
286 elif status == 200:
297 elif status == 200:
287 status = '200 Script output follows'
298 status = '200 Script output follows'
288 elif isinstance(status, int):
299 elif isinstance(status, int):
289 status = statusmessage(status)
300 status = statusmessage(status)
290
301
291 # Various HTTP clients (notably httplib) won't read the HTTP
302 # Various HTTP clients (notably httplib) won't read the HTTP
292 # response until the HTTP request has been sent in full. If servers
303 # response until the HTTP request has been sent in full. If servers
293 # (us) send a response before the HTTP request has been fully sent,
304 # (us) send a response before the HTTP request has been fully sent,
294 # the connection may deadlock because neither end is reading.
305 # the connection may deadlock because neither end is reading.
295 #
306 #
296 # We work around this by "draining" the request data before
307 # We work around this by "draining" the request data before
297 # sending any response in some conditions.
308 # sending any response in some conditions.
298 drain = False
309 drain = False
299 close = False
310 close = False
300
311
301 # If the client sent Expect: 100-continue, we assume it is smart
312 # If the client sent Expect: 100-continue, we assume it is smart
302 # enough to deal with the server sending a response before reading
313 # enough to deal with the server sending a response before reading
303 # the request. (httplib doesn't do this.)
314 # the request. (httplib doesn't do this.)
304 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
315 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
305 pass
316 pass
306 # Only tend to request methods that have bodies. Strictly speaking,
317 # Only tend to request methods that have bodies. Strictly speaking,
307 # we should sniff for a body. But this is fine for our existing
318 # we should sniff for a body. But this is fine for our existing
308 # WSGI applications.
319 # WSGI applications.
309 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
320 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
310 pass
321 pass
311 else:
322 else:
312 # If we don't know how much data to read, there's no guarantee
323 # If we don't know how much data to read, there's no guarantee
313 # that we can drain the request responsibly. The WSGI
324 # that we can drain the request responsibly. The WSGI
314 # specification only says that servers *should* ensure the
325 # specification only says that servers *should* ensure the
315 # input stream doesn't overrun the actual request. So there's
326 # input stream doesn't overrun the actual request. So there's
316 # no guarantee that reading until EOF won't corrupt the stream
327 # no guarantee that reading until EOF won't corrupt the stream
317 # state.
328 # state.
318 if not isinstance(self.inp, util.cappedreader):
329 if not isinstance(self.req.bodyfh, util.cappedreader):
319 close = True
330 close = True
320 else:
331 else:
321 # We /could/ only drain certain HTTP response codes. But 200
332 # We /could/ only drain certain HTTP response codes. But 200
322 # and non-200 wire protocol responses both require draining.
333 # and non-200 wire protocol responses both require draining.
323 # Since we have a capped reader in place for all situations
334 # Since we have a capped reader in place for all situations
324 # where we drain, it is safe to read from that stream. We'll
335 # where we drain, it is safe to read from that stream. We'll
325 # either do a drain or no-op if we're already at EOF.
336 # either do a drain or no-op if we're already at EOF.
326 drain = True
337 drain = True
327
338
328 if close:
339 if close:
329 self.headers.append((r'Connection', r'Close'))
340 self.headers.append((r'Connection', r'Close'))
330
341
331 if drain:
342 if drain:
332 assert isinstance(self.inp, util.cappedreader)
343 assert isinstance(self.req.bodyfh, util.cappedreader)
333 while True:
344 while True:
334 chunk = self.inp.read(32768)
345 chunk = self.req.bodyfh.read(32768)
335 if not chunk:
346 if not chunk:
336 break
347 break
337
348
338 self.server_write = self._start_response(
349 self.server_write = self._start_response(
339 pycompat.sysstr(status), self.headers)
350 pycompat.sysstr(status), self.headers)
340 self._start_response = None
351 self._start_response = None
341 self.headers = []
352 self.headers = []
342 if body is not None:
353 if body is not None:
343 self.write(body)
354 self.write(body)
344 self.server_write = None
355 self.server_write = None
345
356
346 def write(self, thing):
357 def write(self, thing):
347 if thing:
358 if thing:
348 try:
359 try:
349 self.server_write(thing)
360 self.server_write(thing)
350 except socket.error as inst:
361 except socket.error as inst:
351 if inst[0] != errno.ECONNRESET:
362 if inst[0] != errno.ECONNRESET:
352 raise
363 raise
353
364
354 def flush(self):
365 def flush(self):
355 return None
366 return None
356
367
357 def wsgiapplication(app_maker):
368 def wsgiapplication(app_maker):
358 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
369 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
359 can and should now be used as a WSGI application.'''
370 can and should now be used as a WSGI application.'''
360 application = app_maker()
371 application = app_maker()
361 def run_wsgi(env, respond):
372 def run_wsgi(env, respond):
362 return application(env, respond)
373 return application(env, respond)
363 return run_wsgi
374 return run_wsgi
@@ -1,649 +1,649 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10 import struct
10 import struct
11 import sys
11 import sys
12 import threading
12 import threading
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 hook,
18 hook,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireproto,
21 wireproto,
22 wireprototypes,
22 wireprototypes,
23 )
23 )
24
24
25 stringio = util.stringio
25 stringio = util.stringio
26
26
27 urlerr = util.urlerr
27 urlerr = util.urlerr
28 urlreq = util.urlreq
28 urlreq = util.urlreq
29
29
30 HTTP_OK = 200
30 HTTP_OK = 200
31
31
32 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE = 'application/mercurial-0.1'
33 HGTYPE2 = 'application/mercurial-0.2'
33 HGTYPE2 = 'application/mercurial-0.2'
34 HGERRTYPE = 'application/hg-error'
34 HGERRTYPE = 'application/hg-error'
35
35
36 SSHV1 = wireprototypes.SSHV1
36 SSHV1 = wireprototypes.SSHV1
37 SSHV2 = wireprototypes.SSHV2
37 SSHV2 = wireprototypes.SSHV2
38
38
39 def decodevaluefromheaders(req, headerprefix):
39 def decodevaluefromheaders(req, headerprefix):
40 """Decode a long value from multiple HTTP request headers.
40 """Decode a long value from multiple HTTP request headers.
41
41
42 Returns the value as a bytes, not a str.
42 Returns the value as a bytes, not a str.
43 """
43 """
44 chunks = []
44 chunks = []
45 i = 1
45 i = 1
46 while True:
46 while True:
47 v = req.headers.get(b'%s-%d' % (headerprefix, i))
47 v = req.headers.get(b'%s-%d' % (headerprefix, i))
48 if v is None:
48 if v is None:
49 break
49 break
50 chunks.append(pycompat.bytesurl(v))
50 chunks.append(pycompat.bytesurl(v))
51 i += 1
51 i += 1
52
52
53 return ''.join(chunks)
53 return ''.join(chunks)
54
54
55 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
55 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
56 def __init__(self, wsgireq, req, ui, checkperm):
56 def __init__(self, wsgireq, req, ui, checkperm):
57 self._wsgireq = wsgireq
57 self._wsgireq = wsgireq
58 self._req = req
58 self._req = req
59 self._ui = ui
59 self._ui = ui
60 self._checkperm = checkperm
60 self._checkperm = checkperm
61
61
62 @property
62 @property
63 def name(self):
63 def name(self):
64 return 'http-v1'
64 return 'http-v1'
65
65
66 def getargs(self, args):
66 def getargs(self, args):
67 knownargs = self._args()
67 knownargs = self._args()
68 data = {}
68 data = {}
69 keys = args.split()
69 keys = args.split()
70 for k in keys:
70 for k in keys:
71 if k == '*':
71 if k == '*':
72 star = {}
72 star = {}
73 for key in knownargs.keys():
73 for key in knownargs.keys():
74 if key != 'cmd' and key not in keys:
74 if key != 'cmd' and key not in keys:
75 star[key] = knownargs[key][0]
75 star[key] = knownargs[key][0]
76 data['*'] = star
76 data['*'] = star
77 else:
77 else:
78 data[k] = knownargs[k][0]
78 data[k] = knownargs[k][0]
79 return [data[k] for k in keys]
79 return [data[k] for k in keys]
80
80
81 def _args(self):
81 def _args(self):
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
83 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
83 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
84 if postlen:
84 if postlen:
85 args.update(urlreq.parseqs(
85 args.update(urlreq.parseqs(
86 self._wsgireq.inp.read(postlen), keep_blank_values=True))
86 self._req.bodyfh.read(postlen), keep_blank_values=True))
87 return args
87 return args
88
88
89 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
89 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
91 return args
91 return args
92
92
93 def forwardpayload(self, fp):
93 def forwardpayload(self, fp):
94 # Existing clients *always* send Content-Length.
94 # Existing clients *always* send Content-Length.
95 length = int(self._req.headers[b'Content-Length'])
95 length = int(self._req.headers[b'Content-Length'])
96
96
97 # If httppostargs is used, we need to read Content-Length
97 # If httppostargs is used, we need to read Content-Length
98 # minus the amount that was consumed by args.
98 # minus the amount that was consumed by args.
99 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
99 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
100 for s in util.filechunkiter(self._wsgireq.inp, limit=length):
100 for s in util.filechunkiter(self._req.bodyfh, limit=length):
101 fp.write(s)
101 fp.write(s)
102
102
103 @contextlib.contextmanager
103 @contextlib.contextmanager
104 def mayberedirectstdio(self):
104 def mayberedirectstdio(self):
105 oldout = self._ui.fout
105 oldout = self._ui.fout
106 olderr = self._ui.ferr
106 olderr = self._ui.ferr
107
107
108 out = util.stringio()
108 out = util.stringio()
109
109
110 try:
110 try:
111 self._ui.fout = out
111 self._ui.fout = out
112 self._ui.ferr = out
112 self._ui.ferr = out
113 yield out
113 yield out
114 finally:
114 finally:
115 self._ui.fout = oldout
115 self._ui.fout = oldout
116 self._ui.ferr = olderr
116 self._ui.ferr = olderr
117
117
118 def client(self):
118 def client(self):
119 return 'remote:%s:%s:%s' % (
119 return 'remote:%s:%s:%s' % (
120 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
120 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
121 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
121 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
122 urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
122 urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
123
123
124 def addcapabilities(self, repo, caps):
124 def addcapabilities(self, repo, caps):
125 caps.append('httpheader=%d' %
125 caps.append('httpheader=%d' %
126 repo.ui.configint('server', 'maxhttpheaderlen'))
126 repo.ui.configint('server', 'maxhttpheaderlen'))
127 if repo.ui.configbool('experimental', 'httppostargs'):
127 if repo.ui.configbool('experimental', 'httppostargs'):
128 caps.append('httppostargs')
128 caps.append('httppostargs')
129
129
130 # FUTURE advertise 0.2rx once support is implemented
130 # FUTURE advertise 0.2rx once support is implemented
131 # FUTURE advertise minrx and mintx after consulting config option
131 # FUTURE advertise minrx and mintx after consulting config option
132 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
132 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
133
133
134 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
134 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
135 if compengines:
135 if compengines:
136 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
136 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
137 for e in compengines)
137 for e in compengines)
138 caps.append('compression=%s' % comptypes)
138 caps.append('compression=%s' % comptypes)
139
139
140 return caps
140 return caps
141
141
142 def checkperm(self, perm):
142 def checkperm(self, perm):
143 return self._checkperm(perm)
143 return self._checkperm(perm)
144
144
145 # This method exists mostly so that extensions like remotefilelog can
145 # This method exists mostly so that extensions like remotefilelog can
146 # disable a kludgey legacy method only over http. As of early 2018,
146 # disable a kludgey legacy method only over http. As of early 2018,
147 # there are no other known users, so with any luck we can discard this
147 # there are no other known users, so with any luck we can discard this
148 # hook if remotefilelog becomes a first-party extension.
148 # hook if remotefilelog becomes a first-party extension.
149 def iscmd(cmd):
149 def iscmd(cmd):
150 return cmd in wireproto.commands
150 return cmd in wireproto.commands
151
151
152 def handlewsgirequest(rctx, wsgireq, req, checkperm):
152 def handlewsgirequest(rctx, wsgireq, req, checkperm):
153 """Possibly process a wire protocol request.
153 """Possibly process a wire protocol request.
154
154
155 If the current request is a wire protocol request, the request is
155 If the current request is a wire protocol request, the request is
156 processed by this function.
156 processed by this function.
157
157
158 ``wsgireq`` is a ``wsgirequest`` instance.
158 ``wsgireq`` is a ``wsgirequest`` instance.
159 ``req`` is a ``parsedrequest`` instance.
159 ``req`` is a ``parsedrequest`` instance.
160
160
161 Returns a 2-tuple of (bool, response) where the 1st element indicates
161 Returns a 2-tuple of (bool, response) where the 1st element indicates
162 whether the request was handled and the 2nd element is a return
162 whether the request was handled and the 2nd element is a return
163 value for a WSGI application (often a generator of bytes).
163 value for a WSGI application (often a generator of bytes).
164 """
164 """
165 # Avoid cycle involving hg module.
165 # Avoid cycle involving hg module.
166 from .hgweb import common as hgwebcommon
166 from .hgweb import common as hgwebcommon
167
167
168 repo = rctx.repo
168 repo = rctx.repo
169
169
170 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
170 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
171 # string parameter. If it isn't present, this isn't a wire protocol
171 # string parameter. If it isn't present, this isn't a wire protocol
172 # request.
172 # request.
173 if 'cmd' not in req.querystringdict:
173 if 'cmd' not in req.querystringdict:
174 return False, None
174 return False, None
175
175
176 cmd = req.querystringdict['cmd'][0]
176 cmd = req.querystringdict['cmd'][0]
177
177
178 # The "cmd" request parameter is used by both the wire protocol and hgweb.
178 # The "cmd" request parameter is used by both the wire protocol and hgweb.
179 # While not all wire protocol commands are available for all transports,
179 # While not all wire protocol commands are available for all transports,
180 # if we see a "cmd" value that resembles a known wire protocol command, we
180 # if we see a "cmd" value that resembles a known wire protocol command, we
181 # route it to a protocol handler. This is better than routing possible
181 # route it to a protocol handler. This is better than routing possible
182 # wire protocol requests to hgweb because it prevents hgweb from using
182 # wire protocol requests to hgweb because it prevents hgweb from using
183 # known wire protocol commands and it is less confusing for machine
183 # known wire protocol commands and it is less confusing for machine
184 # clients.
184 # clients.
185 if not iscmd(cmd):
185 if not iscmd(cmd):
186 return False, None
186 return False, None
187
187
188 # The "cmd" query string argument is only valid on the root path of the
188 # The "cmd" query string argument is only valid on the root path of the
189 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
189 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
190 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
190 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
191 # in this case. We send an HTTP 404 for backwards compatibility reasons.
191 # in this case. We send an HTTP 404 for backwards compatibility reasons.
192 if req.dispatchpath:
192 if req.dispatchpath:
193 res = _handlehttperror(
193 res = _handlehttperror(
194 hgwebcommon.ErrorResponse(hgwebcommon.HTTP_NOT_FOUND), wsgireq,
194 hgwebcommon.ErrorResponse(hgwebcommon.HTTP_NOT_FOUND), wsgireq,
195 req)
195 req)
196
196
197 return True, res
197 return True, res
198
198
199 proto = httpv1protocolhandler(wsgireq, req, repo.ui,
199 proto = httpv1protocolhandler(wsgireq, req, repo.ui,
200 lambda perm: checkperm(rctx, wsgireq, perm))
200 lambda perm: checkperm(rctx, wsgireq, perm))
201
201
202 # The permissions checker should be the only thing that can raise an
202 # The permissions checker should be the only thing that can raise an
203 # ErrorResponse. It is kind of a layer violation to catch an hgweb
203 # ErrorResponse. It is kind of a layer violation to catch an hgweb
204 # exception here. So consider refactoring into a exception type that
204 # exception here. So consider refactoring into a exception type that
205 # is associated with the wire protocol.
205 # is associated with the wire protocol.
206 try:
206 try:
207 res = _callhttp(repo, wsgireq, req, proto, cmd)
207 res = _callhttp(repo, wsgireq, req, proto, cmd)
208 except hgwebcommon.ErrorResponse as e:
208 except hgwebcommon.ErrorResponse as e:
209 res = _handlehttperror(e, wsgireq, req)
209 res = _handlehttperror(e, wsgireq, req)
210
210
211 return True, res
211 return True, res
212
212
213 def _httpresponsetype(ui, req, prefer_uncompressed):
213 def _httpresponsetype(ui, req, prefer_uncompressed):
214 """Determine the appropriate response type and compression settings.
214 """Determine the appropriate response type and compression settings.
215
215
216 Returns a tuple of (mediatype, compengine, engineopts).
216 Returns a tuple of (mediatype, compengine, engineopts).
217 """
217 """
218 # Determine the response media type and compression engine based
218 # Determine the response media type and compression engine based
219 # on the request parameters.
219 # on the request parameters.
220 protocaps = decodevaluefromheaders(req, 'X-HgProto').split(' ')
220 protocaps = decodevaluefromheaders(req, 'X-HgProto').split(' ')
221
221
222 if '0.2' in protocaps:
222 if '0.2' in protocaps:
223 # All clients are expected to support uncompressed data.
223 # All clients are expected to support uncompressed data.
224 if prefer_uncompressed:
224 if prefer_uncompressed:
225 return HGTYPE2, util._noopengine(), {}
225 return HGTYPE2, util._noopengine(), {}
226
226
227 # Default as defined by wire protocol spec.
227 # Default as defined by wire protocol spec.
228 compformats = ['zlib', 'none']
228 compformats = ['zlib', 'none']
229 for cap in protocaps:
229 for cap in protocaps:
230 if cap.startswith('comp='):
230 if cap.startswith('comp='):
231 compformats = cap[5:].split(',')
231 compformats = cap[5:].split(',')
232 break
232 break
233
233
234 # Now find an agreed upon compression format.
234 # Now find an agreed upon compression format.
235 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
235 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
236 if engine.wireprotosupport().name in compformats:
236 if engine.wireprotosupport().name in compformats:
237 opts = {}
237 opts = {}
238 level = ui.configint('server', '%slevel' % engine.name())
238 level = ui.configint('server', '%slevel' % engine.name())
239 if level is not None:
239 if level is not None:
240 opts['level'] = level
240 opts['level'] = level
241
241
242 return HGTYPE2, engine, opts
242 return HGTYPE2, engine, opts
243
243
244 # No mutually supported compression format. Fall back to the
244 # No mutually supported compression format. Fall back to the
245 # legacy protocol.
245 # legacy protocol.
246
246
247 # Don't allow untrusted settings because disabling compression or
247 # Don't allow untrusted settings because disabling compression or
248 # setting a very high compression level could lead to flooding
248 # setting a very high compression level could lead to flooding
249 # the server's network or CPU.
249 # the server's network or CPU.
250 opts = {'level': ui.configint('server', 'zliblevel')}
250 opts = {'level': ui.configint('server', 'zliblevel')}
251 return HGTYPE, util.compengines['zlib'], opts
251 return HGTYPE, util.compengines['zlib'], opts
252
252
253 def _callhttp(repo, wsgireq, req, proto, cmd):
253 def _callhttp(repo, wsgireq, req, proto, cmd):
254 def genversion2(gen, engine, engineopts):
254 def genversion2(gen, engine, engineopts):
255 # application/mercurial-0.2 always sends a payload header
255 # application/mercurial-0.2 always sends a payload header
256 # identifying the compression engine.
256 # identifying the compression engine.
257 name = engine.wireprotosupport().name
257 name = engine.wireprotosupport().name
258 assert 0 < len(name) < 256
258 assert 0 < len(name) < 256
259 yield struct.pack('B', len(name))
259 yield struct.pack('B', len(name))
260 yield name
260 yield name
261
261
262 for chunk in gen:
262 for chunk in gen:
263 yield chunk
263 yield chunk
264
264
265 if not wireproto.commands.commandavailable(cmd, proto):
265 if not wireproto.commands.commandavailable(cmd, proto):
266 wsgireq.respond(HTTP_OK, HGERRTYPE,
266 wsgireq.respond(HTTP_OK, HGERRTYPE,
267 body=_('requested wire protocol command is not '
267 body=_('requested wire protocol command is not '
268 'available over HTTP'))
268 'available over HTTP'))
269 return []
269 return []
270
270
271 proto.checkperm(wireproto.commands[cmd].permission)
271 proto.checkperm(wireproto.commands[cmd].permission)
272
272
273 rsp = wireproto.dispatch(repo, proto, cmd)
273 rsp = wireproto.dispatch(repo, proto, cmd)
274
274
275 if isinstance(rsp, bytes):
275 if isinstance(rsp, bytes):
276 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
276 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
277 return []
277 return []
278 elif isinstance(rsp, wireprototypes.bytesresponse):
278 elif isinstance(rsp, wireprototypes.bytesresponse):
279 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
279 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
280 return []
280 return []
281 elif isinstance(rsp, wireprototypes.streamreslegacy):
281 elif isinstance(rsp, wireprototypes.streamreslegacy):
282 gen = rsp.gen
282 gen = rsp.gen
283 wsgireq.respond(HTTP_OK, HGTYPE)
283 wsgireq.respond(HTTP_OK, HGTYPE)
284 return gen
284 return gen
285 elif isinstance(rsp, wireprototypes.streamres):
285 elif isinstance(rsp, wireprototypes.streamres):
286 gen = rsp.gen
286 gen = rsp.gen
287
287
288 # This code for compression should not be streamres specific. It
288 # This code for compression should not be streamres specific. It
289 # is here because we only compress streamres at the moment.
289 # is here because we only compress streamres at the moment.
290 mediatype, engine, engineopts = _httpresponsetype(
290 mediatype, engine, engineopts = _httpresponsetype(
291 repo.ui, req, rsp.prefer_uncompressed)
291 repo.ui, req, rsp.prefer_uncompressed)
292 gen = engine.compressstream(gen, engineopts)
292 gen = engine.compressstream(gen, engineopts)
293
293
294 if mediatype == HGTYPE2:
294 if mediatype == HGTYPE2:
295 gen = genversion2(gen, engine, engineopts)
295 gen = genversion2(gen, engine, engineopts)
296
296
297 wsgireq.respond(HTTP_OK, mediatype)
297 wsgireq.respond(HTTP_OK, mediatype)
298 return gen
298 return gen
299 elif isinstance(rsp, wireprototypes.pushres):
299 elif isinstance(rsp, wireprototypes.pushres):
300 rsp = '%d\n%s' % (rsp.res, rsp.output)
300 rsp = '%d\n%s' % (rsp.res, rsp.output)
301 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
301 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
302 return []
302 return []
303 elif isinstance(rsp, wireprototypes.pusherr):
303 elif isinstance(rsp, wireprototypes.pusherr):
304 rsp = '0\n%s\n' % rsp.res
304 rsp = '0\n%s\n' % rsp.res
305 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
305 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
306 return []
306 return []
307 elif isinstance(rsp, wireprototypes.ooberror):
307 elif isinstance(rsp, wireprototypes.ooberror):
308 rsp = rsp.message
308 rsp = rsp.message
309 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
309 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
310 return []
310 return []
311 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
311 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
312
312
313 def _handlehttperror(e, wsgireq, req):
313 def _handlehttperror(e, wsgireq, req):
314 """Called when an ErrorResponse is raised during HTTP request processing."""
314 """Called when an ErrorResponse is raised during HTTP request processing."""
315
315
316 # TODO This response body assumes the failed command was
316 # TODO This response body assumes the failed command was
317 # "unbundle." That assumption is not always valid.
317 # "unbundle." That assumption is not always valid.
318 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
318 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
319
319
320 return ''
320 return ''
321
321
322 def _sshv1respondbytes(fout, value):
322 def _sshv1respondbytes(fout, value):
323 """Send a bytes response for protocol version 1."""
323 """Send a bytes response for protocol version 1."""
324 fout.write('%d\n' % len(value))
324 fout.write('%d\n' % len(value))
325 fout.write(value)
325 fout.write(value)
326 fout.flush()
326 fout.flush()
327
327
328 def _sshv1respondstream(fout, source):
328 def _sshv1respondstream(fout, source):
329 write = fout.write
329 write = fout.write
330 for chunk in source.gen:
330 for chunk in source.gen:
331 write(chunk)
331 write(chunk)
332 fout.flush()
332 fout.flush()
333
333
334 def _sshv1respondooberror(fout, ferr, rsp):
334 def _sshv1respondooberror(fout, ferr, rsp):
335 ferr.write(b'%s\n-\n' % rsp)
335 ferr.write(b'%s\n-\n' % rsp)
336 ferr.flush()
336 ferr.flush()
337 fout.write(b'\n')
337 fout.write(b'\n')
338 fout.flush()
338 fout.flush()
339
339
340 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
340 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
341 """Handler for requests services via version 1 of SSH protocol."""
341 """Handler for requests services via version 1 of SSH protocol."""
342 def __init__(self, ui, fin, fout):
342 def __init__(self, ui, fin, fout):
343 self._ui = ui
343 self._ui = ui
344 self._fin = fin
344 self._fin = fin
345 self._fout = fout
345 self._fout = fout
346
346
347 @property
347 @property
348 def name(self):
348 def name(self):
349 return wireprototypes.SSHV1
349 return wireprototypes.SSHV1
350
350
351 def getargs(self, args):
351 def getargs(self, args):
352 data = {}
352 data = {}
353 keys = args.split()
353 keys = args.split()
354 for n in xrange(len(keys)):
354 for n in xrange(len(keys)):
355 argline = self._fin.readline()[:-1]
355 argline = self._fin.readline()[:-1]
356 arg, l = argline.split()
356 arg, l = argline.split()
357 if arg not in keys:
357 if arg not in keys:
358 raise error.Abort(_("unexpected parameter %r") % arg)
358 raise error.Abort(_("unexpected parameter %r") % arg)
359 if arg == '*':
359 if arg == '*':
360 star = {}
360 star = {}
361 for k in xrange(int(l)):
361 for k in xrange(int(l)):
362 argline = self._fin.readline()[:-1]
362 argline = self._fin.readline()[:-1]
363 arg, l = argline.split()
363 arg, l = argline.split()
364 val = self._fin.read(int(l))
364 val = self._fin.read(int(l))
365 star[arg] = val
365 star[arg] = val
366 data['*'] = star
366 data['*'] = star
367 else:
367 else:
368 val = self._fin.read(int(l))
368 val = self._fin.read(int(l))
369 data[arg] = val
369 data[arg] = val
370 return [data[k] for k in keys]
370 return [data[k] for k in keys]
371
371
372 def forwardpayload(self, fpout):
372 def forwardpayload(self, fpout):
373 # We initially send an empty response. This tells the client it is
373 # We initially send an empty response. This tells the client it is
374 # OK to start sending data. If a client sees any other response, it
374 # OK to start sending data. If a client sees any other response, it
375 # interprets it as an error.
375 # interprets it as an error.
376 _sshv1respondbytes(self._fout, b'')
376 _sshv1respondbytes(self._fout, b'')
377
377
378 # The file is in the form:
378 # The file is in the form:
379 #
379 #
380 # <chunk size>\n<chunk>
380 # <chunk size>\n<chunk>
381 # ...
381 # ...
382 # 0\n
382 # 0\n
383 count = int(self._fin.readline())
383 count = int(self._fin.readline())
384 while count:
384 while count:
385 fpout.write(self._fin.read(count))
385 fpout.write(self._fin.read(count))
386 count = int(self._fin.readline())
386 count = int(self._fin.readline())
387
387
388 @contextlib.contextmanager
388 @contextlib.contextmanager
389 def mayberedirectstdio(self):
389 def mayberedirectstdio(self):
390 yield None
390 yield None
391
391
392 def client(self):
392 def client(self):
393 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
393 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
394 return 'remote:ssh:' + client
394 return 'remote:ssh:' + client
395
395
396 def addcapabilities(self, repo, caps):
396 def addcapabilities(self, repo, caps):
397 return caps
397 return caps
398
398
399 def checkperm(self, perm):
399 def checkperm(self, perm):
400 pass
400 pass
401
401
402 class sshv2protocolhandler(sshv1protocolhandler):
402 class sshv2protocolhandler(sshv1protocolhandler):
403 """Protocol handler for version 2 of the SSH protocol."""
403 """Protocol handler for version 2 of the SSH protocol."""
404
404
405 @property
405 @property
406 def name(self):
406 def name(self):
407 return wireprototypes.SSHV2
407 return wireprototypes.SSHV2
408
408
409 def _runsshserver(ui, repo, fin, fout, ev):
409 def _runsshserver(ui, repo, fin, fout, ev):
410 # This function operates like a state machine of sorts. The following
410 # This function operates like a state machine of sorts. The following
411 # states are defined:
411 # states are defined:
412 #
412 #
413 # protov1-serving
413 # protov1-serving
414 # Server is in protocol version 1 serving mode. Commands arrive on
414 # Server is in protocol version 1 serving mode. Commands arrive on
415 # new lines. These commands are processed in this state, one command
415 # new lines. These commands are processed in this state, one command
416 # after the other.
416 # after the other.
417 #
417 #
418 # protov2-serving
418 # protov2-serving
419 # Server is in protocol version 2 serving mode.
419 # Server is in protocol version 2 serving mode.
420 #
420 #
421 # upgrade-initial
421 # upgrade-initial
422 # The server is going to process an upgrade request.
422 # The server is going to process an upgrade request.
423 #
423 #
424 # upgrade-v2-filter-legacy-handshake
424 # upgrade-v2-filter-legacy-handshake
425 # The protocol is being upgraded to version 2. The server is expecting
425 # The protocol is being upgraded to version 2. The server is expecting
426 # the legacy handshake from version 1.
426 # the legacy handshake from version 1.
427 #
427 #
428 # upgrade-v2-finish
428 # upgrade-v2-finish
429 # The upgrade to version 2 of the protocol is imminent.
429 # The upgrade to version 2 of the protocol is imminent.
430 #
430 #
431 # shutdown
431 # shutdown
432 # The server is shutting down, possibly in reaction to a client event.
432 # The server is shutting down, possibly in reaction to a client event.
433 #
433 #
434 # And here are their transitions:
434 # And here are their transitions:
435 #
435 #
436 # protov1-serving -> shutdown
436 # protov1-serving -> shutdown
437 # When server receives an empty request or encounters another
437 # When server receives an empty request or encounters another
438 # error.
438 # error.
439 #
439 #
440 # protov1-serving -> upgrade-initial
440 # protov1-serving -> upgrade-initial
441 # An upgrade request line was seen.
441 # An upgrade request line was seen.
442 #
442 #
443 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
443 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
444 # Upgrade to version 2 in progress. Server is expecting to
444 # Upgrade to version 2 in progress. Server is expecting to
445 # process a legacy handshake.
445 # process a legacy handshake.
446 #
446 #
447 # upgrade-v2-filter-legacy-handshake -> shutdown
447 # upgrade-v2-filter-legacy-handshake -> shutdown
448 # Client did not fulfill upgrade handshake requirements.
448 # Client did not fulfill upgrade handshake requirements.
449 #
449 #
450 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
450 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
451 # Client fulfilled version 2 upgrade requirements. Finishing that
451 # Client fulfilled version 2 upgrade requirements. Finishing that
452 # upgrade.
452 # upgrade.
453 #
453 #
454 # upgrade-v2-finish -> protov2-serving
454 # upgrade-v2-finish -> protov2-serving
455 # Protocol upgrade to version 2 complete. Server can now speak protocol
455 # Protocol upgrade to version 2 complete. Server can now speak protocol
456 # version 2.
456 # version 2.
457 #
457 #
458 # protov2-serving -> protov1-serving
458 # protov2-serving -> protov1-serving
459 # Ths happens by default since protocol version 2 is the same as
459 # Ths happens by default since protocol version 2 is the same as
460 # version 1 except for the handshake.
460 # version 1 except for the handshake.
461
461
462 state = 'protov1-serving'
462 state = 'protov1-serving'
463 proto = sshv1protocolhandler(ui, fin, fout)
463 proto = sshv1protocolhandler(ui, fin, fout)
464 protoswitched = False
464 protoswitched = False
465
465
466 while not ev.is_set():
466 while not ev.is_set():
467 if state == 'protov1-serving':
467 if state == 'protov1-serving':
468 # Commands are issued on new lines.
468 # Commands are issued on new lines.
469 request = fin.readline()[:-1]
469 request = fin.readline()[:-1]
470
470
471 # Empty lines signal to terminate the connection.
471 # Empty lines signal to terminate the connection.
472 if not request:
472 if not request:
473 state = 'shutdown'
473 state = 'shutdown'
474 continue
474 continue
475
475
476 # It looks like a protocol upgrade request. Transition state to
476 # It looks like a protocol upgrade request. Transition state to
477 # handle it.
477 # handle it.
478 if request.startswith(b'upgrade '):
478 if request.startswith(b'upgrade '):
479 if protoswitched:
479 if protoswitched:
480 _sshv1respondooberror(fout, ui.ferr,
480 _sshv1respondooberror(fout, ui.ferr,
481 b'cannot upgrade protocols multiple '
481 b'cannot upgrade protocols multiple '
482 b'times')
482 b'times')
483 state = 'shutdown'
483 state = 'shutdown'
484 continue
484 continue
485
485
486 state = 'upgrade-initial'
486 state = 'upgrade-initial'
487 continue
487 continue
488
488
489 available = wireproto.commands.commandavailable(request, proto)
489 available = wireproto.commands.commandavailable(request, proto)
490
490
491 # This command isn't available. Send an empty response and go
491 # This command isn't available. Send an empty response and go
492 # back to waiting for a new command.
492 # back to waiting for a new command.
493 if not available:
493 if not available:
494 _sshv1respondbytes(fout, b'')
494 _sshv1respondbytes(fout, b'')
495 continue
495 continue
496
496
497 rsp = wireproto.dispatch(repo, proto, request)
497 rsp = wireproto.dispatch(repo, proto, request)
498
498
499 if isinstance(rsp, bytes):
499 if isinstance(rsp, bytes):
500 _sshv1respondbytes(fout, rsp)
500 _sshv1respondbytes(fout, rsp)
501 elif isinstance(rsp, wireprototypes.bytesresponse):
501 elif isinstance(rsp, wireprototypes.bytesresponse):
502 _sshv1respondbytes(fout, rsp.data)
502 _sshv1respondbytes(fout, rsp.data)
503 elif isinstance(rsp, wireprototypes.streamres):
503 elif isinstance(rsp, wireprototypes.streamres):
504 _sshv1respondstream(fout, rsp)
504 _sshv1respondstream(fout, rsp)
505 elif isinstance(rsp, wireprototypes.streamreslegacy):
505 elif isinstance(rsp, wireprototypes.streamreslegacy):
506 _sshv1respondstream(fout, rsp)
506 _sshv1respondstream(fout, rsp)
507 elif isinstance(rsp, wireprototypes.pushres):
507 elif isinstance(rsp, wireprototypes.pushres):
508 _sshv1respondbytes(fout, b'')
508 _sshv1respondbytes(fout, b'')
509 _sshv1respondbytes(fout, b'%d' % rsp.res)
509 _sshv1respondbytes(fout, b'%d' % rsp.res)
510 elif isinstance(rsp, wireprototypes.pusherr):
510 elif isinstance(rsp, wireprototypes.pusherr):
511 _sshv1respondbytes(fout, rsp.res)
511 _sshv1respondbytes(fout, rsp.res)
512 elif isinstance(rsp, wireprototypes.ooberror):
512 elif isinstance(rsp, wireprototypes.ooberror):
513 _sshv1respondooberror(fout, ui.ferr, rsp.message)
513 _sshv1respondooberror(fout, ui.ferr, rsp.message)
514 else:
514 else:
515 raise error.ProgrammingError('unhandled response type from '
515 raise error.ProgrammingError('unhandled response type from '
516 'wire protocol command: %s' % rsp)
516 'wire protocol command: %s' % rsp)
517
517
518 # For now, protocol version 2 serving just goes back to version 1.
518 # For now, protocol version 2 serving just goes back to version 1.
519 elif state == 'protov2-serving':
519 elif state == 'protov2-serving':
520 state = 'protov1-serving'
520 state = 'protov1-serving'
521 continue
521 continue
522
522
523 elif state == 'upgrade-initial':
523 elif state == 'upgrade-initial':
524 # We should never transition into this state if we've switched
524 # We should never transition into this state if we've switched
525 # protocols.
525 # protocols.
526 assert not protoswitched
526 assert not protoswitched
527 assert proto.name == wireprototypes.SSHV1
527 assert proto.name == wireprototypes.SSHV1
528
528
529 # Expected: upgrade <token> <capabilities>
529 # Expected: upgrade <token> <capabilities>
530 # If we get something else, the request is malformed. It could be
530 # If we get something else, the request is malformed. It could be
531 # from a future client that has altered the upgrade line content.
531 # from a future client that has altered the upgrade line content.
532 # We treat this as an unknown command.
532 # We treat this as an unknown command.
533 try:
533 try:
534 token, caps = request.split(b' ')[1:]
534 token, caps = request.split(b' ')[1:]
535 except ValueError:
535 except ValueError:
536 _sshv1respondbytes(fout, b'')
536 _sshv1respondbytes(fout, b'')
537 state = 'protov1-serving'
537 state = 'protov1-serving'
538 continue
538 continue
539
539
540 # Send empty response if we don't support upgrading protocols.
540 # Send empty response if we don't support upgrading protocols.
541 if not ui.configbool('experimental', 'sshserver.support-v2'):
541 if not ui.configbool('experimental', 'sshserver.support-v2'):
542 _sshv1respondbytes(fout, b'')
542 _sshv1respondbytes(fout, b'')
543 state = 'protov1-serving'
543 state = 'protov1-serving'
544 continue
544 continue
545
545
546 try:
546 try:
547 caps = urlreq.parseqs(caps)
547 caps = urlreq.parseqs(caps)
548 except ValueError:
548 except ValueError:
549 _sshv1respondbytes(fout, b'')
549 _sshv1respondbytes(fout, b'')
550 state = 'protov1-serving'
550 state = 'protov1-serving'
551 continue
551 continue
552
552
553 # We don't see an upgrade request to protocol version 2. Ignore
553 # We don't see an upgrade request to protocol version 2. Ignore
554 # the upgrade request.
554 # the upgrade request.
555 wantedprotos = caps.get(b'proto', [b''])[0]
555 wantedprotos = caps.get(b'proto', [b''])[0]
556 if SSHV2 not in wantedprotos:
556 if SSHV2 not in wantedprotos:
557 _sshv1respondbytes(fout, b'')
557 _sshv1respondbytes(fout, b'')
558 state = 'protov1-serving'
558 state = 'protov1-serving'
559 continue
559 continue
560
560
561 # It looks like we can honor this upgrade request to protocol 2.
561 # It looks like we can honor this upgrade request to protocol 2.
562 # Filter the rest of the handshake protocol request lines.
562 # Filter the rest of the handshake protocol request lines.
563 state = 'upgrade-v2-filter-legacy-handshake'
563 state = 'upgrade-v2-filter-legacy-handshake'
564 continue
564 continue
565
565
566 elif state == 'upgrade-v2-filter-legacy-handshake':
566 elif state == 'upgrade-v2-filter-legacy-handshake':
567 # Client should have sent legacy handshake after an ``upgrade``
567 # Client should have sent legacy handshake after an ``upgrade``
568 # request. Expected lines:
568 # request. Expected lines:
569 #
569 #
570 # hello
570 # hello
571 # between
571 # between
572 # pairs 81
572 # pairs 81
573 # 0000...-0000...
573 # 0000...-0000...
574
574
575 ok = True
575 ok = True
576 for line in (b'hello', b'between', b'pairs 81'):
576 for line in (b'hello', b'between', b'pairs 81'):
577 request = fin.readline()[:-1]
577 request = fin.readline()[:-1]
578
578
579 if request != line:
579 if request != line:
580 _sshv1respondooberror(fout, ui.ferr,
580 _sshv1respondooberror(fout, ui.ferr,
581 b'malformed handshake protocol: '
581 b'malformed handshake protocol: '
582 b'missing %s' % line)
582 b'missing %s' % line)
583 ok = False
583 ok = False
584 state = 'shutdown'
584 state = 'shutdown'
585 break
585 break
586
586
587 if not ok:
587 if not ok:
588 continue
588 continue
589
589
590 request = fin.read(81)
590 request = fin.read(81)
591 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
591 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
592 _sshv1respondooberror(fout, ui.ferr,
592 _sshv1respondooberror(fout, ui.ferr,
593 b'malformed handshake protocol: '
593 b'malformed handshake protocol: '
594 b'missing between argument value')
594 b'missing between argument value')
595 state = 'shutdown'
595 state = 'shutdown'
596 continue
596 continue
597
597
598 state = 'upgrade-v2-finish'
598 state = 'upgrade-v2-finish'
599 continue
599 continue
600
600
601 elif state == 'upgrade-v2-finish':
601 elif state == 'upgrade-v2-finish':
602 # Send the upgrade response.
602 # Send the upgrade response.
603 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
603 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
604 servercaps = wireproto.capabilities(repo, proto)
604 servercaps = wireproto.capabilities(repo, proto)
605 rsp = b'capabilities: %s' % servercaps.data
605 rsp = b'capabilities: %s' % servercaps.data
606 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
606 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
607 fout.flush()
607 fout.flush()
608
608
609 proto = sshv2protocolhandler(ui, fin, fout)
609 proto = sshv2protocolhandler(ui, fin, fout)
610 protoswitched = True
610 protoswitched = True
611
611
612 state = 'protov2-serving'
612 state = 'protov2-serving'
613 continue
613 continue
614
614
615 elif state == 'shutdown':
615 elif state == 'shutdown':
616 break
616 break
617
617
618 else:
618 else:
619 raise error.ProgrammingError('unhandled ssh server state: %s' %
619 raise error.ProgrammingError('unhandled ssh server state: %s' %
620 state)
620 state)
621
621
622 class sshserver(object):
622 class sshserver(object):
623 def __init__(self, ui, repo, logfh=None):
623 def __init__(self, ui, repo, logfh=None):
624 self._ui = ui
624 self._ui = ui
625 self._repo = repo
625 self._repo = repo
626 self._fin = ui.fin
626 self._fin = ui.fin
627 self._fout = ui.fout
627 self._fout = ui.fout
628
628
629 # Log write I/O to stdout and stderr if configured.
629 # Log write I/O to stdout and stderr if configured.
630 if logfh:
630 if logfh:
631 self._fout = util.makeloggingfileobject(
631 self._fout = util.makeloggingfileobject(
632 logfh, self._fout, 'o', logdata=True)
632 logfh, self._fout, 'o', logdata=True)
633 ui.ferr = util.makeloggingfileobject(
633 ui.ferr = util.makeloggingfileobject(
634 logfh, ui.ferr, 'e', logdata=True)
634 logfh, ui.ferr, 'e', logdata=True)
635
635
636 hook.redirect(True)
636 hook.redirect(True)
637 ui.fout = repo.ui.fout = ui.ferr
637 ui.fout = repo.ui.fout = ui.ferr
638
638
639 # Prevent insertion/deletion of CRs
639 # Prevent insertion/deletion of CRs
640 util.setbinary(self._fin)
640 util.setbinary(self._fin)
641 util.setbinary(self._fout)
641 util.setbinary(self._fout)
642
642
643 def serve_forever(self):
643 def serve_forever(self):
644 self.serveuntil(threading.Event())
644 self.serveuntil(threading.Event())
645 sys.exit(0)
645 sys.exit(0)
646
646
647 def serveuntil(self, ev):
647 def serveuntil(self, ev):
648 """Serve until a threading.Event is set."""
648 """Serve until a threading.Event is set."""
649 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
649 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now