##// END OF EJS Templates
wireprotoserver: move error response handling out of hgweb...
Gregory Szorc -
r36004:98a00aa0 default
parent child Browse files
Show More
@@ -1,494 +1,483 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
25 from .request import wsgirequest
26
26
27 from .. import (
27 from .. import (
28 encoding,
28 encoding,
29 error,
29 error,
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 webcommands,
43 webcommands,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 perms = {
48 perms = {
49 'changegroup': 'pull',
49 'changegroup': 'pull',
50 'changegroupsubset': 'pull',
50 'changegroupsubset': 'pull',
51 'getbundle': 'pull',
51 'getbundle': 'pull',
52 'stream_out': 'pull',
52 'stream_out': 'pull',
53 'listkeys': 'pull',
53 'listkeys': 'pull',
54 'unbundle': 'push',
54 'unbundle': 'push',
55 'pushkey': 'push',
55 'pushkey': 'push',
56 }
56 }
57
57
58 archivespecs = util.sortdict((
58 archivespecs = util.sortdict((
59 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 ))
62 ))
63
63
64 def getstyle(req, configfn, templatepath):
64 def getstyle(req, configfn, templatepath):
65 fromreq = req.form.get('style', [None])[0]
65 fromreq = req.form.get('style', [None])[0]
66 if fromreq is not None:
66 if fromreq is not None:
67 fromreq = pycompat.sysbytes(fromreq)
67 fromreq = pycompat.sysbytes(fromreq)
68 styles = (
68 styles = (
69 fromreq,
69 fromreq,
70 configfn('web', 'style'),
70 configfn('web', 'style'),
71 'paper',
71 'paper',
72 )
72 )
73 return styles, templater.stylemap(styles, templatepath)
73 return styles, templater.stylemap(styles, templatepath)
74
74
75 def makebreadcrumb(url, prefix=''):
75 def makebreadcrumb(url, prefix=''):
76 '''Return a 'URL breadcrumb' list
76 '''Return a 'URL breadcrumb' list
77
77
78 A 'URL breadcrumb' is a list of URL-name pairs,
78 A 'URL breadcrumb' is a list of URL-name pairs,
79 corresponding to each of the path items on a URL.
79 corresponding to each of the path items on a URL.
80 This can be used to create path navigation entries.
80 This can be used to create path navigation entries.
81 '''
81 '''
82 if url.endswith('/'):
82 if url.endswith('/'):
83 url = url[:-1]
83 url = url[:-1]
84 if prefix:
84 if prefix:
85 url = '/' + prefix + url
85 url = '/' + prefix + url
86 relpath = url
86 relpath = url
87 if relpath.startswith('/'):
87 if relpath.startswith('/'):
88 relpath = relpath[1:]
88 relpath = relpath[1:]
89
89
90 breadcrumb = []
90 breadcrumb = []
91 urlel = url
91 urlel = url
92 pathitems = [''] + relpath.split('/')
92 pathitems = [''] + relpath.split('/')
93 for pathel in reversed(pathitems):
93 for pathel in reversed(pathitems):
94 if not pathel or not urlel:
94 if not pathel or not urlel:
95 break
95 break
96 breadcrumb.append({'url': urlel, 'name': pathel})
96 breadcrumb.append({'url': urlel, 'name': pathel})
97 urlel = os.path.dirname(urlel)
97 urlel = os.path.dirname(urlel)
98 return reversed(breadcrumb)
98 return reversed(breadcrumb)
99
99
100 class requestcontext(object):
100 class requestcontext(object):
101 """Holds state/context for an individual request.
101 """Holds state/context for an individual request.
102
102
103 Servers can be multi-threaded. Holding state on the WSGI application
103 Servers can be multi-threaded. Holding state on the WSGI application
104 is prone to race conditions. Instances of this class exist to hold
104 is prone to race conditions. Instances of this class exist to hold
105 mutable and race-free state for requests.
105 mutable and race-free state for requests.
106 """
106 """
107 def __init__(self, app, repo):
107 def __init__(self, app, repo):
108 self.repo = repo
108 self.repo = repo
109 self.reponame = app.reponame
109 self.reponame = app.reponame
110
110
111 self.archivespecs = archivespecs
111 self.archivespecs = archivespecs
112
112
113 self.maxchanges = self.configint('web', 'maxchanges')
113 self.maxchanges = self.configint('web', 'maxchanges')
114 self.stripecount = self.configint('web', 'stripes')
114 self.stripecount = self.configint('web', 'stripes')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 self.maxfiles = self.configint('web', 'maxfiles')
116 self.maxfiles = self.configint('web', 'maxfiles')
117 self.allowpull = self.configbool('web', 'allow-pull')
117 self.allowpull = self.configbool('web', 'allow-pull')
118
118
119 # we use untrusted=False to prevent a repo owner from using
119 # we use untrusted=False to prevent a repo owner from using
120 # web.templates in .hg/hgrc to get access to any file readable
120 # web.templates in .hg/hgrc to get access to any file readable
121 # by the user running the CGI script
121 # by the user running the CGI script
122 self.templatepath = self.config('web', 'templates', untrusted=False)
122 self.templatepath = self.config('web', 'templates', untrusted=False)
123
123
124 # This object is more expensive to build than simple config values.
124 # This object is more expensive to build than simple config values.
125 # It is shared across requests. The app will replace the object
125 # It is shared across requests. The app will replace the object
126 # if it is updated. Since this is a reference and nothing should
126 # if it is updated. Since this is a reference and nothing should
127 # modify the underlying object, it should be constant for the lifetime
127 # modify the underlying object, it should be constant for the lifetime
128 # of the request.
128 # of the request.
129 self.websubtable = app.websubtable
129 self.websubtable = app.websubtable
130
130
131 self.csp, self.nonce = cspvalues(self.repo.ui)
131 self.csp, self.nonce = cspvalues(self.repo.ui)
132
132
133 # Trust the settings from the .hg/hgrc files by default.
133 # Trust the settings from the .hg/hgrc files by default.
134 def config(self, section, name, default=uimod._unset, untrusted=True):
134 def config(self, section, name, default=uimod._unset, untrusted=True):
135 return self.repo.ui.config(section, name, default,
135 return self.repo.ui.config(section, name, default,
136 untrusted=untrusted)
136 untrusted=untrusted)
137
137
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 return self.repo.ui.configbool(section, name, default,
139 return self.repo.ui.configbool(section, name, default,
140 untrusted=untrusted)
140 untrusted=untrusted)
141
141
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 return self.repo.ui.configint(section, name, default,
143 return self.repo.ui.configint(section, name, default,
144 untrusted=untrusted)
144 untrusted=untrusted)
145
145
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 return self.repo.ui.configlist(section, name, default,
147 return self.repo.ui.configlist(section, name, default,
148 untrusted=untrusted)
148 untrusted=untrusted)
149
149
150 def archivelist(self, nodeid):
150 def archivelist(self, nodeid):
151 allowed = self.configlist('web', 'allow_archive')
151 allowed = self.configlist('web', 'allow_archive')
152 for typ, spec in self.archivespecs.iteritems():
152 for typ, spec in self.archivespecs.iteritems():
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155
155
156 def templater(self, req):
156 def templater(self, req):
157 # determine scheme, port and server name
157 # determine scheme, port and server name
158 # this is needed to create absolute urls
158 # this is needed to create absolute urls
159
159
160 proto = req.env.get('wsgi.url_scheme')
160 proto = req.env.get('wsgi.url_scheme')
161 if proto == 'https':
161 if proto == 'https':
162 proto = 'https'
162 proto = 'https'
163 default_port = '443'
163 default_port = '443'
164 else:
164 else:
165 proto = 'http'
165 proto = 'http'
166 default_port = '80'
166 default_port = '80'
167
167
168 port = req.env[r'SERVER_PORT']
168 port = req.env[r'SERVER_PORT']
169 port = port != default_port and (r':' + port) or r''
169 port = port != default_port and (r':' + port) or r''
170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
171 logourl = self.config('web', 'logourl')
171 logourl = self.config('web', 'logourl')
172 logoimg = self.config('web', 'logoimg')
172 logoimg = self.config('web', 'logoimg')
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 if not staticurl.endswith('/'):
174 if not staticurl.endswith('/'):
175 staticurl += '/'
175 staticurl += '/'
176
176
177 # some functions for the templater
177 # some functions for the templater
178
178
179 def motd(**map):
179 def motd(**map):
180 yield self.config('web', 'motd')
180 yield self.config('web', 'motd')
181
181
182 # figure out which style to use
182 # figure out which style to use
183
183
184 vars = {}
184 vars = {}
185 styles, (style, mapfile) = getstyle(req, self.config,
185 styles, (style, mapfile) = getstyle(req, self.config,
186 self.templatepath)
186 self.templatepath)
187 if style == styles[0]:
187 if style == styles[0]:
188 vars['style'] = style
188 vars['style'] = style
189
189
190 start = '&' if req.url[-1] == r'?' else '?'
190 start = '&' if req.url[-1] == r'?' else '?'
191 sessionvars = webutil.sessionvars(vars, start)
191 sessionvars = webutil.sessionvars(vars, start)
192
192
193 if not self.reponame:
193 if not self.reponame:
194 self.reponame = (self.config('web', 'name', '')
194 self.reponame = (self.config('web', 'name', '')
195 or req.env.get('REPO_NAME')
195 or req.env.get('REPO_NAME')
196 or req.url.strip('/') or self.repo.root)
196 or req.url.strip('/') or self.repo.root)
197
197
198 def websubfilter(text):
198 def websubfilter(text):
199 return templatefilters.websub(text, self.websubtable)
199 return templatefilters.websub(text, self.websubtable)
200
200
201 # create the templater
201 # create the templater
202
202
203 defaults = {
203 defaults = {
204 'url': req.url,
204 'url': req.url,
205 'logourl': logourl,
205 'logourl': logourl,
206 'logoimg': logoimg,
206 'logoimg': logoimg,
207 'staticurl': staticurl,
207 'staticurl': staticurl,
208 'urlbase': urlbase,
208 'urlbase': urlbase,
209 'repo': self.reponame,
209 'repo': self.reponame,
210 'encoding': encoding.encoding,
210 'encoding': encoding.encoding,
211 'motd': motd,
211 'motd': motd,
212 'sessionvars': sessionvars,
212 'sessionvars': sessionvars,
213 'pathdef': makebreadcrumb(req.url),
213 'pathdef': makebreadcrumb(req.url),
214 'style': style,
214 'style': style,
215 'nonce': self.nonce,
215 'nonce': self.nonce,
216 }
216 }
217 tmpl = templater.templater.frommapfile(mapfile,
217 tmpl = templater.templater.frommapfile(mapfile,
218 filters={'websub': websubfilter},
218 filters={'websub': websubfilter},
219 defaults=defaults)
219 defaults=defaults)
220 return tmpl
220 return tmpl
221
221
222
222
223 class hgweb(object):
223 class hgweb(object):
224 """HTTP server for individual repositories.
224 """HTTP server for individual repositories.
225
225
226 Instances of this class serve HTTP responses for a particular
226 Instances of this class serve HTTP responses for a particular
227 repository.
227 repository.
228
228
229 Instances are typically used as WSGI applications.
229 Instances are typically used as WSGI applications.
230
230
231 Some servers are multi-threaded. On these servers, there may
231 Some servers are multi-threaded. On these servers, there may
232 be multiple active threads inside __call__.
232 be multiple active threads inside __call__.
233 """
233 """
234 def __init__(self, repo, name=None, baseui=None):
234 def __init__(self, repo, name=None, baseui=None):
235 if isinstance(repo, str):
235 if isinstance(repo, str):
236 if baseui:
236 if baseui:
237 u = baseui.copy()
237 u = baseui.copy()
238 else:
238 else:
239 u = uimod.ui.load()
239 u = uimod.ui.load()
240 r = hg.repository(u, repo)
240 r = hg.repository(u, repo)
241 else:
241 else:
242 # we trust caller to give us a private copy
242 # we trust caller to give us a private copy
243 r = repo
243 r = repo
244
244
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 # resolve file patterns relative to repo root
249 # resolve file patterns relative to repo root
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 # displaying bundling progress bar while serving feel wrong and may
252 # displaying bundling progress bar while serving feel wrong and may
253 # break some wsgi implementation.
253 # break some wsgi implementation.
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 self._lastrepo = self._repos[0]
257 self._lastrepo = self._repos[0]
258 hook.redirect(True)
258 hook.redirect(True)
259 self.reponame = name
259 self.reponame = name
260
260
261 def _webifyrepo(self, repo):
261 def _webifyrepo(self, repo):
262 repo = getwebview(repo)
262 repo = getwebview(repo)
263 self.websubtable = webutil.getwebsubs(repo)
263 self.websubtable = webutil.getwebsubs(repo)
264 return repo
264 return repo
265
265
266 @contextlib.contextmanager
266 @contextlib.contextmanager
267 def _obtainrepo(self):
267 def _obtainrepo(self):
268 """Obtain a repo unique to the caller.
268 """Obtain a repo unique to the caller.
269
269
270 Internally we maintain a stack of cachedlocalrepo instances
270 Internally we maintain a stack of cachedlocalrepo instances
271 to be handed out. If one is available, we pop it and return it,
271 to be handed out. If one is available, we pop it and return it,
272 ensuring it is up to date in the process. If one is not available,
272 ensuring it is up to date in the process. If one is not available,
273 we clone the most recently used repo instance and return it.
273 we clone the most recently used repo instance and return it.
274
274
275 It is currently possible for the stack to grow without bounds
275 It is currently possible for the stack to grow without bounds
276 if the server allows infinite threads. However, servers should
276 if the server allows infinite threads. However, servers should
277 have a thread limit, thus establishing our limit.
277 have a thread limit, thus establishing our limit.
278 """
278 """
279 if self._repos:
279 if self._repos:
280 cached = self._repos.pop()
280 cached = self._repos.pop()
281 r, created = cached.fetch()
281 r, created = cached.fetch()
282 else:
282 else:
283 cached = self._lastrepo.copy()
283 cached = self._lastrepo.copy()
284 r, created = cached.fetch()
284 r, created = cached.fetch()
285 if created:
285 if created:
286 r = self._webifyrepo(r)
286 r = self._webifyrepo(r)
287
287
288 self._lastrepo = cached
288 self._lastrepo = cached
289 self.mtime = cached.mtime
289 self.mtime = cached.mtime
290 try:
290 try:
291 yield r
291 yield r
292 finally:
292 finally:
293 self._repos.append(cached)
293 self._repos.append(cached)
294
294
295 def run(self):
295 def run(self):
296 """Start a server from CGI environment.
296 """Start a server from CGI environment.
297
297
298 Modern servers should be using WSGI and should avoid this
298 Modern servers should be using WSGI and should avoid this
299 method, if possible.
299 method, if possible.
300 """
300 """
301 if not encoding.environ.get('GATEWAY_INTERFACE',
301 if not encoding.environ.get('GATEWAY_INTERFACE',
302 '').startswith("CGI/1."):
302 '').startswith("CGI/1."):
303 raise RuntimeError("This function is only intended to be "
303 raise RuntimeError("This function is only intended to be "
304 "called while running as a CGI script.")
304 "called while running as a CGI script.")
305 wsgicgi.launch(self)
305 wsgicgi.launch(self)
306
306
307 def __call__(self, env, respond):
307 def __call__(self, env, respond):
308 """Run the WSGI application.
308 """Run the WSGI application.
309
309
310 This may be called by multiple threads.
310 This may be called by multiple threads.
311 """
311 """
312 req = wsgirequest(env, respond)
312 req = wsgirequest(env, respond)
313 return self.run_wsgi(req)
313 return self.run_wsgi(req)
314
314
315 def run_wsgi(self, req):
315 def run_wsgi(self, req):
316 """Internal method to run the WSGI application.
316 """Internal method to run the WSGI application.
317
317
318 This is typically only called by Mercurial. External consumers
318 This is typically only called by Mercurial. External consumers
319 should be using instances of this class as the WSGI application.
319 should be using instances of this class as the WSGI application.
320 """
320 """
321 with self._obtainrepo() as repo:
321 with self._obtainrepo() as repo:
322 profile = repo.ui.configbool('profiling', 'enabled')
322 profile = repo.ui.configbool('profiling', 'enabled')
323 with profiling.profile(repo.ui, enabled=profile):
323 with profiling.profile(repo.ui, enabled=profile):
324 for r in self._runwsgi(req, repo):
324 for r in self._runwsgi(req, repo):
325 yield r
325 yield r
326
326
327 def _runwsgi(self, req, repo):
327 def _runwsgi(self, req, repo):
328 rctx = requestcontext(self, repo)
328 rctx = requestcontext(self, repo)
329
329
330 # This state is global across all threads.
330 # This state is global across all threads.
331 encoding.encoding = rctx.config('web', 'encoding')
331 encoding.encoding = rctx.config('web', 'encoding')
332 rctx.repo.ui.environ = req.env
332 rctx.repo.ui.environ = req.env
333
333
334 if rctx.csp:
334 if rctx.csp:
335 # hgwebdir may have added CSP header. Since we generate our own,
335 # hgwebdir may have added CSP header. Since we generate our own,
336 # replace it.
336 # replace it.
337 req.headers = [h for h in req.headers
337 req.headers = [h for h in req.headers
338 if h[0] != 'Content-Security-Policy']
338 if h[0] != 'Content-Security-Policy']
339 req.headers.append(('Content-Security-Policy', rctx.csp))
339 req.headers.append(('Content-Security-Policy', rctx.csp))
340
340
341 # work with CGI variables to create coherent structure
341 # work with CGI variables to create coherent structure
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343
343
344 req.url = req.env[r'SCRIPT_NAME']
344 req.url = req.env[r'SCRIPT_NAME']
345 if not req.url.endswith('/'):
345 if not req.url.endswith('/'):
346 req.url += '/'
346 req.url += '/'
347 if req.env.get('REPO_NAME'):
347 if req.env.get('REPO_NAME'):
348 req.url += req.env[r'REPO_NAME'] + r'/'
348 req.url += req.env[r'REPO_NAME'] + r'/'
349
349
350 if r'PATH_INFO' in req.env:
350 if r'PATH_INFO' in req.env:
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
353 if parts[:len(repo_parts)] == repo_parts:
353 if parts[:len(repo_parts)] == repo_parts:
354 parts = parts[len(repo_parts):]
354 parts = parts[len(repo_parts):]
355 query = '/'.join(parts)
355 query = '/'.join(parts)
356 else:
356 else:
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 query = query.partition(r';')[0]
358 query = query.partition(r';')[0]
359
359
360 # Route it to a wire protocol handler if it looks like a wire protocol
360 # Route it to a wire protocol handler if it looks like a wire protocol
361 # request.
361 # request.
362 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
362 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
363
363
364 if protohandler:
364 if protohandler:
365 cmd = protohandler['cmd']
365 cmd = protohandler['cmd']
366 try:
366 try:
367 if query:
367 if query:
368 raise ErrorResponse(HTTP_NOT_FOUND)
368 raise ErrorResponse(HTTP_NOT_FOUND)
369 if cmd in perms:
369 if cmd in perms:
370 self.check_perm(rctx, req, perms[cmd])
370 self.check_perm(rctx, req, perms[cmd])
371 except ErrorResponse as inst:
371 except ErrorResponse as inst:
372 # A client that sends unbundle without 100-continue will
372 return protohandler['handleerror'](inst)
373 # break if we respond early.
374 if (cmd == 'unbundle' and
375 (req.env.get('HTTP_EXPECT',
376 '').lower() != '100-continue') or
377 req.env.get('X-HgHttp2', '')):
378 req.drain()
379 else:
380 req.headers.append((r'Connection', r'Close'))
381 req.respond(inst, wireprotoserver.HGTYPE,
382 body='0\n%s\n' % inst)
383 return ''
384
373
385 return protohandler['dispatch']()
374 return protohandler['dispatch']()
386
375
387 # translate user-visible url structure to internal structure
376 # translate user-visible url structure to internal structure
388
377
389 args = query.split('/', 2)
378 args = query.split('/', 2)
390 if r'cmd' not in req.form and args and args[0]:
379 if r'cmd' not in req.form and args and args[0]:
391 cmd = args.pop(0)
380 cmd = args.pop(0)
392 style = cmd.rfind('-')
381 style = cmd.rfind('-')
393 if style != -1:
382 if style != -1:
394 req.form['style'] = [cmd[:style]]
383 req.form['style'] = [cmd[:style]]
395 cmd = cmd[style + 1:]
384 cmd = cmd[style + 1:]
396
385
397 # avoid accepting e.g. style parameter as command
386 # avoid accepting e.g. style parameter as command
398 if util.safehasattr(webcommands, cmd):
387 if util.safehasattr(webcommands, cmd):
399 req.form[r'cmd'] = [cmd]
388 req.form[r'cmd'] = [cmd]
400
389
401 if cmd == 'static':
390 if cmd == 'static':
402 req.form['file'] = ['/'.join(args)]
391 req.form['file'] = ['/'.join(args)]
403 else:
392 else:
404 if args and args[0]:
393 if args and args[0]:
405 node = args.pop(0).replace('%2F', '/')
394 node = args.pop(0).replace('%2F', '/')
406 req.form['node'] = [node]
395 req.form['node'] = [node]
407 if args:
396 if args:
408 req.form['file'] = args
397 req.form['file'] = args
409
398
410 ua = req.env.get('HTTP_USER_AGENT', '')
399 ua = req.env.get('HTTP_USER_AGENT', '')
411 if cmd == 'rev' and 'mercurial' in ua:
400 if cmd == 'rev' and 'mercurial' in ua:
412 req.form['style'] = ['raw']
401 req.form['style'] = ['raw']
413
402
414 if cmd == 'archive':
403 if cmd == 'archive':
415 fn = req.form['node'][0]
404 fn = req.form['node'][0]
416 for type_, spec in rctx.archivespecs.iteritems():
405 for type_, spec in rctx.archivespecs.iteritems():
417 ext = spec[2]
406 ext = spec[2]
418 if fn.endswith(ext):
407 if fn.endswith(ext):
419 req.form['node'] = [fn[:-len(ext)]]
408 req.form['node'] = [fn[:-len(ext)]]
420 req.form['type'] = [type_]
409 req.form['type'] = [type_]
421 else:
410 else:
422 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
411 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
423
412
424 # process the web interface request
413 # process the web interface request
425
414
426 try:
415 try:
427 tmpl = rctx.templater(req)
416 tmpl = rctx.templater(req)
428 ctype = tmpl('mimetype', encoding=encoding.encoding)
417 ctype = tmpl('mimetype', encoding=encoding.encoding)
429 ctype = templater.stringify(ctype)
418 ctype = templater.stringify(ctype)
430
419
431 # check read permissions non-static content
420 # check read permissions non-static content
432 if cmd != 'static':
421 if cmd != 'static':
433 self.check_perm(rctx, req, None)
422 self.check_perm(rctx, req, None)
434
423
435 if cmd == '':
424 if cmd == '':
436 req.form[r'cmd'] = [tmpl.cache['default']]
425 req.form[r'cmd'] = [tmpl.cache['default']]
437 cmd = req.form[r'cmd'][0]
426 cmd = req.form[r'cmd'][0]
438
427
439 # Don't enable caching if using a CSP nonce because then it wouldn't
428 # Don't enable caching if using a CSP nonce because then it wouldn't
440 # be a nonce.
429 # be a nonce.
441 if rctx.configbool('web', 'cache') and not rctx.nonce:
430 if rctx.configbool('web', 'cache') and not rctx.nonce:
442 caching(self, req) # sets ETag header or raises NOT_MODIFIED
431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
443 if cmd not in webcommands.__all__:
432 if cmd not in webcommands.__all__:
444 msg = 'no such method: %s' % cmd
433 msg = 'no such method: %s' % cmd
445 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
434 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
446 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
435 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
447 rctx.ctype = ctype
436 rctx.ctype = ctype
448 content = webcommands.rawfile(rctx, req, tmpl)
437 content = webcommands.rawfile(rctx, req, tmpl)
449 else:
438 else:
450 content = getattr(webcommands, cmd)(rctx, req, tmpl)
439 content = getattr(webcommands, cmd)(rctx, req, tmpl)
451 req.respond(HTTP_OK, ctype)
440 req.respond(HTTP_OK, ctype)
452
441
453 return content
442 return content
454
443
455 except (error.LookupError, error.RepoLookupError) as err:
444 except (error.LookupError, error.RepoLookupError) as err:
456 req.respond(HTTP_NOT_FOUND, ctype)
445 req.respond(HTTP_NOT_FOUND, ctype)
457 msg = str(err)
446 msg = str(err)
458 if (util.safehasattr(err, 'name') and
447 if (util.safehasattr(err, 'name') and
459 not isinstance(err, error.ManifestLookupError)):
448 not isinstance(err, error.ManifestLookupError)):
460 msg = 'revision not found: %s' % err.name
449 msg = 'revision not found: %s' % err.name
461 return tmpl('error', error=msg)
450 return tmpl('error', error=msg)
462 except (error.RepoError, error.RevlogError) as inst:
451 except (error.RepoError, error.RevlogError) as inst:
463 req.respond(HTTP_SERVER_ERROR, ctype)
452 req.respond(HTTP_SERVER_ERROR, ctype)
464 return tmpl('error', error=str(inst))
453 return tmpl('error', error=str(inst))
465 except ErrorResponse as inst:
454 except ErrorResponse as inst:
466 req.respond(inst, ctype)
455 req.respond(inst, ctype)
467 if inst.code == HTTP_NOT_MODIFIED:
456 if inst.code == HTTP_NOT_MODIFIED:
468 # Not allowed to return a body on a 304
457 # Not allowed to return a body on a 304
469 return ['']
458 return ['']
470 return tmpl('error', error=str(inst))
459 return tmpl('error', error=str(inst))
471
460
472 def check_perm(self, rctx, req, op):
461 def check_perm(self, rctx, req, op):
473 for permhook in permhooks:
462 for permhook in permhooks:
474 permhook(rctx, req, op)
463 permhook(rctx, req, op)
475
464
476 def getwebview(repo):
465 def getwebview(repo):
477 """The 'web.view' config controls changeset filter to hgweb. Possible
466 """The 'web.view' config controls changeset filter to hgweb. Possible
478 values are ``served``, ``visible`` and ``all``. Default is ``served``.
467 values are ``served``, ``visible`` and ``all``. Default is ``served``.
479 The ``served`` filter only shows changesets that can be pulled from the
468 The ``served`` filter only shows changesets that can be pulled from the
480 hgweb instance. The``visible`` filter includes secret changesets but
469 hgweb instance. The``visible`` filter includes secret changesets but
481 still excludes "hidden" one.
470 still excludes "hidden" one.
482
471
483 See the repoview module for details.
472 See the repoview module for details.
484
473
485 The option has been around undocumented since Mercurial 2.5, but no
474 The option has been around undocumented since Mercurial 2.5, but no
486 user ever asked about it. So we better keep it undocumented for now."""
475 user ever asked about it. So we better keep it undocumented for now."""
487 # experimental config: web.view
476 # experimental config: web.view
488 viewconfig = repo.ui.config('web', 'view', untrusted=True)
477 viewconfig = repo.ui.config('web', 'view', untrusted=True)
489 if viewconfig == 'all':
478 if viewconfig == 'all':
490 return repo.unfiltered()
479 return repo.unfiltered()
491 elif viewconfig in repoview.filtertable:
480 elif viewconfig in repoview.filtertable:
492 return repo.filtered(viewconfig)
481 return repo.filtered(viewconfig)
493 else:
482 else:
494 return repo.filtered('served')
483 return repo.filtered('served')
@@ -1,404 +1,421 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import abc
9 import abc
10 import cgi
10 import cgi
11 import struct
11 import struct
12 import sys
12 import sys
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 )
22 )
23
23
24 stringio = util.stringio
24 stringio = util.stringio
25
25
26 urlerr = util.urlerr
26 urlerr = util.urlerr
27 urlreq = util.urlreq
27 urlreq = util.urlreq
28
28
29 HTTP_OK = 200
29 HTTP_OK = 200
30
30
31 HGTYPE = 'application/mercurial-0.1'
31 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE2 = 'application/mercurial-0.2'
32 HGTYPE2 = 'application/mercurial-0.2'
33 HGERRTYPE = 'application/hg-error'
33 HGERRTYPE = 'application/hg-error'
34
34
35 # Names of the SSH protocol implementations.
35 # Names of the SSH protocol implementations.
36 SSHV1 = 'ssh-v1'
36 SSHV1 = 'ssh-v1'
37 # This is advertised over the wire. Incremental the counter at the end
37 # This is advertised over the wire. Incremental the counter at the end
38 # to reflect BC breakages.
38 # to reflect BC breakages.
39 SSHV2 = 'exp-ssh-v2-0001'
39 SSHV2 = 'exp-ssh-v2-0001'
40
40
41 class abstractserverproto(object):
41 class abstractserverproto(object):
42 """abstract class that summarizes the protocol API
42 """abstract class that summarizes the protocol API
43
43
44 Used as reference and documentation.
44 Used as reference and documentation.
45 """
45 """
46
46
47 __metaclass__ = abc.ABCMeta
47 __metaclass__ = abc.ABCMeta
48
48
49 @abc.abstractproperty
49 @abc.abstractproperty
50 def name(self):
50 def name(self):
51 """The name of the protocol implementation.
51 """The name of the protocol implementation.
52
52
53 Used for uniquely identifying the transport type.
53 Used for uniquely identifying the transport type.
54 """
54 """
55
55
56 @abc.abstractmethod
56 @abc.abstractmethod
57 def getargs(self, args):
57 def getargs(self, args):
58 """return the value for arguments in <args>
58 """return the value for arguments in <args>
59
59
60 returns a list of values (same order as <args>)"""
60 returns a list of values (same order as <args>)"""
61
61
62 @abc.abstractmethod
62 @abc.abstractmethod
63 def getfile(self, fp):
63 def getfile(self, fp):
64 """write the whole content of a file into a file like object
64 """write the whole content of a file into a file like object
65
65
66 The file is in the form::
66 The file is in the form::
67
67
68 (<chunk-size>\n<chunk>)+0\n
68 (<chunk-size>\n<chunk>)+0\n
69
69
70 chunk size is the ascii version of the int.
70 chunk size is the ascii version of the int.
71 """
71 """
72
72
73 @abc.abstractmethod
73 @abc.abstractmethod
74 def redirect(self):
74 def redirect(self):
75 """may setup interception for stdout and stderr
75 """may setup interception for stdout and stderr
76
76
77 See also the `restore` method."""
77 See also the `restore` method."""
78
78
79 # If the `redirect` function does install interception, the `restore`
79 # If the `redirect` function does install interception, the `restore`
80 # function MUST be defined. If interception is not used, this function
80 # function MUST be defined. If interception is not used, this function
81 # MUST NOT be defined.
81 # MUST NOT be defined.
82 #
82 #
83 # left commented here on purpose
83 # left commented here on purpose
84 #
84 #
85 #def restore(self):
85 #def restore(self):
86 # """reinstall previous stdout and stderr and return intercepted stdout
86 # """reinstall previous stdout and stderr and return intercepted stdout
87 # """
87 # """
88 # raise NotImplementedError()
88 # raise NotImplementedError()
89
89
90 def decodevaluefromheaders(req, headerprefix):
90 def decodevaluefromheaders(req, headerprefix):
91 """Decode a long value from multiple HTTP request headers.
91 """Decode a long value from multiple HTTP request headers.
92
92
93 Returns the value as a bytes, not a str.
93 Returns the value as a bytes, not a str.
94 """
94 """
95 chunks = []
95 chunks = []
96 i = 1
96 i = 1
97 prefix = headerprefix.upper().replace(r'-', r'_')
97 prefix = headerprefix.upper().replace(r'-', r'_')
98 while True:
98 while True:
99 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
99 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
100 if v is None:
100 if v is None:
101 break
101 break
102 chunks.append(pycompat.bytesurl(v))
102 chunks.append(pycompat.bytesurl(v))
103 i += 1
103 i += 1
104
104
105 return ''.join(chunks)
105 return ''.join(chunks)
106
106
107 class webproto(abstractserverproto):
107 class webproto(abstractserverproto):
108 def __init__(self, req, ui):
108 def __init__(self, req, ui):
109 self._req = req
109 self._req = req
110 self._ui = ui
110 self._ui = ui
111
111
112 @property
112 @property
113 def name(self):
113 def name(self):
114 return 'http'
114 return 'http'
115
115
116 def getargs(self, args):
116 def getargs(self, args):
117 knownargs = self._args()
117 knownargs = self._args()
118 data = {}
118 data = {}
119 keys = args.split()
119 keys = args.split()
120 for k in keys:
120 for k in keys:
121 if k == '*':
121 if k == '*':
122 star = {}
122 star = {}
123 for key in knownargs.keys():
123 for key in knownargs.keys():
124 if key != 'cmd' and key not in keys:
124 if key != 'cmd' and key not in keys:
125 star[key] = knownargs[key][0]
125 star[key] = knownargs[key][0]
126 data['*'] = star
126 data['*'] = star
127 else:
127 else:
128 data[k] = knownargs[k][0]
128 data[k] = knownargs[k][0]
129 return [data[k] for k in keys]
129 return [data[k] for k in keys]
130
130
131 def _args(self):
131 def _args(self):
132 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
132 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
133 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
133 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
134 if postlen:
134 if postlen:
135 args.update(cgi.parse_qs(
135 args.update(cgi.parse_qs(
136 self._req.read(postlen), keep_blank_values=True))
136 self._req.read(postlen), keep_blank_values=True))
137 return args
137 return args
138
138
139 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
139 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
140 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
140 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
141 return args
141 return args
142
142
143 def getfile(self, fp):
143 def getfile(self, fp):
144 length = int(self._req.env[r'CONTENT_LENGTH'])
144 length = int(self._req.env[r'CONTENT_LENGTH'])
145 # If httppostargs is used, we need to read Content-Length
145 # If httppostargs is used, we need to read Content-Length
146 # minus the amount that was consumed by args.
146 # minus the amount that was consumed by args.
147 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
147 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
148 for s in util.filechunkiter(self._req, limit=length):
148 for s in util.filechunkiter(self._req, limit=length):
149 fp.write(s)
149 fp.write(s)
150
150
151 def redirect(self):
151 def redirect(self):
152 self._oldio = self._ui.fout, self._ui.ferr
152 self._oldio = self._ui.fout, self._ui.ferr
153 self._ui.ferr = self._ui.fout = stringio()
153 self._ui.ferr = self._ui.fout = stringio()
154
154
155 def restore(self):
155 def restore(self):
156 val = self._ui.fout.getvalue()
156 val = self._ui.fout.getvalue()
157 self._ui.ferr, self._ui.fout = self._oldio
157 self._ui.ferr, self._ui.fout = self._oldio
158 return val
158 return val
159
159
160 def _client(self):
160 def _client(self):
161 return 'remote:%s:%s:%s' % (
161 return 'remote:%s:%s:%s' % (
162 self._req.env.get('wsgi.url_scheme') or 'http',
162 self._req.env.get('wsgi.url_scheme') or 'http',
163 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
163 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
164 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
164 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
165
165
166 def responsetype(self, prefer_uncompressed):
166 def responsetype(self, prefer_uncompressed):
167 """Determine the appropriate response type and compression settings.
167 """Determine the appropriate response type and compression settings.
168
168
169 Returns a tuple of (mediatype, compengine, engineopts).
169 Returns a tuple of (mediatype, compengine, engineopts).
170 """
170 """
171 # Determine the response media type and compression engine based
171 # Determine the response media type and compression engine based
172 # on the request parameters.
172 # on the request parameters.
173 protocaps = decodevaluefromheaders(self._req, r'X-HgProto').split(' ')
173 protocaps = decodevaluefromheaders(self._req, r'X-HgProto').split(' ')
174
174
175 if '0.2' in protocaps:
175 if '0.2' in protocaps:
176 # All clients are expected to support uncompressed data.
176 # All clients are expected to support uncompressed data.
177 if prefer_uncompressed:
177 if prefer_uncompressed:
178 return HGTYPE2, util._noopengine(), {}
178 return HGTYPE2, util._noopengine(), {}
179
179
180 # Default as defined by wire protocol spec.
180 # Default as defined by wire protocol spec.
181 compformats = ['zlib', 'none']
181 compformats = ['zlib', 'none']
182 for cap in protocaps:
182 for cap in protocaps:
183 if cap.startswith('comp='):
183 if cap.startswith('comp='):
184 compformats = cap[5:].split(',')
184 compformats = cap[5:].split(',')
185 break
185 break
186
186
187 # Now find an agreed upon compression format.
187 # Now find an agreed upon compression format.
188 for engine in wireproto.supportedcompengines(self._ui, self,
188 for engine in wireproto.supportedcompengines(self._ui, self,
189 util.SERVERROLE):
189 util.SERVERROLE):
190 if engine.wireprotosupport().name in compformats:
190 if engine.wireprotosupport().name in compformats:
191 opts = {}
191 opts = {}
192 level = self._ui.configint('server',
192 level = self._ui.configint('server',
193 '%slevel' % engine.name())
193 '%slevel' % engine.name())
194 if level is not None:
194 if level is not None:
195 opts['level'] = level
195 opts['level'] = level
196
196
197 return HGTYPE2, engine, opts
197 return HGTYPE2, engine, opts
198
198
199 # No mutually supported compression format. Fall back to the
199 # No mutually supported compression format. Fall back to the
200 # legacy protocol.
200 # legacy protocol.
201
201
202 # Don't allow untrusted settings because disabling compression or
202 # Don't allow untrusted settings because disabling compression or
203 # setting a very high compression level could lead to flooding
203 # setting a very high compression level could lead to flooding
204 # the server's network or CPU.
204 # the server's network or CPU.
205 opts = {'level': self._ui.configint('server', 'zliblevel')}
205 opts = {'level': self._ui.configint('server', 'zliblevel')}
206 return HGTYPE, util.compengines['zlib'], opts
206 return HGTYPE, util.compengines['zlib'], opts
207
207
208 def iscmd(cmd):
208 def iscmd(cmd):
209 return cmd in wireproto.commands
209 return cmd in wireproto.commands
210
210
211 def parsehttprequest(repo, req, query):
211 def parsehttprequest(repo, req, query):
212 """Parse the HTTP request for a wire protocol request.
212 """Parse the HTTP request for a wire protocol request.
213
213
214 If the current request appears to be a wire protocol request, this
214 If the current request appears to be a wire protocol request, this
215 function returns a dict with details about that request, including
215 function returns a dict with details about that request, including
216 an ``abstractprotocolserver`` instance suitable for handling the
216 an ``abstractprotocolserver`` instance suitable for handling the
217 request. Otherwise, ``None`` is returned.
217 request. Otherwise, ``None`` is returned.
218
218
219 ``req`` is a ``wsgirequest`` instance.
219 ``req`` is a ``wsgirequest`` instance.
220 """
220 """
221 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
221 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
222 # string parameter. If it isn't present, this isn't a wire protocol
222 # string parameter. If it isn't present, this isn't a wire protocol
223 # request.
223 # request.
224 if r'cmd' not in req.form:
224 if r'cmd' not in req.form:
225 return None
225 return None
226
226
227 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
227 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
228
228
229 # The "cmd" request parameter is used by both the wire protocol and hgweb.
229 # The "cmd" request parameter is used by both the wire protocol and hgweb.
230 # While not all wire protocol commands are available for all transports,
230 # While not all wire protocol commands are available for all transports,
231 # if we see a "cmd" value that resembles a known wire protocol command, we
231 # if we see a "cmd" value that resembles a known wire protocol command, we
232 # route it to a protocol handler. This is better than routing possible
232 # route it to a protocol handler. This is better than routing possible
233 # wire protocol requests to hgweb because it prevents hgweb from using
233 # wire protocol requests to hgweb because it prevents hgweb from using
234 # known wire protocol commands and it is less confusing for machine
234 # known wire protocol commands and it is less confusing for machine
235 # clients.
235 # clients.
236 if cmd not in wireproto.commands:
236 if cmd not in wireproto.commands:
237 return None
237 return None
238
238
239 proto = webproto(req, repo.ui)
239 proto = webproto(req, repo.ui)
240
240
241 return {
241 return {
242 'cmd': cmd,
242 'cmd': cmd,
243 'proto': proto,
243 'proto': proto,
244 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
244 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
245 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
245 }
246 }
246
247
247 def _callhttp(repo, req, proto, cmd):
248 def _callhttp(repo, req, proto, cmd):
248 def genversion2(gen, engine, engineopts):
249 def genversion2(gen, engine, engineopts):
249 # application/mercurial-0.2 always sends a payload header
250 # application/mercurial-0.2 always sends a payload header
250 # identifying the compression engine.
251 # identifying the compression engine.
251 name = engine.wireprotosupport().name
252 name = engine.wireprotosupport().name
252 assert 0 < len(name) < 256
253 assert 0 < len(name) < 256
253 yield struct.pack('B', len(name))
254 yield struct.pack('B', len(name))
254 yield name
255 yield name
255
256
256 for chunk in gen:
257 for chunk in gen:
257 yield chunk
258 yield chunk
258
259
259 rsp = wireproto.dispatch(repo, proto, cmd)
260 rsp = wireproto.dispatch(repo, proto, cmd)
260
261
261 if not wireproto.commands.commandavailable(cmd, proto):
262 if not wireproto.commands.commandavailable(cmd, proto):
262 req.respond(HTTP_OK, HGERRTYPE,
263 req.respond(HTTP_OK, HGERRTYPE,
263 body=_('requested wire protocol command is not available '
264 body=_('requested wire protocol command is not available '
264 'over HTTP'))
265 'over HTTP'))
265 return []
266 return []
266
267
267 if isinstance(rsp, bytes):
268 if isinstance(rsp, bytes):
268 req.respond(HTTP_OK, HGTYPE, body=rsp)
269 req.respond(HTTP_OK, HGTYPE, body=rsp)
269 return []
270 return []
270 elif isinstance(rsp, wireproto.streamres_legacy):
271 elif isinstance(rsp, wireproto.streamres_legacy):
271 gen = rsp.gen
272 gen = rsp.gen
272 req.respond(HTTP_OK, HGTYPE)
273 req.respond(HTTP_OK, HGTYPE)
273 return gen
274 return gen
274 elif isinstance(rsp, wireproto.streamres):
275 elif isinstance(rsp, wireproto.streamres):
275 gen = rsp.gen
276 gen = rsp.gen
276
277
277 # This code for compression should not be streamres specific. It
278 # This code for compression should not be streamres specific. It
278 # is here because we only compress streamres at the moment.
279 # is here because we only compress streamres at the moment.
279 mediatype, engine, engineopts = proto.responsetype(
280 mediatype, engine, engineopts = proto.responsetype(
280 rsp.prefer_uncompressed)
281 rsp.prefer_uncompressed)
281 gen = engine.compressstream(gen, engineopts)
282 gen = engine.compressstream(gen, engineopts)
282
283
283 if mediatype == HGTYPE2:
284 if mediatype == HGTYPE2:
284 gen = genversion2(gen, engine, engineopts)
285 gen = genversion2(gen, engine, engineopts)
285
286
286 req.respond(HTTP_OK, mediatype)
287 req.respond(HTTP_OK, mediatype)
287 return gen
288 return gen
288 elif isinstance(rsp, wireproto.pushres):
289 elif isinstance(rsp, wireproto.pushres):
289 val = proto.restore()
290 val = proto.restore()
290 rsp = '%d\n%s' % (rsp.res, val)
291 rsp = '%d\n%s' % (rsp.res, val)
291 req.respond(HTTP_OK, HGTYPE, body=rsp)
292 req.respond(HTTP_OK, HGTYPE, body=rsp)
292 return []
293 return []
293 elif isinstance(rsp, wireproto.pusherr):
294 elif isinstance(rsp, wireproto.pusherr):
294 # drain the incoming bundle
295 # drain the incoming bundle
295 req.drain()
296 req.drain()
296 proto.restore()
297 proto.restore()
297 rsp = '0\n%s\n' % rsp.res
298 rsp = '0\n%s\n' % rsp.res
298 req.respond(HTTP_OK, HGTYPE, body=rsp)
299 req.respond(HTTP_OK, HGTYPE, body=rsp)
299 return []
300 return []
300 elif isinstance(rsp, wireproto.ooberror):
301 elif isinstance(rsp, wireproto.ooberror):
301 rsp = rsp.message
302 rsp = rsp.message
302 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
303 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
303 return []
304 return []
304 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
305 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
305
306
307 def _handlehttperror(e, req, cmd):
308 """Called when an ErrorResponse is raised during HTTP request processing."""
309 # A client that sends unbundle without 100-continue will
310 # break if we respond early.
311 if (cmd == 'unbundle' and
312 (req.env.get('HTTP_EXPECT',
313 '').lower() != '100-continue') or
314 req.env.get('X-HgHttp2', '')):
315 req.drain()
316 else:
317 req.headers.append((r'Connection', r'Close'))
318
319 req.respond(e, HGTYPE, body='0\n%s\n' % e)
320
321 return ''
322
306 class sshserver(abstractserverproto):
323 class sshserver(abstractserverproto):
307 def __init__(self, ui, repo):
324 def __init__(self, ui, repo):
308 self._ui = ui
325 self._ui = ui
309 self._repo = repo
326 self._repo = repo
310 self._fin = ui.fin
327 self._fin = ui.fin
311 self._fout = ui.fout
328 self._fout = ui.fout
312
329
313 hook.redirect(True)
330 hook.redirect(True)
314 ui.fout = repo.ui.fout = ui.ferr
331 ui.fout = repo.ui.fout = ui.ferr
315
332
316 # Prevent insertion/deletion of CRs
333 # Prevent insertion/deletion of CRs
317 util.setbinary(self._fin)
334 util.setbinary(self._fin)
318 util.setbinary(self._fout)
335 util.setbinary(self._fout)
319
336
320 @property
337 @property
321 def name(self):
338 def name(self):
322 return 'ssh'
339 return 'ssh'
323
340
324 def getargs(self, args):
341 def getargs(self, args):
325 data = {}
342 data = {}
326 keys = args.split()
343 keys = args.split()
327 for n in xrange(len(keys)):
344 for n in xrange(len(keys)):
328 argline = self._fin.readline()[:-1]
345 argline = self._fin.readline()[:-1]
329 arg, l = argline.split()
346 arg, l = argline.split()
330 if arg not in keys:
347 if arg not in keys:
331 raise error.Abort(_("unexpected parameter %r") % arg)
348 raise error.Abort(_("unexpected parameter %r") % arg)
332 if arg == '*':
349 if arg == '*':
333 star = {}
350 star = {}
334 for k in xrange(int(l)):
351 for k in xrange(int(l)):
335 argline = self._fin.readline()[:-1]
352 argline = self._fin.readline()[:-1]
336 arg, l = argline.split()
353 arg, l = argline.split()
337 val = self._fin.read(int(l))
354 val = self._fin.read(int(l))
338 star[arg] = val
355 star[arg] = val
339 data['*'] = star
356 data['*'] = star
340 else:
357 else:
341 val = self._fin.read(int(l))
358 val = self._fin.read(int(l))
342 data[arg] = val
359 data[arg] = val
343 return [data[k] for k in keys]
360 return [data[k] for k in keys]
344
361
345 def getfile(self, fpout):
362 def getfile(self, fpout):
346 self._sendresponse('')
363 self._sendresponse('')
347 count = int(self._fin.readline())
364 count = int(self._fin.readline())
348 while count:
365 while count:
349 fpout.write(self._fin.read(count))
366 fpout.write(self._fin.read(count))
350 count = int(self._fin.readline())
367 count = int(self._fin.readline())
351
368
352 def redirect(self):
369 def redirect(self):
353 pass
370 pass
354
371
355 def _sendresponse(self, v):
372 def _sendresponse(self, v):
356 self._fout.write("%d\n" % len(v))
373 self._fout.write("%d\n" % len(v))
357 self._fout.write(v)
374 self._fout.write(v)
358 self._fout.flush()
375 self._fout.flush()
359
376
360 def _sendstream(self, source):
377 def _sendstream(self, source):
361 write = self._fout.write
378 write = self._fout.write
362 for chunk in source.gen:
379 for chunk in source.gen:
363 write(chunk)
380 write(chunk)
364 self._fout.flush()
381 self._fout.flush()
365
382
366 def _sendpushresponse(self, rsp):
383 def _sendpushresponse(self, rsp):
367 self._sendresponse('')
384 self._sendresponse('')
368 self._sendresponse(str(rsp.res))
385 self._sendresponse(str(rsp.res))
369
386
370 def _sendpusherror(self, rsp):
387 def _sendpusherror(self, rsp):
371 self._sendresponse(rsp.res)
388 self._sendresponse(rsp.res)
372
389
373 def _sendooberror(self, rsp):
390 def _sendooberror(self, rsp):
374 self._ui.ferr.write('%s\n-\n' % rsp.message)
391 self._ui.ferr.write('%s\n-\n' % rsp.message)
375 self._ui.ferr.flush()
392 self._ui.ferr.flush()
376 self._fout.write('\n')
393 self._fout.write('\n')
377 self._fout.flush()
394 self._fout.flush()
378
395
379 def serve_forever(self):
396 def serve_forever(self):
380 while self.serve_one():
397 while self.serve_one():
381 pass
398 pass
382 sys.exit(0)
399 sys.exit(0)
383
400
384 _handlers = {
401 _handlers = {
385 str: _sendresponse,
402 str: _sendresponse,
386 wireproto.streamres: _sendstream,
403 wireproto.streamres: _sendstream,
387 wireproto.streamres_legacy: _sendstream,
404 wireproto.streamres_legacy: _sendstream,
388 wireproto.pushres: _sendpushresponse,
405 wireproto.pushres: _sendpushresponse,
389 wireproto.pusherr: _sendpusherror,
406 wireproto.pusherr: _sendpusherror,
390 wireproto.ooberror: _sendooberror,
407 wireproto.ooberror: _sendooberror,
391 }
408 }
392
409
393 def serve_one(self):
410 def serve_one(self):
394 cmd = self._fin.readline()[:-1]
411 cmd = self._fin.readline()[:-1]
395 if cmd and wireproto.commands.commandavailable(cmd, self):
412 if cmd and wireproto.commands.commandavailable(cmd, self):
396 rsp = wireproto.dispatch(self._repo, self, cmd)
413 rsp = wireproto.dispatch(self._repo, self, cmd)
397 self._handlers[rsp.__class__](self, rsp)
414 self._handlers[rsp.__class__](self, rsp)
398 elif cmd:
415 elif cmd:
399 self._sendresponse("")
416 self._sendresponse("")
400 return cmd != ''
417 return cmd != ''
401
418
402 def _client(self):
419 def _client(self):
403 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
420 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
404 return 'remote:ssh:' + client
421 return 'remote:ssh:' + client
General Comments 0
You need to be logged in to leave comments. Login now