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