##// END OF EJS Templates
sshserver: do setbinary() by caller (API)...
Yuya Nishihara -
r37963:dc1ed7fe default
parent child Browse files
Show More
@@ -1,93 +1,96
1 1 #!/usr/bin/env python
2 2 #
3 3 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10
11 11 """
12 12 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
13 13
14 14 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
15 15 command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
16 16 (probably together with these other useful options:
17 17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
18 18
19 19 This allows pull/push over ssh from/to the repositories given as arguments.
20 20
21 21 If all your repositories are subdirectories of a common directory, you can
22 22 allow shorter paths with:
23 23 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
24 24
25 25 You can use pattern matching of your normal shell, e.g.:
26 26 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
27 27
28 28 You can also add a --read-only flag to allow read-only access to a key, e.g.:
29 29 command="hg-ssh --read-only repos/*"
30 30 """
31 31 from __future__ import absolute_import
32 32
33 33 import os
34 34 import shlex
35 35 import sys
36 36
37 37 # enable importing on demand to reduce startup time
38 38 import hgdemandimport ; hgdemandimport.enable()
39 39
40 40 from mercurial import (
41 41 dispatch,
42 42 ui as uimod,
43 43 )
44 44
45 45 def main():
46 # Prevent insertion/deletion of CRs
47 dispatch.initstdio()
48
46 49 cwd = os.getcwd()
47 50 readonly = False
48 51 args = sys.argv[1:]
49 52 while len(args):
50 53 if args[0] == '--read-only':
51 54 readonly = True
52 55 args.pop(0)
53 56 else:
54 57 break
55 58 allowed_paths = [os.path.normpath(os.path.join(cwd,
56 59 os.path.expanduser(path)))
57 60 for path in args]
58 61 orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
59 62 try:
60 63 cmdargv = shlex.split(orig_cmd)
61 64 except ValueError as e:
62 65 sys.stderr.write('Illegal command "%s": %s\n' % (orig_cmd, e))
63 66 sys.exit(255)
64 67
65 68 if cmdargv[:2] == ['hg', '-R'] and cmdargv[3:] == ['serve', '--stdio']:
66 69 path = cmdargv[2]
67 70 repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
68 71 if repo in allowed_paths:
69 72 cmd = ['-R', repo, 'serve', '--stdio']
70 73 req = dispatch.request(cmd)
71 74 if readonly:
72 75 if not req.ui:
73 76 req.ui = uimod.ui.load()
74 77 req.ui.setconfig('hooks', 'pretxnopen.hg-ssh',
75 78 'python:__main__.rejectpush', 'hg-ssh')
76 79 req.ui.setconfig('hooks', 'prepushkey.hg-ssh',
77 80 'python:__main__.rejectpush', 'hg-ssh')
78 81 dispatch.dispatch(req)
79 82 else:
80 83 sys.stderr.write('Illegal repository "%s"\n' % repo)
81 84 sys.exit(255)
82 85 else:
83 86 sys.stderr.write('Illegal command "%s"\n' % orig_cmd)
84 87 sys.exit(255)
85 88
86 89 def rejectpush(ui, **kwargs):
87 90 ui.warn(("Permission denied\n"))
88 91 # mercurial hooks use unix process conventions for hook return values
89 92 # so a truthy return means failure
90 93 return True
91 94
92 95 if __name__ == '__main__':
93 96 main()
@@ -1,1053 +1,1053
1 1 # dispatch.py - command dispatching for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import difflib
11 11 import errno
12 12 import getopt
13 13 import os
14 14 import pdb
15 15 import re
16 16 import signal
17 17 import sys
18 18 import time
19 19 import traceback
20 20
21 21
22 22 from .i18n import _
23 23
24 24 from . import (
25 25 cmdutil,
26 26 color,
27 27 commands,
28 28 demandimport,
29 29 encoding,
30 30 error,
31 31 extensions,
32 32 fancyopts,
33 33 help,
34 34 hg,
35 35 hook,
36 36 profiling,
37 37 pycompat,
38 38 scmutil,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from .utils import (
44 44 procutil,
45 45 stringutil,
46 46 )
47 47
48 48 class request(object):
49 49 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
50 50 ferr=None, prereposetups=None):
51 51 self.args = args
52 52 self.ui = ui
53 53 self.repo = repo
54 54
55 55 # input/output/error streams
56 56 self.fin = fin
57 57 self.fout = fout
58 58 self.ferr = ferr
59 59
60 60 # remember options pre-parsed by _earlyparseopts()
61 61 self.earlyoptions = {}
62 62
63 63 # reposetups which run before extensions, useful for chg to pre-fill
64 64 # low-level repo state (for example, changelog) before extensions.
65 65 self.prereposetups = prereposetups or []
66 66
67 67 def _runexithandlers(self):
68 68 exc = None
69 69 handlers = self.ui._exithandlers
70 70 try:
71 71 while handlers:
72 72 func, args, kwargs = handlers.pop()
73 73 try:
74 74 func(*args, **kwargs)
75 75 except: # re-raises below
76 76 if exc is None:
77 77 exc = sys.exc_info()[1]
78 78 self.ui.warn(('error in exit handlers:\n'))
79 79 self.ui.traceback(force=True)
80 80 finally:
81 81 if exc is not None:
82 82 raise exc
83 83
84 84 def run():
85 85 "run the command in sys.argv"
86 _initstdio()
86 initstdio()
87 87 req = request(pycompat.sysargv[1:])
88 88 err = None
89 89 try:
90 90 status = (dispatch(req) or 0)
91 91 except error.StdioError as e:
92 92 err = e
93 93 status = -1
94 94 if util.safehasattr(req.ui, 'fout'):
95 95 try:
96 96 req.ui.fout.flush()
97 97 except IOError as e:
98 98 err = e
99 99 status = -1
100 100 if util.safehasattr(req.ui, 'ferr'):
101 101 try:
102 102 if err is not None and err.errno != errno.EPIPE:
103 103 req.ui.ferr.write('abort: %s\n' %
104 104 encoding.strtolocal(err.strerror))
105 105 req.ui.ferr.flush()
106 106 # There's not much we can do about an I/O error here. So (possibly)
107 107 # change the status code and move on.
108 108 except IOError:
109 109 status = -1
110 110
111 111 _silencestdio()
112 112 sys.exit(status & 255)
113 113
114 114 if pycompat.ispy3:
115 def _initstdio():
115 def initstdio():
116 116 pass
117 117
118 118 def _silencestdio():
119 119 for fp in (sys.stdout, sys.stderr):
120 120 # Check if the file is okay
121 121 try:
122 122 fp.flush()
123 123 continue
124 124 except IOError:
125 125 pass
126 126 # Otherwise mark it as closed to silence "Exception ignored in"
127 127 # message emitted by the interpreter finalizer. Be careful to
128 128 # not close procutil.stdout, which may be a fdopen-ed file object
129 129 # and its close() actually closes the underlying file descriptor.
130 130 try:
131 131 fp.close()
132 132 except IOError:
133 133 pass
134 134 else:
135 def _initstdio():
135 def initstdio():
136 136 for fp in (sys.stdin, sys.stdout, sys.stderr):
137 137 procutil.setbinary(fp)
138 138
139 139 def _silencestdio():
140 140 pass
141 141
142 142 def _getsimilar(symbols, value):
143 143 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
144 144 # The cutoff for similarity here is pretty arbitrary. It should
145 145 # probably be investigated and tweaked.
146 146 return [s for s in symbols if sim(s) > 0.6]
147 147
148 148 def _reportsimilar(write, similar):
149 149 if len(similar) == 1:
150 150 write(_("(did you mean %s?)\n") % similar[0])
151 151 elif similar:
152 152 ss = ", ".join(sorted(similar))
153 153 write(_("(did you mean one of %s?)\n") % ss)
154 154
155 155 def _formatparse(write, inst):
156 156 similar = []
157 157 if isinstance(inst, error.UnknownIdentifier):
158 158 # make sure to check fileset first, as revset can invoke fileset
159 159 similar = _getsimilar(inst.symbols, inst.function)
160 160 if len(inst.args) > 1:
161 161 write(_("hg: parse error at %s: %s\n") %
162 162 (pycompat.bytestr(inst.args[1]), inst.args[0]))
163 163 if inst.args[0].startswith(' '):
164 164 write(_("unexpected leading whitespace\n"))
165 165 else:
166 166 write(_("hg: parse error: %s\n") % inst.args[0])
167 167 _reportsimilar(write, similar)
168 168 if inst.hint:
169 169 write(_("(%s)\n") % inst.hint)
170 170
171 171 def _formatargs(args):
172 172 return ' '.join(procutil.shellquote(a) for a in args)
173 173
174 174 def dispatch(req):
175 175 "run the command specified in req.args"
176 176 if req.ferr:
177 177 ferr = req.ferr
178 178 elif req.ui:
179 179 ferr = req.ui.ferr
180 180 else:
181 181 ferr = procutil.stderr
182 182
183 183 try:
184 184 if not req.ui:
185 185 req.ui = uimod.ui.load()
186 186 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
187 187 if req.earlyoptions['traceback']:
188 188 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
189 189
190 190 # set ui streams from the request
191 191 if req.fin:
192 192 req.ui.fin = req.fin
193 193 if req.fout:
194 194 req.ui.fout = req.fout
195 195 if req.ferr:
196 196 req.ui.ferr = req.ferr
197 197 except error.Abort as inst:
198 198 ferr.write(_("abort: %s\n") % inst)
199 199 if inst.hint:
200 200 ferr.write(_("(%s)\n") % inst.hint)
201 201 return -1
202 202 except error.ParseError as inst:
203 203 _formatparse(ferr.write, inst)
204 204 return -1
205 205
206 206 msg = _formatargs(req.args)
207 207 starttime = util.timer()
208 208 ret = None
209 209 try:
210 210 ret = _runcatch(req)
211 211 except error.ProgrammingError as inst:
212 212 req.ui.warn(_('** ProgrammingError: %s\n') % inst)
213 213 if inst.hint:
214 214 req.ui.warn(_('** (%s)\n') % inst.hint)
215 215 raise
216 216 except KeyboardInterrupt as inst:
217 217 try:
218 218 if isinstance(inst, error.SignalInterrupt):
219 219 msg = _("killed!\n")
220 220 else:
221 221 msg = _("interrupted!\n")
222 222 req.ui.warn(msg)
223 223 except error.SignalInterrupt:
224 224 # maybe pager would quit without consuming all the output, and
225 225 # SIGPIPE was raised. we cannot print anything in this case.
226 226 pass
227 227 except IOError as inst:
228 228 if inst.errno != errno.EPIPE:
229 229 raise
230 230 ret = -1
231 231 finally:
232 232 duration = util.timer() - starttime
233 233 req.ui.flush()
234 234 if req.ui.logblockedtimes:
235 235 req.ui._blockedtimes['command_duration'] = duration * 1000
236 236 req.ui.log('uiblocked', 'ui blocked ms',
237 237 **pycompat.strkwargs(req.ui._blockedtimes))
238 238 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
239 239 msg, ret or 0, duration)
240 240 try:
241 241 req._runexithandlers()
242 242 except: # exiting, so no re-raises
243 243 ret = ret or -1
244 244 return ret
245 245
246 246 def _runcatch(req):
247 247 def catchterm(*args):
248 248 raise error.SignalInterrupt
249 249
250 250 ui = req.ui
251 251 try:
252 252 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
253 253 num = getattr(signal, name, None)
254 254 if num:
255 255 signal.signal(num, catchterm)
256 256 except ValueError:
257 257 pass # happens if called in a thread
258 258
259 259 def _runcatchfunc():
260 260 realcmd = None
261 261 try:
262 262 cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {})
263 263 cmd = cmdargs[0]
264 264 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
265 265 realcmd = aliases[0]
266 266 except (error.UnknownCommand, error.AmbiguousCommand,
267 267 IndexError, getopt.GetoptError):
268 268 # Don't handle this here. We know the command is
269 269 # invalid, but all we're worried about for now is that
270 270 # it's not a command that server operators expect to
271 271 # be safe to offer to users in a sandbox.
272 272 pass
273 273 if realcmd == 'serve' and '--stdio' in cmdargs:
274 274 # We want to constrain 'hg serve --stdio' instances pretty
275 275 # closely, as many shared-ssh access tools want to grant
276 276 # access to run *only* 'hg -R $repo serve --stdio'. We
277 277 # restrict to exactly that set of arguments, and prohibit
278 278 # any repo name that starts with '--' to prevent
279 279 # shenanigans wherein a user does something like pass
280 280 # --debugger or --config=ui.debugger=1 as a repo
281 281 # name. This used to actually run the debugger.
282 282 if (len(req.args) != 4 or
283 283 req.args[0] != '-R' or
284 284 req.args[1].startswith('--') or
285 285 req.args[2] != 'serve' or
286 286 req.args[3] != '--stdio'):
287 287 raise error.Abort(
288 288 _('potentially unsafe serve --stdio invocation: %r') %
289 289 (req.args,))
290 290
291 291 try:
292 292 debugger = 'pdb'
293 293 debugtrace = {
294 294 'pdb': pdb.set_trace
295 295 }
296 296 debugmortem = {
297 297 'pdb': pdb.post_mortem
298 298 }
299 299
300 300 # read --config before doing anything else
301 301 # (e.g. to change trust settings for reading .hg/hgrc)
302 302 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
303 303
304 304 if req.repo:
305 305 # copy configs that were passed on the cmdline (--config) to
306 306 # the repo ui
307 307 for sec, name, val in cfgs:
308 308 req.repo.ui.setconfig(sec, name, val, source='--config')
309 309
310 310 # developer config: ui.debugger
311 311 debugger = ui.config("ui", "debugger")
312 312 debugmod = pdb
313 313 if not debugger or ui.plain():
314 314 # if we are in HGPLAIN mode, then disable custom debugging
315 315 debugger = 'pdb'
316 316 elif req.earlyoptions['debugger']:
317 317 # This import can be slow for fancy debuggers, so only
318 318 # do it when absolutely necessary, i.e. when actual
319 319 # debugging has been requested
320 320 with demandimport.deactivated():
321 321 try:
322 322 debugmod = __import__(debugger)
323 323 except ImportError:
324 324 pass # Leave debugmod = pdb
325 325
326 326 debugtrace[debugger] = debugmod.set_trace
327 327 debugmortem[debugger] = debugmod.post_mortem
328 328
329 329 # enter the debugger before command execution
330 330 if req.earlyoptions['debugger']:
331 331 ui.warn(_("entering debugger - "
332 332 "type c to continue starting hg or h for help\n"))
333 333
334 334 if (debugger != 'pdb' and
335 335 debugtrace[debugger] == debugtrace['pdb']):
336 336 ui.warn(_("%s debugger specified "
337 337 "but its module was not found\n") % debugger)
338 338 with demandimport.deactivated():
339 339 debugtrace[debugger]()
340 340 try:
341 341 return _dispatch(req)
342 342 finally:
343 343 ui.flush()
344 344 except: # re-raises
345 345 # enter the debugger when we hit an exception
346 346 if req.earlyoptions['debugger']:
347 347 traceback.print_exc()
348 348 debugmortem[debugger](sys.exc_info()[2])
349 349 raise
350 350
351 351 return _callcatch(ui, _runcatchfunc)
352 352
353 353 def _callcatch(ui, func):
354 354 """like scmutil.callcatch but handles more high-level exceptions about
355 355 config parsing and commands. besides, use handlecommandexception to handle
356 356 uncaught exceptions.
357 357 """
358 358 try:
359 359 return scmutil.callcatch(ui, func)
360 360 except error.AmbiguousCommand as inst:
361 361 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
362 362 (inst.args[0], " ".join(inst.args[1])))
363 363 except error.CommandError as inst:
364 364 if inst.args[0]:
365 365 ui.pager('help')
366 366 msgbytes = pycompat.bytestr(inst.args[1])
367 367 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
368 368 commands.help_(ui, inst.args[0], full=False, command=True)
369 369 else:
370 370 ui.pager('help')
371 371 ui.warn(_("hg: %s\n") % inst.args[1])
372 372 commands.help_(ui, 'shortlist')
373 373 except error.ParseError as inst:
374 374 _formatparse(ui.warn, inst)
375 375 return -1
376 376 except error.UnknownCommand as inst:
377 377 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
378 378 try:
379 379 # check if the command is in a disabled extension
380 380 # (but don't check for extensions themselves)
381 381 formatted = help.formattedhelp(ui, commands, inst.args[0],
382 382 unknowncmd=True)
383 383 ui.warn(nocmdmsg)
384 384 ui.write(formatted)
385 385 except (error.UnknownCommand, error.Abort):
386 386 suggested = False
387 387 if len(inst.args) == 2:
388 388 sim = _getsimilar(inst.args[1], inst.args[0])
389 389 if sim:
390 390 ui.warn(nocmdmsg)
391 391 _reportsimilar(ui.warn, sim)
392 392 suggested = True
393 393 if not suggested:
394 394 ui.pager('help')
395 395 ui.warn(nocmdmsg)
396 396 commands.help_(ui, 'shortlist')
397 397 except IOError:
398 398 raise
399 399 except KeyboardInterrupt:
400 400 raise
401 401 except: # probably re-raises
402 402 if not handlecommandexception(ui):
403 403 raise
404 404
405 405 return -1
406 406
407 407 def aliasargs(fn, givenargs):
408 408 args = []
409 409 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
410 410 if not util.safehasattr(fn, '_origfunc'):
411 411 args = getattr(fn, 'args', args)
412 412 if args:
413 413 cmd = ' '.join(map(procutil.shellquote, args))
414 414
415 415 nums = []
416 416 def replacer(m):
417 417 num = int(m.group(1)) - 1
418 418 nums.append(num)
419 419 if num < len(givenargs):
420 420 return givenargs[num]
421 421 raise error.Abort(_('too few arguments for command alias'))
422 422 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
423 423 givenargs = [x for i, x in enumerate(givenargs)
424 424 if i not in nums]
425 425 args = pycompat.shlexsplit(cmd)
426 426 return args + givenargs
427 427
428 428 def aliasinterpolate(name, args, cmd):
429 429 '''interpolate args into cmd for shell aliases
430 430
431 431 This also handles $0, $@ and "$@".
432 432 '''
433 433 # util.interpolate can't deal with "$@" (with quotes) because it's only
434 434 # built to match prefix + patterns.
435 435 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
436 436 replacemap['$0'] = name
437 437 replacemap['$$'] = '$'
438 438 replacemap['$@'] = ' '.join(args)
439 439 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
440 440 # parameters, separated out into words. Emulate the same behavior here by
441 441 # quoting the arguments individually. POSIX shells will then typically
442 442 # tokenize each argument into exactly one word.
443 443 replacemap['"$@"'] = ' '.join(procutil.shellquote(arg) for arg in args)
444 444 # escape '\$' for regex
445 445 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
446 446 r = re.compile(regex)
447 447 return r.sub(lambda x: replacemap[x.group()], cmd)
448 448
449 449 class cmdalias(object):
450 450 def __init__(self, ui, name, definition, cmdtable, source):
451 451 self.name = self.cmd = name
452 452 self.cmdname = ''
453 453 self.definition = definition
454 454 self.fn = None
455 455 self.givenargs = []
456 456 self.opts = []
457 457 self.help = ''
458 458 self.badalias = None
459 459 self.unknowncmd = False
460 460 self.source = source
461 461
462 462 try:
463 463 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
464 464 for alias, e in cmdtable.iteritems():
465 465 if e is entry:
466 466 self.cmd = alias
467 467 break
468 468 self.shadows = True
469 469 except error.UnknownCommand:
470 470 self.shadows = False
471 471
472 472 if not self.definition:
473 473 self.badalias = _("no definition for alias '%s'") % self.name
474 474 return
475 475
476 476 if self.definition.startswith('!'):
477 477 shdef = self.definition[1:]
478 478 self.shell = True
479 479 def fn(ui, *args):
480 480 env = {'HG_ARGS': ' '.join((self.name,) + args)}
481 481 def _checkvar(m):
482 482 if m.groups()[0] == '$':
483 483 return m.group()
484 484 elif int(m.groups()[0]) <= len(args):
485 485 return m.group()
486 486 else:
487 487 ui.debug("No argument found for substitution "
488 488 "of %i variable in alias '%s' definition.\n"
489 489 % (int(m.groups()[0]), self.name))
490 490 return ''
491 491 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
492 492 cmd = aliasinterpolate(self.name, args, cmd)
493 493 return ui.system(cmd, environ=env,
494 494 blockedtag='alias_%s' % self.name)
495 495 self.fn = fn
496 496 self._populatehelp(ui, name, shdef, self.fn)
497 497 return
498 498
499 499 try:
500 500 args = pycompat.shlexsplit(self.definition)
501 501 except ValueError as inst:
502 502 self.badalias = (_("error in definition for alias '%s': %s")
503 503 % (self.name, stringutil.forcebytestr(inst)))
504 504 return
505 505 earlyopts, args = _earlysplitopts(args)
506 506 if earlyopts:
507 507 self.badalias = (_("error in definition for alias '%s': %s may "
508 508 "only be given on the command line")
509 509 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
510 510 [0])))
511 511 return
512 512 self.cmdname = cmd = args.pop(0)
513 513 self.givenargs = args
514 514
515 515 try:
516 516 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
517 517 if len(tableentry) > 2:
518 518 self.fn, self.opts, cmdhelp = tableentry
519 519 else:
520 520 self.fn, self.opts = tableentry
521 521 cmdhelp = None
522 522
523 523 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
524 524
525 525 except error.UnknownCommand:
526 526 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
527 527 % (self.name, cmd))
528 528 self.unknowncmd = True
529 529 except error.AmbiguousCommand:
530 530 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
531 531 % (self.name, cmd))
532 532
533 533 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
534 534 # confine strings to be passed to i18n.gettext()
535 535 cfg = {}
536 536 for k in ('doc', 'help'):
537 537 v = ui.config('alias', '%s:%s' % (name, k), None)
538 538 if v is None:
539 539 continue
540 540 if not encoding.isasciistr(v):
541 541 self.badalias = (_("non-ASCII character in alias definition "
542 542 "'%s:%s'") % (name, k))
543 543 return
544 544 cfg[k] = v
545 545
546 546 self.help = cfg.get('help', defaulthelp or '')
547 547 if self.help and self.help.startswith("hg " + cmd):
548 548 # drop prefix in old-style help lines so hg shows the alias
549 549 self.help = self.help[4 + len(cmd):]
550 550
551 551 doc = cfg.get('doc', pycompat.getdoc(fn))
552 552 if doc is not None:
553 553 doc = pycompat.sysstr(doc)
554 554 self.__doc__ = doc
555 555
556 556 @property
557 557 def args(self):
558 558 args = pycompat.maplist(util.expandpath, self.givenargs)
559 559 return aliasargs(self.fn, args)
560 560
561 561 def __getattr__(self, name):
562 562 adefaults = {r'norepo': True, r'intents': set(),
563 563 r'optionalrepo': False, r'inferrepo': False}
564 564 if name not in adefaults:
565 565 raise AttributeError(name)
566 566 if self.badalias or util.safehasattr(self, 'shell'):
567 567 return adefaults[name]
568 568 return getattr(self.fn, name)
569 569
570 570 def __call__(self, ui, *args, **opts):
571 571 if self.badalias:
572 572 hint = None
573 573 if self.unknowncmd:
574 574 try:
575 575 # check if the command is in a disabled extension
576 576 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
577 577 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
578 578 except error.UnknownCommand:
579 579 pass
580 580 raise error.Abort(self.badalias, hint=hint)
581 581 if self.shadows:
582 582 ui.debug("alias '%s' shadows command '%s'\n" %
583 583 (self.name, self.cmdname))
584 584
585 585 ui.log('commandalias', "alias '%s' expands to '%s'\n",
586 586 self.name, self.definition)
587 587 if util.safehasattr(self, 'shell'):
588 588 return self.fn(ui, *args, **opts)
589 589 else:
590 590 try:
591 591 return util.checksignature(self.fn)(ui, *args, **opts)
592 592 except error.SignatureError:
593 593 args = ' '.join([self.cmdname] + self.args)
594 594 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
595 595 raise
596 596
597 597 class lazyaliasentry(object):
598 598 """like a typical command entry (func, opts, help), but is lazy"""
599 599
600 600 def __init__(self, ui, name, definition, cmdtable, source):
601 601 self.ui = ui
602 602 self.name = name
603 603 self.definition = definition
604 604 self.cmdtable = cmdtable.copy()
605 605 self.source = source
606 606
607 607 @util.propertycache
608 608 def _aliasdef(self):
609 609 return cmdalias(self.ui, self.name, self.definition, self.cmdtable,
610 610 self.source)
611 611
612 612 def __getitem__(self, n):
613 613 aliasdef = self._aliasdef
614 614 if n == 0:
615 615 return aliasdef
616 616 elif n == 1:
617 617 return aliasdef.opts
618 618 elif n == 2:
619 619 return aliasdef.help
620 620 else:
621 621 raise IndexError
622 622
623 623 def __iter__(self):
624 624 for i in range(3):
625 625 yield self[i]
626 626
627 627 def __len__(self):
628 628 return 3
629 629
630 630 def addaliases(ui, cmdtable):
631 631 # aliases are processed after extensions have been loaded, so they
632 632 # may use extension commands. Aliases can also use other alias definitions,
633 633 # but only if they have been defined prior to the current definition.
634 634 for alias, definition in ui.configitems('alias', ignoresub=True):
635 635 try:
636 636 if cmdtable[alias].definition == definition:
637 637 continue
638 638 except (KeyError, AttributeError):
639 639 # definition might not exist or it might not be a cmdalias
640 640 pass
641 641
642 642 source = ui.configsource('alias', alias)
643 643 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
644 644 cmdtable[alias] = entry
645 645
646 646 def _parse(ui, args):
647 647 options = {}
648 648 cmdoptions = {}
649 649
650 650 try:
651 651 args = fancyopts.fancyopts(args, commands.globalopts, options)
652 652 except getopt.GetoptError as inst:
653 653 raise error.CommandError(None, stringutil.forcebytestr(inst))
654 654
655 655 if args:
656 656 cmd, args = args[0], args[1:]
657 657 aliases, entry = cmdutil.findcmd(cmd, commands.table,
658 658 ui.configbool("ui", "strict"))
659 659 cmd = aliases[0]
660 660 args = aliasargs(entry[0], args)
661 661 defaults = ui.config("defaults", cmd)
662 662 if defaults:
663 663 args = pycompat.maplist(
664 664 util.expandpath, pycompat.shlexsplit(defaults)) + args
665 665 c = list(entry[1])
666 666 else:
667 667 cmd = None
668 668 c = []
669 669
670 670 # combine global options into local
671 671 for o in commands.globalopts:
672 672 c.append((o[0], o[1], options[o[1]], o[3]))
673 673
674 674 try:
675 675 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
676 676 except getopt.GetoptError as inst:
677 677 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
678 678
679 679 # separate global options back out
680 680 for o in commands.globalopts:
681 681 n = o[1]
682 682 options[n] = cmdoptions[n]
683 683 del cmdoptions[n]
684 684
685 685 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
686 686
687 687 def _parseconfig(ui, config):
688 688 """parse the --config options from the command line"""
689 689 configs = []
690 690
691 691 for cfg in config:
692 692 try:
693 693 name, value = [cfgelem.strip()
694 694 for cfgelem in cfg.split('=', 1)]
695 695 section, name = name.split('.', 1)
696 696 if not section or not name:
697 697 raise IndexError
698 698 ui.setconfig(section, name, value, '--config')
699 699 configs.append((section, name, value))
700 700 except (IndexError, ValueError):
701 701 raise error.Abort(_('malformed --config option: %r '
702 702 '(use --config section.name=value)')
703 703 % pycompat.bytestr(cfg))
704 704
705 705 return configs
706 706
707 707 def _earlyparseopts(ui, args):
708 708 options = {}
709 709 fancyopts.fancyopts(args, commands.globalopts, options,
710 710 gnu=not ui.plain('strictflags'), early=True,
711 711 optaliases={'repository': ['repo']})
712 712 return options
713 713
714 714 def _earlysplitopts(args):
715 715 """Split args into a list of possible early options and remainder args"""
716 716 shortoptions = 'R:'
717 717 # TODO: perhaps 'debugger' should be included
718 718 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
719 719 return fancyopts.earlygetopt(args, shortoptions, longoptions,
720 720 gnu=True, keepsep=True)
721 721
722 722 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
723 723 # run pre-hook, and abort if it fails
724 724 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
725 725 pats=cmdpats, opts=cmdoptions)
726 726 try:
727 727 ret = _runcommand(ui, options, cmd, d)
728 728 # run post-hook, passing command result
729 729 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
730 730 result=ret, pats=cmdpats, opts=cmdoptions)
731 731 except Exception:
732 732 # run failure hook and re-raise
733 733 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
734 734 pats=cmdpats, opts=cmdoptions)
735 735 raise
736 736 return ret
737 737
738 738 def _getlocal(ui, rpath, wd=None):
739 739 """Return (path, local ui object) for the given target path.
740 740
741 741 Takes paths in [cwd]/.hg/hgrc into account."
742 742 """
743 743 if wd is None:
744 744 try:
745 745 wd = pycompat.getcwd()
746 746 except OSError as e:
747 747 raise error.Abort(_("error getting current working directory: %s") %
748 748 encoding.strtolocal(e.strerror))
749 749 path = cmdutil.findrepo(wd) or ""
750 750 if not path:
751 751 lui = ui
752 752 else:
753 753 lui = ui.copy()
754 754 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
755 755
756 756 if rpath:
757 757 path = lui.expandpath(rpath)
758 758 lui = ui.copy()
759 759 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
760 760
761 761 return path, lui
762 762
763 763 def _checkshellalias(lui, ui, args):
764 764 """Return the function to run the shell alias, if it is required"""
765 765 options = {}
766 766
767 767 try:
768 768 args = fancyopts.fancyopts(args, commands.globalopts, options)
769 769 except getopt.GetoptError:
770 770 return
771 771
772 772 if not args:
773 773 return
774 774
775 775 cmdtable = commands.table
776 776
777 777 cmd = args[0]
778 778 try:
779 779 strict = ui.configbool("ui", "strict")
780 780 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
781 781 except (error.AmbiguousCommand, error.UnknownCommand):
782 782 return
783 783
784 784 cmd = aliases[0]
785 785 fn = entry[0]
786 786
787 787 if cmd and util.safehasattr(fn, 'shell'):
788 788 # shell alias shouldn't receive early options which are consumed by hg
789 789 _earlyopts, args = _earlysplitopts(args)
790 790 d = lambda: fn(ui, *args[1:])
791 791 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
792 792 [], {})
793 793
794 794 def _dispatch(req):
795 795 args = req.args
796 796 ui = req.ui
797 797
798 798 # check for cwd
799 799 cwd = req.earlyoptions['cwd']
800 800 if cwd:
801 801 os.chdir(cwd)
802 802
803 803 rpath = req.earlyoptions['repository']
804 804 path, lui = _getlocal(ui, rpath)
805 805
806 806 uis = {ui, lui}
807 807
808 808 if req.repo:
809 809 uis.add(req.repo.ui)
810 810
811 811 if req.earlyoptions['profile']:
812 812 for ui_ in uis:
813 813 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
814 814
815 815 profile = lui.configbool('profiling', 'enabled')
816 816 with profiling.profile(lui, enabled=profile) as profiler:
817 817 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
818 818 # reposetup
819 819 extensions.loadall(lui)
820 820 # Propagate any changes to lui.__class__ by extensions
821 821 ui.__class__ = lui.__class__
822 822
823 823 # (uisetup and extsetup are handled in extensions.loadall)
824 824
825 825 # (reposetup is handled in hg.repository)
826 826
827 827 addaliases(lui, commands.table)
828 828
829 829 # All aliases and commands are completely defined, now.
830 830 # Check abbreviation/ambiguity of shell alias.
831 831 shellaliasfn = _checkshellalias(lui, ui, args)
832 832 if shellaliasfn:
833 833 return shellaliasfn()
834 834
835 835 # check for fallback encoding
836 836 fallback = lui.config('ui', 'fallbackencoding')
837 837 if fallback:
838 838 encoding.fallbackencoding = fallback
839 839
840 840 fullargs = args
841 841 cmd, func, args, options, cmdoptions = _parse(lui, args)
842 842
843 843 if options["config"] != req.earlyoptions["config"]:
844 844 raise error.Abort(_("option --config may not be abbreviated!"))
845 845 if options["cwd"] != req.earlyoptions["cwd"]:
846 846 raise error.Abort(_("option --cwd may not be abbreviated!"))
847 847 if options["repository"] != req.earlyoptions["repository"]:
848 848 raise error.Abort(_(
849 849 "option -R has to be separated from other options (e.g. not "
850 850 "-qR) and --repository may only be abbreviated as --repo!"))
851 851 if options["debugger"] != req.earlyoptions["debugger"]:
852 852 raise error.Abort(_("option --debugger may not be abbreviated!"))
853 853 # don't validate --profile/--traceback, which can be enabled from now
854 854
855 855 if options["encoding"]:
856 856 encoding.encoding = options["encoding"]
857 857 if options["encodingmode"]:
858 858 encoding.encodingmode = options["encodingmode"]
859 859 if options["time"]:
860 860 def get_times():
861 861 t = os.times()
862 862 if t[4] == 0.0:
863 863 # Windows leaves this as zero, so use time.clock()
864 864 t = (t[0], t[1], t[2], t[3], time.clock())
865 865 return t
866 866 s = get_times()
867 867 def print_time():
868 868 t = get_times()
869 869 ui.warn(
870 870 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
871 871 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
872 872 ui.atexit(print_time)
873 873 if options["profile"]:
874 874 profiler.start()
875 875
876 876 if options['verbose'] or options['debug'] or options['quiet']:
877 877 for opt in ('verbose', 'debug', 'quiet'):
878 878 val = pycompat.bytestr(bool(options[opt]))
879 879 for ui_ in uis:
880 880 ui_.setconfig('ui', opt, val, '--' + opt)
881 881
882 882 if options['traceback']:
883 883 for ui_ in uis:
884 884 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
885 885
886 886 if options['noninteractive']:
887 887 for ui_ in uis:
888 888 ui_.setconfig('ui', 'interactive', 'off', '-y')
889 889
890 890 if cmdoptions.get('insecure', False):
891 891 for ui_ in uis:
892 892 ui_.insecureconnections = True
893 893
894 894 # setup color handling before pager, because setting up pager
895 895 # might cause incorrect console information
896 896 coloropt = options['color']
897 897 for ui_ in uis:
898 898 if coloropt:
899 899 ui_.setconfig('ui', 'color', coloropt, '--color')
900 900 color.setup(ui_)
901 901
902 902 if stringutil.parsebool(options['pager']):
903 903 # ui.pager() expects 'internal-always-' prefix in this case
904 904 ui.pager('internal-always-' + cmd)
905 905 elif options['pager'] != 'auto':
906 906 for ui_ in uis:
907 907 ui_.disablepager()
908 908
909 909 if options['version']:
910 910 return commands.version_(ui)
911 911 if options['help']:
912 912 return commands.help_(ui, cmd, command=cmd is not None)
913 913 elif not cmd:
914 914 return commands.help_(ui, 'shortlist')
915 915
916 916 repo = None
917 917 cmdpats = args[:]
918 918 if not func.norepo:
919 919 # use the repo from the request only if we don't have -R
920 920 if not rpath and not cwd:
921 921 repo = req.repo
922 922
923 923 if repo:
924 924 # set the descriptors of the repo ui to those of ui
925 925 repo.ui.fin = ui.fin
926 926 repo.ui.fout = ui.fout
927 927 repo.ui.ferr = ui.ferr
928 928 else:
929 929 try:
930 930 repo = hg.repository(ui, path=path,
931 931 presetupfuncs=req.prereposetups,
932 932 intents=func.intents)
933 933 if not repo.local():
934 934 raise error.Abort(_("repository '%s' is not local")
935 935 % path)
936 936 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
937 937 'repo')
938 938 except error.RequirementError:
939 939 raise
940 940 except error.RepoError:
941 941 if rpath: # invalid -R path
942 942 raise
943 943 if not func.optionalrepo:
944 944 if func.inferrepo and args and not path:
945 945 # try to infer -R from command args
946 946 repos = pycompat.maplist(cmdutil.findrepo, args)
947 947 guess = repos[0]
948 948 if guess and repos.count(guess) == len(repos):
949 949 req.args = ['--repository', guess] + fullargs
950 950 req.earlyoptions['repository'] = guess
951 951 return _dispatch(req)
952 952 if not path:
953 953 raise error.RepoError(_("no repository found in"
954 954 " '%s' (.hg not found)")
955 955 % pycompat.getcwd())
956 956 raise
957 957 if repo:
958 958 ui = repo.ui
959 959 if options['hidden']:
960 960 repo = repo.unfiltered()
961 961 args.insert(0, repo)
962 962 elif rpath:
963 963 ui.warn(_("warning: --repository ignored\n"))
964 964
965 965 msg = _formatargs(fullargs)
966 966 ui.log("command", '%s\n', msg)
967 967 strcmdopt = pycompat.strkwargs(cmdoptions)
968 968 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
969 969 try:
970 970 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
971 971 cmdpats, cmdoptions)
972 972 finally:
973 973 if repo and repo != req.repo:
974 974 repo.close()
975 975
976 976 def _runcommand(ui, options, cmd, cmdfunc):
977 977 """Run a command function, possibly with profiling enabled."""
978 978 try:
979 979 return cmdfunc()
980 980 except error.SignatureError:
981 981 raise error.CommandError(cmd, _('invalid arguments'))
982 982
983 983 def _exceptionwarning(ui):
984 984 """Produce a warning message for the current active exception"""
985 985
986 986 # For compatibility checking, we discard the portion of the hg
987 987 # version after the + on the assumption that if a "normal
988 988 # user" is running a build with a + in it the packager
989 989 # probably built from fairly close to a tag and anyone with a
990 990 # 'make local' copy of hg (where the version number can be out
991 991 # of date) will be clueful enough to notice the implausible
992 992 # version number and try updating.
993 993 ct = util.versiontuple(n=2)
994 994 worst = None, ct, ''
995 995 if ui.config('ui', 'supportcontact') is None:
996 996 for name, mod in extensions.extensions():
997 997 # 'testedwith' should be bytes, but not all extensions are ported
998 998 # to py3 and we don't want UnicodeException because of that.
999 999 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
1000 1000 report = getattr(mod, 'buglink', _('the extension author.'))
1001 1001 if not testedwith.strip():
1002 1002 # We found an untested extension. It's likely the culprit.
1003 1003 worst = name, 'unknown', report
1004 1004 break
1005 1005
1006 1006 # Never blame on extensions bundled with Mercurial.
1007 1007 if extensions.ismoduleinternal(mod):
1008 1008 continue
1009 1009
1010 1010 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1011 1011 if ct in tested:
1012 1012 continue
1013 1013
1014 1014 lower = [t for t in tested if t < ct]
1015 1015 nearest = max(lower or tested)
1016 1016 if worst[0] is None or nearest < worst[1]:
1017 1017 worst = name, nearest, report
1018 1018 if worst[0] is not None:
1019 1019 name, testedwith, report = worst
1020 1020 if not isinstance(testedwith, (bytes, str)):
1021 1021 testedwith = '.'.join([stringutil.forcebytestr(c)
1022 1022 for c in testedwith])
1023 1023 warning = (_('** Unknown exception encountered with '
1024 1024 'possibly-broken third-party extension %s\n'
1025 1025 '** which supports versions %s of Mercurial.\n'
1026 1026 '** Please disable %s and try your action again.\n'
1027 1027 '** If that fixes the bug please report it to %s\n')
1028 1028 % (name, testedwith, name, report))
1029 1029 else:
1030 1030 bugtracker = ui.config('ui', 'supportcontact')
1031 1031 if bugtracker is None:
1032 1032 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1033 1033 warning = (_("** unknown exception encountered, "
1034 1034 "please report by visiting\n** ") + bugtracker + '\n')
1035 1035 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1036 1036 warning += ((_("** Python %s\n") % sysversion) +
1037 1037 (_("** Mercurial Distributed SCM (version %s)\n") %
1038 1038 util.version()) +
1039 1039 (_("** Extensions loaded: %s\n") %
1040 1040 ", ".join([x[0] for x in extensions.extensions()])))
1041 1041 return warning
1042 1042
1043 1043 def handlecommandexception(ui):
1044 1044 """Produce a warning message for broken commands
1045 1045
1046 1046 Called when handling an exception; the exception is reraised if
1047 1047 this function returns False, ignored otherwise.
1048 1048 """
1049 1049 warning = _exceptionwarning(ui)
1050 1050 ui.log("commandexception", "%s\n%s\n", warning,
1051 1051 pycompat.sysbytes(traceback.format_exc()))
1052 1052 ui.warn(warning)
1053 1053 return False # re-raise the exception
@@ -1,811 +1,807
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 .thirdparty import (
16 16 cbor,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 hook,
22 22 pycompat,
23 23 util,
24 24 wireprototypes,
25 25 wireprotov1server,
26 26 wireprotov2server,
27 27 )
28 28 from .utils import (
29 29 interfaceutil,
30 30 procutil,
31 31 )
32 32
33 33 stringio = util.stringio
34 34
35 35 urlerr = util.urlerr
36 36 urlreq = util.urlreq
37 37
38 38 HTTP_OK = 200
39 39
40 40 HGTYPE = 'application/mercurial-0.1'
41 41 HGTYPE2 = 'application/mercurial-0.2'
42 42 HGERRTYPE = 'application/hg-error'
43 43
44 44 SSHV1 = wireprototypes.SSHV1
45 45 SSHV2 = wireprototypes.SSHV2
46 46
47 47 def decodevaluefromheaders(req, headerprefix):
48 48 """Decode a long value from multiple HTTP request headers.
49 49
50 50 Returns the value as a bytes, not a str.
51 51 """
52 52 chunks = []
53 53 i = 1
54 54 while True:
55 55 v = req.headers.get(b'%s-%d' % (headerprefix, i))
56 56 if v is None:
57 57 break
58 58 chunks.append(pycompat.bytesurl(v))
59 59 i += 1
60 60
61 61 return ''.join(chunks)
62 62
63 63 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
64 64 class httpv1protocolhandler(object):
65 65 def __init__(self, req, ui, checkperm):
66 66 self._req = req
67 67 self._ui = ui
68 68 self._checkperm = checkperm
69 69 self._protocaps = None
70 70
71 71 @property
72 72 def name(self):
73 73 return 'http-v1'
74 74
75 75 def getargs(self, args):
76 76 knownargs = self._args()
77 77 data = {}
78 78 keys = args.split()
79 79 for k in keys:
80 80 if k == '*':
81 81 star = {}
82 82 for key in knownargs.keys():
83 83 if key != 'cmd' and key not in keys:
84 84 star[key] = knownargs[key][0]
85 85 data['*'] = star
86 86 else:
87 87 data[k] = knownargs[k][0]
88 88 return [data[k] for k in keys]
89 89
90 90 def _args(self):
91 91 args = self._req.qsparams.asdictoflists()
92 92 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
93 93 if postlen:
94 94 args.update(urlreq.parseqs(
95 95 self._req.bodyfh.read(postlen), keep_blank_values=True))
96 96 return args
97 97
98 98 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
99 99 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
100 100 return args
101 101
102 102 def getprotocaps(self):
103 103 if self._protocaps is None:
104 104 value = decodevaluefromheaders(self._req, b'X-HgProto')
105 105 self._protocaps = set(value.split(' '))
106 106 return self._protocaps
107 107
108 108 def getpayload(self):
109 109 # Existing clients *always* send Content-Length.
110 110 length = int(self._req.headers[b'Content-Length'])
111 111
112 112 # If httppostargs is used, we need to read Content-Length
113 113 # minus the amount that was consumed by args.
114 114 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
115 115 return util.filechunkiter(self._req.bodyfh, limit=length)
116 116
117 117 @contextlib.contextmanager
118 118 def mayberedirectstdio(self):
119 119 oldout = self._ui.fout
120 120 olderr = self._ui.ferr
121 121
122 122 out = util.stringio()
123 123
124 124 try:
125 125 self._ui.fout = out
126 126 self._ui.ferr = out
127 127 yield out
128 128 finally:
129 129 self._ui.fout = oldout
130 130 self._ui.ferr = olderr
131 131
132 132 def client(self):
133 133 return 'remote:%s:%s:%s' % (
134 134 self._req.urlscheme,
135 135 urlreq.quote(self._req.remotehost or ''),
136 136 urlreq.quote(self._req.remoteuser or ''))
137 137
138 138 def addcapabilities(self, repo, caps):
139 139 caps.append(b'batch')
140 140
141 141 caps.append('httpheader=%d' %
142 142 repo.ui.configint('server', 'maxhttpheaderlen'))
143 143 if repo.ui.configbool('experimental', 'httppostargs'):
144 144 caps.append('httppostargs')
145 145
146 146 # FUTURE advertise 0.2rx once support is implemented
147 147 # FUTURE advertise minrx and mintx after consulting config option
148 148 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
149 149
150 150 compengines = wireprototypes.supportedcompengines(repo.ui,
151 151 util.SERVERROLE)
152 152 if compengines:
153 153 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
154 154 for e in compengines)
155 155 caps.append('compression=%s' % comptypes)
156 156
157 157 return caps
158 158
159 159 def checkperm(self, perm):
160 160 return self._checkperm(perm)
161 161
162 162 # This method exists mostly so that extensions like remotefilelog can
163 163 # disable a kludgey legacy method only over http. As of early 2018,
164 164 # there are no other known users, so with any luck we can discard this
165 165 # hook if remotefilelog becomes a first-party extension.
166 166 def iscmd(cmd):
167 167 return cmd in wireprotov1server.commands
168 168
169 169 def handlewsgirequest(rctx, req, res, checkperm):
170 170 """Possibly process a wire protocol request.
171 171
172 172 If the current request is a wire protocol request, the request is
173 173 processed by this function.
174 174
175 175 ``req`` is a ``parsedrequest`` instance.
176 176 ``res`` is a ``wsgiresponse`` instance.
177 177
178 178 Returns a bool indicating if the request was serviced. If set, the caller
179 179 should stop processing the request, as a response has already been issued.
180 180 """
181 181 # Avoid cycle involving hg module.
182 182 from .hgweb import common as hgwebcommon
183 183
184 184 repo = rctx.repo
185 185
186 186 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
187 187 # string parameter. If it isn't present, this isn't a wire protocol
188 188 # request.
189 189 if 'cmd' not in req.qsparams:
190 190 return False
191 191
192 192 cmd = req.qsparams['cmd']
193 193
194 194 # The "cmd" request parameter is used by both the wire protocol and hgweb.
195 195 # While not all wire protocol commands are available for all transports,
196 196 # if we see a "cmd" value that resembles a known wire protocol command, we
197 197 # route it to a protocol handler. This is better than routing possible
198 198 # wire protocol requests to hgweb because it prevents hgweb from using
199 199 # known wire protocol commands and it is less confusing for machine
200 200 # clients.
201 201 if not iscmd(cmd):
202 202 return False
203 203
204 204 # The "cmd" query string argument is only valid on the root path of the
205 205 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
206 206 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
207 207 # in this case. We send an HTTP 404 for backwards compatibility reasons.
208 208 if req.dispatchpath:
209 209 res.status = hgwebcommon.statusmessage(404)
210 210 res.headers['Content-Type'] = HGTYPE
211 211 # TODO This is not a good response to issue for this request. This
212 212 # is mostly for BC for now.
213 213 res.setbodybytes('0\n%s\n' % b'Not Found')
214 214 return True
215 215
216 216 proto = httpv1protocolhandler(req, repo.ui,
217 217 lambda perm: checkperm(rctx, req, perm))
218 218
219 219 # The permissions checker should be the only thing that can raise an
220 220 # ErrorResponse. It is kind of a layer violation to catch an hgweb
221 221 # exception here. So consider refactoring into a exception type that
222 222 # is associated with the wire protocol.
223 223 try:
224 224 _callhttp(repo, req, res, proto, cmd)
225 225 except hgwebcommon.ErrorResponse as e:
226 226 for k, v in e.headers:
227 227 res.headers[k] = v
228 228 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
229 229 # TODO This response body assumes the failed command was
230 230 # "unbundle." That assumption is not always valid.
231 231 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
232 232
233 233 return True
234 234
235 235 def _availableapis(repo):
236 236 apis = set()
237 237
238 238 # Registered APIs are made available via config options of the name of
239 239 # the protocol.
240 240 for k, v in API_HANDLERS.items():
241 241 section, option = v['config']
242 242 if repo.ui.configbool(section, option):
243 243 apis.add(k)
244 244
245 245 return apis
246 246
247 247 def handlewsgiapirequest(rctx, req, res, checkperm):
248 248 """Handle requests to /api/*."""
249 249 assert req.dispatchparts[0] == b'api'
250 250
251 251 repo = rctx.repo
252 252
253 253 # This whole URL space is experimental for now. But we want to
254 254 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
255 255 if not repo.ui.configbool('experimental', 'web.apiserver'):
256 256 res.status = b'404 Not Found'
257 257 res.headers[b'Content-Type'] = b'text/plain'
258 258 res.setbodybytes(_('Experimental API server endpoint not enabled'))
259 259 return
260 260
261 261 # The URL space is /api/<protocol>/*. The structure of URLs under varies
262 262 # by <protocol>.
263 263
264 264 availableapis = _availableapis(repo)
265 265
266 266 # Requests to /api/ list available APIs.
267 267 if req.dispatchparts == [b'api']:
268 268 res.status = b'200 OK'
269 269 res.headers[b'Content-Type'] = b'text/plain'
270 270 lines = [_('APIs can be accessed at /api/<name>, where <name> can be '
271 271 'one of the following:\n')]
272 272 if availableapis:
273 273 lines.extend(sorted(availableapis))
274 274 else:
275 275 lines.append(_('(no available APIs)\n'))
276 276 res.setbodybytes(b'\n'.join(lines))
277 277 return
278 278
279 279 proto = req.dispatchparts[1]
280 280
281 281 if proto not in API_HANDLERS:
282 282 res.status = b'404 Not Found'
283 283 res.headers[b'Content-Type'] = b'text/plain'
284 284 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % (
285 285 proto, b', '.join(sorted(availableapis))))
286 286 return
287 287
288 288 if proto not in availableapis:
289 289 res.status = b'404 Not Found'
290 290 res.headers[b'Content-Type'] = b'text/plain'
291 291 res.setbodybytes(_('API %s not enabled\n') % proto)
292 292 return
293 293
294 294 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
295 295 req.dispatchparts[2:])
296 296
297 297 # Maps API name to metadata so custom API can be registered.
298 298 # Keys are:
299 299 #
300 300 # config
301 301 # Config option that controls whether service is enabled.
302 302 # handler
303 303 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
304 304 # when a request to this API is received.
305 305 # apidescriptor
306 306 # Callable receiving (req, repo) that is called to obtain an API
307 307 # descriptor for this service. The response must be serializable to CBOR.
308 308 API_HANDLERS = {
309 309 wireprotov2server.HTTP_WIREPROTO_V2: {
310 310 'config': ('experimental', 'web.api.http-v2'),
311 311 'handler': wireprotov2server.handlehttpv2request,
312 312 'apidescriptor': wireprotov2server.httpv2apidescriptor,
313 313 },
314 314 }
315 315
316 316 def _httpresponsetype(ui, proto, prefer_uncompressed):
317 317 """Determine the appropriate response type and compression settings.
318 318
319 319 Returns a tuple of (mediatype, compengine, engineopts).
320 320 """
321 321 # Determine the response media type and compression engine based
322 322 # on the request parameters.
323 323
324 324 if '0.2' in proto.getprotocaps():
325 325 # All clients are expected to support uncompressed data.
326 326 if prefer_uncompressed:
327 327 return HGTYPE2, util._noopengine(), {}
328 328
329 329 # Now find an agreed upon compression format.
330 330 compformats = wireprotov1server.clientcompressionsupport(proto)
331 331 for engine in wireprototypes.supportedcompengines(ui, util.SERVERROLE):
332 332 if engine.wireprotosupport().name in compformats:
333 333 opts = {}
334 334 level = ui.configint('server', '%slevel' % engine.name())
335 335 if level is not None:
336 336 opts['level'] = level
337 337
338 338 return HGTYPE2, engine, opts
339 339
340 340 # No mutually supported compression format. Fall back to the
341 341 # legacy protocol.
342 342
343 343 # Don't allow untrusted settings because disabling compression or
344 344 # setting a very high compression level could lead to flooding
345 345 # the server's network or CPU.
346 346 opts = {'level': ui.configint('server', 'zliblevel')}
347 347 return HGTYPE, util.compengines['zlib'], opts
348 348
349 349 def processcapabilitieshandshake(repo, req, res, proto):
350 350 """Called during a ?cmd=capabilities request.
351 351
352 352 If the client is advertising support for a newer protocol, we send
353 353 a CBOR response with information about available services. If no
354 354 advertised services are available, we don't handle the request.
355 355 """
356 356 # Fall back to old behavior unless the API server is enabled.
357 357 if not repo.ui.configbool('experimental', 'web.apiserver'):
358 358 return False
359 359
360 360 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
361 361 protocaps = decodevaluefromheaders(req, b'X-HgProto')
362 362 if not clientapis or not protocaps:
363 363 return False
364 364
365 365 # We currently only support CBOR responses.
366 366 protocaps = set(protocaps.split(' '))
367 367 if b'cbor' not in protocaps:
368 368 return False
369 369
370 370 descriptors = {}
371 371
372 372 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
373 373 handler = API_HANDLERS[api]
374 374
375 375 descriptorfn = handler.get('apidescriptor')
376 376 if not descriptorfn:
377 377 continue
378 378
379 379 descriptors[api] = descriptorfn(req, repo)
380 380
381 381 v1caps = wireprotov1server.dispatch(repo, proto, 'capabilities')
382 382 assert isinstance(v1caps, wireprototypes.bytesresponse)
383 383
384 384 m = {
385 385 # TODO allow this to be configurable.
386 386 'apibase': 'api/',
387 387 'apis': descriptors,
388 388 'v1capabilities': v1caps.data,
389 389 }
390 390
391 391 res.status = b'200 OK'
392 392 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
393 393 res.setbodybytes(cbor.dumps(m, canonical=True))
394 394
395 395 return True
396 396
397 397 def _callhttp(repo, req, res, proto, cmd):
398 398 # Avoid cycle involving hg module.
399 399 from .hgweb import common as hgwebcommon
400 400
401 401 def genversion2(gen, engine, engineopts):
402 402 # application/mercurial-0.2 always sends a payload header
403 403 # identifying the compression engine.
404 404 name = engine.wireprotosupport().name
405 405 assert 0 < len(name) < 256
406 406 yield struct.pack('B', len(name))
407 407 yield name
408 408
409 409 for chunk in gen:
410 410 yield chunk
411 411
412 412 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
413 413 if code == HTTP_OK:
414 414 res.status = '200 Script output follows'
415 415 else:
416 416 res.status = hgwebcommon.statusmessage(code)
417 417
418 418 res.headers['Content-Type'] = contenttype
419 419
420 420 if bodybytes is not None:
421 421 res.setbodybytes(bodybytes)
422 422 if bodygen is not None:
423 423 res.setbodygen(bodygen)
424 424
425 425 if not wireprotov1server.commands.commandavailable(cmd, proto):
426 426 setresponse(HTTP_OK, HGERRTYPE,
427 427 _('requested wire protocol command is not available over '
428 428 'HTTP'))
429 429 return
430 430
431 431 proto.checkperm(wireprotov1server.commands[cmd].permission)
432 432
433 433 # Possibly handle a modern client wanting to switch protocols.
434 434 if (cmd == 'capabilities' and
435 435 processcapabilitieshandshake(repo, req, res, proto)):
436 436
437 437 return
438 438
439 439 rsp = wireprotov1server.dispatch(repo, proto, cmd)
440 440
441 441 if isinstance(rsp, bytes):
442 442 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
443 443 elif isinstance(rsp, wireprototypes.bytesresponse):
444 444 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
445 445 elif isinstance(rsp, wireprototypes.streamreslegacy):
446 446 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
447 447 elif isinstance(rsp, wireprototypes.streamres):
448 448 gen = rsp.gen
449 449
450 450 # This code for compression should not be streamres specific. It
451 451 # is here because we only compress streamres at the moment.
452 452 mediatype, engine, engineopts = _httpresponsetype(
453 453 repo.ui, proto, rsp.prefer_uncompressed)
454 454 gen = engine.compressstream(gen, engineopts)
455 455
456 456 if mediatype == HGTYPE2:
457 457 gen = genversion2(gen, engine, engineopts)
458 458
459 459 setresponse(HTTP_OK, mediatype, bodygen=gen)
460 460 elif isinstance(rsp, wireprototypes.pushres):
461 461 rsp = '%d\n%s' % (rsp.res, rsp.output)
462 462 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
463 463 elif isinstance(rsp, wireprototypes.pusherr):
464 464 rsp = '0\n%s\n' % rsp.res
465 465 res.drain = True
466 466 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
467 467 elif isinstance(rsp, wireprototypes.ooberror):
468 468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
469 469 else:
470 470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
471 471
472 472 def _sshv1respondbytes(fout, value):
473 473 """Send a bytes response for protocol version 1."""
474 474 fout.write('%d\n' % len(value))
475 475 fout.write(value)
476 476 fout.flush()
477 477
478 478 def _sshv1respondstream(fout, source):
479 479 write = fout.write
480 480 for chunk in source.gen:
481 481 write(chunk)
482 482 fout.flush()
483 483
484 484 def _sshv1respondooberror(fout, ferr, rsp):
485 485 ferr.write(b'%s\n-\n' % rsp)
486 486 ferr.flush()
487 487 fout.write(b'\n')
488 488 fout.flush()
489 489
490 490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
491 491 class sshv1protocolhandler(object):
492 492 """Handler for requests services via version 1 of SSH protocol."""
493 493 def __init__(self, ui, fin, fout):
494 494 self._ui = ui
495 495 self._fin = fin
496 496 self._fout = fout
497 497 self._protocaps = set()
498 498
499 499 @property
500 500 def name(self):
501 501 return wireprototypes.SSHV1
502 502
503 503 def getargs(self, args):
504 504 data = {}
505 505 keys = args.split()
506 506 for n in xrange(len(keys)):
507 507 argline = self._fin.readline()[:-1]
508 508 arg, l = argline.split()
509 509 if arg not in keys:
510 510 raise error.Abort(_("unexpected parameter %r") % arg)
511 511 if arg == '*':
512 512 star = {}
513 513 for k in xrange(int(l)):
514 514 argline = self._fin.readline()[:-1]
515 515 arg, l = argline.split()
516 516 val = self._fin.read(int(l))
517 517 star[arg] = val
518 518 data['*'] = star
519 519 else:
520 520 val = self._fin.read(int(l))
521 521 data[arg] = val
522 522 return [data[k] for k in keys]
523 523
524 524 def getprotocaps(self):
525 525 return self._protocaps
526 526
527 527 def getpayload(self):
528 528 # We initially send an empty response. This tells the client it is
529 529 # OK to start sending data. If a client sees any other response, it
530 530 # interprets it as an error.
531 531 _sshv1respondbytes(self._fout, b'')
532 532
533 533 # The file is in the form:
534 534 #
535 535 # <chunk size>\n<chunk>
536 536 # ...
537 537 # 0\n
538 538 count = int(self._fin.readline())
539 539 while count:
540 540 yield self._fin.read(count)
541 541 count = int(self._fin.readline())
542 542
543 543 @contextlib.contextmanager
544 544 def mayberedirectstdio(self):
545 545 yield None
546 546
547 547 def client(self):
548 548 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
549 549 return 'remote:ssh:' + client
550 550
551 551 def addcapabilities(self, repo, caps):
552 552 if self.name == wireprototypes.SSHV1:
553 553 caps.append(b'protocaps')
554 554 caps.append(b'batch')
555 555 return caps
556 556
557 557 def checkperm(self, perm):
558 558 pass
559 559
560 560 class sshv2protocolhandler(sshv1protocolhandler):
561 561 """Protocol handler for version 2 of the SSH protocol."""
562 562
563 563 @property
564 564 def name(self):
565 565 return wireprototypes.SSHV2
566 566
567 567 def addcapabilities(self, repo, caps):
568 568 return caps
569 569
570 570 def _runsshserver(ui, repo, fin, fout, ev):
571 571 # This function operates like a state machine of sorts. The following
572 572 # states are defined:
573 573 #
574 574 # protov1-serving
575 575 # Server is in protocol version 1 serving mode. Commands arrive on
576 576 # new lines. These commands are processed in this state, one command
577 577 # after the other.
578 578 #
579 579 # protov2-serving
580 580 # Server is in protocol version 2 serving mode.
581 581 #
582 582 # upgrade-initial
583 583 # The server is going to process an upgrade request.
584 584 #
585 585 # upgrade-v2-filter-legacy-handshake
586 586 # The protocol is being upgraded to version 2. The server is expecting
587 587 # the legacy handshake from version 1.
588 588 #
589 589 # upgrade-v2-finish
590 590 # The upgrade to version 2 of the protocol is imminent.
591 591 #
592 592 # shutdown
593 593 # The server is shutting down, possibly in reaction to a client event.
594 594 #
595 595 # And here are their transitions:
596 596 #
597 597 # protov1-serving -> shutdown
598 598 # When server receives an empty request or encounters another
599 599 # error.
600 600 #
601 601 # protov1-serving -> upgrade-initial
602 602 # An upgrade request line was seen.
603 603 #
604 604 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
605 605 # Upgrade to version 2 in progress. Server is expecting to
606 606 # process a legacy handshake.
607 607 #
608 608 # upgrade-v2-filter-legacy-handshake -> shutdown
609 609 # Client did not fulfill upgrade handshake requirements.
610 610 #
611 611 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
612 612 # Client fulfilled version 2 upgrade requirements. Finishing that
613 613 # upgrade.
614 614 #
615 615 # upgrade-v2-finish -> protov2-serving
616 616 # Protocol upgrade to version 2 complete. Server can now speak protocol
617 617 # version 2.
618 618 #
619 619 # protov2-serving -> protov1-serving
620 620 # Ths happens by default since protocol version 2 is the same as
621 621 # version 1 except for the handshake.
622 622
623 623 state = 'protov1-serving'
624 624 proto = sshv1protocolhandler(ui, fin, fout)
625 625 protoswitched = False
626 626
627 627 while not ev.is_set():
628 628 if state == 'protov1-serving':
629 629 # Commands are issued on new lines.
630 630 request = fin.readline()[:-1]
631 631
632 632 # Empty lines signal to terminate the connection.
633 633 if not request:
634 634 state = 'shutdown'
635 635 continue
636 636
637 637 # It looks like a protocol upgrade request. Transition state to
638 638 # handle it.
639 639 if request.startswith(b'upgrade '):
640 640 if protoswitched:
641 641 _sshv1respondooberror(fout, ui.ferr,
642 642 b'cannot upgrade protocols multiple '
643 643 b'times')
644 644 state = 'shutdown'
645 645 continue
646 646
647 647 state = 'upgrade-initial'
648 648 continue
649 649
650 650 available = wireprotov1server.commands.commandavailable(
651 651 request, proto)
652 652
653 653 # This command isn't available. Send an empty response and go
654 654 # back to waiting for a new command.
655 655 if not available:
656 656 _sshv1respondbytes(fout, b'')
657 657 continue
658 658
659 659 rsp = wireprotov1server.dispatch(repo, proto, request)
660 660
661 661 if isinstance(rsp, bytes):
662 662 _sshv1respondbytes(fout, rsp)
663 663 elif isinstance(rsp, wireprototypes.bytesresponse):
664 664 _sshv1respondbytes(fout, rsp.data)
665 665 elif isinstance(rsp, wireprototypes.streamres):
666 666 _sshv1respondstream(fout, rsp)
667 667 elif isinstance(rsp, wireprototypes.streamreslegacy):
668 668 _sshv1respondstream(fout, rsp)
669 669 elif isinstance(rsp, wireprototypes.pushres):
670 670 _sshv1respondbytes(fout, b'')
671 671 _sshv1respondbytes(fout, b'%d' % rsp.res)
672 672 elif isinstance(rsp, wireprototypes.pusherr):
673 673 _sshv1respondbytes(fout, rsp.res)
674 674 elif isinstance(rsp, wireprototypes.ooberror):
675 675 _sshv1respondooberror(fout, ui.ferr, rsp.message)
676 676 else:
677 677 raise error.ProgrammingError('unhandled response type from '
678 678 'wire protocol command: %s' % rsp)
679 679
680 680 # For now, protocol version 2 serving just goes back to version 1.
681 681 elif state == 'protov2-serving':
682 682 state = 'protov1-serving'
683 683 continue
684 684
685 685 elif state == 'upgrade-initial':
686 686 # We should never transition into this state if we've switched
687 687 # protocols.
688 688 assert not protoswitched
689 689 assert proto.name == wireprototypes.SSHV1
690 690
691 691 # Expected: upgrade <token> <capabilities>
692 692 # If we get something else, the request is malformed. It could be
693 693 # from a future client that has altered the upgrade line content.
694 694 # We treat this as an unknown command.
695 695 try:
696 696 token, caps = request.split(b' ')[1:]
697 697 except ValueError:
698 698 _sshv1respondbytes(fout, b'')
699 699 state = 'protov1-serving'
700 700 continue
701 701
702 702 # Send empty response if we don't support upgrading protocols.
703 703 if not ui.configbool('experimental', 'sshserver.support-v2'):
704 704 _sshv1respondbytes(fout, b'')
705 705 state = 'protov1-serving'
706 706 continue
707 707
708 708 try:
709 709 caps = urlreq.parseqs(caps)
710 710 except ValueError:
711 711 _sshv1respondbytes(fout, b'')
712 712 state = 'protov1-serving'
713 713 continue
714 714
715 715 # We don't see an upgrade request to protocol version 2. Ignore
716 716 # the upgrade request.
717 717 wantedprotos = caps.get(b'proto', [b''])[0]
718 718 if SSHV2 not in wantedprotos:
719 719 _sshv1respondbytes(fout, b'')
720 720 state = 'protov1-serving'
721 721 continue
722 722
723 723 # It looks like we can honor this upgrade request to protocol 2.
724 724 # Filter the rest of the handshake protocol request lines.
725 725 state = 'upgrade-v2-filter-legacy-handshake'
726 726 continue
727 727
728 728 elif state == 'upgrade-v2-filter-legacy-handshake':
729 729 # Client should have sent legacy handshake after an ``upgrade``
730 730 # request. Expected lines:
731 731 #
732 732 # hello
733 733 # between
734 734 # pairs 81
735 735 # 0000...-0000...
736 736
737 737 ok = True
738 738 for line in (b'hello', b'between', b'pairs 81'):
739 739 request = fin.readline()[:-1]
740 740
741 741 if request != line:
742 742 _sshv1respondooberror(fout, ui.ferr,
743 743 b'malformed handshake protocol: '
744 744 b'missing %s' % line)
745 745 ok = False
746 746 state = 'shutdown'
747 747 break
748 748
749 749 if not ok:
750 750 continue
751 751
752 752 request = fin.read(81)
753 753 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
754 754 _sshv1respondooberror(fout, ui.ferr,
755 755 b'malformed handshake protocol: '
756 756 b'missing between argument value')
757 757 state = 'shutdown'
758 758 continue
759 759
760 760 state = 'upgrade-v2-finish'
761 761 continue
762 762
763 763 elif state == 'upgrade-v2-finish':
764 764 # Send the upgrade response.
765 765 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
766 766 servercaps = wireprotov1server.capabilities(repo, proto)
767 767 rsp = b'capabilities: %s' % servercaps.data
768 768 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
769 769 fout.flush()
770 770
771 771 proto = sshv2protocolhandler(ui, fin, fout)
772 772 protoswitched = True
773 773
774 774 state = 'protov2-serving'
775 775 continue
776 776
777 777 elif state == 'shutdown':
778 778 break
779 779
780 780 else:
781 781 raise error.ProgrammingError('unhandled ssh server state: %s' %
782 782 state)
783 783
784 784 class sshserver(object):
785 785 def __init__(self, ui, repo, logfh=None):
786 786 self._ui = ui
787 787 self._repo = repo
788 788 self._fin = ui.fin
789 789 self._fout = ui.fout
790 790
791 791 # Log write I/O to stdout and stderr if configured.
792 792 if logfh:
793 793 self._fout = util.makeloggingfileobject(
794 794 logfh, self._fout, 'o', logdata=True)
795 795 ui.ferr = util.makeloggingfileobject(
796 796 logfh, ui.ferr, 'e', logdata=True)
797 797
798 798 hook.redirect(True)
799 799 ui.fout = repo.ui.fout = ui.ferr
800 800
801 # Prevent insertion/deletion of CRs
802 procutil.setbinary(self._fin)
803 procutil.setbinary(self._fout)
804
805 801 def serve_forever(self):
806 802 self.serveuntil(threading.Event())
807 803 sys.exit(0)
808 804
809 805 def serveuntil(self, ev):
810 806 """Serve until a threading.Event is set."""
811 807 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now