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