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