##// END OF EJS Templates
hgweb: only recognize wire protocol commands from query string (BC)...
Gregory Szorc -
r36828:886fba19 default
parent child Browse files
Show More
@@ -1,452 +1,452 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
25
26 from .. import (
26 from .. import (
27 encoding,
27 encoding,
28 error,
28 error,
29 formatter,
29 formatter,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 pycompat,
33 pycompat,
34 repoview,
34 repoview,
35 templatefilters,
35 templatefilters,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 wireprotoserver,
39 wireprotoserver,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 request as requestmod,
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, wsgireq, req):
145 def templater(self, wsgireq, req):
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 logourl = self.config('web', 'logourl')
148 logourl = self.config('web', 'logourl')
149 logoimg = self.config('web', 'logoimg')
149 logoimg = self.config('web', 'logoimg')
150 staticurl = (self.config('web', 'staticurl')
150 staticurl = (self.config('web', 'staticurl')
151 or req.apppath + '/static/')
151 or req.apppath + '/static/')
152 if not staticurl.endswith('/'):
152 if not staticurl.endswith('/'):
153 staticurl += '/'
153 staticurl += '/'
154
154
155 # some functions for the templater
155 # some functions for the templater
156
156
157 def motd(**map):
157 def motd(**map):
158 yield self.config('web', 'motd')
158 yield self.config('web', 'motd')
159
159
160 # figure out which style to use
160 # figure out which style to use
161
161
162 vars = {}
162 vars = {}
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
164 self.templatepath)
164 self.templatepath)
165 if style == styles[0]:
165 if style == styles[0]:
166 vars['style'] = style
166 vars['style'] = style
167
167
168 sessionvars = webutil.sessionvars(vars, '?')
168 sessionvars = webutil.sessionvars(vars, '?')
169
169
170 if not self.reponame:
170 if not self.reponame:
171 self.reponame = (self.config('web', 'name', '')
171 self.reponame = (self.config('web', 'name', '')
172 or wsgireq.env.get('REPO_NAME')
172 or wsgireq.env.get('REPO_NAME')
173 or req.apppath or self.repo.root)
173 or req.apppath or self.repo.root)
174
174
175 def websubfilter(text):
175 def websubfilter(text):
176 return templatefilters.websub(text, self.websubtable)
176 return templatefilters.websub(text, self.websubtable)
177
177
178 # create the templater
178 # create the templater
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 defaults = {
180 defaults = {
181 'url': req.apppath + '/',
181 'url': req.apppath + '/',
182 'logourl': logourl,
182 'logourl': logourl,
183 'logoimg': logoimg,
183 'logoimg': logoimg,
184 'staticurl': staticurl,
184 'staticurl': staticurl,
185 'urlbase': req.advertisedbaseurl,
185 'urlbase': req.advertisedbaseurl,
186 'repo': self.reponame,
186 'repo': self.reponame,
187 'encoding': encoding.encoding,
187 'encoding': encoding.encoding,
188 'motd': motd,
188 'motd': motd,
189 'sessionvars': sessionvars,
189 'sessionvars': sessionvars,
190 'pathdef': makebreadcrumb(req.apppath),
190 'pathdef': makebreadcrumb(req.apppath),
191 'style': style,
191 'style': style,
192 'nonce': self.nonce,
192 'nonce': self.nonce,
193 }
193 }
194 tres = formatter.templateresources(self.repo.ui, self.repo)
194 tres = formatter.templateresources(self.repo.ui, self.repo)
195 tmpl = templater.templater.frommapfile(mapfile,
195 tmpl = templater.templater.frommapfile(mapfile,
196 filters={'websub': websubfilter},
196 filters={'websub': websubfilter},
197 defaults=defaults,
197 defaults=defaults,
198 resources=tres)
198 resources=tres)
199 return tmpl
199 return tmpl
200
200
201
201
202 class hgweb(object):
202 class hgweb(object):
203 """HTTP server for individual repositories.
203 """HTTP server for individual repositories.
204
204
205 Instances of this class serve HTTP responses for a particular
205 Instances of this class serve HTTP responses for a particular
206 repository.
206 repository.
207
207
208 Instances are typically used as WSGI applications.
208 Instances are typically used as WSGI applications.
209
209
210 Some servers are multi-threaded. On these servers, there may
210 Some servers are multi-threaded. On these servers, there may
211 be multiple active threads inside __call__.
211 be multiple active threads inside __call__.
212 """
212 """
213 def __init__(self, repo, name=None, baseui=None):
213 def __init__(self, repo, name=None, baseui=None):
214 if isinstance(repo, str):
214 if isinstance(repo, str):
215 if baseui:
215 if baseui:
216 u = baseui.copy()
216 u = baseui.copy()
217 else:
217 else:
218 u = uimod.ui.load()
218 u = uimod.ui.load()
219 r = hg.repository(u, repo)
219 r = hg.repository(u, repo)
220 else:
220 else:
221 # we trust caller to give us a private copy
221 # we trust caller to give us a private copy
222 r = repo
222 r = repo
223
223
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 # resolve file patterns relative to repo root
228 # resolve file patterns relative to repo root
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 # displaying bundling progress bar while serving feel wrong and may
231 # displaying bundling progress bar while serving feel wrong and may
232 # break some wsgi implementation.
232 # break some wsgi implementation.
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 self._lastrepo = self._repos[0]
236 self._lastrepo = self._repos[0]
237 hook.redirect(True)
237 hook.redirect(True)
238 self.reponame = name
238 self.reponame = name
239
239
240 def _webifyrepo(self, repo):
240 def _webifyrepo(self, repo):
241 repo = getwebview(repo)
241 repo = getwebview(repo)
242 self.websubtable = webutil.getwebsubs(repo)
242 self.websubtable = webutil.getwebsubs(repo)
243 return repo
243 return repo
244
244
245 @contextlib.contextmanager
245 @contextlib.contextmanager
246 def _obtainrepo(self):
246 def _obtainrepo(self):
247 """Obtain a repo unique to the caller.
247 """Obtain a repo unique to the caller.
248
248
249 Internally we maintain a stack of cachedlocalrepo instances
249 Internally we maintain a stack of cachedlocalrepo instances
250 to be handed out. If one is available, we pop it and return it,
250 to be handed out. If one is available, we pop it and return it,
251 ensuring it is up to date in the process. If one is not available,
251 ensuring it is up to date in the process. If one is not available,
252 we clone the most recently used repo instance and return it.
252 we clone the most recently used repo instance and return it.
253
253
254 It is currently possible for the stack to grow without bounds
254 It is currently possible for the stack to grow without bounds
255 if the server allows infinite threads. However, servers should
255 if the server allows infinite threads. However, servers should
256 have a thread limit, thus establishing our limit.
256 have a thread limit, thus establishing our limit.
257 """
257 """
258 if self._repos:
258 if self._repos:
259 cached = self._repos.pop()
259 cached = self._repos.pop()
260 r, created = cached.fetch()
260 r, created = cached.fetch()
261 else:
261 else:
262 cached = self._lastrepo.copy()
262 cached = self._lastrepo.copy()
263 r, created = cached.fetch()
263 r, created = cached.fetch()
264 if created:
264 if created:
265 r = self._webifyrepo(r)
265 r = self._webifyrepo(r)
266
266
267 self._lastrepo = cached
267 self._lastrepo = cached
268 self.mtime = cached.mtime
268 self.mtime = cached.mtime
269 try:
269 try:
270 yield r
270 yield r
271 finally:
271 finally:
272 self._repos.append(cached)
272 self._repos.append(cached)
273
273
274 def run(self):
274 def run(self):
275 """Start a server from CGI environment.
275 """Start a server from CGI environment.
276
276
277 Modern servers should be using WSGI and should avoid this
277 Modern servers should be using WSGI and should avoid this
278 method, if possible.
278 method, if possible.
279 """
279 """
280 if not encoding.environ.get('GATEWAY_INTERFACE',
280 if not encoding.environ.get('GATEWAY_INTERFACE',
281 '').startswith("CGI/1."):
281 '').startswith("CGI/1."):
282 raise RuntimeError("This function is only intended to be "
282 raise RuntimeError("This function is only intended to be "
283 "called while running as a CGI script.")
283 "called while running as a CGI script.")
284 wsgicgi.launch(self)
284 wsgicgi.launch(self)
285
285
286 def __call__(self, env, respond):
286 def __call__(self, env, respond):
287 """Run the WSGI application.
287 """Run the WSGI application.
288
288
289 This may be called by multiple threads.
289 This may be called by multiple threads.
290 """
290 """
291 req = requestmod.wsgirequest(env, respond)
291 req = requestmod.wsgirequest(env, respond)
292 return self.run_wsgi(req)
292 return self.run_wsgi(req)
293
293
294 def run_wsgi(self, wsgireq):
294 def run_wsgi(self, wsgireq):
295 """Internal method to run the WSGI application.
295 """Internal method to run the WSGI application.
296
296
297 This is typically only called by Mercurial. External consumers
297 This is typically only called by Mercurial. External consumers
298 should be using instances of this class as the WSGI application.
298 should be using instances of this class as the WSGI application.
299 """
299 """
300 with self._obtainrepo() as repo:
300 with self._obtainrepo() as repo:
301 profile = repo.ui.configbool('profiling', 'enabled')
301 profile = repo.ui.configbool('profiling', 'enabled')
302 with profiling.profile(repo.ui, enabled=profile):
302 with profiling.profile(repo.ui, enabled=profile):
303 for r in self._runwsgi(wsgireq, repo):
303 for r in self._runwsgi(wsgireq, repo):
304 yield r
304 yield r
305
305
306 def _runwsgi(self, wsgireq, repo):
306 def _runwsgi(self, wsgireq, repo):
307 req = requestmod.parserequestfromenv(wsgireq.env)
307 req = requestmod.parserequestfromenv(wsgireq.env)
308 rctx = requestcontext(self, repo)
308 rctx = requestcontext(self, repo)
309
309
310 # This state is global across all threads.
310 # This state is global across all threads.
311 encoding.encoding = rctx.config('web', 'encoding')
311 encoding.encoding = rctx.config('web', 'encoding')
312 rctx.repo.ui.environ = wsgireq.env
312 rctx.repo.ui.environ = wsgireq.env
313
313
314 if rctx.csp:
314 if rctx.csp:
315 # hgwebdir may have added CSP header. Since we generate our own,
315 # hgwebdir may have added CSP header. Since we generate our own,
316 # replace it.
316 # replace it.
317 wsgireq.headers = [h for h in wsgireq.headers
317 wsgireq.headers = [h for h in wsgireq.headers
318 if h[0] != 'Content-Security-Policy']
318 if h[0] != 'Content-Security-Policy']
319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
320
320
321 if r'PATH_INFO' in wsgireq.env:
321 if r'PATH_INFO' in wsgireq.env:
322 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
322 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
323 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
323 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
324 if parts[:len(repo_parts)] == repo_parts:
324 if parts[:len(repo_parts)] == repo_parts:
325 parts = parts[len(repo_parts):]
325 parts = parts[len(repo_parts):]
326 query = r'/'.join(parts)
326 query = r'/'.join(parts)
327 else:
327 else:
328 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
328 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
329 query = query.partition(r';')[0]
329 query = query.partition(r';')[0]
330
330
331 # Route it to a wire protocol handler if it looks like a wire protocol
331 # Route it to a wire protocol handler if it looks like a wire protocol
332 # request.
332 # request.
333 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
333 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, req,
334 self.check_perm)
334 self.check_perm)
335
335
336 if protohandler:
336 if protohandler:
337 try:
337 try:
338 if query:
338 if query:
339 raise ErrorResponse(HTTP_NOT_FOUND)
339 raise ErrorResponse(HTTP_NOT_FOUND)
340
340
341 return protohandler['dispatch']()
341 return protohandler['dispatch']()
342 except ErrorResponse as inst:
342 except ErrorResponse as inst:
343 return protohandler['handleerror'](inst)
343 return protohandler['handleerror'](inst)
344
344
345 # translate user-visible url structure to internal structure
345 # translate user-visible url structure to internal structure
346
346
347 args = query.split(r'/', 2)
347 args = query.split(r'/', 2)
348 if 'cmd' not in wsgireq.form and args and args[0]:
348 if 'cmd' not in wsgireq.form and args and args[0]:
349 cmd = args.pop(0)
349 cmd = args.pop(0)
350 style = cmd.rfind('-')
350 style = cmd.rfind('-')
351 if style != -1:
351 if style != -1:
352 wsgireq.form['style'] = [cmd[:style]]
352 wsgireq.form['style'] = [cmd[:style]]
353 cmd = cmd[style + 1:]
353 cmd = cmd[style + 1:]
354
354
355 # avoid accepting e.g. style parameter as command
355 # avoid accepting e.g. style parameter as command
356 if util.safehasattr(webcommands, cmd):
356 if util.safehasattr(webcommands, cmd):
357 wsgireq.form['cmd'] = [cmd]
357 wsgireq.form['cmd'] = [cmd]
358
358
359 if cmd == 'static':
359 if cmd == 'static':
360 wsgireq.form['file'] = ['/'.join(args)]
360 wsgireq.form['file'] = ['/'.join(args)]
361 else:
361 else:
362 if args and args[0]:
362 if args and args[0]:
363 node = args.pop(0).replace('%2F', '/')
363 node = args.pop(0).replace('%2F', '/')
364 wsgireq.form['node'] = [node]
364 wsgireq.form['node'] = [node]
365 if args:
365 if args:
366 wsgireq.form['file'] = args
366 wsgireq.form['file'] = args
367
367
368 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
368 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
369 if cmd == 'rev' and 'mercurial' in ua:
369 if cmd == 'rev' and 'mercurial' in ua:
370 wsgireq.form['style'] = ['raw']
370 wsgireq.form['style'] = ['raw']
371
371
372 if cmd == 'archive':
372 if cmd == 'archive':
373 fn = wsgireq.form['node'][0]
373 fn = wsgireq.form['node'][0]
374 for type_, spec in rctx.archivespecs.iteritems():
374 for type_, spec in rctx.archivespecs.iteritems():
375 ext = spec[2]
375 ext = spec[2]
376 if fn.endswith(ext):
376 if fn.endswith(ext):
377 wsgireq.form['node'] = [fn[:-len(ext)]]
377 wsgireq.form['node'] = [fn[:-len(ext)]]
378 wsgireq.form['type'] = [type_]
378 wsgireq.form['type'] = [type_]
379 else:
379 else:
380 cmd = wsgireq.form.get('cmd', [''])[0]
380 cmd = wsgireq.form.get('cmd', [''])[0]
381
381
382 # process the web interface request
382 # process the web interface request
383
383
384 try:
384 try:
385 tmpl = rctx.templater(wsgireq, req)
385 tmpl = rctx.templater(wsgireq, req)
386 ctype = tmpl('mimetype', encoding=encoding.encoding)
386 ctype = tmpl('mimetype', encoding=encoding.encoding)
387 ctype = templater.stringify(ctype)
387 ctype = templater.stringify(ctype)
388
388
389 # check read permissions non-static content
389 # check read permissions non-static content
390 if cmd != 'static':
390 if cmd != 'static':
391 self.check_perm(rctx, wsgireq, None)
391 self.check_perm(rctx, wsgireq, None)
392
392
393 if cmd == '':
393 if cmd == '':
394 wsgireq.form['cmd'] = [tmpl.cache['default']]
394 wsgireq.form['cmd'] = [tmpl.cache['default']]
395 cmd = wsgireq.form['cmd'][0]
395 cmd = wsgireq.form['cmd'][0]
396
396
397 # Don't enable caching if using a CSP nonce because then it wouldn't
397 # Don't enable caching if using a CSP nonce because then it wouldn't
398 # be a nonce.
398 # be a nonce.
399 if rctx.configbool('web', 'cache') and not rctx.nonce:
399 if rctx.configbool('web', 'cache') and not rctx.nonce:
400 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
400 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
401 if cmd not in webcommands.__all__:
401 if cmd not in webcommands.__all__:
402 msg = 'no such method: %s' % cmd
402 msg = 'no such method: %s' % cmd
403 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
403 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
404 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
404 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
405 rctx.ctype = ctype
405 rctx.ctype = ctype
406 content = webcommands.rawfile(rctx, wsgireq, tmpl)
406 content = webcommands.rawfile(rctx, wsgireq, tmpl)
407 else:
407 else:
408 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
408 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
409 wsgireq.respond(HTTP_OK, ctype)
409 wsgireq.respond(HTTP_OK, ctype)
410
410
411 return content
411 return content
412
412
413 except (error.LookupError, error.RepoLookupError) as err:
413 except (error.LookupError, error.RepoLookupError) as err:
414 wsgireq.respond(HTTP_NOT_FOUND, ctype)
414 wsgireq.respond(HTTP_NOT_FOUND, ctype)
415 msg = pycompat.bytestr(err)
415 msg = pycompat.bytestr(err)
416 if (util.safehasattr(err, 'name') and
416 if (util.safehasattr(err, 'name') and
417 not isinstance(err, error.ManifestLookupError)):
417 not isinstance(err, error.ManifestLookupError)):
418 msg = 'revision not found: %s' % err.name
418 msg = 'revision not found: %s' % err.name
419 return tmpl('error', error=msg)
419 return tmpl('error', error=msg)
420 except (error.RepoError, error.RevlogError) as inst:
420 except (error.RepoError, error.RevlogError) as inst:
421 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
421 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
422 return tmpl('error', error=pycompat.bytestr(inst))
422 return tmpl('error', error=pycompat.bytestr(inst))
423 except ErrorResponse as inst:
423 except ErrorResponse as inst:
424 wsgireq.respond(inst, ctype)
424 wsgireq.respond(inst, ctype)
425 if inst.code == HTTP_NOT_MODIFIED:
425 if inst.code == HTTP_NOT_MODIFIED:
426 # Not allowed to return a body on a 304
426 # Not allowed to return a body on a 304
427 return ['']
427 return ['']
428 return tmpl('error', error=pycompat.bytestr(inst))
428 return tmpl('error', error=pycompat.bytestr(inst))
429
429
430 def check_perm(self, rctx, req, op):
430 def check_perm(self, rctx, req, op):
431 for permhook in permhooks:
431 for permhook in permhooks:
432 permhook(rctx, req, op)
432 permhook(rctx, req, op)
433
433
434 def getwebview(repo):
434 def getwebview(repo):
435 """The 'web.view' config controls changeset filter to hgweb. Possible
435 """The 'web.view' config controls changeset filter to hgweb. Possible
436 values are ``served``, ``visible`` and ``all``. Default is ``served``.
436 values are ``served``, ``visible`` and ``all``. Default is ``served``.
437 The ``served`` filter only shows changesets that can be pulled from the
437 The ``served`` filter only shows changesets that can be pulled from the
438 hgweb instance. The``visible`` filter includes secret changesets but
438 hgweb instance. The``visible`` filter includes secret changesets but
439 still excludes "hidden" one.
439 still excludes "hidden" one.
440
440
441 See the repoview module for details.
441 See the repoview module for details.
442
442
443 The option has been around undocumented since Mercurial 2.5, but no
443 The option has been around undocumented since Mercurial 2.5, but no
444 user ever asked about it. So we better keep it undocumented for now."""
444 user ever asked about it. So we better keep it undocumented for now."""
445 # experimental config: web.view
445 # experimental config: web.view
446 viewconfig = repo.ui.config('web', 'view', untrusted=True)
446 viewconfig = repo.ui.config('web', 'view', untrusted=True)
447 if viewconfig == 'all':
447 if viewconfig == 'all':
448 return repo.unfiltered()
448 return repo.unfiltered()
449 elif viewconfig in repoview.filtertable:
449 elif viewconfig in repoview.filtertable:
450 return repo.filtered(viewconfig)
450 return repo.filtered(viewconfig)
451 else:
451 else:
452 return repo.filtered('served')
452 return repo.filtered('served')
@@ -1,651 +1,652 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(wsgireq, 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 = wsgireq.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, wsgireq, ui, checkperm):
57 def __init__(self, wsgireq, ui, checkperm):
58 self._wsgireq = wsgireq
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._wsgireq.form.copy())
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
83 postlen = int(self._wsgireq.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._wsgireq.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._wsgireq, 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._wsgireq.env:
94 if r'HTTP_CONTENT_LENGTH' in self._wsgireq.env:
95 length = int(self._wsgireq.env[r'HTTP_CONTENT_LENGTH'])
95 length = int(self._wsgireq.env[r'HTTP_CONTENT_LENGTH'])
96 else:
96 else:
97 length = int(self._wsgireq.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._wsgireq.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._wsgireq, 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._wsgireq.env.get('wsgi.url_scheme') or 'http',
121 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
122 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
122 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
123 urlreq.quote(self._wsgireq.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, wsgireq, query, checkperm):
153 def parsehttprequest(rctx, wsgireq, req, 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 ``wsgireq`` is a ``wsgirequest`` instance.
161 ``wsgireq`` is a ``wsgirequest`` instance.
162 ``req`` is a ``parsedrequest`` instance.
162 """
163 """
163 repo = rctx.repo
164 repo = rctx.repo
164
165
165 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
166 # 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
167 # string parameter. If it isn't present, this isn't a wire protocol
167 # request.
168 # request.
168 if 'cmd' not in wsgireq.form:
169 if 'cmd' not in req.querystringdict:
169 return None
170 return None
170
171
171 cmd = wsgireq.form['cmd'][0]
172 cmd = req.querystringdict['cmd'][0]
172
173
173 # The "cmd" request parameter is used by both the wire protocol and hgweb.
174 # 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,
175 # 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
176 # 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
177 # route it to a protocol handler. This is better than routing possible
177 # wire protocol requests to hgweb because it prevents hgweb from using
178 # wire protocol requests to hgweb because it prevents hgweb from using
178 # known wire protocol commands and it is less confusing for machine
179 # known wire protocol commands and it is less confusing for machine
179 # clients.
180 # clients.
180 if not iscmd(cmd):
181 if not iscmd(cmd):
181 return None
182 return None
182
183
183 proto = httpv1protocolhandler(wsgireq, repo.ui,
184 proto = httpv1protocolhandler(wsgireq, repo.ui,
184 lambda perm: checkperm(rctx, wsgireq, perm))
185 lambda perm: checkperm(rctx, wsgireq, perm))
185
186
186 return {
187 return {
187 'cmd': cmd,
188 'cmd': cmd,
188 'proto': proto,
189 'proto': proto,
189 'dispatch': lambda: _callhttp(repo, wsgireq, proto, cmd),
190 'dispatch': lambda: _callhttp(repo, wsgireq, proto, cmd),
190 'handleerror': lambda ex: _handlehttperror(ex, wsgireq, cmd),
191 'handleerror': lambda ex: _handlehttperror(ex, wsgireq, cmd),
191 }
192 }
192
193
193 def _httpresponsetype(ui, wsgireq, prefer_uncompressed):
194 def _httpresponsetype(ui, wsgireq, prefer_uncompressed):
194 """Determine the appropriate response type and compression settings.
195 """Determine the appropriate response type and compression settings.
195
196
196 Returns a tuple of (mediatype, compengine, engineopts).
197 Returns a tuple of (mediatype, compengine, engineopts).
197 """
198 """
198 # Determine the response media type and compression engine based
199 # Determine the response media type and compression engine based
199 # on the request parameters.
200 # on the request parameters.
200 protocaps = decodevaluefromheaders(wsgireq, r'X-HgProto').split(' ')
201 protocaps = decodevaluefromheaders(wsgireq, r'X-HgProto').split(' ')
201
202
202 if '0.2' in protocaps:
203 if '0.2' in protocaps:
203 # All clients are expected to support uncompressed data.
204 # All clients are expected to support uncompressed data.
204 if prefer_uncompressed:
205 if prefer_uncompressed:
205 return HGTYPE2, util._noopengine(), {}
206 return HGTYPE2, util._noopengine(), {}
206
207
207 # Default as defined by wire protocol spec.
208 # Default as defined by wire protocol spec.
208 compformats = ['zlib', 'none']
209 compformats = ['zlib', 'none']
209 for cap in protocaps:
210 for cap in protocaps:
210 if cap.startswith('comp='):
211 if cap.startswith('comp='):
211 compformats = cap[5:].split(',')
212 compformats = cap[5:].split(',')
212 break
213 break
213
214
214 # Now find an agreed upon compression format.
215 # Now find an agreed upon compression format.
215 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
216 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
216 if engine.wireprotosupport().name in compformats:
217 if engine.wireprotosupport().name in compformats:
217 opts = {}
218 opts = {}
218 level = ui.configint('server', '%slevel' % engine.name())
219 level = ui.configint('server', '%slevel' % engine.name())
219 if level is not None:
220 if level is not None:
220 opts['level'] = level
221 opts['level'] = level
221
222
222 return HGTYPE2, engine, opts
223 return HGTYPE2, engine, opts
223
224
224 # No mutually supported compression format. Fall back to the
225 # No mutually supported compression format. Fall back to the
225 # legacy protocol.
226 # legacy protocol.
226
227
227 # Don't allow untrusted settings because disabling compression or
228 # Don't allow untrusted settings because disabling compression or
228 # setting a very high compression level could lead to flooding
229 # setting a very high compression level could lead to flooding
229 # the server's network or CPU.
230 # the server's network or CPU.
230 opts = {'level': ui.configint('server', 'zliblevel')}
231 opts = {'level': ui.configint('server', 'zliblevel')}
231 return HGTYPE, util.compengines['zlib'], opts
232 return HGTYPE, util.compengines['zlib'], opts
232
233
233 def _callhttp(repo, wsgireq, proto, cmd):
234 def _callhttp(repo, wsgireq, proto, cmd):
234 def genversion2(gen, engine, engineopts):
235 def genversion2(gen, engine, engineopts):
235 # application/mercurial-0.2 always sends a payload header
236 # application/mercurial-0.2 always sends a payload header
236 # identifying the compression engine.
237 # identifying the compression engine.
237 name = engine.wireprotosupport().name
238 name = engine.wireprotosupport().name
238 assert 0 < len(name) < 256
239 assert 0 < len(name) < 256
239 yield struct.pack('B', len(name))
240 yield struct.pack('B', len(name))
240 yield name
241 yield name
241
242
242 for chunk in gen:
243 for chunk in gen:
243 yield chunk
244 yield chunk
244
245
245 if not wireproto.commands.commandavailable(cmd, proto):
246 if not wireproto.commands.commandavailable(cmd, proto):
246 wsgireq.respond(HTTP_OK, HGERRTYPE,
247 wsgireq.respond(HTTP_OK, HGERRTYPE,
247 body=_('requested wire protocol command is not '
248 body=_('requested wire protocol command is not '
248 'available over HTTP'))
249 'available over HTTP'))
249 return []
250 return []
250
251
251 proto.checkperm(wireproto.commands[cmd].permission)
252 proto.checkperm(wireproto.commands[cmd].permission)
252
253
253 rsp = wireproto.dispatch(repo, proto, cmd)
254 rsp = wireproto.dispatch(repo, proto, cmd)
254
255
255 if isinstance(rsp, bytes):
256 if isinstance(rsp, bytes):
256 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
257 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
257 return []
258 return []
258 elif isinstance(rsp, wireprototypes.bytesresponse):
259 elif isinstance(rsp, wireprototypes.bytesresponse):
259 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
260 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp.data)
260 return []
261 return []
261 elif isinstance(rsp, wireprototypes.streamreslegacy):
262 elif isinstance(rsp, wireprototypes.streamreslegacy):
262 gen = rsp.gen
263 gen = rsp.gen
263 wsgireq.respond(HTTP_OK, HGTYPE)
264 wsgireq.respond(HTTP_OK, HGTYPE)
264 return gen
265 return gen
265 elif isinstance(rsp, wireprototypes.streamres):
266 elif isinstance(rsp, wireprototypes.streamres):
266 gen = rsp.gen
267 gen = rsp.gen
267
268
268 # This code for compression should not be streamres specific. It
269 # This code for compression should not be streamres specific. It
269 # is here because we only compress streamres at the moment.
270 # is here because we only compress streamres at the moment.
270 mediatype, engine, engineopts = _httpresponsetype(
271 mediatype, engine, engineopts = _httpresponsetype(
271 repo.ui, wsgireq, rsp.prefer_uncompressed)
272 repo.ui, wsgireq, rsp.prefer_uncompressed)
272 gen = engine.compressstream(gen, engineopts)
273 gen = engine.compressstream(gen, engineopts)
273
274
274 if mediatype == HGTYPE2:
275 if mediatype == HGTYPE2:
275 gen = genversion2(gen, engine, engineopts)
276 gen = genversion2(gen, engine, engineopts)
276
277
277 wsgireq.respond(HTTP_OK, mediatype)
278 wsgireq.respond(HTTP_OK, mediatype)
278 return gen
279 return gen
279 elif isinstance(rsp, wireprototypes.pushres):
280 elif isinstance(rsp, wireprototypes.pushres):
280 rsp = '%d\n%s' % (rsp.res, rsp.output)
281 rsp = '%d\n%s' % (rsp.res, rsp.output)
281 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
282 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
282 return []
283 return []
283 elif isinstance(rsp, wireprototypes.pusherr):
284 elif isinstance(rsp, wireprototypes.pusherr):
284 # This is the httplib workaround documented in _handlehttperror().
285 # This is the httplib workaround documented in _handlehttperror().
285 wsgireq.drain()
286 wsgireq.drain()
286
287
287 rsp = '0\n%s\n' % rsp.res
288 rsp = '0\n%s\n' % rsp.res
288 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
289 wsgireq.respond(HTTP_OK, HGTYPE, body=rsp)
289 return []
290 return []
290 elif isinstance(rsp, wireprototypes.ooberror):
291 elif isinstance(rsp, wireprototypes.ooberror):
291 rsp = rsp.message
292 rsp = rsp.message
292 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
293 wsgireq.respond(HTTP_OK, HGERRTYPE, body=rsp)
293 return []
294 return []
294 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
295 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
295
296
296 def _handlehttperror(e, wsgireq, cmd):
297 def _handlehttperror(e, wsgireq, cmd):
297 """Called when an ErrorResponse is raised during HTTP request processing."""
298 """Called when an ErrorResponse is raised during HTTP request processing."""
298
299
299 # Clients using Python's httplib are stateful: the HTTP client
300 # Clients using Python's httplib are stateful: the HTTP client
300 # won't process an HTTP response until all request data is
301 # won't process an HTTP response until all request data is
301 # sent to the server. The intent of this code is to ensure
302 # sent to the server. The intent of this code is to ensure
302 # we always read HTTP request data from the client, thus
303 # we always read HTTP request data from the client, thus
303 # ensuring httplib transitions to a state that allows it to read
304 # ensuring httplib transitions to a state that allows it to read
304 # the HTTP response. In other words, it helps prevent deadlocks
305 # the HTTP response. In other words, it helps prevent deadlocks
305 # on clients using httplib.
306 # on clients using httplib.
306
307
307 if (wsgireq.env[r'REQUEST_METHOD'] == r'POST' and
308 if (wsgireq.env[r'REQUEST_METHOD'] == r'POST' and
308 # But not if Expect: 100-continue is being used.
309 # But not if Expect: 100-continue is being used.
309 (wsgireq.env.get('HTTP_EXPECT',
310 (wsgireq.env.get('HTTP_EXPECT',
310 '').lower() != '100-continue') or
311 '').lower() != '100-continue') or
311 # Or the non-httplib HTTP library is being advertised by
312 # Or the non-httplib HTTP library is being advertised by
312 # the client.
313 # the client.
313 wsgireq.env.get('X-HgHttp2', '')):
314 wsgireq.env.get('X-HgHttp2', '')):
314 wsgireq.drain()
315 wsgireq.drain()
315 else:
316 else:
316 wsgireq.headers.append((r'Connection', r'Close'))
317 wsgireq.headers.append((r'Connection', r'Close'))
317
318
318 # TODO This response body assumes the failed command was
319 # TODO This response body assumes the failed command was
319 # "unbundle." That assumption is not always valid.
320 # "unbundle." That assumption is not always valid.
320 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
321 wsgireq.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
321
322
322 return ''
323 return ''
323
324
324 def _sshv1respondbytes(fout, value):
325 def _sshv1respondbytes(fout, value):
325 """Send a bytes response for protocol version 1."""
326 """Send a bytes response for protocol version 1."""
326 fout.write('%d\n' % len(value))
327 fout.write('%d\n' % len(value))
327 fout.write(value)
328 fout.write(value)
328 fout.flush()
329 fout.flush()
329
330
330 def _sshv1respondstream(fout, source):
331 def _sshv1respondstream(fout, source):
331 write = fout.write
332 write = fout.write
332 for chunk in source.gen:
333 for chunk in source.gen:
333 write(chunk)
334 write(chunk)
334 fout.flush()
335 fout.flush()
335
336
336 def _sshv1respondooberror(fout, ferr, rsp):
337 def _sshv1respondooberror(fout, ferr, rsp):
337 ferr.write(b'%s\n-\n' % rsp)
338 ferr.write(b'%s\n-\n' % rsp)
338 ferr.flush()
339 ferr.flush()
339 fout.write(b'\n')
340 fout.write(b'\n')
340 fout.flush()
341 fout.flush()
341
342
342 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
343 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
343 """Handler for requests services via version 1 of SSH protocol."""
344 """Handler for requests services via version 1 of SSH protocol."""
344 def __init__(self, ui, fin, fout):
345 def __init__(self, ui, fin, fout):
345 self._ui = ui
346 self._ui = ui
346 self._fin = fin
347 self._fin = fin
347 self._fout = fout
348 self._fout = fout
348
349
349 @property
350 @property
350 def name(self):
351 def name(self):
351 return wireprototypes.SSHV1
352 return wireprototypes.SSHV1
352
353
353 def getargs(self, args):
354 def getargs(self, args):
354 data = {}
355 data = {}
355 keys = args.split()
356 keys = args.split()
356 for n in xrange(len(keys)):
357 for n in xrange(len(keys)):
357 argline = self._fin.readline()[:-1]
358 argline = self._fin.readline()[:-1]
358 arg, l = argline.split()
359 arg, l = argline.split()
359 if arg not in keys:
360 if arg not in keys:
360 raise error.Abort(_("unexpected parameter %r") % arg)
361 raise error.Abort(_("unexpected parameter %r") % arg)
361 if arg == '*':
362 if arg == '*':
362 star = {}
363 star = {}
363 for k in xrange(int(l)):
364 for k in xrange(int(l)):
364 argline = self._fin.readline()[:-1]
365 argline = self._fin.readline()[:-1]
365 arg, l = argline.split()
366 arg, l = argline.split()
366 val = self._fin.read(int(l))
367 val = self._fin.read(int(l))
367 star[arg] = val
368 star[arg] = val
368 data['*'] = star
369 data['*'] = star
369 else:
370 else:
370 val = self._fin.read(int(l))
371 val = self._fin.read(int(l))
371 data[arg] = val
372 data[arg] = val
372 return [data[k] for k in keys]
373 return [data[k] for k in keys]
373
374
374 def forwardpayload(self, fpout):
375 def forwardpayload(self, fpout):
375 # We initially send an empty response. This tells the client it is
376 # 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
377 # OK to start sending data. If a client sees any other response, it
377 # interprets it as an error.
378 # interprets it as an error.
378 _sshv1respondbytes(self._fout, b'')
379 _sshv1respondbytes(self._fout, b'')
379
380
380 # The file is in the form:
381 # The file is in the form:
381 #
382 #
382 # <chunk size>\n<chunk>
383 # <chunk size>\n<chunk>
383 # ...
384 # ...
384 # 0\n
385 # 0\n
385 count = int(self._fin.readline())
386 count = int(self._fin.readline())
386 while count:
387 while count:
387 fpout.write(self._fin.read(count))
388 fpout.write(self._fin.read(count))
388 count = int(self._fin.readline())
389 count = int(self._fin.readline())
389
390
390 @contextlib.contextmanager
391 @contextlib.contextmanager
391 def mayberedirectstdio(self):
392 def mayberedirectstdio(self):
392 yield None
393 yield None
393
394
394 def client(self):
395 def client(self):
395 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
396 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
396 return 'remote:ssh:' + client
397 return 'remote:ssh:' + client
397
398
398 def addcapabilities(self, repo, caps):
399 def addcapabilities(self, repo, caps):
399 return caps
400 return caps
400
401
401 def checkperm(self, perm):
402 def checkperm(self, perm):
402 pass
403 pass
403
404
404 class sshv2protocolhandler(sshv1protocolhandler):
405 class sshv2protocolhandler(sshv1protocolhandler):
405 """Protocol handler for version 2 of the SSH protocol."""
406 """Protocol handler for version 2 of the SSH protocol."""
406
407
407 @property
408 @property
408 def name(self):
409 def name(self):
409 return wireprototypes.SSHV2
410 return wireprototypes.SSHV2
410
411
411 def _runsshserver(ui, repo, fin, fout, ev):
412 def _runsshserver(ui, repo, fin, fout, ev):
412 # This function operates like a state machine of sorts. The following
413 # This function operates like a state machine of sorts. The following
413 # states are defined:
414 # states are defined:
414 #
415 #
415 # protov1-serving
416 # protov1-serving
416 # Server is in protocol version 1 serving mode. Commands arrive on
417 # Server is in protocol version 1 serving mode. Commands arrive on
417 # new lines. These commands are processed in this state, one command
418 # new lines. These commands are processed in this state, one command
418 # after the other.
419 # after the other.
419 #
420 #
420 # protov2-serving
421 # protov2-serving
421 # Server is in protocol version 2 serving mode.
422 # Server is in protocol version 2 serving mode.
422 #
423 #
423 # upgrade-initial
424 # upgrade-initial
424 # The server is going to process an upgrade request.
425 # The server is going to process an upgrade request.
425 #
426 #
426 # upgrade-v2-filter-legacy-handshake
427 # upgrade-v2-filter-legacy-handshake
427 # The protocol is being upgraded to version 2. The server is expecting
428 # The protocol is being upgraded to version 2. The server is expecting
428 # the legacy handshake from version 1.
429 # the legacy handshake from version 1.
429 #
430 #
430 # upgrade-v2-finish
431 # upgrade-v2-finish
431 # The upgrade to version 2 of the protocol is imminent.
432 # The upgrade to version 2 of the protocol is imminent.
432 #
433 #
433 # shutdown
434 # shutdown
434 # The server is shutting down, possibly in reaction to a client event.
435 # The server is shutting down, possibly in reaction to a client event.
435 #
436 #
436 # And here are their transitions:
437 # And here are their transitions:
437 #
438 #
438 # protov1-serving -> shutdown
439 # protov1-serving -> shutdown
439 # When server receives an empty request or encounters another
440 # When server receives an empty request or encounters another
440 # error.
441 # error.
441 #
442 #
442 # protov1-serving -> upgrade-initial
443 # protov1-serving -> upgrade-initial
443 # An upgrade request line was seen.
444 # An upgrade request line was seen.
444 #
445 #
445 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
446 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
446 # Upgrade to version 2 in progress. Server is expecting to
447 # Upgrade to version 2 in progress. Server is expecting to
447 # process a legacy handshake.
448 # process a legacy handshake.
448 #
449 #
449 # upgrade-v2-filter-legacy-handshake -> shutdown
450 # upgrade-v2-filter-legacy-handshake -> shutdown
450 # Client did not fulfill upgrade handshake requirements.
451 # Client did not fulfill upgrade handshake requirements.
451 #
452 #
452 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
453 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
453 # Client fulfilled version 2 upgrade requirements. Finishing that
454 # Client fulfilled version 2 upgrade requirements. Finishing that
454 # upgrade.
455 # upgrade.
455 #
456 #
456 # upgrade-v2-finish -> protov2-serving
457 # upgrade-v2-finish -> protov2-serving
457 # Protocol upgrade to version 2 complete. Server can now speak protocol
458 # Protocol upgrade to version 2 complete. Server can now speak protocol
458 # version 2.
459 # version 2.
459 #
460 #
460 # protov2-serving -> protov1-serving
461 # protov2-serving -> protov1-serving
461 # Ths happens by default since protocol version 2 is the same as
462 # Ths happens by default since protocol version 2 is the same as
462 # version 1 except for the handshake.
463 # version 1 except for the handshake.
463
464
464 state = 'protov1-serving'
465 state = 'protov1-serving'
465 proto = sshv1protocolhandler(ui, fin, fout)
466 proto = sshv1protocolhandler(ui, fin, fout)
466 protoswitched = False
467 protoswitched = False
467
468
468 while not ev.is_set():
469 while not ev.is_set():
469 if state == 'protov1-serving':
470 if state == 'protov1-serving':
470 # Commands are issued on new lines.
471 # Commands are issued on new lines.
471 request = fin.readline()[:-1]
472 request = fin.readline()[:-1]
472
473
473 # Empty lines signal to terminate the connection.
474 # Empty lines signal to terminate the connection.
474 if not request:
475 if not request:
475 state = 'shutdown'
476 state = 'shutdown'
476 continue
477 continue
477
478
478 # It looks like a protocol upgrade request. Transition state to
479 # It looks like a protocol upgrade request. Transition state to
479 # handle it.
480 # handle it.
480 if request.startswith(b'upgrade '):
481 if request.startswith(b'upgrade '):
481 if protoswitched:
482 if protoswitched:
482 _sshv1respondooberror(fout, ui.ferr,
483 _sshv1respondooberror(fout, ui.ferr,
483 b'cannot upgrade protocols multiple '
484 b'cannot upgrade protocols multiple '
484 b'times')
485 b'times')
485 state = 'shutdown'
486 state = 'shutdown'
486 continue
487 continue
487
488
488 state = 'upgrade-initial'
489 state = 'upgrade-initial'
489 continue
490 continue
490
491
491 available = wireproto.commands.commandavailable(request, proto)
492 available = wireproto.commands.commandavailable(request, proto)
492
493
493 # This command isn't available. Send an empty response and go
494 # This command isn't available. Send an empty response and go
494 # back to waiting for a new command.
495 # back to waiting for a new command.
495 if not available:
496 if not available:
496 _sshv1respondbytes(fout, b'')
497 _sshv1respondbytes(fout, b'')
497 continue
498 continue
498
499
499 rsp = wireproto.dispatch(repo, proto, request)
500 rsp = wireproto.dispatch(repo, proto, request)
500
501
501 if isinstance(rsp, bytes):
502 if isinstance(rsp, bytes):
502 _sshv1respondbytes(fout, rsp)
503 _sshv1respondbytes(fout, rsp)
503 elif isinstance(rsp, wireprototypes.bytesresponse):
504 elif isinstance(rsp, wireprototypes.bytesresponse):
504 _sshv1respondbytes(fout, rsp.data)
505 _sshv1respondbytes(fout, rsp.data)
505 elif isinstance(rsp, wireprototypes.streamres):
506 elif isinstance(rsp, wireprototypes.streamres):
506 _sshv1respondstream(fout, rsp)
507 _sshv1respondstream(fout, rsp)
507 elif isinstance(rsp, wireprototypes.streamreslegacy):
508 elif isinstance(rsp, wireprototypes.streamreslegacy):
508 _sshv1respondstream(fout, rsp)
509 _sshv1respondstream(fout, rsp)
509 elif isinstance(rsp, wireprototypes.pushres):
510 elif isinstance(rsp, wireprototypes.pushres):
510 _sshv1respondbytes(fout, b'')
511 _sshv1respondbytes(fout, b'')
511 _sshv1respondbytes(fout, b'%d' % rsp.res)
512 _sshv1respondbytes(fout, b'%d' % rsp.res)
512 elif isinstance(rsp, wireprototypes.pusherr):
513 elif isinstance(rsp, wireprototypes.pusherr):
513 _sshv1respondbytes(fout, rsp.res)
514 _sshv1respondbytes(fout, rsp.res)
514 elif isinstance(rsp, wireprototypes.ooberror):
515 elif isinstance(rsp, wireprototypes.ooberror):
515 _sshv1respondooberror(fout, ui.ferr, rsp.message)
516 _sshv1respondooberror(fout, ui.ferr, rsp.message)
516 else:
517 else:
517 raise error.ProgrammingError('unhandled response type from '
518 raise error.ProgrammingError('unhandled response type from '
518 'wire protocol command: %s' % rsp)
519 'wire protocol command: %s' % rsp)
519
520
520 # For now, protocol version 2 serving just goes back to version 1.
521 # For now, protocol version 2 serving just goes back to version 1.
521 elif state == 'protov2-serving':
522 elif state == 'protov2-serving':
522 state = 'protov1-serving'
523 state = 'protov1-serving'
523 continue
524 continue
524
525
525 elif state == 'upgrade-initial':
526 elif state == 'upgrade-initial':
526 # We should never transition into this state if we've switched
527 # We should never transition into this state if we've switched
527 # protocols.
528 # protocols.
528 assert not protoswitched
529 assert not protoswitched
529 assert proto.name == wireprototypes.SSHV1
530 assert proto.name == wireprototypes.SSHV1
530
531
531 # Expected: upgrade <token> <capabilities>
532 # Expected: upgrade <token> <capabilities>
532 # If we get something else, the request is malformed. It could be
533 # If we get something else, the request is malformed. It could be
533 # from a future client that has altered the upgrade line content.
534 # from a future client that has altered the upgrade line content.
534 # We treat this as an unknown command.
535 # We treat this as an unknown command.
535 try:
536 try:
536 token, caps = request.split(b' ')[1:]
537 token, caps = request.split(b' ')[1:]
537 except ValueError:
538 except ValueError:
538 _sshv1respondbytes(fout, b'')
539 _sshv1respondbytes(fout, b'')
539 state = 'protov1-serving'
540 state = 'protov1-serving'
540 continue
541 continue
541
542
542 # Send empty response if we don't support upgrading protocols.
543 # Send empty response if we don't support upgrading protocols.
543 if not ui.configbool('experimental', 'sshserver.support-v2'):
544 if not ui.configbool('experimental', 'sshserver.support-v2'):
544 _sshv1respondbytes(fout, b'')
545 _sshv1respondbytes(fout, b'')
545 state = 'protov1-serving'
546 state = 'protov1-serving'
546 continue
547 continue
547
548
548 try:
549 try:
549 caps = urlreq.parseqs(caps)
550 caps = urlreq.parseqs(caps)
550 except ValueError:
551 except ValueError:
551 _sshv1respondbytes(fout, b'')
552 _sshv1respondbytes(fout, b'')
552 state = 'protov1-serving'
553 state = 'protov1-serving'
553 continue
554 continue
554
555
555 # We don't see an upgrade request to protocol version 2. Ignore
556 # We don't see an upgrade request to protocol version 2. Ignore
556 # the upgrade request.
557 # the upgrade request.
557 wantedprotos = caps.get(b'proto', [b''])[0]
558 wantedprotos = caps.get(b'proto', [b''])[0]
558 if SSHV2 not in wantedprotos:
559 if SSHV2 not in wantedprotos:
559 _sshv1respondbytes(fout, b'')
560 _sshv1respondbytes(fout, b'')
560 state = 'protov1-serving'
561 state = 'protov1-serving'
561 continue
562 continue
562
563
563 # It looks like we can honor this upgrade request to protocol 2.
564 # It looks like we can honor this upgrade request to protocol 2.
564 # Filter the rest of the handshake protocol request lines.
565 # Filter the rest of the handshake protocol request lines.
565 state = 'upgrade-v2-filter-legacy-handshake'
566 state = 'upgrade-v2-filter-legacy-handshake'
566 continue
567 continue
567
568
568 elif state == 'upgrade-v2-filter-legacy-handshake':
569 elif state == 'upgrade-v2-filter-legacy-handshake':
569 # Client should have sent legacy handshake after an ``upgrade``
570 # Client should have sent legacy handshake after an ``upgrade``
570 # request. Expected lines:
571 # request. Expected lines:
571 #
572 #
572 # hello
573 # hello
573 # between
574 # between
574 # pairs 81
575 # pairs 81
575 # 0000...-0000...
576 # 0000...-0000...
576
577
577 ok = True
578 ok = True
578 for line in (b'hello', b'between', b'pairs 81'):
579 for line in (b'hello', b'between', b'pairs 81'):
579 request = fin.readline()[:-1]
580 request = fin.readline()[:-1]
580
581
581 if request != line:
582 if request != line:
582 _sshv1respondooberror(fout, ui.ferr,
583 _sshv1respondooberror(fout, ui.ferr,
583 b'malformed handshake protocol: '
584 b'malformed handshake protocol: '
584 b'missing %s' % line)
585 b'missing %s' % line)
585 ok = False
586 ok = False
586 state = 'shutdown'
587 state = 'shutdown'
587 break
588 break
588
589
589 if not ok:
590 if not ok:
590 continue
591 continue
591
592
592 request = fin.read(81)
593 request = fin.read(81)
593 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
594 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
594 _sshv1respondooberror(fout, ui.ferr,
595 _sshv1respondooberror(fout, ui.ferr,
595 b'malformed handshake protocol: '
596 b'malformed handshake protocol: '
596 b'missing between argument value')
597 b'missing between argument value')
597 state = 'shutdown'
598 state = 'shutdown'
598 continue
599 continue
599
600
600 state = 'upgrade-v2-finish'
601 state = 'upgrade-v2-finish'
601 continue
602 continue
602
603
603 elif state == 'upgrade-v2-finish':
604 elif state == 'upgrade-v2-finish':
604 # Send the upgrade response.
605 # Send the upgrade response.
605 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
606 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
606 servercaps = wireproto.capabilities(repo, proto)
607 servercaps = wireproto.capabilities(repo, proto)
607 rsp = b'capabilities: %s' % servercaps.data
608 rsp = b'capabilities: %s' % servercaps.data
608 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
609 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
609 fout.flush()
610 fout.flush()
610
611
611 proto = sshv2protocolhandler(ui, fin, fout)
612 proto = sshv2protocolhandler(ui, fin, fout)
612 protoswitched = True
613 protoswitched = True
613
614
614 state = 'protov2-serving'
615 state = 'protov2-serving'
615 continue
616 continue
616
617
617 elif state == 'shutdown':
618 elif state == 'shutdown':
618 break
619 break
619
620
620 else:
621 else:
621 raise error.ProgrammingError('unhandled ssh server state: %s' %
622 raise error.ProgrammingError('unhandled ssh server state: %s' %
622 state)
623 state)
623
624
624 class sshserver(object):
625 class sshserver(object):
625 def __init__(self, ui, repo, logfh=None):
626 def __init__(self, ui, repo, logfh=None):
626 self._ui = ui
627 self._ui = ui
627 self._repo = repo
628 self._repo = repo
628 self._fin = ui.fin
629 self._fin = ui.fin
629 self._fout = ui.fout
630 self._fout = ui.fout
630
631
631 # Log write I/O to stdout and stderr if configured.
632 # Log write I/O to stdout and stderr if configured.
632 if logfh:
633 if logfh:
633 self._fout = util.makeloggingfileobject(
634 self._fout = util.makeloggingfileobject(
634 logfh, self._fout, 'o', logdata=True)
635 logfh, self._fout, 'o', logdata=True)
635 ui.ferr = util.makeloggingfileobject(
636 ui.ferr = util.makeloggingfileobject(
636 logfh, ui.ferr, 'e', logdata=True)
637 logfh, ui.ferr, 'e', logdata=True)
637
638
638 hook.redirect(True)
639 hook.redirect(True)
639 ui.fout = repo.ui.fout = ui.ferr
640 ui.fout = repo.ui.fout = ui.ferr
640
641
641 # Prevent insertion/deletion of CRs
642 # Prevent insertion/deletion of CRs
642 util.setbinary(self._fin)
643 util.setbinary(self._fin)
643 util.setbinary(self._fout)
644 util.setbinary(self._fout)
644
645
645 def serve_forever(self):
646 def serve_forever(self):
646 self.serveuntil(threading.Event())
647 self.serveuntil(threading.Event())
647 sys.exit(0)
648 sys.exit(0)
648
649
649 def serveuntil(self, ev):
650 def serveuntil(self, ev):
650 """Serve until a threading.Event is set."""
651 """Serve until a threading.Event is set."""
651 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
652 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now