##// END OF EJS Templates
hgweb: rename req to wsgireq...
Gregory Szorc -
r36822:b9b968e2 default
parent child Browse files
Show More
@@ -1,473 +1,473 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms 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 contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
26
25
27 from .. import (
26 from .. import (
28 encoding,
27 encoding,
29 error,
28 error,
30 formatter,
29 formatter,
31 hg,
30 hg,
32 hook,
31 hook,
33 profiling,
32 profiling,
34 pycompat,
33 pycompat,
35 repoview,
34 repoview,
36 templatefilters,
35 templatefilters,
37 templater,
36 templater,
38 ui as uimod,
37 ui as uimod,
39 util,
38 util,
40 wireprotoserver,
39 wireprotoserver,
41 )
40 )
42
41
43 from . import (
42 from . import (
43 request as requestmod,
44 webcommands,
44 webcommands,
45 webutil,
45 webutil,
46 wsgicgi,
46 wsgicgi,
47 )
47 )
48
48
49 archivespecs = util.sortdict((
49 archivespecs = util.sortdict((
50 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 ))
53 ))
54
54
55 def getstyle(req, configfn, templatepath):
55 def getstyle(req, configfn, templatepath):
56 fromreq = req.form.get('style', [None])[0]
56 fromreq = req.form.get('style', [None])[0]
57 styles = (
57 styles = (
58 fromreq,
58 fromreq,
59 configfn('web', 'style'),
59 configfn('web', 'style'),
60 'paper',
60 'paper',
61 )
61 )
62 return styles, templater.stylemap(styles, templatepath)
62 return styles, templater.stylemap(styles, templatepath)
63
63
64 def makebreadcrumb(url, prefix=''):
64 def makebreadcrumb(url, prefix=''):
65 '''Return a 'URL breadcrumb' list
65 '''Return a 'URL breadcrumb' list
66
66
67 A 'URL breadcrumb' is a list of URL-name pairs,
67 A 'URL breadcrumb' is a list of URL-name pairs,
68 corresponding to each of the path items on a URL.
68 corresponding to each of the path items on a URL.
69 This can be used to create path navigation entries.
69 This can be used to create path navigation entries.
70 '''
70 '''
71 if url.endswith('/'):
71 if url.endswith('/'):
72 url = url[:-1]
72 url = url[:-1]
73 if prefix:
73 if prefix:
74 url = '/' + prefix + url
74 url = '/' + prefix + url
75 relpath = url
75 relpath = url
76 if relpath.startswith('/'):
76 if relpath.startswith('/'):
77 relpath = relpath[1:]
77 relpath = relpath[1:]
78
78
79 breadcrumb = []
79 breadcrumb = []
80 urlel = url
80 urlel = url
81 pathitems = [''] + relpath.split('/')
81 pathitems = [''] + relpath.split('/')
82 for pathel in reversed(pathitems):
82 for pathel in reversed(pathitems):
83 if not pathel or not urlel:
83 if not pathel or not urlel:
84 break
84 break
85 breadcrumb.append({'url': urlel, 'name': pathel})
85 breadcrumb.append({'url': urlel, 'name': pathel})
86 urlel = os.path.dirname(urlel)
86 urlel = os.path.dirname(urlel)
87 return reversed(breadcrumb)
87 return reversed(breadcrumb)
88
88
89 class requestcontext(object):
89 class requestcontext(object):
90 """Holds state/context for an individual request.
90 """Holds state/context for an individual request.
91
91
92 Servers can be multi-threaded. Holding state on the WSGI application
92 Servers can be multi-threaded. Holding state on the WSGI application
93 is prone to race conditions. Instances of this class exist to hold
93 is prone to race conditions. Instances of this class exist to hold
94 mutable and race-free state for requests.
94 mutable and race-free state for requests.
95 """
95 """
96 def __init__(self, app, repo):
96 def __init__(self, app, repo):
97 self.repo = repo
97 self.repo = repo
98 self.reponame = app.reponame
98 self.reponame = app.reponame
99
99
100 self.archivespecs = archivespecs
100 self.archivespecs = archivespecs
101
101
102 self.maxchanges = self.configint('web', 'maxchanges')
102 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
103 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
105 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
106 self.allowpull = self.configbool('web', 'allow-pull')
107
107
108 # we use untrusted=False to prevent a repo owner from using
108 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
109 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
110 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
111 self.templatepath = self.config('web', 'templates', untrusted=False)
112
112
113 # This object is more expensive to build than simple config values.
113 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
114 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
115 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
116 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
117 # of the request.
118 self.websubtable = app.websubtable
118 self.websubtable = app.websubtable
119
119
120 self.csp, self.nonce = cspvalues(self.repo.ui)
120 self.csp, self.nonce = cspvalues(self.repo.ui)
121
121
122 # Trust the settings from the .hg/hgrc files by default.
122 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
124 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
125 untrusted=untrusted)
126
126
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
128 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
129 untrusted=untrusted)
130
130
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
132 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
133 untrusted=untrusted)
134
134
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
136 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
137 untrusted=untrusted)
138
138
139 def archivelist(self, nodeid):
139 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
144
145 def templater(self, req):
145 def templater(self, wsgireq):
146 # determine scheme, port and server name
146 # determine scheme, port and server name
147 # this is needed to create absolute urls
147 # this is needed to create absolute urls
148
148
149 proto = req.env.get('wsgi.url_scheme')
149 proto = wsgireq.env.get('wsgi.url_scheme')
150 if proto == 'https':
150 if proto == 'https':
151 proto = 'https'
151 proto = 'https'
152 default_port = '443'
152 default_port = '443'
153 else:
153 else:
154 proto = 'http'
154 proto = 'http'
155 default_port = '80'
155 default_port = '80'
156
156
157 port = req.env[r'SERVER_PORT']
157 port = wsgireq.env[r'SERVER_PORT']
158 port = port != default_port and (r':' + port) or r''
158 port = port != default_port and (r':' + port) or r''
159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
159 urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port)
160 logourl = self.config('web', 'logourl')
160 logourl = self.config('web', 'logourl')
161 logoimg = self.config('web', 'logoimg')
161 logoimg = self.config('web', 'logoimg')
162 staticurl = (self.config('web', 'staticurl')
162 staticurl = (self.config('web', 'staticurl')
163 or pycompat.sysbytes(req.url) + 'static/')
163 or pycompat.sysbytes(wsgireq.url) + 'static/')
164 if not staticurl.endswith('/'):
164 if not staticurl.endswith('/'):
165 staticurl += '/'
165 staticurl += '/'
166
166
167 # some functions for the templater
167 # some functions for the templater
168
168
169 def motd(**map):
169 def motd(**map):
170 yield self.config('web', 'motd')
170 yield self.config('web', 'motd')
171
171
172 # figure out which style to use
172 # figure out which style to use
173
173
174 vars = {}
174 vars = {}
175 styles, (style, mapfile) = getstyle(req, self.config,
175 styles, (style, mapfile) = getstyle(wsgireq, self.config,
176 self.templatepath)
176 self.templatepath)
177 if style == styles[0]:
177 if style == styles[0]:
178 vars['style'] = style
178 vars['style'] = style
179
179
180 start = '&' if req.url[-1] == r'?' else '?'
180 start = '&' if wsgireq.url[-1] == r'?' else '?'
181 sessionvars = webutil.sessionvars(vars, start)
181 sessionvars = webutil.sessionvars(vars, start)
182
182
183 if not self.reponame:
183 if not self.reponame:
184 self.reponame = (self.config('web', 'name', '')
184 self.reponame = (self.config('web', 'name', '')
185 or req.env.get('REPO_NAME')
185 or wsgireq.env.get('REPO_NAME')
186 or req.url.strip(r'/') or self.repo.root)
186 or wsgireq.url.strip(r'/') or self.repo.root)
187
187
188 def websubfilter(text):
188 def websubfilter(text):
189 return templatefilters.websub(text, self.websubtable)
189 return templatefilters.websub(text, self.websubtable)
190
190
191 # create the templater
191 # create the templater
192 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 # TODO: export all keywords: defaults = templatekw.keywords.copy()
193 defaults = {
193 defaults = {
194 'url': pycompat.sysbytes(req.url),
194 'url': pycompat.sysbytes(wsgireq.url),
195 'logourl': logourl,
195 'logourl': logourl,
196 'logoimg': logoimg,
196 'logoimg': logoimg,
197 'staticurl': staticurl,
197 'staticurl': staticurl,
198 'urlbase': urlbase,
198 'urlbase': urlbase,
199 'repo': self.reponame,
199 'repo': self.reponame,
200 'encoding': encoding.encoding,
200 'encoding': encoding.encoding,
201 'motd': motd,
201 'motd': motd,
202 'sessionvars': sessionvars,
202 'sessionvars': sessionvars,
203 'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)),
203 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
204 'style': style,
204 'style': style,
205 'nonce': self.nonce,
205 'nonce': self.nonce,
206 }
206 }
207 tres = formatter.templateresources(self.repo.ui, self.repo)
207 tres = formatter.templateresources(self.repo.ui, self.repo)
208 tmpl = templater.templater.frommapfile(mapfile,
208 tmpl = templater.templater.frommapfile(mapfile,
209 filters={'websub': websubfilter},
209 filters={'websub': websubfilter},
210 defaults=defaults,
210 defaults=defaults,
211 resources=tres)
211 resources=tres)
212 return tmpl
212 return tmpl
213
213
214
214
215 class hgweb(object):
215 class hgweb(object):
216 """HTTP server for individual repositories.
216 """HTTP server for individual repositories.
217
217
218 Instances of this class serve HTTP responses for a particular
218 Instances of this class serve HTTP responses for a particular
219 repository.
219 repository.
220
220
221 Instances are typically used as WSGI applications.
221 Instances are typically used as WSGI applications.
222
222
223 Some servers are multi-threaded. On these servers, there may
223 Some servers are multi-threaded. On these servers, there may
224 be multiple active threads inside __call__.
224 be multiple active threads inside __call__.
225 """
225 """
226 def __init__(self, repo, name=None, baseui=None):
226 def __init__(self, repo, name=None, baseui=None):
227 if isinstance(repo, str):
227 if isinstance(repo, str):
228 if baseui:
228 if baseui:
229 u = baseui.copy()
229 u = baseui.copy()
230 else:
230 else:
231 u = uimod.ui.load()
231 u = uimod.ui.load()
232 r = hg.repository(u, repo)
232 r = hg.repository(u, repo)
233 else:
233 else:
234 # we trust caller to give us a private copy
234 # we trust caller to give us a private copy
235 r = repo
235 r = repo
236
236
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
241 # resolve file patterns relative to repo root
241 # resolve file patterns relative to repo root
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
244 # displaying bundling progress bar while serving feel wrong and may
244 # displaying bundling progress bar while serving feel wrong and may
245 # break some wsgi implementation.
245 # break some wsgi implementation.
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
249 self._lastrepo = self._repos[0]
249 self._lastrepo = self._repos[0]
250 hook.redirect(True)
250 hook.redirect(True)
251 self.reponame = name
251 self.reponame = name
252
252
253 def _webifyrepo(self, repo):
253 def _webifyrepo(self, repo):
254 repo = getwebview(repo)
254 repo = getwebview(repo)
255 self.websubtable = webutil.getwebsubs(repo)
255 self.websubtable = webutil.getwebsubs(repo)
256 return repo
256 return repo
257
257
258 @contextlib.contextmanager
258 @contextlib.contextmanager
259 def _obtainrepo(self):
259 def _obtainrepo(self):
260 """Obtain a repo unique to the caller.
260 """Obtain a repo unique to the caller.
261
261
262 Internally we maintain a stack of cachedlocalrepo instances
262 Internally we maintain a stack of cachedlocalrepo instances
263 to be handed out. If one is available, we pop it and return it,
263 to be handed out. If one is available, we pop it and return it,
264 ensuring it is up to date in the process. If one is not available,
264 ensuring it is up to date in the process. If one is not available,
265 we clone the most recently used repo instance and return it.
265 we clone the most recently used repo instance and return it.
266
266
267 It is currently possible for the stack to grow without bounds
267 It is currently possible for the stack to grow without bounds
268 if the server allows infinite threads. However, servers should
268 if the server allows infinite threads. However, servers should
269 have a thread limit, thus establishing our limit.
269 have a thread limit, thus establishing our limit.
270 """
270 """
271 if self._repos:
271 if self._repos:
272 cached = self._repos.pop()
272 cached = self._repos.pop()
273 r, created = cached.fetch()
273 r, created = cached.fetch()
274 else:
274 else:
275 cached = self._lastrepo.copy()
275 cached = self._lastrepo.copy()
276 r, created = cached.fetch()
276 r, created = cached.fetch()
277 if created:
277 if created:
278 r = self._webifyrepo(r)
278 r = self._webifyrepo(r)
279
279
280 self._lastrepo = cached
280 self._lastrepo = cached
281 self.mtime = cached.mtime
281 self.mtime = cached.mtime
282 try:
282 try:
283 yield r
283 yield r
284 finally:
284 finally:
285 self._repos.append(cached)
285 self._repos.append(cached)
286
286
287 def run(self):
287 def run(self):
288 """Start a server from CGI environment.
288 """Start a server from CGI environment.
289
289
290 Modern servers should be using WSGI and should avoid this
290 Modern servers should be using WSGI and should avoid this
291 method, if possible.
291 method, if possible.
292 """
292 """
293 if not encoding.environ.get('GATEWAY_INTERFACE',
293 if not encoding.environ.get('GATEWAY_INTERFACE',
294 '').startswith("CGI/1."):
294 '').startswith("CGI/1."):
295 raise RuntimeError("This function is only intended to be "
295 raise RuntimeError("This function is only intended to be "
296 "called while running as a CGI script.")
296 "called while running as a CGI script.")
297 wsgicgi.launch(self)
297 wsgicgi.launch(self)
298
298
299 def __call__(self, env, respond):
299 def __call__(self, env, respond):
300 """Run the WSGI application.
300 """Run the WSGI application.
301
301
302 This may be called by multiple threads.
302 This may be called by multiple threads.
303 """
303 """
304 req = wsgirequest(env, respond)
304 req = requestmod.wsgirequest(env, respond)
305 return self.run_wsgi(req)
305 return self.run_wsgi(req)
306
306
307 def run_wsgi(self, req):
307 def run_wsgi(self, wsgireq):
308 """Internal method to run the WSGI application.
308 """Internal method to run the WSGI application.
309
309
310 This is typically only called by Mercurial. External consumers
310 This is typically only called by Mercurial. External consumers
311 should be using instances of this class as the WSGI application.
311 should be using instances of this class as the WSGI application.
312 """
312 """
313 with self._obtainrepo() as repo:
313 with self._obtainrepo() as repo:
314 profile = repo.ui.configbool('profiling', 'enabled')
314 profile = repo.ui.configbool('profiling', 'enabled')
315 with profiling.profile(repo.ui, enabled=profile):
315 with profiling.profile(repo.ui, enabled=profile):
316 for r in self._runwsgi(req, repo):
316 for r in self._runwsgi(wsgireq, repo):
317 yield r
317 yield r
318
318
319 def _runwsgi(self, req, repo):
319 def _runwsgi(self, wsgireq, repo):
320 rctx = requestcontext(self, repo)
320 rctx = requestcontext(self, repo)
321
321
322 # This state is global across all threads.
322 # This state is global across all threads.
323 encoding.encoding = rctx.config('web', 'encoding')
323 encoding.encoding = rctx.config('web', 'encoding')
324 rctx.repo.ui.environ = req.env
324 rctx.repo.ui.environ = wsgireq.env
325
325
326 if rctx.csp:
326 if rctx.csp:
327 # hgwebdir may have added CSP header. Since we generate our own,
327 # hgwebdir may have added CSP header. Since we generate our own,
328 # replace it.
328 # replace it.
329 req.headers = [h for h in req.headers
329 wsgireq.headers = [h for h in wsgireq.headers
330 if h[0] != 'Content-Security-Policy']
330 if h[0] != 'Content-Security-Policy']
331 req.headers.append(('Content-Security-Policy', rctx.csp))
331 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
332
332
333 # work with CGI variables to create coherent structure
333 # work with CGI variables to create coherent structure
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
335
335
336 req.url = req.env[r'SCRIPT_NAME']
336 wsgireq.url = wsgireq.env[r'SCRIPT_NAME']
337 if not req.url.endswith(r'/'):
337 if not wsgireq.url.endswith(r'/'):
338 req.url += r'/'
338 wsgireq.url += r'/'
339 if req.env.get('REPO_NAME'):
339 if wsgireq.env.get('REPO_NAME'):
340 req.url += req.env[r'REPO_NAME'] + r'/'
340 wsgireq.url += wsgireq.env[r'REPO_NAME'] + r'/'
341
341
342 if r'PATH_INFO' in req.env:
342 if r'PATH_INFO' in wsgireq.env:
343 parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/')
343 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
344 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
344 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
345 if parts[:len(repo_parts)] == repo_parts:
345 if parts[:len(repo_parts)] == repo_parts:
346 parts = parts[len(repo_parts):]
346 parts = parts[len(repo_parts):]
347 query = r'/'.join(parts)
347 query = r'/'.join(parts)
348 else:
348 else:
349 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
349 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
350 query = query.partition(r';')[0]
350 query = query.partition(r';')[0]
351
351
352 # Route it to a wire protocol handler if it looks like a wire protocol
352 # Route it to a wire protocol handler if it looks like a wire protocol
353 # request.
353 # request.
354 protohandler = wireprotoserver.parsehttprequest(rctx, req, query,
354 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
355 self.check_perm)
355 self.check_perm)
356
356
357 if protohandler:
357 if protohandler:
358 try:
358 try:
359 if query:
359 if query:
360 raise ErrorResponse(HTTP_NOT_FOUND)
360 raise ErrorResponse(HTTP_NOT_FOUND)
361
361
362 return protohandler['dispatch']()
362 return protohandler['dispatch']()
363 except ErrorResponse as inst:
363 except ErrorResponse as inst:
364 return protohandler['handleerror'](inst)
364 return protohandler['handleerror'](inst)
365
365
366 # translate user-visible url structure to internal structure
366 # translate user-visible url structure to internal structure
367
367
368 args = query.split(r'/', 2)
368 args = query.split(r'/', 2)
369 if 'cmd' not in req.form and args and args[0]:
369 if 'cmd' not in wsgireq.form and args and args[0]:
370 cmd = args.pop(0)
370 cmd = args.pop(0)
371 style = cmd.rfind('-')
371 style = cmd.rfind('-')
372 if style != -1:
372 if style != -1:
373 req.form['style'] = [cmd[:style]]
373 wsgireq.form['style'] = [cmd[:style]]
374 cmd = cmd[style + 1:]
374 cmd = cmd[style + 1:]
375
375
376 # avoid accepting e.g. style parameter as command
376 # avoid accepting e.g. style parameter as command
377 if util.safehasattr(webcommands, cmd):
377 if util.safehasattr(webcommands, cmd):
378 req.form['cmd'] = [cmd]
378 wsgireq.form['cmd'] = [cmd]
379
379
380 if cmd == 'static':
380 if cmd == 'static':
381 req.form['file'] = ['/'.join(args)]
381 wsgireq.form['file'] = ['/'.join(args)]
382 else:
382 else:
383 if args and args[0]:
383 if args and args[0]:
384 node = args.pop(0).replace('%2F', '/')
384 node = args.pop(0).replace('%2F', '/')
385 req.form['node'] = [node]
385 wsgireq.form['node'] = [node]
386 if args:
386 if args:
387 req.form['file'] = args
387 wsgireq.form['file'] = args
388
388
389 ua = req.env.get('HTTP_USER_AGENT', '')
389 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
390 if cmd == 'rev' and 'mercurial' in ua:
390 if cmd == 'rev' and 'mercurial' in ua:
391 req.form['style'] = ['raw']
391 wsgireq.form['style'] = ['raw']
392
392
393 if cmd == 'archive':
393 if cmd == 'archive':
394 fn = req.form['node'][0]
394 fn = wsgireq.form['node'][0]
395 for type_, spec in rctx.archivespecs.iteritems():
395 for type_, spec in rctx.archivespecs.iteritems():
396 ext = spec[2]
396 ext = spec[2]
397 if fn.endswith(ext):
397 if fn.endswith(ext):
398 req.form['node'] = [fn[:-len(ext)]]
398 wsgireq.form['node'] = [fn[:-len(ext)]]
399 req.form['type'] = [type_]
399 wsgireq.form['type'] = [type_]
400 else:
400 else:
401 cmd = req.form.get('cmd', [''])[0]
401 cmd = wsgireq.form.get('cmd', [''])[0]
402
402
403 # process the web interface request
403 # process the web interface request
404
404
405 try:
405 try:
406 tmpl = rctx.templater(req)
406 tmpl = rctx.templater(wsgireq)
407 ctype = tmpl('mimetype', encoding=encoding.encoding)
407 ctype = tmpl('mimetype', encoding=encoding.encoding)
408 ctype = templater.stringify(ctype)
408 ctype = templater.stringify(ctype)
409
409
410 # check read permissions non-static content
410 # check read permissions non-static content
411 if cmd != 'static':
411 if cmd != 'static':
412 self.check_perm(rctx, req, None)
412 self.check_perm(rctx, wsgireq, None)
413
413
414 if cmd == '':
414 if cmd == '':
415 req.form['cmd'] = [tmpl.cache['default']]
415 wsgireq.form['cmd'] = [tmpl.cache['default']]
416 cmd = req.form['cmd'][0]
416 cmd = wsgireq.form['cmd'][0]
417
417
418 # Don't enable caching if using a CSP nonce because then it wouldn't
418 # Don't enable caching if using a CSP nonce because then it wouldn't
419 # be a nonce.
419 # be a nonce.
420 if rctx.configbool('web', 'cache') and not rctx.nonce:
420 if rctx.configbool('web', 'cache') and not rctx.nonce:
421 caching(self, req) # sets ETag header or raises NOT_MODIFIED
421 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
422 if cmd not in webcommands.__all__:
422 if cmd not in webcommands.__all__:
423 msg = 'no such method: %s' % cmd
423 msg = 'no such method: %s' % cmd
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
425 elif cmd == 'file' and 'raw' in req.form.get('style', []):
425 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
426 rctx.ctype = ctype
426 rctx.ctype = ctype
427 content = webcommands.rawfile(rctx, req, tmpl)
427 content = webcommands.rawfile(rctx, wsgireq, tmpl)
428 else:
428 else:
429 content = getattr(webcommands, cmd)(rctx, req, tmpl)
429 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
430 req.respond(HTTP_OK, ctype)
430 wsgireq.respond(HTTP_OK, ctype)
431
431
432 return content
432 return content
433
433
434 except (error.LookupError, error.RepoLookupError) as err:
434 except (error.LookupError, error.RepoLookupError) as err:
435 req.respond(HTTP_NOT_FOUND, ctype)
435 wsgireq.respond(HTTP_NOT_FOUND, ctype)
436 msg = pycompat.bytestr(err)
436 msg = pycompat.bytestr(err)
437 if (util.safehasattr(err, 'name') and
437 if (util.safehasattr(err, 'name') and
438 not isinstance(err, error.ManifestLookupError)):
438 not isinstance(err, error.ManifestLookupError)):
439 msg = 'revision not found: %s' % err.name
439 msg = 'revision not found: %s' % err.name
440 return tmpl('error', error=msg)
440 return tmpl('error', error=msg)
441 except (error.RepoError, error.RevlogError) as inst:
441 except (error.RepoError, error.RevlogError) as inst:
442 req.respond(HTTP_SERVER_ERROR, ctype)
442 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
443 return tmpl('error', error=pycompat.bytestr(inst))
443 return tmpl('error', error=pycompat.bytestr(inst))
444 except ErrorResponse as inst:
444 except ErrorResponse as inst:
445 req.respond(inst, ctype)
445 wsgireq.respond(inst, ctype)
446 if inst.code == HTTP_NOT_MODIFIED:
446 if inst.code == HTTP_NOT_MODIFIED:
447 # Not allowed to return a body on a 304
447 # Not allowed to return a body on a 304
448 return ['']
448 return ['']
449 return tmpl('error', error=pycompat.bytestr(inst))
449 return tmpl('error', error=pycompat.bytestr(inst))
450
450
451 def check_perm(self, rctx, req, op):
451 def check_perm(self, rctx, req, op):
452 for permhook in permhooks:
452 for permhook in permhooks:
453 permhook(rctx, req, op)
453 permhook(rctx, req, op)
454
454
455 def getwebview(repo):
455 def getwebview(repo):
456 """The 'web.view' config controls changeset filter to hgweb. Possible
456 """The 'web.view' config controls changeset filter to hgweb. Possible
457 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 values are ``served``, ``visible`` and ``all``. Default is ``served``.
458 The ``served`` filter only shows changesets that can be pulled from the
458 The ``served`` filter only shows changesets that can be pulled from the
459 hgweb instance. The``visible`` filter includes secret changesets but
459 hgweb instance. The``visible`` filter includes secret changesets but
460 still excludes "hidden" one.
460 still excludes "hidden" one.
461
461
462 See the repoview module for details.
462 See the repoview module for details.
463
463
464 The option has been around undocumented since Mercurial 2.5, but no
464 The option has been around undocumented since Mercurial 2.5, but no
465 user ever asked about it. So we better keep it undocumented for now."""
465 user ever asked about it. So we better keep it undocumented for now."""
466 # experimental config: web.view
466 # experimental config: web.view
467 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 viewconfig = repo.ui.config('web', 'view', untrusted=True)
468 if viewconfig == 'all':
468 if viewconfig == 'all':
469 return repo.unfiltered()
469 return repo.unfiltered()
470 elif viewconfig in repoview.filtertable:
470 elif viewconfig in repoview.filtertable:
471 return repo.filtered(viewconfig)
471 return repo.filtered(viewconfig)
472 else:
472 else:
473 return repo.filtered('served')
473 return repo.filtered('served')
@@ -1,539 +1,539 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 from .request import wsgirequest
30
29
31 from .. import (
30 from .. import (
32 configitems,
31 configitems,
33 encoding,
32 encoding,
34 error,
33 error,
35 hg,
34 hg,
36 profiling,
35 profiling,
37 pycompat,
36 pycompat,
38 scmutil,
37 scmutil,
39 templater,
38 templater,
40 ui as uimod,
39 ui as uimod,
41 util,
40 util,
42 )
41 )
43
42
44 from . import (
43 from . import (
45 hgweb_mod,
44 hgweb_mod,
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 req = wsgirequest(env, respond)
200 wsgireq = requestmod.wsgirequest(env, respond)
201 return self.run_wsgi(req)
201 return self.run_wsgi(wsgireq)
202
202
203 def read_allowed(self, ui, req):
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 = req.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, req):
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(req):
228 for r in self._runwsgi(wsgireq):
229 yield r
229 yield r
230
230
231 def _runwsgi(self, req):
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 req.headers.append(('Content-Security-Policy', csp))
237 wsgireq.headers.append(('Content-Security-Policy', csp))
238
238
239 virtual = req.env.get("PATH_INFO", "").strip('/')
239 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
240 tmpl = self.templater(req, 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 req.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 = req.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, req)
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 req.respond(HTTP_OK, ctype)
265 wsgireq.respond(HTTP_OK, ctype)
266 return self.makeindex(req, 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 req.respond(HTTP_OK, ctype)
273 wsgireq.respond(HTTP_OK, ctype)
274 return self.makeindex(req, 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 req.env['REPO_NAME'] = virtualrepo
289 wsgireq.env['REPO_NAME'] = virtualrepo
290 try:
290 try:
291 # ensure caller gets private copy of ui
291 # ensure caller gets private copy of ui
292 repo = hg.repository(self.ui.copy(), real)
292 repo = hg.repository(self.ui.copy(), real)
293 return hgweb_mod.hgweb(repo).run_wsgi(req)
293 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
294 except IOError as inst:
294 except IOError as inst:
295 msg = encoding.strtolocal(inst.strerror)
295 msg = encoding.strtolocal(inst.strerror)
296 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
296 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
297 except error.RepoError as inst:
297 except error.RepoError as inst:
298 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
298 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
299
299
300 # browse subdirectories
300 # browse subdirectories
301 subdir = virtual + '/'
301 subdir = virtual + '/'
302 if [r for r in repos if r.startswith(subdir)]:
302 if [r for r in repos if r.startswith(subdir)]:
303 req.respond(HTTP_OK, ctype)
303 wsgireq.respond(HTTP_OK, ctype)
304 return self.makeindex(req, tmpl, subdir)
304 return self.makeindex(wsgireq, tmpl, subdir)
305
305
306 # prefixes not found
306 # prefixes not found
307 req.respond(HTTP_NOT_FOUND, ctype)
307 wsgireq.respond(HTTP_NOT_FOUND, ctype)
308 return tmpl("notfound", repo=virtual)
308 return tmpl("notfound", repo=virtual)
309
309
310 except ErrorResponse as err:
310 except ErrorResponse as err:
311 req.respond(err, ctype)
311 wsgireq.respond(err, ctype)
312 return tmpl('error', error=err.message or '')
312 return tmpl('error', error=err.message or '')
313 finally:
313 finally:
314 tmpl = None
314 tmpl = None
315
315
316 def makeindex(self, req, tmpl, subdir=""):
316 def makeindex(self, wsgireq, tmpl, subdir=""):
317
317
318 def archivelist(ui, nodeid, url):
318 def archivelist(ui, nodeid, url):
319 allowed = ui.configlist("web", "allow_archive", untrusted=True)
319 allowed = ui.configlist("web", "allow_archive", untrusted=True)
320 archives = []
320 archives = []
321 for typ, spec in hgweb_mod.archivespecs.iteritems():
321 for typ, spec in hgweb_mod.archivespecs.iteritems():
322 if typ in allowed or ui.configbool("web", "allow" + typ,
322 if typ in allowed or ui.configbool("web", "allow" + typ,
323 untrusted=True):
323 untrusted=True):
324 archives.append({"type": typ, "extension": spec[2],
324 archives.append({"type": typ, "extension": spec[2],
325 "node": nodeid, "url": url})
325 "node": nodeid, "url": url})
326 return archives
326 return archives
327
327
328 def rawentries(subdir="", **map):
328 def rawentries(subdir="", **map):
329
329
330 descend = self.ui.configbool('web', 'descend')
330 descend = self.ui.configbool('web', 'descend')
331 collapse = self.ui.configbool('web', 'collapse')
331 collapse = self.ui.configbool('web', 'collapse')
332 seenrepos = set()
332 seenrepos = set()
333 seendirs = set()
333 seendirs = set()
334 for name, path in self.repos:
334 for name, path in self.repos:
335
335
336 if not name.startswith(subdir):
336 if not name.startswith(subdir):
337 continue
337 continue
338 name = name[len(subdir):]
338 name = name[len(subdir):]
339 directory = False
339 directory = False
340
340
341 if '/' in name:
341 if '/' in name:
342 if not descend:
342 if not descend:
343 continue
343 continue
344
344
345 nameparts = name.split('/')
345 nameparts = name.split('/')
346 rootname = nameparts[0]
346 rootname = nameparts[0]
347
347
348 if not collapse:
348 if not collapse:
349 pass
349 pass
350 elif rootname in seendirs:
350 elif rootname in seendirs:
351 continue
351 continue
352 elif rootname in seenrepos:
352 elif rootname in seenrepos:
353 pass
353 pass
354 else:
354 else:
355 directory = True
355 directory = True
356 name = rootname
356 name = rootname
357
357
358 # redefine the path to refer to the directory
358 # redefine the path to refer to the directory
359 discarded = '/'.join(nameparts[1:])
359 discarded = '/'.join(nameparts[1:])
360
360
361 # remove name parts plus accompanying slash
361 # remove name parts plus accompanying slash
362 path = path[:-len(discarded) - 1]
362 path = path[:-len(discarded) - 1]
363
363
364 try:
364 try:
365 r = hg.repository(self.ui, path)
365 r = hg.repository(self.ui, path)
366 directory = False
366 directory = False
367 except (IOError, error.RepoError):
367 except (IOError, error.RepoError):
368 pass
368 pass
369
369
370 parts = [name]
370 parts = [name]
371 parts.insert(0, '/' + subdir.rstrip('/'))
371 parts.insert(0, '/' + subdir.rstrip('/'))
372 if req.env['SCRIPT_NAME']:
372 if wsgireq.env['SCRIPT_NAME']:
373 parts.insert(0, req.env['SCRIPT_NAME'])
373 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
374 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
374 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
375
375
376 # show either a directory entry or a repository
376 # show either a directory entry or a repository
377 if directory:
377 if directory:
378 # get the directory's time information
378 # get the directory's time information
379 try:
379 try:
380 d = (get_mtime(path), dateutil.makedate()[1])
380 d = (get_mtime(path), dateutil.makedate()[1])
381 except OSError:
381 except OSError:
382 continue
382 continue
383
383
384 # add '/' to the name to make it obvious that
384 # add '/' to the name to make it obvious that
385 # the entry is a directory, not a regular repository
385 # the entry is a directory, not a regular repository
386 row = {'contact': "",
386 row = {'contact': "",
387 'contact_sort': "",
387 'contact_sort': "",
388 'name': name + '/',
388 'name': name + '/',
389 'name_sort': name,
389 'name_sort': name,
390 'url': url,
390 'url': url,
391 'description': "",
391 'description': "",
392 'description_sort': "",
392 'description_sort': "",
393 'lastchange': d,
393 'lastchange': d,
394 'lastchange_sort': d[1]-d[0],
394 'lastchange_sort': d[1]-d[0],
395 'archives': [],
395 'archives': [],
396 'isdirectory': True,
396 'isdirectory': True,
397 'labels': [],
397 'labels': [],
398 }
398 }
399
399
400 seendirs.add(name)
400 seendirs.add(name)
401 yield row
401 yield row
402 continue
402 continue
403
403
404 u = self.ui.copy()
404 u = self.ui.copy()
405 try:
405 try:
406 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
406 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
407 except Exception as e:
407 except Exception as e:
408 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
408 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
409 continue
409 continue
410 def get(section, name, default=uimod._unset):
410 def get(section, name, default=uimod._unset):
411 return u.config(section, name, default, untrusted=True)
411 return u.config(section, name, default, untrusted=True)
412
412
413 if u.configbool("web", "hidden", untrusted=True):
413 if u.configbool("web", "hidden", untrusted=True):
414 continue
414 continue
415
415
416 if not self.read_allowed(u, req):
416 if not self.read_allowed(u, wsgireq):
417 continue
417 continue
418
418
419 # update time with local timezone
419 # update time with local timezone
420 try:
420 try:
421 r = hg.repository(self.ui, path)
421 r = hg.repository(self.ui, path)
422 except IOError:
422 except IOError:
423 u.warn(_('error accessing repository at %s\n') % path)
423 u.warn(_('error accessing repository at %s\n') % path)
424 continue
424 continue
425 except error.RepoError:
425 except error.RepoError:
426 u.warn(_('error accessing repository at %s\n') % path)
426 u.warn(_('error accessing repository at %s\n') % path)
427 continue
427 continue
428 try:
428 try:
429 d = (get_mtime(r.spath), dateutil.makedate()[1])
429 d = (get_mtime(r.spath), dateutil.makedate()[1])
430 except OSError:
430 except OSError:
431 continue
431 continue
432
432
433 contact = get_contact(get)
433 contact = get_contact(get)
434 description = get("web", "description")
434 description = get("web", "description")
435 seenrepos.add(name)
435 seenrepos.add(name)
436 name = get("web", "name", name)
436 name = get("web", "name", name)
437 row = {'contact': contact or "unknown",
437 row = {'contact': contact or "unknown",
438 'contact_sort': contact.upper() or "unknown",
438 'contact_sort': contact.upper() or "unknown",
439 'name': name,
439 'name': name,
440 'name_sort': name,
440 'name_sort': name,
441 'url': url,
441 'url': url,
442 'description': description or "unknown",
442 'description': description or "unknown",
443 'description_sort': description.upper() or "unknown",
443 'description_sort': description.upper() or "unknown",
444 'lastchange': d,
444 'lastchange': d,
445 'lastchange_sort': d[1]-d[0],
445 'lastchange_sort': d[1]-d[0],
446 'archives': archivelist(u, "tip", url),
446 'archives': archivelist(u, "tip", url),
447 'isdirectory': None,
447 'isdirectory': None,
448 'labels': u.configlist('web', 'labels', untrusted=True),
448 'labels': u.configlist('web', 'labels', untrusted=True),
449 }
449 }
450
450
451 yield row
451 yield row
452
452
453 sortdefault = None, False
453 sortdefault = None, False
454 def entries(sortcolumn="", descending=False, subdir="", **map):
454 def entries(sortcolumn="", descending=False, subdir="", **map):
455 rows = rawentries(subdir=subdir, **map)
455 rows = rawentries(subdir=subdir, **map)
456
456
457 if sortcolumn and sortdefault != (sortcolumn, descending):
457 if sortcolumn and sortdefault != (sortcolumn, descending):
458 sortkey = '%s_sort' % sortcolumn
458 sortkey = '%s_sort' % sortcolumn
459 rows = sorted(rows, key=lambda x: x[sortkey],
459 rows = sorted(rows, key=lambda x: x[sortkey],
460 reverse=descending)
460 reverse=descending)
461 for row, parity in zip(rows, paritygen(self.stripecount)):
461 for row, parity in zip(rows, paritygen(self.stripecount)):
462 row['parity'] = parity
462 row['parity'] = parity
463 yield row
463 yield row
464
464
465 self.refresh()
465 self.refresh()
466 sortable = ["name", "description", "contact", "lastchange"]
466 sortable = ["name", "description", "contact", "lastchange"]
467 sortcolumn, descending = sortdefault
467 sortcolumn, descending = sortdefault
468 if 'sort' in req.form:
468 if 'sort' in wsgireq.form:
469 sortcolumn = req.form['sort'][0]
469 sortcolumn = wsgireq.form['sort'][0]
470 descending = sortcolumn.startswith('-')
470 descending = sortcolumn.startswith('-')
471 if descending:
471 if descending:
472 sortcolumn = sortcolumn[1:]
472 sortcolumn = sortcolumn[1:]
473 if sortcolumn not in sortable:
473 if sortcolumn not in sortable:
474 sortcolumn = ""
474 sortcolumn = ""
475
475
476 sort = [("sort_%s" % column,
476 sort = [("sort_%s" % column,
477 "%s%s" % ((not descending and column == sortcolumn)
477 "%s%s" % ((not descending and column == sortcolumn)
478 and "-" or "", column))
478 and "-" or "", column))
479 for column in sortable]
479 for column in sortable]
480
480
481 self.refresh()
481 self.refresh()
482 self.updatereqenv(req.env)
482 self.updatereqenv(wsgireq.env)
483
483
484 return tmpl("index", entries=entries, subdir=subdir,
484 return tmpl("index", entries=entries, subdir=subdir,
485 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
485 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
486 sortcolumn=sortcolumn, descending=descending,
486 sortcolumn=sortcolumn, descending=descending,
487 **dict(sort))
487 **dict(sort))
488
488
489 def templater(self, req, nonce):
489 def templater(self, wsgireq, nonce):
490
490
491 def motd(**map):
491 def motd(**map):
492 if self.motd is not None:
492 if self.motd is not None:
493 yield self.motd
493 yield self.motd
494 else:
494 else:
495 yield config('web', 'motd')
495 yield config('web', 'motd')
496
496
497 def config(section, name, default=uimod._unset, untrusted=True):
497 def config(section, name, default=uimod._unset, untrusted=True):
498 return self.ui.config(section, name, default, untrusted)
498 return self.ui.config(section, name, default, untrusted)
499
499
500 self.updatereqenv(req.env)
500 self.updatereqenv(wsgireq.env)
501
501
502 url = req.env.get('SCRIPT_NAME', '')
502 url = wsgireq.env.get('SCRIPT_NAME', '')
503 if not url.endswith('/'):
503 if not url.endswith('/'):
504 url += '/'
504 url += '/'
505
505
506 vars = {}
506 vars = {}
507 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
507 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq, config,
508 self.templatepath)
508 self.templatepath)
509 if style == styles[0]:
509 if style == styles[0]:
510 vars['style'] = style
510 vars['style'] = style
511
511
512 start = r'&' if url[-1] == r'?' else r'?'
512 start = r'&' if url[-1] == r'?' else r'?'
513 sessionvars = webutil.sessionvars(vars, start)
513 sessionvars = webutil.sessionvars(vars, start)
514 logourl = config('web', 'logourl')
514 logourl = config('web', 'logourl')
515 logoimg = config('web', 'logoimg')
515 logoimg = config('web', 'logoimg')
516 staticurl = config('web', 'staticurl') or url + 'static/'
516 staticurl = config('web', 'staticurl') or url + 'static/'
517 if not staticurl.endswith('/'):
517 if not staticurl.endswith('/'):
518 staticurl += '/'
518 staticurl += '/'
519
519
520 defaults = {
520 defaults = {
521 "encoding": encoding.encoding,
521 "encoding": encoding.encoding,
522 "motd": motd,
522 "motd": motd,
523 "url": url,
523 "url": url,
524 "logourl": logourl,
524 "logourl": logourl,
525 "logoimg": logoimg,
525 "logoimg": logoimg,
526 "staticurl": staticurl,
526 "staticurl": staticurl,
527 "sessionvars": sessionvars,
527 "sessionvars": sessionvars,
528 "style": style,
528 "style": style,
529 "nonce": nonce,
529 "nonce": nonce,
530 }
530 }
531 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
531 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 return tmpl
532 return tmpl
533
533
534 def updatereqenv(self, env):
534 def updatereqenv(self, env):
535 if self._baseurl is not None:
535 if self._baseurl is not None:
536 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
536 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
537 env['SERVER_NAME'] = name
537 env['SERVER_NAME'] = name
538 env['SERVER_PORT'] = port
538 env['SERVER_PORT'] = port
539 env['SCRIPT_NAME'] = path
539 env['SCRIPT_NAME'] = path
@@ -1,651 +1,651 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(wsgireq, 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 prefix = headerprefix.upper().replace(r'-', r'_')
46 prefix = headerprefix.upper().replace(r'-', r'_')
47 while True:
47 while True:
48 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
48 v = wsgireq.env.get(r'HTTP_%s_%d' % (prefix, i))
49 if v is None:
49 if v is None:
50 break
50 break
51 chunks.append(pycompat.bytesurl(v))
51 chunks.append(pycompat.bytesurl(v))
52 i += 1
52 i += 1
53
53
54 return ''.join(chunks)
54 return ''.join(chunks)
55
55
56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
56 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
57 def __init__(self, req, ui, checkperm):
57 def __init__(self, wsgireq, ui, checkperm):
58 self._req = req
58 self._wsgireq = wsgireq
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._req.form.copy())
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
83 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
83 postlen = int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0))
84 if postlen:
84 if postlen:
85 args.update(urlreq.parseqs(
85 args.update(urlreq.parseqs(
86 self._req.read(postlen), keep_blank_values=True))
86 self._wsgireq.read(postlen), keep_blank_values=True))
87 return args
87 return args
88
88
89 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
89 argvalue = decodevaluefromheaders(self._wsgireq, r'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 if r'HTTP_CONTENT_LENGTH' in self._req.env:
94 if r'HTTP_CONTENT_LENGTH' in self._wsgireq.env:
95 length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
95 length = int(self._wsgireq.env[r'HTTP_CONTENT_LENGTH'])
96 else:
96 else:
97 length = int(self._req.env[r'CONTENT_LENGTH'])
97 length = int(self._wsgireq.env[r'CONTENT_LENGTH'])
98 # If httppostargs is used, we need to read Content-Length
98 # If httppostargs is used, we need to read Content-Length
99 # minus the amount that was consumed by args.
99 # minus the amount that was consumed by args.
100 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
100 length -= int(self._wsgireq.env.get(r'HTTP_X_HGARGS_POST', 0))
101 for s in util.filechunkiter(self._req, limit=length):
101 for s in util.filechunkiter(self._wsgireq, limit=length):
102 fp.write(s)
102 fp.write(s)
103
103
104 @contextlib.contextmanager
104 @contextlib.contextmanager
105 def mayberedirectstdio(self):
105 def mayberedirectstdio(self):
106 oldout = self._ui.fout
106 oldout = self._ui.fout
107 olderr = self._ui.ferr
107 olderr = self._ui.ferr
108
108
109 out = util.stringio()
109 out = util.stringio()
110
110
111 try:
111 try:
112 self._ui.fout = out
112 self._ui.fout = out
113 self._ui.ferr = out
113 self._ui.ferr = out
114 yield out
114 yield out
115 finally:
115 finally:
116 self._ui.fout = oldout
116 self._ui.fout = oldout
117 self._ui.ferr = olderr
117 self._ui.ferr = olderr
118
118
119 def client(self):
119 def client(self):
120 return 'remote:%s:%s:%s' % (
120 return 'remote:%s:%s:%s' % (
121 self._req.env.get('wsgi.url_scheme') or 'http',
121 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
122 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
122 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
123 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
123 urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
124
124
125 def addcapabilities(self, repo, caps):
125 def addcapabilities(self, repo, caps):
126 caps.append('httpheader=%d' %
126 caps.append('httpheader=%d' %
127 repo.ui.configint('server', 'maxhttpheaderlen'))
127 repo.ui.configint('server', 'maxhttpheaderlen'))
128 if repo.ui.configbool('experimental', 'httppostargs'):
128 if repo.ui.configbool('experimental', 'httppostargs'):
129 caps.append('httppostargs')
129 caps.append('httppostargs')
130
130
131 # FUTURE advertise 0.2rx once support is implemented
131 # FUTURE advertise 0.2rx once support is implemented
132 # FUTURE advertise minrx and mintx after consulting config option
132 # FUTURE advertise minrx and mintx after consulting config option
133 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
133 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
134
134
135 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
135 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
136 if compengines:
136 if compengines:
137 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
137 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
138 for e in compengines)
138 for e in compengines)
139 caps.append('compression=%s' % comptypes)
139 caps.append('compression=%s' % comptypes)
140
140
141 return caps
141 return caps
142
142
143 def checkperm(self, perm):
143 def checkperm(self, perm):
144 return self._checkperm(perm)
144 return self._checkperm(perm)
145
145
146 # This method exists mostly so that extensions like remotefilelog can
146 # This method exists mostly so that extensions like remotefilelog can
147 # disable a kludgey legacy method only over http. As of early 2018,
147 # disable a kludgey legacy method only over http. As of early 2018,
148 # there are no other known users, so with any luck we can discard this
148 # there are no other known users, so with any luck we can discard this
149 # hook if remotefilelog becomes a first-party extension.
149 # hook if remotefilelog becomes a first-party extension.
150 def iscmd(cmd):
150 def iscmd(cmd):
151 return cmd in wireproto.commands
151 return cmd in wireproto.commands
152
152
153 def parsehttprequest(rctx, req, query, checkperm):
153 def parsehttprequest(rctx, wsgireq, query, checkperm):
154 """Parse the HTTP request for a wire protocol request.
154 """Parse the HTTP request for a wire protocol request.
155
155
156 If the current request appears to be a wire protocol request, this
156 If the current request appears to be a wire protocol request, this
157 function returns a dict with details about that request, including
157 function returns a dict with details about that request, including
158 an ``abstractprotocolserver`` instance suitable for handling the
158 an ``abstractprotocolserver`` instance suitable for handling the
159 request. Otherwise, ``None`` is returned.
159 request. Otherwise, ``None`` is returned.
160
160
161 ``req`` is a ``wsgirequest`` instance.
161 ``wsgireq`` is a ``wsgirequest`` instance.
162 """
162 """
163 repo = rctx.repo
163 repo = rctx.repo
164
164
165 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
165 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
166 # string parameter. If it isn't present, this isn't a wire protocol
166 # string parameter. If it isn't present, this isn't a wire protocol
167 # request.
167 # request.
168 if 'cmd' not in req.form:
168 if 'cmd' not in wsgireq.form:
169 return None
169 return None
170
170
171 cmd = req.form['cmd'][0]
171 cmd = wsgireq.form['cmd'][0]
172
172
173 # The "cmd" request parameter is used by both the wire protocol and hgweb.
173 # The "cmd" request parameter is used by both the wire protocol and hgweb.
174 # While not all wire protocol commands are available for all transports,
174 # While not all wire protocol commands are available for all transports,
175 # if we see a "cmd" value that resembles a known wire protocol command, we
175 # if we see a "cmd" value that resembles a known wire protocol command, we
176 # route it to a protocol handler. This is better than routing possible
176 # route it to a protocol handler. This is better than routing possible
177 # wire protocol requests to hgweb because it prevents hgweb from using
177 # wire protocol requests to hgweb because it prevents hgweb from using
178 # known wire protocol commands and it is less confusing for machine
178 # known wire protocol commands and it is less confusing for machine
179 # clients.
179 # clients.
180 if not iscmd(cmd):
180 if not iscmd(cmd):
181 return None
181 return None
182
182
183 proto = httpv1protocolhandler(req, repo.ui,
183 proto = httpv1protocolhandler(wsgireq, repo.ui,
184 lambda perm: checkperm(rctx, req, perm))
184 lambda perm: checkperm(rctx, wsgireq, perm))
185
185
186 return {
186 return {
187 'cmd': cmd,
187 'cmd': cmd,
188 'proto': proto,
188 'proto': proto,
189 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
189 'dispatch': lambda: _callhttp(repo, wsgireq, proto, cmd),
190 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
190 'handleerror': lambda ex: _handlehttperror(ex, wsgireq, cmd),
191 }
191 }
192
192
193 def _httpresponsetype(ui, req, prefer_uncompressed):
193 def _httpresponsetype(ui, wsgireq, prefer_uncompressed):
194 """Determine the appropriate response type and compression settings.
194 """Determine the appropriate response type and compression settings.
195
195
196 Returns a tuple of (mediatype, compengine, engineopts).
196 Returns a tuple of (mediatype, compengine, engineopts).
197 """
197 """
198 # Determine the response media type and compression engine based
198 # Determine the response media type and compression engine based
199 # on the request parameters.
199 # on the request parameters.
200 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
200 protocaps = decodevaluefromheaders(wsgireq, r'X-HgProto').split(' ')
201
201
202 if '0.2' in protocaps:
202 if '0.2' in protocaps:
203 # All clients are expected to support uncompressed data.
203 # All clients are expected to support uncompressed data.
204 if prefer_uncompressed:
204 if prefer_uncompressed:
205 return HGTYPE2, util._noopengine(), {}
205 return HGTYPE2, util._noopengine(), {}
206
206
207 # Default as defined by wire protocol spec.
207 # Default as defined by wire protocol spec.
208 compformats = ['zlib', 'none']
208 compformats = ['zlib', 'none']
209 for cap in protocaps:
209 for cap in protocaps:
210 if cap.startswith('comp='):
210 if cap.startswith('comp='):
211 compformats = cap[5:].split(',')
211 compformats = cap[5:].split(',')
212 break
212 break
213
213
214 # Now find an agreed upon compression format.
214 # Now find an agreed upon compression format.
215 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
215 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
216 if engine.wireprotosupport().name in compformats:
216 if engine.wireprotosupport().name in compformats:
217 opts = {}
217 opts = {}
218 level = ui.configint('server', '%slevel' % engine.name())
218 level = ui.configint('server', '%slevel' % engine.name())
219 if level is not None:
219 if level is not None:
220 opts['level'] = level
220 opts['level'] = level
221
221
222 return HGTYPE2, engine, opts
222 return HGTYPE2, engine, opts
223
223
224 # No mutually supported compression format. Fall back to the
224 # No mutually supported compression format. Fall back to the
225 # legacy protocol.
225 # legacy protocol.
226
226
227 # Don't allow untrusted settings because disabling compression or
227 # Don't allow untrusted settings because disabling compression or
228 # setting a very high compression level could lead to flooding
228 # setting a very high compression level could lead to flooding
229 # the server's network or CPU.
229 # the server's network or CPU.
230 opts = {'level': ui.configint('server', 'zliblevel')}
230 opts = {'level': ui.configint('server', 'zliblevel')}
231 return HGTYPE, util.compengines['zlib'], opts
231 return HGTYPE, util.compengines['zlib'], opts
232
232
233 def _callhttp(repo, req, proto, cmd):
233 def _callhttp(repo, wsgireq, proto, cmd):
234 def genversion2(gen, engine, engineopts):
234 def genversion2(gen, engine, engineopts):
235 # application/mercurial-0.2 always sends a payload header
235 # application/mercurial-0.2 always sends a payload header
236 # identifying the compression engine.
236 # identifying the compression engine.
237 name = engine.wireprotosupport().name
237 name = engine.wireprotosupport().name
238 assert 0 < len(name) < 256
238 assert 0 < len(name) < 256
239 yield struct.pack('B', len(name))
239 yield struct.pack('B', len(name))
240 yield name
240 yield name
241
241
242 for chunk in gen:
242 for chunk in gen:
243 yield chunk
243 yield chunk
244
244
245 if not wireproto.commands.commandavailable(cmd, proto):
245 if not wireproto.commands.commandavailable(cmd, proto):
246 req.respond(HTTP_OK, HGERRTYPE,
246 wsgireq.respond(HTTP_OK, HGERRTYPE,
247 body=_('requested wire protocol command is not available '
247 body=_('requested wire protocol command is not '
248 'over HTTP'))
248 'available over HTTP'))
249 return []
249 return []
250
250
251 proto.checkperm(wireproto.commands[cmd].permission)
251 proto.checkperm(wireproto.commands[cmd].permission)
252
252
253 rsp = wireproto.dispatch(repo, proto, cmd)
253 rsp = wireproto.dispatch(repo, proto, cmd)
254
254
255 if isinstance(rsp, bytes):
255 if isinstance(rsp, bytes):
256 req.respond(HTTP_OK, HGTYPE, body=rsp)
256 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
257 return []
257 return []
258 elif isinstance(rsp, wireprototypes.bytesresponse):
258 elif isinstance(rsp, wireprototypes.bytesresponse):
259 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
259 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
260 return []
260 return []
261 elif isinstance(rsp, wireprototypes.streamreslegacy):
261 elif isinstance(rsp, wireprototypes.streamreslegacy):
262 gen = rsp.gen
262 gen = rsp.gen
263 req.respond(HTTP_OK, HGTYPE)
263 wsgireq.respond(HTTP_OK, HGTYPE)
264 return gen
264 return gen
265 elif isinstance(rsp, wireprototypes.streamres):
265 elif isinstance(rsp, wireprototypes.streamres):
266 gen = rsp.gen
266 gen = rsp.gen
267
267
268 # This code for compression should not be streamres specific. It
268 # This code for compression should not be streamres specific. It
269 # is here because we only compress streamres at the moment.
269 # is here because we only compress streamres at the moment.
270 mediatype, engine, engineopts = _httpresponsetype(
270 mediatype, engine, engineopts = _httpresponsetype(
271 repo.ui, req, rsp.prefer_uncompressed)
271 repo.ui, wsgireq, rsp.prefer_uncompressed)
272 gen = engine.compressstream(gen, engineopts)
272 gen = engine.compressstream(gen, engineopts)
273
273
274 if mediatype == HGTYPE2:
274 if mediatype == HGTYPE2:
275 gen = genversion2(gen, engine, engineopts)
275 gen = genversion2(gen, engine, engineopts)
276
276
277 req.respond(HTTP_OK, mediatype)
277 wsgireq.respond(HTTP_OK, mediatype)
278 return gen
278 return gen
279 elif isinstance(rsp, wireprototypes.pushres):
279 elif isinstance(rsp, wireprototypes.pushres):
280 rsp = '%d\n%s' % (rsp.res, rsp.output)
280 rsp = '%d\n%s' % (rsp.res, rsp.output)
281 req.respond(HTTP_OK, HGTYPE, body=rsp)
281 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
282 return []
282 return []
283 elif isinstance(rsp, wireprototypes.pusherr):
283 elif isinstance(rsp, wireprototypes.pusherr):
284 # This is the httplib workaround documented in _handlehttperror().
284 # This is the httplib workaround documented in _handlehttperror().
285 req.drain()
285 wsgireq.drain()
286
286
287 rsp = '0\n%s\n' % rsp.res
287 rsp = '0\n%s\n' % rsp.res
288 req.respond(HTTP_OK, HGTYPE, body=rsp)
288 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
289 return []
289 return []
290 elif isinstance(rsp, wireprototypes.ooberror):
290 elif isinstance(rsp, wireprototypes.ooberror):
291 rsp = rsp.message
291 rsp = rsp.message
292 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
292 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
293 return []
293 return []
294 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
294 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
295
295
296 def _handlehttperror(e, req, cmd):
296 def _handlehttperror(e, wsgireq, cmd):
297 """Called when an ErrorResponse is raised during HTTP request processing."""
297 """Called when an ErrorResponse is raised during HTTP request processing."""
298
298
299 # Clients using Python's httplib are stateful: the HTTP client
299 # Clients using Python's httplib are stateful: the HTTP client
300 # won't process an HTTP response until all request data is
300 # won't process an HTTP response until all request data is
301 # sent to the server. The intent of this code is to ensure
301 # sent to the server. The intent of this code is to ensure
302 # we always read HTTP request data from the client, thus
302 # we always read HTTP request data from the client, thus
303 # ensuring httplib transitions to a state that allows it to read
303 # ensuring httplib transitions to a state that allows it to read
304 # the HTTP response. In other words, it helps prevent deadlocks
304 # the HTTP response. In other words, it helps prevent deadlocks
305 # on clients using httplib.
305 # on clients using httplib.
306
306
307 if (req.env[r'REQUEST_METHOD'] == r'POST' and
307 if (wsgireq.env[r'REQUEST_METHOD'] == r'POST' and
308 # But not if Expect: 100-continue is being used.
308 # But not if Expect: 100-continue is being used.
309 (req.env.get('HTTP_EXPECT',
309 (wsgireq.env.get('HTTP_EXPECT',
310 '').lower() != '100-continue') or
310 '').lower() != '100-continue') or
311 # Or the non-httplib HTTP library is being advertised by
311 # Or the non-httplib HTTP library is being advertised by
312 # the client.
312 # the client.
313 req.env.get('X-HgHttp2', '')):
313 wsgireq.env.get('X-HgHttp2', '')):
314 req.drain()
314 wsgireq.drain()
315 else:
315 else:
316 req.headers.append((r'Connection', r'Close'))
316 wsgireq.headers.append((r'Connection', r'Close'))
317
317
318 # TODO This response body assumes the failed command was
318 # TODO This response body assumes the failed command was
319 # "unbundle." That assumption is not always valid.
319 # "unbundle." That assumption is not always valid.
320 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
320 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
321
321
322 return ''
322 return ''
323
323
324 def _sshv1respondbytes(fout, value):
324 def _sshv1respondbytes(fout, value):
325 """Send a bytes response for protocol version 1."""
325 """Send a bytes response for protocol version 1."""
326 fout.write('%d\n' % len(value))
326 fout.write('%d\n' % len(value))
327 fout.write(value)
327 fout.write(value)
328 fout.flush()
328 fout.flush()
329
329
330 def _sshv1respondstream(fout, source):
330 def _sshv1respondstream(fout, source):
331 write = fout.write
331 write = fout.write
332 for chunk in source.gen:
332 for chunk in source.gen:
333 write(chunk)
333 write(chunk)
334 fout.flush()
334 fout.flush()
335
335
336 def _sshv1respondooberror(fout, ferr, rsp):
336 def _sshv1respondooberror(fout, ferr, rsp):
337 ferr.write(b'%s\n-\n' % rsp)
337 ferr.write(b'%s\n-\n' % rsp)
338 ferr.flush()
338 ferr.flush()
339 fout.write(b'\n')
339 fout.write(b'\n')
340 fout.flush()
340 fout.flush()
341
341
342 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
342 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
343 """Handler for requests services via version 1 of SSH protocol."""
343 """Handler for requests services via version 1 of SSH protocol."""
344 def __init__(self, ui, fin, fout):
344 def __init__(self, ui, fin, fout):
345 self._ui = ui
345 self._ui = ui
346 self._fin = fin
346 self._fin = fin
347 self._fout = fout
347 self._fout = fout
348
348
349 @property
349 @property
350 def name(self):
350 def name(self):
351 return wireprototypes.SSHV1
351 return wireprototypes.SSHV1
352
352
353 def getargs(self, args):
353 def getargs(self, args):
354 data = {}
354 data = {}
355 keys = args.split()
355 keys = args.split()
356 for n in xrange(len(keys)):
356 for n in xrange(len(keys)):
357 argline = self._fin.readline()[:-1]
357 argline = self._fin.readline()[:-1]
358 arg, l = argline.split()
358 arg, l = argline.split()
359 if arg not in keys:
359 if arg not in keys:
360 raise error.Abort(_("unexpected parameter %r") % arg)
360 raise error.Abort(_("unexpected parameter %r") % arg)
361 if arg == '*':
361 if arg == '*':
362 star = {}
362 star = {}
363 for k in xrange(int(l)):
363 for k in xrange(int(l)):
364 argline = self._fin.readline()[:-1]
364 argline = self._fin.readline()[:-1]
365 arg, l = argline.split()
365 arg, l = argline.split()
366 val = self._fin.read(int(l))
366 val = self._fin.read(int(l))
367 star[arg] = val
367 star[arg] = val
368 data['*'] = star
368 data['*'] = star
369 else:
369 else:
370 val = self._fin.read(int(l))
370 val = self._fin.read(int(l))
371 data[arg] = val
371 data[arg] = val
372 return [data[k] for k in keys]
372 return [data[k] for k in keys]
373
373
374 def forwardpayload(self, fpout):
374 def forwardpayload(self, fpout):
375 # We initially send an empty response. This tells the client it is
375 # We initially send an empty response. This tells the client it is
376 # OK to start sending data. If a client sees any other response, it
376 # OK to start sending data. If a client sees any other response, it
377 # interprets it as an error.
377 # interprets it as an error.
378 _sshv1respondbytes(self._fout, b'')
378 _sshv1respondbytes(self._fout, b'')
379
379
380 # The file is in the form:
380 # The file is in the form:
381 #
381 #
382 # <chunk size>\n<chunk>
382 # <chunk size>\n<chunk>
383 # ...
383 # ...
384 # 0\n
384 # 0\n
385 count = int(self._fin.readline())
385 count = int(self._fin.readline())
386 while count:
386 while count:
387 fpout.write(self._fin.read(count))
387 fpout.write(self._fin.read(count))
388 count = int(self._fin.readline())
388 count = int(self._fin.readline())
389
389
390 @contextlib.contextmanager
390 @contextlib.contextmanager
391 def mayberedirectstdio(self):
391 def mayberedirectstdio(self):
392 yield None
392 yield None
393
393
394 def client(self):
394 def client(self):
395 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
395 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
396 return 'remote:ssh:' + client
396 return 'remote:ssh:' + client
397
397
398 def addcapabilities(self, repo, caps):
398 def addcapabilities(self, repo, caps):
399 return caps
399 return caps
400
400
401 def checkperm(self, perm):
401 def checkperm(self, perm):
402 pass
402 pass
403
403
404 class sshv2protocolhandler(sshv1protocolhandler):
404 class sshv2protocolhandler(sshv1protocolhandler):
405 """Protocol handler for version 2 of the SSH protocol."""
405 """Protocol handler for version 2 of the SSH protocol."""
406
406
407 @property
407 @property
408 def name(self):
408 def name(self):
409 return wireprototypes.SSHV2
409 return wireprototypes.SSHV2
410
410
411 def _runsshserver(ui, repo, fin, fout, ev):
411 def _runsshserver(ui, repo, fin, fout, ev):
412 # This function operates like a state machine of sorts. The following
412 # This function operates like a state machine of sorts. The following
413 # states are defined:
413 # states are defined:
414 #
414 #
415 # protov1-serving
415 # protov1-serving
416 # Server is in protocol version 1 serving mode. Commands arrive on
416 # Server is in protocol version 1 serving mode. Commands arrive on
417 # new lines. These commands are processed in this state, one command
417 # new lines. These commands are processed in this state, one command
418 # after the other.
418 # after the other.
419 #
419 #
420 # protov2-serving
420 # protov2-serving
421 # Server is in protocol version 2 serving mode.
421 # Server is in protocol version 2 serving mode.
422 #
422 #
423 # upgrade-initial
423 # upgrade-initial
424 # The server is going to process an upgrade request.
424 # The server is going to process an upgrade request.
425 #
425 #
426 # upgrade-v2-filter-legacy-handshake
426 # upgrade-v2-filter-legacy-handshake
427 # The protocol is being upgraded to version 2. The server is expecting
427 # The protocol is being upgraded to version 2. The server is expecting
428 # the legacy handshake from version 1.
428 # the legacy handshake from version 1.
429 #
429 #
430 # upgrade-v2-finish
430 # upgrade-v2-finish
431 # The upgrade to version 2 of the protocol is imminent.
431 # The upgrade to version 2 of the protocol is imminent.
432 #
432 #
433 # shutdown
433 # shutdown
434 # The server is shutting down, possibly in reaction to a client event.
434 # The server is shutting down, possibly in reaction to a client event.
435 #
435 #
436 # And here are their transitions:
436 # And here are their transitions:
437 #
437 #
438 # protov1-serving -> shutdown
438 # protov1-serving -> shutdown
439 # When server receives an empty request or encounters another
439 # When server receives an empty request or encounters another
440 # error.
440 # error.
441 #
441 #
442 # protov1-serving -> upgrade-initial
442 # protov1-serving -> upgrade-initial
443 # An upgrade request line was seen.
443 # An upgrade request line was seen.
444 #
444 #
445 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
445 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
446 # Upgrade to version 2 in progress. Server is expecting to
446 # Upgrade to version 2 in progress. Server is expecting to
447 # process a legacy handshake.
447 # process a legacy handshake.
448 #
448 #
449 # upgrade-v2-filter-legacy-handshake -> shutdown
449 # upgrade-v2-filter-legacy-handshake -> shutdown
450 # Client did not fulfill upgrade handshake requirements.
450 # Client did not fulfill upgrade handshake requirements.
451 #
451 #
452 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
452 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
453 # Client fulfilled version 2 upgrade requirements. Finishing that
453 # Client fulfilled version 2 upgrade requirements. Finishing that
454 # upgrade.
454 # upgrade.
455 #
455 #
456 # upgrade-v2-finish -> protov2-serving
456 # upgrade-v2-finish -> protov2-serving
457 # Protocol upgrade to version 2 complete. Server can now speak protocol
457 # Protocol upgrade to version 2 complete. Server can now speak protocol
458 # version 2.
458 # version 2.
459 #
459 #
460 # protov2-serving -> protov1-serving
460 # protov2-serving -> protov1-serving
461 # Ths happens by default since protocol version 2 is the same as
461 # Ths happens by default since protocol version 2 is the same as
462 # version 1 except for the handshake.
462 # version 1 except for the handshake.
463
463
464 state = 'protov1-serving'
464 state = 'protov1-serving'
465 proto = sshv1protocolhandler(ui, fin, fout)
465 proto = sshv1protocolhandler(ui, fin, fout)
466 protoswitched = False
466 protoswitched = False
467
467
468 while not ev.is_set():
468 while not ev.is_set():
469 if state == 'protov1-serving':
469 if state == 'protov1-serving':
470 # Commands are issued on new lines.
470 # Commands are issued on new lines.
471 request = fin.readline()[:-1]
471 request = fin.readline()[:-1]
472
472
473 # Empty lines signal to terminate the connection.
473 # Empty lines signal to terminate the connection.
474 if not request:
474 if not request:
475 state = 'shutdown'
475 state = 'shutdown'
476 continue
476 continue
477
477
478 # It looks like a protocol upgrade request. Transition state to
478 # It looks like a protocol upgrade request. Transition state to
479 # handle it.
479 # handle it.
480 if request.startswith(b'upgrade '):
480 if request.startswith(b'upgrade '):
481 if protoswitched:
481 if protoswitched:
482 _sshv1respondooberror(fout, ui.ferr,
482 _sshv1respondooberror(fout, ui.ferr,
483 b'cannot upgrade protocols multiple '
483 b'cannot upgrade protocols multiple '
484 b'times')
484 b'times')
485 state = 'shutdown'
485 state = 'shutdown'
486 continue
486 continue
487
487
488 state = 'upgrade-initial'
488 state = 'upgrade-initial'
489 continue
489 continue
490
490
491 available = wireproto.commands.commandavailable(request, proto)
491 available = wireproto.commands.commandavailable(request, proto)
492
492
493 # This command isn't available. Send an empty response and go
493 # This command isn't available. Send an empty response and go
494 # back to waiting for a new command.
494 # back to waiting for a new command.
495 if not available:
495 if not available:
496 _sshv1respondbytes(fout, b'')
496 _sshv1respondbytes(fout, b'')
497 continue
497 continue
498
498
499 rsp = wireproto.dispatch(repo, proto, request)
499 rsp = wireproto.dispatch(repo, proto, request)
500
500
501 if isinstance(rsp, bytes):
501 if isinstance(rsp, bytes):
502 _sshv1respondbytes(fout, rsp)
502 _sshv1respondbytes(fout, rsp)
503 elif isinstance(rsp, wireprototypes.bytesresponse):
503 elif isinstance(rsp, wireprototypes.bytesresponse):
504 _sshv1respondbytes(fout, rsp.data)
504 _sshv1respondbytes(fout, rsp.data)
505 elif isinstance(rsp, wireprototypes.streamres):
505 elif isinstance(rsp, wireprototypes.streamres):
506 _sshv1respondstream(fout, rsp)
506 _sshv1respondstream(fout, rsp)
507 elif isinstance(rsp, wireprototypes.streamreslegacy):
507 elif isinstance(rsp, wireprototypes.streamreslegacy):
508 _sshv1respondstream(fout, rsp)
508 _sshv1respondstream(fout, rsp)
509 elif isinstance(rsp, wireprototypes.pushres):
509 elif isinstance(rsp, wireprototypes.pushres):
510 _sshv1respondbytes(fout, b'')
510 _sshv1respondbytes(fout, b'')
511 _sshv1respondbytes(fout, b'%d' % rsp.res)
511 _sshv1respondbytes(fout, b'%d' % rsp.res)
512 elif isinstance(rsp, wireprototypes.pusherr):
512 elif isinstance(rsp, wireprototypes.pusherr):
513 _sshv1respondbytes(fout, rsp.res)
513 _sshv1respondbytes(fout, rsp.res)
514 elif isinstance(rsp, wireprototypes.ooberror):
514 elif isinstance(rsp, wireprototypes.ooberror):
515 _sshv1respondooberror(fout, ui.ferr, rsp.message)
515 _sshv1respondooberror(fout, ui.ferr, rsp.message)
516 else:
516 else:
517 raise error.ProgrammingError('unhandled response type from '
517 raise error.ProgrammingError('unhandled response type from '
518 'wire protocol command: %s' % rsp)
518 'wire protocol command: %s' % rsp)
519
519
520 # For now, protocol version 2 serving just goes back to version 1.
520 # For now, protocol version 2 serving just goes back to version 1.
521 elif state == 'protov2-serving':
521 elif state == 'protov2-serving':
522 state = 'protov1-serving'
522 state = 'protov1-serving'
523 continue
523 continue
524
524
525 elif state == 'upgrade-initial':
525 elif state == 'upgrade-initial':
526 # We should never transition into this state if we've switched
526 # We should never transition into this state if we've switched
527 # protocols.
527 # protocols.
528 assert not protoswitched
528 assert not protoswitched
529 assert proto.name == wireprototypes.SSHV1
529 assert proto.name == wireprototypes.SSHV1
530
530
531 # Expected: upgrade <token> <capabilities>
531 # Expected: upgrade <token> <capabilities>
532 # If we get something else, the request is malformed. It could be
532 # If we get something else, the request is malformed. It could be
533 # from a future client that has altered the upgrade line content.
533 # from a future client that has altered the upgrade line content.
534 # We treat this as an unknown command.
534 # We treat this as an unknown command.
535 try:
535 try:
536 token, caps = request.split(b' ')[1:]
536 token, caps = request.split(b' ')[1:]
537 except ValueError:
537 except ValueError:
538 _sshv1respondbytes(fout, b'')
538 _sshv1respondbytes(fout, b'')
539 state = 'protov1-serving'
539 state = 'protov1-serving'
540 continue
540 continue
541
541
542 # Send empty response if we don't support upgrading protocols.
542 # Send empty response if we don't support upgrading protocols.
543 if not ui.configbool('experimental', 'sshserver.support-v2'):
543 if not ui.configbool('experimental', 'sshserver.support-v2'):
544 _sshv1respondbytes(fout, b'')
544 _sshv1respondbytes(fout, b'')
545 state = 'protov1-serving'
545 state = 'protov1-serving'
546 continue
546 continue
547
547
548 try:
548 try:
549 caps = urlreq.parseqs(caps)
549 caps = urlreq.parseqs(caps)
550 except ValueError:
550 except ValueError:
551 _sshv1respondbytes(fout, b'')
551 _sshv1respondbytes(fout, b'')
552 state = 'protov1-serving'
552 state = 'protov1-serving'
553 continue
553 continue
554
554
555 # We don't see an upgrade request to protocol version 2. Ignore
555 # We don't see an upgrade request to protocol version 2. Ignore
556 # the upgrade request.
556 # the upgrade request.
557 wantedprotos = caps.get(b'proto', [b''])[0]
557 wantedprotos = caps.get(b'proto', [b''])[0]
558 if SSHV2 not in wantedprotos:
558 if SSHV2 not in wantedprotos:
559 _sshv1respondbytes(fout, b'')
559 _sshv1respondbytes(fout, b'')
560 state = 'protov1-serving'
560 state = 'protov1-serving'
561 continue
561 continue
562
562
563 # It looks like we can honor this upgrade request to protocol 2.
563 # It looks like we can honor this upgrade request to protocol 2.
564 # Filter the rest of the handshake protocol request lines.
564 # Filter the rest of the handshake protocol request lines.
565 state = 'upgrade-v2-filter-legacy-handshake'
565 state = 'upgrade-v2-filter-legacy-handshake'
566 continue
566 continue
567
567
568 elif state == 'upgrade-v2-filter-legacy-handshake':
568 elif state == 'upgrade-v2-filter-legacy-handshake':
569 # Client should have sent legacy handshake after an ``upgrade``
569 # Client should have sent legacy handshake after an ``upgrade``
570 # request. Expected lines:
570 # request. Expected lines:
571 #
571 #
572 # hello
572 # hello
573 # between
573 # between
574 # pairs 81
574 # pairs 81
575 # 0000...-0000...
575 # 0000...-0000...
576
576
577 ok = True
577 ok = True
578 for line in (b'hello', b'between', b'pairs 81'):
578 for line in (b'hello', b'between', b'pairs 81'):
579 request = fin.readline()[:-1]
579 request = fin.readline()[:-1]
580
580
581 if request != line:
581 if request != line:
582 _sshv1respondooberror(fout, ui.ferr,
582 _sshv1respondooberror(fout, ui.ferr,
583 b'malformed handshake protocol: '
583 b'malformed handshake protocol: '
584 b'missing %s' % line)
584 b'missing %s' % line)
585 ok = False
585 ok = False
586 state = 'shutdown'
586 state = 'shutdown'
587 break
587 break
588
588
589 if not ok:
589 if not ok:
590 continue
590 continue
591
591
592 request = fin.read(81)
592 request = fin.read(81)
593 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
593 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
594 _sshv1respondooberror(fout, ui.ferr,
594 _sshv1respondooberror(fout, ui.ferr,
595 b'malformed handshake protocol: '
595 b'malformed handshake protocol: '
596 b'missing between argument value')
596 b'missing between argument value')
597 state = 'shutdown'
597 state = 'shutdown'
598 continue
598 continue
599
599
600 state = 'upgrade-v2-finish'
600 state = 'upgrade-v2-finish'
601 continue
601 continue
602
602
603 elif state == 'upgrade-v2-finish':
603 elif state == 'upgrade-v2-finish':
604 # Send the upgrade response.
604 # Send the upgrade response.
605 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
605 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
606 servercaps = wireproto.capabilities(repo, proto)
606 servercaps = wireproto.capabilities(repo, proto)
607 rsp = b'capabilities: %s' % servercaps.data
607 rsp = b'capabilities: %s' % servercaps.data
608 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
608 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
609 fout.flush()
609 fout.flush()
610
610
611 proto = sshv2protocolhandler(ui, fin, fout)
611 proto = sshv2protocolhandler(ui, fin, fout)
612 protoswitched = True
612 protoswitched = True
613
613
614 state = 'protov2-serving'
614 state = 'protov2-serving'
615 continue
615 continue
616
616
617 elif state == 'shutdown':
617 elif state == 'shutdown':
618 break
618 break
619
619
620 else:
620 else:
621 raise error.ProgrammingError('unhandled ssh server state: %s' %
621 raise error.ProgrammingError('unhandled ssh server state: %s' %
622 state)
622 state)
623
623
624 class sshserver(object):
624 class sshserver(object):
625 def __init__(self, ui, repo, logfh=None):
625 def __init__(self, ui, repo, logfh=None):
626 self._ui = ui
626 self._ui = ui
627 self._repo = repo
627 self._repo = repo
628 self._fin = ui.fin
628 self._fin = ui.fin
629 self._fout = ui.fout
629 self._fout = ui.fout
630
630
631 # Log write I/O to stdout and stderr if configured.
631 # Log write I/O to stdout and stderr if configured.
632 if logfh:
632 if logfh:
633 self._fout = util.makeloggingfileobject(
633 self._fout = util.makeloggingfileobject(
634 logfh, self._fout, 'o', logdata=True)
634 logfh, self._fout, 'o', logdata=True)
635 ui.ferr = util.makeloggingfileobject(
635 ui.ferr = util.makeloggingfileobject(
636 logfh, ui.ferr, 'e', logdata=True)
636 logfh, ui.ferr, 'e', logdata=True)
637
637
638 hook.redirect(True)
638 hook.redirect(True)
639 ui.fout = repo.ui.fout = ui.ferr
639 ui.fout = repo.ui.fout = ui.ferr
640
640
641 # Prevent insertion/deletion of CRs
641 # Prevent insertion/deletion of CRs
642 util.setbinary(self._fin)
642 util.setbinary(self._fin)
643 util.setbinary(self._fout)
643 util.setbinary(self._fout)
644
644
645 def serve_forever(self):
645 def serve_forever(self):
646 self.serveuntil(threading.Event())
646 self.serveuntil(threading.Event())
647 sys.exit(0)
647 sys.exit(0)
648
648
649 def serveuntil(self, ev):
649 def serveuntil(self, ev):
650 """Serve until a threading.Event is set."""
650 """Serve until a threading.Event is set."""
651 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
651 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now