##// END OF EJS Templates
typing: disable module attribute warnings for properly conditionalized code...
Matt Harbison -
r47544:52528570 stable
parent child Browse files
Show More
@@ -1,1375 +1,1379 b''
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 errno
11 11 import getopt
12 12 import io
13 13 import os
14 14 import pdb
15 15 import re
16 16 import signal
17 17 import sys
18 18 import traceback
19 19
20 20
21 21 from .i18n import _
22 22 from .pycompat import getattr
23 23
24 24 from hgdemandimport import tracing
25 25
26 26 from . import (
27 27 cmdutil,
28 28 color,
29 29 commands,
30 30 demandimport,
31 31 encoding,
32 32 error,
33 33 extensions,
34 34 fancyopts,
35 35 help,
36 36 hg,
37 37 hook,
38 38 localrepo,
39 39 profiling,
40 40 pycompat,
41 41 rcutil,
42 42 registrar,
43 43 requirements as requirementsmod,
44 44 scmutil,
45 45 ui as uimod,
46 46 util,
47 47 vfs,
48 48 )
49 49
50 50 from .utils import (
51 51 procutil,
52 52 stringutil,
53 53 )
54 54
55 55
56 56 class request(object):
57 57 def __init__(
58 58 self,
59 59 args,
60 60 ui=None,
61 61 repo=None,
62 62 fin=None,
63 63 fout=None,
64 64 ferr=None,
65 65 fmsg=None,
66 66 prereposetups=None,
67 67 ):
68 68 self.args = args
69 69 self.ui = ui
70 70 self.repo = repo
71 71
72 72 # input/output/error streams
73 73 self.fin = fin
74 74 self.fout = fout
75 75 self.ferr = ferr
76 76 # separate stream for status/error messages
77 77 self.fmsg = fmsg
78 78
79 79 # remember options pre-parsed by _earlyparseopts()
80 80 self.earlyoptions = {}
81 81
82 82 # reposetups which run before extensions, useful for chg to pre-fill
83 83 # low-level repo state (for example, changelog) before extensions.
84 84 self.prereposetups = prereposetups or []
85 85
86 86 # store the parsed and canonical command
87 87 self.canonical_command = None
88 88
89 89 def _runexithandlers(self):
90 90 exc = None
91 91 handlers = self.ui._exithandlers
92 92 try:
93 93 while handlers:
94 94 func, args, kwargs = handlers.pop()
95 95 try:
96 96 func(*args, **kwargs)
97 97 except: # re-raises below
98 98 if exc is None:
99 99 exc = sys.exc_info()[1]
100 100 self.ui.warnnoi18n(b'error in exit handlers:\n')
101 101 self.ui.traceback(force=True)
102 102 finally:
103 103 if exc is not None:
104 104 raise exc
105 105
106 106
107 107 def _flushstdio(ui, err):
108 108 status = None
109 109 # In all cases we try to flush stdio streams.
110 110 if util.safehasattr(ui, b'fout'):
111 111 assert ui is not None # help pytype
112 112 assert ui.fout is not None # help pytype
113 113 try:
114 114 ui.fout.flush()
115 115 except IOError as e:
116 116 err = e
117 117 status = -1
118 118
119 119 if util.safehasattr(ui, b'ferr'):
120 120 assert ui is not None # help pytype
121 121 assert ui.ferr is not None # help pytype
122 122 try:
123 123 if err is not None and err.errno != errno.EPIPE:
124 124 ui.ferr.write(
125 125 b'abort: %s\n' % encoding.strtolocal(err.strerror)
126 126 )
127 127 ui.ferr.flush()
128 128 # There's not much we can do about an I/O error here. So (possibly)
129 129 # change the status code and move on.
130 130 except IOError:
131 131 status = -1
132 132
133 133 return status
134 134
135 135
136 136 def run():
137 137 """run the command in sys.argv"""
138 138 try:
139 139 initstdio()
140 140 with tracing.log('parse args into request'):
141 141 req = request(pycompat.sysargv[1:])
142 142
143 143 status = dispatch(req)
144 144 _silencestdio()
145 145 except KeyboardInterrupt:
146 146 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
147 147 # be printed to console to avoid another IOError/KeyboardInterrupt.
148 148 status = -1
149 149 sys.exit(status & 255)
150 150
151 151
152 152 if pycompat.ispy3:
153 153
154 154 def initstdio():
155 155 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
156 156 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
157 157 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
158 158 # instances, which write to the underlying stdio file descriptor in binary
159 159 # mode. ui.write() uses \n for line endings and no line ending normalization
160 160 # is attempted through this interface. This "just works," even if the system
161 161 # preferred line ending is not \n.
162 162 #
163 163 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
164 164 # and sys.stderr. They will inherit the line ending normalization settings,
165 165 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
166 166 # "just work," here we change the sys.* streams to disable line ending
167 167 # normalization, ensuring compatibility with our ui type.
168 168
169 169 if sys.stdout is not None:
170 170 # write_through is new in Python 3.7.
171 171 kwargs = {
172 172 "newline": "\n",
173 173 "line_buffering": sys.stdout.line_buffering,
174 174 }
175 175 if util.safehasattr(sys.stdout, "write_through"):
176 # pytype: disable=attribute-error
176 177 kwargs["write_through"] = sys.stdout.write_through
178 # pytype: enable=attribute-error
177 179 sys.stdout = io.TextIOWrapper(
178 180 sys.stdout.buffer,
179 181 sys.stdout.encoding,
180 182 sys.stdout.errors,
181 183 **kwargs
182 184 )
183 185
184 186 if sys.stderr is not None:
185 187 kwargs = {
186 188 "newline": "\n",
187 189 "line_buffering": sys.stderr.line_buffering,
188 190 }
189 191 if util.safehasattr(sys.stderr, "write_through"):
192 # pytype: disable=attribute-error
190 193 kwargs["write_through"] = sys.stderr.write_through
194 # pytype: enable=attribute-error
191 195 sys.stderr = io.TextIOWrapper(
192 196 sys.stderr.buffer,
193 197 sys.stderr.encoding,
194 198 sys.stderr.errors,
195 199 **kwargs
196 200 )
197 201
198 202 if sys.stdin is not None:
199 203 # No write_through on read-only stream.
200 204 sys.stdin = io.TextIOWrapper(
201 205 sys.stdin.buffer,
202 206 sys.stdin.encoding,
203 207 sys.stdin.errors,
204 208 # None is universal newlines mode.
205 209 newline=None,
206 210 line_buffering=sys.stdin.line_buffering,
207 211 )
208 212
209 213 def _silencestdio():
210 214 for fp in (sys.stdout, sys.stderr):
211 215 if fp is None:
212 216 continue
213 217 # Check if the file is okay
214 218 try:
215 219 fp.flush()
216 220 continue
217 221 except IOError:
218 222 pass
219 223 # Otherwise mark it as closed to silence "Exception ignored in"
220 224 # message emitted by the interpreter finalizer.
221 225 try:
222 226 fp.close()
223 227 except IOError:
224 228 pass
225 229
226 230
227 231 else:
228 232
229 233 def initstdio():
230 234 for fp in (sys.stdin, sys.stdout, sys.stderr):
231 235 procutil.setbinary(fp)
232 236
233 237 def _silencestdio():
234 238 pass
235 239
236 240
237 241 def _formatargs(args):
238 242 return b' '.join(procutil.shellquote(a) for a in args)
239 243
240 244
241 245 def dispatch(req):
242 246 """run the command specified in req.args; returns an integer status code"""
243 247 err = None
244 248 try:
245 249 status = _rundispatch(req)
246 250 except error.StdioError as e:
247 251 err = e
248 252 status = -1
249 253
250 254 ret = _flushstdio(req.ui, err)
251 255 if ret:
252 256 status = ret
253 257 return status
254 258
255 259
256 260 def _rundispatch(req):
257 261 with tracing.log('dispatch._rundispatch'):
258 262 if req.ferr:
259 263 ferr = req.ferr
260 264 elif req.ui:
261 265 ferr = req.ui.ferr
262 266 else:
263 267 ferr = procutil.stderr
264 268
265 269 try:
266 270 if not req.ui:
267 271 req.ui = uimod.ui.load()
268 272 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
269 273 if req.earlyoptions[b'traceback']:
270 274 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
271 275
272 276 # set ui streams from the request
273 277 if req.fin:
274 278 req.ui.fin = req.fin
275 279 if req.fout:
276 280 req.ui.fout = req.fout
277 281 if req.ferr:
278 282 req.ui.ferr = req.ferr
279 283 if req.fmsg:
280 284 req.ui.fmsg = req.fmsg
281 285 except error.Abort as inst:
282 286 ferr.write(inst.format())
283 287 return -1
284 288
285 289 msg = _formatargs(req.args)
286 290 starttime = util.timer()
287 291 ret = 1 # default of Python exit code on unhandled exception
288 292 try:
289 293 ret = _runcatch(req) or 0
290 294 except error.ProgrammingError as inst:
291 295 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
292 296 if inst.hint:
293 297 req.ui.error(_(b'** (%s)\n') % inst.hint)
294 298 raise
295 299 except KeyboardInterrupt as inst:
296 300 try:
297 301 if isinstance(inst, error.SignalInterrupt):
298 302 msg = _(b"killed!\n")
299 303 else:
300 304 msg = _(b"interrupted!\n")
301 305 req.ui.error(msg)
302 306 except error.SignalInterrupt:
303 307 # maybe pager would quit without consuming all the output, and
304 308 # SIGPIPE was raised. we cannot print anything in this case.
305 309 pass
306 310 except IOError as inst:
307 311 if inst.errno != errno.EPIPE:
308 312 raise
309 313 ret = -1
310 314 finally:
311 315 duration = util.timer() - starttime
312 316 req.ui.flush() # record blocked times
313 317 if req.ui.logblockedtimes:
314 318 req.ui._blockedtimes[b'command_duration'] = duration * 1000
315 319 req.ui.log(
316 320 b'uiblocked',
317 321 b'ui blocked ms\n',
318 322 **pycompat.strkwargs(req.ui._blockedtimes)
319 323 )
320 324 return_code = ret & 255
321 325 req.ui.log(
322 326 b"commandfinish",
323 327 b"%s exited %d after %0.2f seconds\n",
324 328 msg,
325 329 return_code,
326 330 duration,
327 331 return_code=return_code,
328 332 duration=duration,
329 333 canonical_command=req.canonical_command,
330 334 )
331 335 try:
332 336 req._runexithandlers()
333 337 except: # exiting, so no re-raises
334 338 ret = ret or -1
335 339 # do flush again since ui.log() and exit handlers may write to ui
336 340 req.ui.flush()
337 341 return ret
338 342
339 343
340 344 def _runcatch(req):
341 345 with tracing.log('dispatch._runcatch'):
342 346
343 347 def catchterm(*args):
344 348 raise error.SignalInterrupt
345 349
346 350 ui = req.ui
347 351 try:
348 352 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
349 353 num = getattr(signal, name, None)
350 354 if num:
351 355 signal.signal(num, catchterm)
352 356 except ValueError:
353 357 pass # happens if called in a thread
354 358
355 359 def _runcatchfunc():
356 360 realcmd = None
357 361 try:
358 362 cmdargs = fancyopts.fancyopts(
359 363 req.args[:], commands.globalopts, {}
360 364 )
361 365 cmd = cmdargs[0]
362 366 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
363 367 realcmd = aliases[0]
364 368 except (
365 369 error.UnknownCommand,
366 370 error.AmbiguousCommand,
367 371 IndexError,
368 372 getopt.GetoptError,
369 373 ):
370 374 # Don't handle this here. We know the command is
371 375 # invalid, but all we're worried about for now is that
372 376 # it's not a command that server operators expect to
373 377 # be safe to offer to users in a sandbox.
374 378 pass
375 379 if realcmd == b'serve' and b'--stdio' in cmdargs:
376 380 # We want to constrain 'hg serve --stdio' instances pretty
377 381 # closely, as many shared-ssh access tools want to grant
378 382 # access to run *only* 'hg -R $repo serve --stdio'. We
379 383 # restrict to exactly that set of arguments, and prohibit
380 384 # any repo name that starts with '--' to prevent
381 385 # shenanigans wherein a user does something like pass
382 386 # --debugger or --config=ui.debugger=1 as a repo
383 387 # name. This used to actually run the debugger.
384 388 if (
385 389 len(req.args) != 4
386 390 or req.args[0] != b'-R'
387 391 or req.args[1].startswith(b'--')
388 392 or req.args[2] != b'serve'
389 393 or req.args[3] != b'--stdio'
390 394 ):
391 395 raise error.Abort(
392 396 _(b'potentially unsafe serve --stdio invocation: %s')
393 397 % (stringutil.pprint(req.args),)
394 398 )
395 399
396 400 try:
397 401 debugger = b'pdb'
398 402 debugtrace = {b'pdb': pdb.set_trace}
399 403 debugmortem = {b'pdb': pdb.post_mortem}
400 404
401 405 # read --config before doing anything else
402 406 # (e.g. to change trust settings for reading .hg/hgrc)
403 407 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
404 408
405 409 if req.repo:
406 410 # copy configs that were passed on the cmdline (--config) to
407 411 # the repo ui
408 412 for sec, name, val in cfgs:
409 413 req.repo.ui.setconfig(
410 414 sec, name, val, source=b'--config'
411 415 )
412 416
413 417 # developer config: ui.debugger
414 418 debugger = ui.config(b"ui", b"debugger")
415 419 debugmod = pdb
416 420 if not debugger or ui.plain():
417 421 # if we are in HGPLAIN mode, then disable custom debugging
418 422 debugger = b'pdb'
419 423 elif req.earlyoptions[b'debugger']:
420 424 # This import can be slow for fancy debuggers, so only
421 425 # do it when absolutely necessary, i.e. when actual
422 426 # debugging has been requested
423 427 with demandimport.deactivated():
424 428 try:
425 429 debugmod = __import__(debugger)
426 430 except ImportError:
427 431 pass # Leave debugmod = pdb
428 432
429 433 debugtrace[debugger] = debugmod.set_trace
430 434 debugmortem[debugger] = debugmod.post_mortem
431 435
432 436 # enter the debugger before command execution
433 437 if req.earlyoptions[b'debugger']:
434 438 ui.warn(
435 439 _(
436 440 b"entering debugger - "
437 441 b"type c to continue starting hg or h for help\n"
438 442 )
439 443 )
440 444
441 445 if (
442 446 debugger != b'pdb'
443 447 and debugtrace[debugger] == debugtrace[b'pdb']
444 448 ):
445 449 ui.warn(
446 450 _(
447 451 b"%s debugger specified "
448 452 b"but its module was not found\n"
449 453 )
450 454 % debugger
451 455 )
452 456 with demandimport.deactivated():
453 457 debugtrace[debugger]()
454 458 try:
455 459 return _dispatch(req)
456 460 finally:
457 461 ui.flush()
458 462 except: # re-raises
459 463 # enter the debugger when we hit an exception
460 464 if req.earlyoptions[b'debugger']:
461 465 traceback.print_exc()
462 466 debugmortem[debugger](sys.exc_info()[2])
463 467 raise
464 468
465 469 return _callcatch(ui, _runcatchfunc)
466 470
467 471
468 472 def _callcatch(ui, func):
469 473 """like scmutil.callcatch but handles more high-level exceptions about
470 474 config parsing and commands. besides, use handlecommandexception to handle
471 475 uncaught exceptions.
472 476 """
473 477 detailed_exit_code = -1
474 478 try:
475 479 return scmutil.callcatch(ui, func)
476 480 except error.AmbiguousCommand as inst:
477 481 detailed_exit_code = 10
478 482 ui.warn(
479 483 _(b"hg: command '%s' is ambiguous:\n %s\n")
480 484 % (inst.prefix, b" ".join(inst.matches))
481 485 )
482 486 except error.CommandError as inst:
483 487 detailed_exit_code = 10
484 488 if inst.command:
485 489 ui.pager(b'help')
486 490 msgbytes = pycompat.bytestr(inst.message)
487 491 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
488 492 commands.help_(ui, inst.command, full=False, command=True)
489 493 else:
490 494 ui.warn(_(b"hg: %s\n") % inst.message)
491 495 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
492 496 except error.UnknownCommand as inst:
493 497 detailed_exit_code = 10
494 498 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
495 499 try:
496 500 # check if the command is in a disabled extension
497 501 # (but don't check for extensions themselves)
498 502 formatted = help.formattedhelp(
499 503 ui, commands, inst.command, unknowncmd=True
500 504 )
501 505 ui.warn(nocmdmsg)
502 506 ui.write(formatted)
503 507 except (error.UnknownCommand, error.Abort):
504 508 suggested = False
505 509 if inst.all_commands:
506 510 sim = error.getsimilar(inst.all_commands, inst.command)
507 511 if sim:
508 512 ui.warn(nocmdmsg)
509 513 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
510 514 suggested = True
511 515 if not suggested:
512 516 ui.warn(nocmdmsg)
513 517 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
514 518 except IOError:
515 519 raise
516 520 except KeyboardInterrupt:
517 521 raise
518 522 except: # probably re-raises
519 523 if not handlecommandexception(ui):
520 524 raise
521 525
522 526 if ui.configbool(b'ui', b'detailed-exit-code'):
523 527 return detailed_exit_code
524 528 else:
525 529 return -1
526 530
527 531
528 532 def aliasargs(fn, givenargs):
529 533 args = []
530 534 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
531 535 if not util.safehasattr(fn, b'_origfunc'):
532 536 args = getattr(fn, 'args', args)
533 537 if args:
534 538 cmd = b' '.join(map(procutil.shellquote, args))
535 539
536 540 nums = []
537 541
538 542 def replacer(m):
539 543 num = int(m.group(1)) - 1
540 544 nums.append(num)
541 545 if num < len(givenargs):
542 546 return givenargs[num]
543 547 raise error.InputError(_(b'too few arguments for command alias'))
544 548
545 549 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
546 550 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
547 551 args = pycompat.shlexsplit(cmd)
548 552 return args + givenargs
549 553
550 554
551 555 def aliasinterpolate(name, args, cmd):
552 556 """interpolate args into cmd for shell aliases
553 557
554 558 This also handles $0, $@ and "$@".
555 559 """
556 560 # util.interpolate can't deal with "$@" (with quotes) because it's only
557 561 # built to match prefix + patterns.
558 562 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
559 563 replacemap[b'$0'] = name
560 564 replacemap[b'$$'] = b'$'
561 565 replacemap[b'$@'] = b' '.join(args)
562 566 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
563 567 # parameters, separated out into words. Emulate the same behavior here by
564 568 # quoting the arguments individually. POSIX shells will then typically
565 569 # tokenize each argument into exactly one word.
566 570 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
567 571 # escape '\$' for regex
568 572 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
569 573 r = re.compile(regex)
570 574 return r.sub(lambda x: replacemap[x.group()], cmd)
571 575
572 576
573 577 class cmdalias(object):
574 578 def __init__(self, ui, name, definition, cmdtable, source):
575 579 self.name = self.cmd = name
576 580 self.cmdname = b''
577 581 self.definition = definition
578 582 self.fn = None
579 583 self.givenargs = []
580 584 self.opts = []
581 585 self.help = b''
582 586 self.badalias = None
583 587 self.unknowncmd = False
584 588 self.source = source
585 589
586 590 try:
587 591 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
588 592 for alias, e in pycompat.iteritems(cmdtable):
589 593 if e is entry:
590 594 self.cmd = alias
591 595 break
592 596 self.shadows = True
593 597 except error.UnknownCommand:
594 598 self.shadows = False
595 599
596 600 if not self.definition:
597 601 self.badalias = _(b"no definition for alias '%s'") % self.name
598 602 return
599 603
600 604 if self.definition.startswith(b'!'):
601 605 shdef = self.definition[1:]
602 606 self.shell = True
603 607
604 608 def fn(ui, *args):
605 609 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
606 610
607 611 def _checkvar(m):
608 612 if m.groups()[0] == b'$':
609 613 return m.group()
610 614 elif int(m.groups()[0]) <= len(args):
611 615 return m.group()
612 616 else:
613 617 ui.debug(
614 618 b"No argument found for substitution "
615 619 b"of %i variable in alias '%s' definition.\n"
616 620 % (int(m.groups()[0]), self.name)
617 621 )
618 622 return b''
619 623
620 624 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
621 625 cmd = aliasinterpolate(self.name, args, cmd)
622 626 return ui.system(
623 627 cmd, environ=env, blockedtag=b'alias_%s' % self.name
624 628 )
625 629
626 630 self.fn = fn
627 631 self.alias = True
628 632 self._populatehelp(ui, name, shdef, self.fn)
629 633 return
630 634
631 635 try:
632 636 args = pycompat.shlexsplit(self.definition)
633 637 except ValueError as inst:
634 638 self.badalias = _(b"error in definition for alias '%s': %s") % (
635 639 self.name,
636 640 stringutil.forcebytestr(inst),
637 641 )
638 642 return
639 643 earlyopts, args = _earlysplitopts(args)
640 644 if earlyopts:
641 645 self.badalias = _(
642 646 b"error in definition for alias '%s': %s may "
643 647 b"only be given on the command line"
644 648 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
645 649 return
646 650 self.cmdname = cmd = args.pop(0)
647 651 self.givenargs = args
648 652
649 653 try:
650 654 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
651 655 if len(tableentry) > 2:
652 656 self.fn, self.opts, cmdhelp = tableentry
653 657 else:
654 658 self.fn, self.opts = tableentry
655 659 cmdhelp = None
656 660
657 661 self.alias = True
658 662 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
659 663
660 664 except error.UnknownCommand:
661 665 self.badalias = _(
662 666 b"alias '%s' resolves to unknown command '%s'"
663 667 ) % (
664 668 self.name,
665 669 cmd,
666 670 )
667 671 self.unknowncmd = True
668 672 except error.AmbiguousCommand:
669 673 self.badalias = _(
670 674 b"alias '%s' resolves to ambiguous command '%s'"
671 675 ) % (
672 676 self.name,
673 677 cmd,
674 678 )
675 679
676 680 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
677 681 # confine strings to be passed to i18n.gettext()
678 682 cfg = {}
679 683 for k in (b'doc', b'help', b'category'):
680 684 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
681 685 if v is None:
682 686 continue
683 687 if not encoding.isasciistr(v):
684 688 self.badalias = _(
685 689 b"non-ASCII character in alias definition '%s:%s'"
686 690 ) % (name, k)
687 691 return
688 692 cfg[k] = v
689 693
690 694 self.help = cfg.get(b'help', defaulthelp or b'')
691 695 if self.help and self.help.startswith(b"hg " + cmd):
692 696 # drop prefix in old-style help lines so hg shows the alias
693 697 self.help = self.help[4 + len(cmd) :]
694 698
695 699 self.owndoc = b'doc' in cfg
696 700 doc = cfg.get(b'doc', pycompat.getdoc(fn))
697 701 if doc is not None:
698 702 doc = pycompat.sysstr(doc)
699 703 self.__doc__ = doc
700 704
701 705 self.helpcategory = cfg.get(
702 706 b'category', registrar.command.CATEGORY_NONE
703 707 )
704 708
705 709 @property
706 710 def args(self):
707 711 args = pycompat.maplist(util.expandpath, self.givenargs)
708 712 return aliasargs(self.fn, args)
709 713
710 714 def __getattr__(self, name):
711 715 adefaults = {
712 716 'norepo': True,
713 717 'intents': set(),
714 718 'optionalrepo': False,
715 719 'inferrepo': False,
716 720 }
717 721 if name not in adefaults:
718 722 raise AttributeError(name)
719 723 if self.badalias or util.safehasattr(self, b'shell'):
720 724 return adefaults[name]
721 725 return getattr(self.fn, name)
722 726
723 727 def __call__(self, ui, *args, **opts):
724 728 if self.badalias:
725 729 hint = None
726 730 if self.unknowncmd:
727 731 try:
728 732 # check if the command is in a disabled extension
729 733 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
730 734 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
731 735 except error.UnknownCommand:
732 736 pass
733 737 raise error.ConfigError(self.badalias, hint=hint)
734 738 if self.shadows:
735 739 ui.debug(
736 740 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
737 741 )
738 742
739 743 ui.log(
740 744 b'commandalias',
741 745 b"alias '%s' expands to '%s'\n",
742 746 self.name,
743 747 self.definition,
744 748 )
745 749 if util.safehasattr(self, b'shell'):
746 750 return self.fn(ui, *args, **opts)
747 751 else:
748 752 try:
749 753 return util.checksignature(self.fn)(ui, *args, **opts)
750 754 except error.SignatureError:
751 755 args = b' '.join([self.cmdname] + self.args)
752 756 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
753 757 raise
754 758
755 759
756 760 class lazyaliasentry(object):
757 761 """like a typical command entry (func, opts, help), but is lazy"""
758 762
759 763 def __init__(self, ui, name, definition, cmdtable, source):
760 764 self.ui = ui
761 765 self.name = name
762 766 self.definition = definition
763 767 self.cmdtable = cmdtable.copy()
764 768 self.source = source
765 769 self.alias = True
766 770
767 771 @util.propertycache
768 772 def _aliasdef(self):
769 773 return cmdalias(
770 774 self.ui, self.name, self.definition, self.cmdtable, self.source
771 775 )
772 776
773 777 def __getitem__(self, n):
774 778 aliasdef = self._aliasdef
775 779 if n == 0:
776 780 return aliasdef
777 781 elif n == 1:
778 782 return aliasdef.opts
779 783 elif n == 2:
780 784 return aliasdef.help
781 785 else:
782 786 raise IndexError
783 787
784 788 def __iter__(self):
785 789 for i in range(3):
786 790 yield self[i]
787 791
788 792 def __len__(self):
789 793 return 3
790 794
791 795
792 796 def addaliases(ui, cmdtable):
793 797 # aliases are processed after extensions have been loaded, so they
794 798 # may use extension commands. Aliases can also use other alias definitions,
795 799 # but only if they have been defined prior to the current definition.
796 800 for alias, definition in ui.configitems(b'alias', ignoresub=True):
797 801 try:
798 802 if cmdtable[alias].definition == definition:
799 803 continue
800 804 except (KeyError, AttributeError):
801 805 # definition might not exist or it might not be a cmdalias
802 806 pass
803 807
804 808 source = ui.configsource(b'alias', alias)
805 809 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
806 810 cmdtable[alias] = entry
807 811
808 812
809 813 def _parse(ui, args):
810 814 options = {}
811 815 cmdoptions = {}
812 816
813 817 try:
814 818 args = fancyopts.fancyopts(args, commands.globalopts, options)
815 819 except getopt.GetoptError as inst:
816 820 raise error.CommandError(None, stringutil.forcebytestr(inst))
817 821
818 822 if args:
819 823 cmd, args = args[0], args[1:]
820 824 aliases, entry = cmdutil.findcmd(
821 825 cmd, commands.table, ui.configbool(b"ui", b"strict")
822 826 )
823 827 cmd = aliases[0]
824 828 args = aliasargs(entry[0], args)
825 829 defaults = ui.config(b"defaults", cmd)
826 830 if defaults:
827 831 args = (
828 832 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
829 833 + args
830 834 )
831 835 c = list(entry[1])
832 836 else:
833 837 cmd = None
834 838 c = []
835 839
836 840 # combine global options into local
837 841 for o in commands.globalopts:
838 842 c.append((o[0], o[1], options[o[1]], o[3]))
839 843
840 844 try:
841 845 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
842 846 except getopt.GetoptError as inst:
843 847 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
844 848
845 849 # separate global options back out
846 850 for o in commands.globalopts:
847 851 n = o[1]
848 852 options[n] = cmdoptions[n]
849 853 del cmdoptions[n]
850 854
851 855 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
852 856
853 857
854 858 def _parseconfig(ui, config):
855 859 """parse the --config options from the command line"""
856 860 configs = []
857 861
858 862 for cfg in config:
859 863 try:
860 864 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
861 865 section, name = name.split(b'.', 1)
862 866 if not section or not name:
863 867 raise IndexError
864 868 ui.setconfig(section, name, value, b'--config')
865 869 configs.append((section, name, value))
866 870 except (IndexError, ValueError):
867 871 raise error.InputError(
868 872 _(
869 873 b'malformed --config option: %r '
870 874 b'(use --config section.name=value)'
871 875 )
872 876 % pycompat.bytestr(cfg)
873 877 )
874 878
875 879 return configs
876 880
877 881
878 882 def _earlyparseopts(ui, args):
879 883 options = {}
880 884 fancyopts.fancyopts(
881 885 args,
882 886 commands.globalopts,
883 887 options,
884 888 gnu=not ui.plain(b'strictflags'),
885 889 early=True,
886 890 optaliases={b'repository': [b'repo']},
887 891 )
888 892 return options
889 893
890 894
891 895 def _earlysplitopts(args):
892 896 """Split args into a list of possible early options and remainder args"""
893 897 shortoptions = b'R:'
894 898 # TODO: perhaps 'debugger' should be included
895 899 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
896 900 return fancyopts.earlygetopt(
897 901 args, shortoptions, longoptions, gnu=True, keepsep=True
898 902 )
899 903
900 904
901 905 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
902 906 # run pre-hook, and abort if it fails
903 907 hook.hook(
904 908 lui,
905 909 repo,
906 910 b"pre-%s" % cmd,
907 911 True,
908 912 args=b" ".join(fullargs),
909 913 pats=cmdpats,
910 914 opts=cmdoptions,
911 915 )
912 916 try:
913 917 ret = _runcommand(ui, options, cmd, d)
914 918 # run post-hook, passing command result
915 919 hook.hook(
916 920 lui,
917 921 repo,
918 922 b"post-%s" % cmd,
919 923 False,
920 924 args=b" ".join(fullargs),
921 925 result=ret,
922 926 pats=cmdpats,
923 927 opts=cmdoptions,
924 928 )
925 929 except Exception:
926 930 # run failure hook and re-raise
927 931 hook.hook(
928 932 lui,
929 933 repo,
930 934 b"fail-%s" % cmd,
931 935 False,
932 936 args=b" ".join(fullargs),
933 937 pats=cmdpats,
934 938 opts=cmdoptions,
935 939 )
936 940 raise
937 941 return ret
938 942
939 943
940 944 def _readsharedsourceconfig(ui, path):
941 945 """if the current repository is shared one, this tries to read
942 946 .hg/hgrc of shared source if we are in share-safe mode
943 947
944 948 Config read is loaded into the ui object passed
945 949
946 950 This should be called before reading .hg/hgrc or the main repo
947 951 as that overrides config set in shared source"""
948 952 try:
949 953 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
950 954 requirements = set(fp.read().splitlines())
951 955 if not (
952 956 requirementsmod.SHARESAFE_REQUIREMENT in requirements
953 957 and requirementsmod.SHARED_REQUIREMENT in requirements
954 958 ):
955 959 return
956 960 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
957 961 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
958 962 root = sharedvfs.base
959 963 ui.readconfig(sharedvfs.join(b"hgrc"), root)
960 964 except IOError:
961 965 pass
962 966
963 967
964 968 def _getlocal(ui, rpath, wd=None):
965 969 """Return (path, local ui object) for the given target path.
966 970
967 971 Takes paths in [cwd]/.hg/hgrc into account."
968 972 """
969 973 if wd is None:
970 974 try:
971 975 wd = encoding.getcwd()
972 976 except OSError as e:
973 977 raise error.Abort(
974 978 _(b"error getting current working directory: %s")
975 979 % encoding.strtolocal(e.strerror)
976 980 )
977 981
978 982 path = cmdutil.findrepo(wd) or b""
979 983 if not path:
980 984 lui = ui
981 985 else:
982 986 lui = ui.copy()
983 987 if rcutil.use_repo_hgrc():
984 988 _readsharedsourceconfig(lui, path)
985 989 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
986 990 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
987 991
988 992 if rpath:
989 993 path = lui.expandpath(rpath)
990 994 lui = ui.copy()
991 995 if rcutil.use_repo_hgrc():
992 996 _readsharedsourceconfig(lui, path)
993 997 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
994 998 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
995 999
996 1000 return path, lui
997 1001
998 1002
999 1003 def _checkshellalias(lui, ui, args):
1000 1004 """Return the function to run the shell alias, if it is required"""
1001 1005 options = {}
1002 1006
1003 1007 try:
1004 1008 args = fancyopts.fancyopts(args, commands.globalopts, options)
1005 1009 except getopt.GetoptError:
1006 1010 return
1007 1011
1008 1012 if not args:
1009 1013 return
1010 1014
1011 1015 cmdtable = commands.table
1012 1016
1013 1017 cmd = args[0]
1014 1018 try:
1015 1019 strict = ui.configbool(b"ui", b"strict")
1016 1020 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
1017 1021 except (error.AmbiguousCommand, error.UnknownCommand):
1018 1022 return
1019 1023
1020 1024 cmd = aliases[0]
1021 1025 fn = entry[0]
1022 1026
1023 1027 if cmd and util.safehasattr(fn, b'shell'):
1024 1028 # shell alias shouldn't receive early options which are consumed by hg
1025 1029 _earlyopts, args = _earlysplitopts(args)
1026 1030 d = lambda: fn(ui, *args[1:])
1027 1031 return lambda: runcommand(
1028 1032 lui, None, cmd, args[:1], ui, options, d, [], {}
1029 1033 )
1030 1034
1031 1035
1032 1036 def _dispatch(req):
1033 1037 args = req.args
1034 1038 ui = req.ui
1035 1039
1036 1040 # check for cwd
1037 1041 cwd = req.earlyoptions[b'cwd']
1038 1042 if cwd:
1039 1043 os.chdir(cwd)
1040 1044
1041 1045 rpath = req.earlyoptions[b'repository']
1042 1046 path, lui = _getlocal(ui, rpath)
1043 1047
1044 1048 uis = {ui, lui}
1045 1049
1046 1050 if req.repo:
1047 1051 uis.add(req.repo.ui)
1048 1052
1049 1053 if (
1050 1054 req.earlyoptions[b'verbose']
1051 1055 or req.earlyoptions[b'debug']
1052 1056 or req.earlyoptions[b'quiet']
1053 1057 ):
1054 1058 for opt in (b'verbose', b'debug', b'quiet'):
1055 1059 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1056 1060 for ui_ in uis:
1057 1061 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1058 1062
1059 1063 if req.earlyoptions[b'profile']:
1060 1064 for ui_ in uis:
1061 1065 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1062 1066
1063 1067 profile = lui.configbool(b'profiling', b'enabled')
1064 1068 with profiling.profile(lui, enabled=profile) as profiler:
1065 1069 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1066 1070 # reposetup
1067 1071 extensions.loadall(lui)
1068 1072 # Propagate any changes to lui.__class__ by extensions
1069 1073 ui.__class__ = lui.__class__
1070 1074
1071 1075 # (uisetup and extsetup are handled in extensions.loadall)
1072 1076
1073 1077 # (reposetup is handled in hg.repository)
1074 1078
1075 1079 addaliases(lui, commands.table)
1076 1080
1077 1081 # All aliases and commands are completely defined, now.
1078 1082 # Check abbreviation/ambiguity of shell alias.
1079 1083 shellaliasfn = _checkshellalias(lui, ui, args)
1080 1084 if shellaliasfn:
1081 1085 # no additional configs will be set, set up the ui instances
1082 1086 for ui_ in uis:
1083 1087 extensions.populateui(ui_)
1084 1088 return shellaliasfn()
1085 1089
1086 1090 # check for fallback encoding
1087 1091 fallback = lui.config(b'ui', b'fallbackencoding')
1088 1092 if fallback:
1089 1093 encoding.fallbackencoding = fallback
1090 1094
1091 1095 fullargs = args
1092 1096 cmd, func, args, options, cmdoptions = _parse(lui, args)
1093 1097
1094 1098 # store the canonical command name in request object for later access
1095 1099 req.canonical_command = cmd
1096 1100
1097 1101 if options[b"config"] != req.earlyoptions[b"config"]:
1098 1102 raise error.InputError(_(b"option --config may not be abbreviated"))
1099 1103 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1100 1104 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1101 1105 if options[b"repository"] != req.earlyoptions[b"repository"]:
1102 1106 raise error.InputError(
1103 1107 _(
1104 1108 b"option -R has to be separated from other options (e.g. not "
1105 1109 b"-qR) and --repository may only be abbreviated as --repo"
1106 1110 )
1107 1111 )
1108 1112 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1109 1113 raise error.InputError(
1110 1114 _(b"option --debugger may not be abbreviated")
1111 1115 )
1112 1116 # don't validate --profile/--traceback, which can be enabled from now
1113 1117
1114 1118 if options[b"encoding"]:
1115 1119 encoding.encoding = options[b"encoding"]
1116 1120 if options[b"encodingmode"]:
1117 1121 encoding.encodingmode = options[b"encodingmode"]
1118 1122 if options[b"time"]:
1119 1123
1120 1124 def get_times():
1121 1125 t = os.times()
1122 1126 if t[4] == 0.0:
1123 1127 # Windows leaves this as zero, so use time.perf_counter()
1124 1128 t = (t[0], t[1], t[2], t[3], util.timer())
1125 1129 return t
1126 1130
1127 1131 s = get_times()
1128 1132
1129 1133 def print_time():
1130 1134 t = get_times()
1131 1135 ui.warn(
1132 1136 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1133 1137 % (
1134 1138 t[4] - s[4],
1135 1139 t[0] - s[0],
1136 1140 t[2] - s[2],
1137 1141 t[1] - s[1],
1138 1142 t[3] - s[3],
1139 1143 )
1140 1144 )
1141 1145
1142 1146 ui.atexit(print_time)
1143 1147 if options[b"profile"]:
1144 1148 profiler.start()
1145 1149
1146 1150 # if abbreviated version of this were used, take them in account, now
1147 1151 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1148 1152 for opt in (b'verbose', b'debug', b'quiet'):
1149 1153 if options[opt] == req.earlyoptions[opt]:
1150 1154 continue
1151 1155 val = pycompat.bytestr(bool(options[opt]))
1152 1156 for ui_ in uis:
1153 1157 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1154 1158
1155 1159 if options[b'traceback']:
1156 1160 for ui_ in uis:
1157 1161 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1158 1162
1159 1163 if options[b'noninteractive']:
1160 1164 for ui_ in uis:
1161 1165 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1162 1166
1163 1167 if cmdoptions.get(b'insecure', False):
1164 1168 for ui_ in uis:
1165 1169 ui_.insecureconnections = True
1166 1170
1167 1171 # setup color handling before pager, because setting up pager
1168 1172 # might cause incorrect console information
1169 1173 coloropt = options[b'color']
1170 1174 for ui_ in uis:
1171 1175 if coloropt:
1172 1176 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1173 1177 color.setup(ui_)
1174 1178
1175 1179 if stringutil.parsebool(options[b'pager']):
1176 1180 # ui.pager() expects 'internal-always-' prefix in this case
1177 1181 ui.pager(b'internal-always-' + cmd)
1178 1182 elif options[b'pager'] != b'auto':
1179 1183 for ui_ in uis:
1180 1184 ui_.disablepager()
1181 1185
1182 1186 # configs are fully loaded, set up the ui instances
1183 1187 for ui_ in uis:
1184 1188 extensions.populateui(ui_)
1185 1189
1186 1190 if options[b'version']:
1187 1191 return commands.version_(ui)
1188 1192 if options[b'help']:
1189 1193 return commands.help_(ui, cmd, command=cmd is not None)
1190 1194 elif not cmd:
1191 1195 return commands.help_(ui, b'shortlist')
1192 1196
1193 1197 repo = None
1194 1198 cmdpats = args[:]
1195 1199 assert func is not None # help out pytype
1196 1200 if not func.norepo:
1197 1201 # use the repo from the request only if we don't have -R
1198 1202 if not rpath and not cwd:
1199 1203 repo = req.repo
1200 1204
1201 1205 if repo:
1202 1206 # set the descriptors of the repo ui to those of ui
1203 1207 repo.ui.fin = ui.fin
1204 1208 repo.ui.fout = ui.fout
1205 1209 repo.ui.ferr = ui.ferr
1206 1210 repo.ui.fmsg = ui.fmsg
1207 1211 else:
1208 1212 try:
1209 1213 repo = hg.repository(
1210 1214 ui,
1211 1215 path=path,
1212 1216 presetupfuncs=req.prereposetups,
1213 1217 intents=func.intents,
1214 1218 )
1215 1219 if not repo.local():
1216 1220 raise error.InputError(
1217 1221 _(b"repository '%s' is not local") % path
1218 1222 )
1219 1223 repo.ui.setconfig(
1220 1224 b"bundle", b"mainreporoot", repo.root, b'repo'
1221 1225 )
1222 1226 except error.RequirementError:
1223 1227 raise
1224 1228 except error.RepoError:
1225 1229 if rpath: # invalid -R path
1226 1230 raise
1227 1231 if not func.optionalrepo:
1228 1232 if func.inferrepo and args and not path:
1229 1233 # try to infer -R from command args
1230 1234 repos = pycompat.maplist(cmdutil.findrepo, args)
1231 1235 guess = repos[0]
1232 1236 if guess and repos.count(guess) == len(repos):
1233 1237 req.args = [b'--repository', guess] + fullargs
1234 1238 req.earlyoptions[b'repository'] = guess
1235 1239 return _dispatch(req)
1236 1240 if not path:
1237 1241 raise error.InputError(
1238 1242 _(
1239 1243 b"no repository found in"
1240 1244 b" '%s' (.hg not found)"
1241 1245 )
1242 1246 % encoding.getcwd()
1243 1247 )
1244 1248 raise
1245 1249 if repo:
1246 1250 ui = repo.ui
1247 1251 if options[b'hidden']:
1248 1252 repo = repo.unfiltered()
1249 1253 args.insert(0, repo)
1250 1254 elif rpath:
1251 1255 ui.warn(_(b"warning: --repository ignored\n"))
1252 1256
1253 1257 msg = _formatargs(fullargs)
1254 1258 ui.log(b"command", b'%s\n', msg)
1255 1259 strcmdopt = pycompat.strkwargs(cmdoptions)
1256 1260 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1257 1261 try:
1258 1262 return runcommand(
1259 1263 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1260 1264 )
1261 1265 finally:
1262 1266 if repo and repo != req.repo:
1263 1267 repo.close()
1264 1268
1265 1269
1266 1270 def _runcommand(ui, options, cmd, cmdfunc):
1267 1271 """Run a command function, possibly with profiling enabled."""
1268 1272 try:
1269 1273 with tracing.log("Running %s command" % cmd):
1270 1274 return cmdfunc()
1271 1275 except error.SignatureError:
1272 1276 raise error.CommandError(cmd, _(b'invalid arguments'))
1273 1277
1274 1278
1275 1279 def _exceptionwarning(ui):
1276 1280 """Produce a warning message for the current active exception"""
1277 1281
1278 1282 # For compatibility checking, we discard the portion of the hg
1279 1283 # version after the + on the assumption that if a "normal
1280 1284 # user" is running a build with a + in it the packager
1281 1285 # probably built from fairly close to a tag and anyone with a
1282 1286 # 'make local' copy of hg (where the version number can be out
1283 1287 # of date) will be clueful enough to notice the implausible
1284 1288 # version number and try updating.
1285 1289 ct = util.versiontuple(n=2)
1286 1290 worst = None, ct, b'', b''
1287 1291 if ui.config(b'ui', b'supportcontact') is None:
1288 1292 for name, mod in extensions.extensions():
1289 1293 # 'testedwith' should be bytes, but not all extensions are ported
1290 1294 # to py3 and we don't want UnicodeException because of that.
1291 1295 testedwith = stringutil.forcebytestr(
1292 1296 getattr(mod, 'testedwith', b'')
1293 1297 )
1294 1298 version = extensions.moduleversion(mod)
1295 1299 report = getattr(mod, 'buglink', _(b'the extension author.'))
1296 1300 if not testedwith.strip():
1297 1301 # We found an untested extension. It's likely the culprit.
1298 1302 worst = name, b'unknown', report, version
1299 1303 break
1300 1304
1301 1305 # Never blame on extensions bundled with Mercurial.
1302 1306 if extensions.ismoduleinternal(mod):
1303 1307 continue
1304 1308
1305 1309 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1306 1310 if ct in tested:
1307 1311 continue
1308 1312
1309 1313 lower = [t for t in tested if t < ct]
1310 1314 nearest = max(lower or tested)
1311 1315 if worst[0] is None or nearest < worst[1]:
1312 1316 worst = name, nearest, report, version
1313 1317 if worst[0] is not None:
1314 1318 name, testedwith, report, version = worst
1315 1319 if not isinstance(testedwith, (bytes, str)):
1316 1320 testedwith = b'.'.join(
1317 1321 [stringutil.forcebytestr(c) for c in testedwith]
1318 1322 )
1319 1323 extver = version or _(b"(version N/A)")
1320 1324 warning = _(
1321 1325 b'** Unknown exception encountered with '
1322 1326 b'possibly-broken third-party extension "%s" %s\n'
1323 1327 b'** which supports versions %s of Mercurial.\n'
1324 1328 b'** Please disable "%s" and try your action again.\n'
1325 1329 b'** If that fixes the bug please report it to %s\n'
1326 1330 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1327 1331 else:
1328 1332 bugtracker = ui.config(b'ui', b'supportcontact')
1329 1333 if bugtracker is None:
1330 1334 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1331 1335 warning = (
1332 1336 _(
1333 1337 b"** unknown exception encountered, "
1334 1338 b"please report by visiting\n** "
1335 1339 )
1336 1340 + bugtracker
1337 1341 + b'\n'
1338 1342 )
1339 1343 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1340 1344
1341 1345 def ext_with_ver(x):
1342 1346 ext = x[0]
1343 1347 ver = extensions.moduleversion(x[1])
1344 1348 if ver:
1345 1349 ext += b' ' + ver
1346 1350 return ext
1347 1351
1348 1352 warning += (
1349 1353 (_(b"** Python %s\n") % sysversion)
1350 1354 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1351 1355 + (
1352 1356 _(b"** Extensions loaded: %s\n")
1353 1357 % b", ".join(
1354 1358 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1355 1359 )
1356 1360 )
1357 1361 )
1358 1362 return warning
1359 1363
1360 1364
1361 1365 def handlecommandexception(ui):
1362 1366 """Produce a warning message for broken commands
1363 1367
1364 1368 Called when handling an exception; the exception is reraised if
1365 1369 this function returns False, ignored otherwise.
1366 1370 """
1367 1371 warning = _exceptionwarning(ui)
1368 1372 ui.log(
1369 1373 b"commandexception",
1370 1374 b"%s\n%s\n",
1371 1375 warning,
1372 1376 pycompat.sysbytes(traceback.format_exc()),
1373 1377 )
1374 1378 ui.warn(warning)
1375 1379 return False # re-raise the exception
@@ -1,305 +1,305 b''
1 1 # osutil.py - pure Python version of osutil.c
2 2 #
3 3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
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, division
9 9
10 10 import ctypes
11 11 import ctypes.util
12 12 import os
13 13 import socket
14 14 import stat as statmod
15 15
16 16 from ..pycompat import getattr
17 17 from .. import (
18 18 encoding,
19 19 pycompat,
20 20 )
21 21
22 22
23 23 def _mode_to_kind(mode):
24 24 if statmod.S_ISREG(mode):
25 25 return statmod.S_IFREG
26 26 if statmod.S_ISDIR(mode):
27 27 return statmod.S_IFDIR
28 28 if statmod.S_ISLNK(mode):
29 29 return statmod.S_IFLNK
30 30 if statmod.S_ISBLK(mode):
31 31 return statmod.S_IFBLK
32 32 if statmod.S_ISCHR(mode):
33 33 return statmod.S_IFCHR
34 34 if statmod.S_ISFIFO(mode):
35 35 return statmod.S_IFIFO
36 36 if statmod.S_ISSOCK(mode):
37 37 return statmod.S_IFSOCK
38 38 return mode
39 39
40 40
41 41 def listdir(path, stat=False, skip=None):
42 42 """listdir(path, stat=False) -> list_of_tuples
43 43
44 44 Return a sorted list containing information about the entries
45 45 in the directory.
46 46
47 47 If stat is True, each element is a 3-tuple:
48 48
49 49 (name, type, stat object)
50 50
51 51 Otherwise, each element is a 2-tuple:
52 52
53 53 (name, type)
54 54 """
55 55 result = []
56 56 prefix = path
57 57 if not prefix.endswith(pycompat.ossep):
58 58 prefix += pycompat.ossep
59 59 names = os.listdir(path)
60 60 names.sort()
61 61 for fn in names:
62 62 st = os.lstat(prefix + fn)
63 63 if fn == skip and statmod.S_ISDIR(st.st_mode):
64 64 return []
65 65 if stat:
66 66 result.append((fn, _mode_to_kind(st.st_mode), st))
67 67 else:
68 68 result.append((fn, _mode_to_kind(st.st_mode)))
69 69 return result
70 70
71 71
72 72 if not pycompat.iswindows:
73 73 posixfile = open
74 74
75 75 _SCM_RIGHTS = 0x01
76 76 _socklen_t = ctypes.c_uint
77 77
78 78 if pycompat.sysplatform.startswith(b'linux'):
79 79 # socket.h says "the type should be socklen_t but the definition of
80 80 # the kernel is incompatible with this."
81 81 _cmsg_len_t = ctypes.c_size_t
82 82 _msg_controllen_t = ctypes.c_size_t
83 83 _msg_iovlen_t = ctypes.c_size_t
84 84 else:
85 85 _cmsg_len_t = _socklen_t
86 86 _msg_controllen_t = _socklen_t
87 87 _msg_iovlen_t = ctypes.c_int
88 88
89 89 class _iovec(ctypes.Structure):
90 90 _fields_ = [
91 91 (u'iov_base', ctypes.c_void_p),
92 92 (u'iov_len', ctypes.c_size_t),
93 93 ]
94 94
95 95 class _msghdr(ctypes.Structure):
96 96 _fields_ = [
97 97 (u'msg_name', ctypes.c_void_p),
98 98 (u'msg_namelen', _socklen_t),
99 99 (u'msg_iov', ctypes.POINTER(_iovec)),
100 100 (u'msg_iovlen', _msg_iovlen_t),
101 101 (u'msg_control', ctypes.c_void_p),
102 102 (u'msg_controllen', _msg_controllen_t),
103 103 (u'msg_flags', ctypes.c_int),
104 104 ]
105 105
106 106 class _cmsghdr(ctypes.Structure):
107 107 _fields_ = [
108 108 (u'cmsg_len', _cmsg_len_t),
109 109 (u'cmsg_level', ctypes.c_int),
110 110 (u'cmsg_type', ctypes.c_int),
111 111 (u'cmsg_data', ctypes.c_ubyte * 0),
112 112 ]
113 113
114 114 _libc = ctypes.CDLL(ctypes.util.find_library(u'c'), use_errno=True)
115 115 _recvmsg = getattr(_libc, 'recvmsg', None)
116 116 if _recvmsg:
117 117 _recvmsg.restype = getattr(ctypes, 'c_ssize_t', ctypes.c_long)
118 118 _recvmsg.argtypes = (
119 119 ctypes.c_int,
120 120 ctypes.POINTER(_msghdr),
121 121 ctypes.c_int,
122 122 )
123 123 else:
124 124 # recvmsg isn't always provided by libc; such systems are unsupported
125 125 def _recvmsg(sockfd, msg, flags):
126 126 raise NotImplementedError(b'unsupported platform')
127 127
128 128 def _CMSG_FIRSTHDR(msgh):
129 129 if msgh.msg_controllen < ctypes.sizeof(_cmsghdr):
130 130 return
131 131 cmsgptr = ctypes.cast(msgh.msg_control, ctypes.POINTER(_cmsghdr))
132 132 return cmsgptr.contents
133 133
134 134 # The pure version is less portable than the native version because the
135 135 # handling of socket ancillary data heavily depends on C preprocessor.
136 136 # Also, some length fields are wrongly typed in Linux kernel.
137 137 def recvfds(sockfd):
138 138 """receive list of file descriptors via socket"""
139 139 dummy = (ctypes.c_ubyte * 1)()
140 140 iov = _iovec(ctypes.cast(dummy, ctypes.c_void_p), ctypes.sizeof(dummy))
141 141 cbuf = ctypes.create_string_buffer(256)
142 142 msgh = _msghdr(
143 143 None,
144 144 0,
145 145 ctypes.pointer(iov),
146 146 1,
147 147 ctypes.cast(cbuf, ctypes.c_void_p),
148 148 ctypes.sizeof(cbuf),
149 149 0,
150 150 )
151 151 r = _recvmsg(sockfd, ctypes.byref(msgh), 0)
152 152 if r < 0:
153 153 e = ctypes.get_errno()
154 154 raise OSError(e, os.strerror(e))
155 155 # assumes that the first cmsg has fds because it isn't easy to write
156 156 # portable CMSG_NXTHDR() with ctypes.
157 157 cmsg = _CMSG_FIRSTHDR(msgh)
158 158 if not cmsg:
159 159 return []
160 160 if (
161 161 cmsg.cmsg_level != socket.SOL_SOCKET
162 162 or cmsg.cmsg_type != _SCM_RIGHTS
163 163 ):
164 164 return []
165 165 rfds = ctypes.cast(cmsg.cmsg_data, ctypes.POINTER(ctypes.c_int))
166 166 rfdscount = (
167 167 cmsg.cmsg_len - _cmsghdr.cmsg_data.offset
168 168 ) // ctypes.sizeof(ctypes.c_int)
169 169 return [rfds[i] for i in pycompat.xrange(rfdscount)]
170 170
171 171
172 172 else:
173 173 import msvcrt
174 174
175 _kernel32 = ctypes.windll.kernel32
175 _kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
176 176
177 177 _DWORD = ctypes.c_ulong
178 178 _LPCSTR = _LPSTR = ctypes.c_char_p
179 179 _HANDLE = ctypes.c_void_p
180 180
181 181 _INVALID_HANDLE_VALUE = _HANDLE(-1).value
182 182
183 183 # CreateFile
184 184 _FILE_SHARE_READ = 0x00000001
185 185 _FILE_SHARE_WRITE = 0x00000002
186 186 _FILE_SHARE_DELETE = 0x00000004
187 187
188 188 _CREATE_ALWAYS = 2
189 189 _OPEN_EXISTING = 3
190 190 _OPEN_ALWAYS = 4
191 191
192 192 _GENERIC_READ = 0x80000000
193 193 _GENERIC_WRITE = 0x40000000
194 194
195 195 _FILE_ATTRIBUTE_NORMAL = 0x80
196 196
197 197 # open_osfhandle flags
198 198 _O_RDONLY = 0x0000
199 199 _O_RDWR = 0x0002
200 200 _O_APPEND = 0x0008
201 201
202 202 _O_TEXT = 0x4000
203 203 _O_BINARY = 0x8000
204 204
205 205 # types of parameters of C functions used (required by pypy)
206 206
207 207 _kernel32.CreateFileA.argtypes = [
208 208 _LPCSTR,
209 209 _DWORD,
210 210 _DWORD,
211 211 ctypes.c_void_p,
212 212 _DWORD,
213 213 _DWORD,
214 214 _HANDLE,
215 215 ]
216 216 _kernel32.CreateFileA.restype = _HANDLE
217 217
218 218 def _raiseioerror(name):
219 err = ctypes.WinError()
219 err = ctypes.WinError() # pytype: disable=module-attr
220 220 raise IOError(
221 221 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
222 222 )
223 223
224 224 class posixfile(object):
225 225 """a file object aiming for POSIX-like semantics
226 226
227 227 CPython's open() returns a file that was opened *without* setting the
228 228 _FILE_SHARE_DELETE flag, which causes rename and unlink to abort.
229 229 This even happens if any hardlinked copy of the file is in open state.
230 230 We set _FILE_SHARE_DELETE here, so files opened with posixfile can be
231 231 renamed and deleted while they are held open.
232 232 Note that if a file opened with posixfile is unlinked, the file
233 233 remains but cannot be opened again or be recreated under the same name,
234 234 until all reading processes have closed the file."""
235 235
236 236 def __init__(self, name, mode=b'r', bufsize=-1):
237 237 if b'b' in mode:
238 238 flags = _O_BINARY
239 239 else:
240 240 flags = _O_TEXT
241 241
242 242 m0 = mode[0:1]
243 243 if m0 == b'r' and b'+' not in mode:
244 244 flags |= _O_RDONLY
245 245 access = _GENERIC_READ
246 246 else:
247 247 # work around http://support.microsoft.com/kb/899149 and
248 248 # set _O_RDWR for 'w' and 'a', even if mode has no '+'
249 249 flags |= _O_RDWR
250 250 access = _GENERIC_READ | _GENERIC_WRITE
251 251
252 252 if m0 == b'r':
253 253 creation = _OPEN_EXISTING
254 254 elif m0 == b'w':
255 255 creation = _CREATE_ALWAYS
256 256 elif m0 == b'a':
257 257 creation = _OPEN_ALWAYS
258 258 flags |= _O_APPEND
259 259 else:
260 260 raise ValueError("invalid mode: %s" % pycompat.sysstr(mode))
261 261
262 262 fh = _kernel32.CreateFileA(
263 263 name,
264 264 access,
265 265 _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
266 266 None,
267 267 creation,
268 268 _FILE_ATTRIBUTE_NORMAL,
269 269 None,
270 270 )
271 271 if fh == _INVALID_HANDLE_VALUE:
272 272 _raiseioerror(name)
273 273
274 fd = msvcrt.open_osfhandle(fh, flags)
274 fd = msvcrt.open_osfhandle(fh, flags) # pytype: disable=module-attr
275 275 if fd == -1:
276 276 _kernel32.CloseHandle(fh)
277 277 _raiseioerror(name)
278 278
279 279 f = os.fdopen(fd, pycompat.sysstr(mode), bufsize)
280 280 # unfortunately, f.name is '<fdopen>' at this point -- so we store
281 281 # the name on this wrapper. We cannot just assign to f.name,
282 282 # because that attribute is read-only.
283 283 object.__setattr__(self, 'name', name)
284 284 object.__setattr__(self, '_file', f)
285 285
286 286 def __iter__(self):
287 287 return self._file
288 288
289 289 def __getattr__(self, name):
290 290 return getattr(self._file, name)
291 291
292 292 def __setattr__(self, name, value):
293 293 """mimics the read-only attributes of Python file objects
294 294 by raising 'TypeError: readonly attribute' if someone tries:
295 295 f = posixfile('foo.txt')
296 296 f.name = 'bla'
297 297 """
298 298 return self._file.__setattr__(name, value)
299 299
300 300 def __enter__(self):
301 301 self._file.__enter__()
302 302 return self
303 303
304 304 def __exit__(self, exc_type, exc_value, exc_tb):
305 305 return self._file.__exit__(exc_type, exc_value, exc_tb)
@@ -1,836 +1,838 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import ssl
16 16
17 17 from .i18n import _
18 18 from .pycompat import getattr
19 19 from .node import hex
20 20 from . import (
21 21 encoding,
22 22 error,
23 23 pycompat,
24 24 util,
25 25 )
26 26 from .utils import (
27 27 hashutil,
28 28 resourceutil,
29 29 stringutil,
30 30 )
31 31
32 32 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
33 33 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
34 34 # all exposed via the "ssl" module.
35 35 #
36 36 # We require in setup.py the presence of ssl.SSLContext, which indicates modern
37 37 # SSL/TLS support.
38 38
39 39 configprotocols = {
40 40 b'tls1.0',
41 41 b'tls1.1',
42 42 b'tls1.2',
43 43 }
44 44
45 45 hassni = getattr(ssl, 'HAS_SNI', False)
46 46
47 47 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
48 48 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
49 49 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
50 50 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
51 51 # support. At the mentioned commit, they were unconditionally defined.
52 52 supportedprotocols = set()
53 53 if getattr(ssl, 'HAS_TLSv1', util.safehasattr(ssl, 'PROTOCOL_TLSv1')):
54 54 supportedprotocols.add(b'tls1.0')
55 55 if getattr(ssl, 'HAS_TLSv1_1', util.safehasattr(ssl, 'PROTOCOL_TLSv1_1')):
56 56 supportedprotocols.add(b'tls1.1')
57 57 if getattr(ssl, 'HAS_TLSv1_2', util.safehasattr(ssl, 'PROTOCOL_TLSv1_2')):
58 58 supportedprotocols.add(b'tls1.2')
59 59
60 60
61 61 def _hostsettings(ui, hostname):
62 62 """Obtain security settings for a hostname.
63 63
64 64 Returns a dict of settings relevant to that hostname.
65 65 """
66 66 bhostname = pycompat.bytesurl(hostname)
67 67 s = {
68 68 # Whether we should attempt to load default/available CA certs
69 69 # if an explicit ``cafile`` is not defined.
70 70 b'allowloaddefaultcerts': True,
71 71 # List of 2-tuple of (hash algorithm, hash).
72 72 b'certfingerprints': [],
73 73 # Path to file containing concatenated CA certs. Used by
74 74 # SSLContext.load_verify_locations().
75 75 b'cafile': None,
76 76 # Whether certificate verification should be disabled.
77 77 b'disablecertverification': False,
78 78 # Whether the legacy [hostfingerprints] section has data for this host.
79 79 b'legacyfingerprint': False,
80 80 # String representation of minimum protocol to be used for UI
81 81 # presentation.
82 82 b'minimumprotocol': None,
83 83 # ssl.CERT_* constant used by SSLContext.verify_mode.
84 84 b'verifymode': None,
85 85 # OpenSSL Cipher List to use (instead of default).
86 86 b'ciphers': None,
87 87 }
88 88
89 89 # Allow minimum TLS protocol to be specified in the config.
90 90 def validateprotocol(protocol, key):
91 91 if protocol not in configprotocols:
92 92 raise error.Abort(
93 93 _(b'unsupported protocol from hostsecurity.%s: %s')
94 94 % (key, protocol),
95 95 hint=_(b'valid protocols: %s')
96 96 % b' '.join(sorted(configprotocols)),
97 97 )
98 98
99 99 # We default to TLS 1.1+ because TLS 1.0 has known vulnerabilities (like
100 100 # BEAST and POODLE). We allow users to downgrade to TLS 1.0+ via config
101 101 # options in case a legacy server is encountered.
102 102
103 103 # setup.py checks that TLS 1.1 or TLS 1.2 is present, so the following
104 104 # assert should not fail.
105 105 assert supportedprotocols - {b'tls1.0'}
106 106 defaultminimumprotocol = b'tls1.1'
107 107
108 108 key = b'minimumprotocol'
109 109 minimumprotocol = ui.config(b'hostsecurity', key, defaultminimumprotocol)
110 110 validateprotocol(minimumprotocol, key)
111 111
112 112 key = b'%s:minimumprotocol' % bhostname
113 113 minimumprotocol = ui.config(b'hostsecurity', key, minimumprotocol)
114 114 validateprotocol(minimumprotocol, key)
115 115
116 116 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
117 117 # We always print a "connection security to %s is disabled..." message when
118 118 # --insecure is used. So no need to print anything more here.
119 119 if ui.insecureconnections:
120 120 minimumprotocol = b'tls1.0'
121 121
122 122 s[b'minimumprotocol'] = minimumprotocol
123 123
124 124 ciphers = ui.config(b'hostsecurity', b'ciphers')
125 125 ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers)
126 126 s[b'ciphers'] = ciphers
127 127
128 128 # Look for fingerprints in [hostsecurity] section. Value is a list
129 129 # of <alg>:<fingerprint> strings.
130 130 fingerprints = ui.configlist(
131 131 b'hostsecurity', b'%s:fingerprints' % bhostname
132 132 )
133 133 for fingerprint in fingerprints:
134 134 if not (fingerprint.startswith((b'sha1:', b'sha256:', b'sha512:'))):
135 135 raise error.Abort(
136 136 _(b'invalid fingerprint for %s: %s') % (bhostname, fingerprint),
137 137 hint=_(b'must begin with "sha1:", "sha256:", or "sha512:"'),
138 138 )
139 139
140 140 alg, fingerprint = fingerprint.split(b':', 1)
141 141 fingerprint = fingerprint.replace(b':', b'').lower()
142 142 s[b'certfingerprints'].append((alg, fingerprint))
143 143
144 144 # Fingerprints from [hostfingerprints] are always SHA-1.
145 145 for fingerprint in ui.configlist(b'hostfingerprints', bhostname):
146 146 fingerprint = fingerprint.replace(b':', b'').lower()
147 147 s[b'certfingerprints'].append((b'sha1', fingerprint))
148 148 s[b'legacyfingerprint'] = True
149 149
150 150 # If a host cert fingerprint is defined, it is the only thing that
151 151 # matters. No need to validate CA certs.
152 152 if s[b'certfingerprints']:
153 153 s[b'verifymode'] = ssl.CERT_NONE
154 154 s[b'allowloaddefaultcerts'] = False
155 155
156 156 # If --insecure is used, don't take CAs into consideration.
157 157 elif ui.insecureconnections:
158 158 s[b'disablecertverification'] = True
159 159 s[b'verifymode'] = ssl.CERT_NONE
160 160 s[b'allowloaddefaultcerts'] = False
161 161
162 162 if ui.configbool(b'devel', b'disableloaddefaultcerts'):
163 163 s[b'allowloaddefaultcerts'] = False
164 164
165 165 # If both fingerprints and a per-host ca file are specified, issue a warning
166 166 # because users should not be surprised about what security is or isn't
167 167 # being performed.
168 168 cafile = ui.config(b'hostsecurity', b'%s:verifycertsfile' % bhostname)
169 169 if s[b'certfingerprints'] and cafile:
170 170 ui.warn(
171 171 _(
172 172 b'(hostsecurity.%s:verifycertsfile ignored when host '
173 173 b'fingerprints defined; using host fingerprints for '
174 174 b'verification)\n'
175 175 )
176 176 % bhostname
177 177 )
178 178
179 179 # Try to hook up CA certificate validation unless something above
180 180 # makes it not necessary.
181 181 if s[b'verifymode'] is None:
182 182 # Look at per-host ca file first.
183 183 if cafile:
184 184 cafile = util.expandpath(cafile)
185 185 if not os.path.exists(cafile):
186 186 raise error.Abort(
187 187 _(b'path specified by %s does not exist: %s')
188 188 % (
189 189 b'hostsecurity.%s:verifycertsfile' % (bhostname,),
190 190 cafile,
191 191 )
192 192 )
193 193 s[b'cafile'] = cafile
194 194 else:
195 195 # Find global certificates file in config.
196 196 cafile = ui.config(b'web', b'cacerts')
197 197
198 198 if cafile:
199 199 cafile = util.expandpath(cafile)
200 200 if not os.path.exists(cafile):
201 201 raise error.Abort(
202 202 _(b'could not find web.cacerts: %s') % cafile
203 203 )
204 204 elif s[b'allowloaddefaultcerts']:
205 205 # CAs not defined in config. Try to find system bundles.
206 206 cafile = _defaultcacerts(ui)
207 207 if cafile:
208 208 ui.debug(b'using %s for CA file\n' % cafile)
209 209
210 210 s[b'cafile'] = cafile
211 211
212 212 # Require certificate validation if CA certs are being loaded and
213 213 # verification hasn't been disabled above.
214 214 if cafile or s[b'allowloaddefaultcerts']:
215 215 s[b'verifymode'] = ssl.CERT_REQUIRED
216 216 else:
217 217 # At this point we don't have a fingerprint, aren't being
218 218 # explicitly insecure, and can't load CA certs. Connecting
219 219 # is insecure. We allow the connection and abort during
220 220 # validation (once we have the fingerprint to print to the
221 221 # user).
222 222 s[b'verifymode'] = ssl.CERT_NONE
223 223
224 224 assert s[b'verifymode'] is not None
225 225
226 226 return s
227 227
228 228
229 229 def commonssloptions(minimumprotocol):
230 230 """Return SSLContext options common to servers and clients."""
231 231 if minimumprotocol not in configprotocols:
232 232 raise ValueError(b'protocol value not supported: %s' % minimumprotocol)
233 233
234 234 # SSLv2 and SSLv3 are broken. We ban them outright.
235 235 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
236 236
237 237 if minimumprotocol == b'tls1.0':
238 238 # Defaults above are to use TLS 1.0+
239 239 pass
240 240 elif minimumprotocol == b'tls1.1':
241 241 options |= ssl.OP_NO_TLSv1
242 242 elif minimumprotocol == b'tls1.2':
243 243 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
244 244 else:
245 245 raise error.Abort(_(b'this should not happen'))
246 246
247 247 # Prevent CRIME.
248 248 # There is no guarantee this attribute is defined on the module.
249 249 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
250 250
251 251 return options
252 252
253 253
254 254 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
255 255 """Add SSL/TLS to a socket.
256 256
257 257 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
258 258 choices based on what security options are available.
259 259
260 260 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
261 261 the following additional arguments:
262 262
263 263 * serverhostname - The expected hostname of the remote server. If the
264 264 server (and client) support SNI, this tells the server which certificate
265 265 to use.
266 266 """
267 267 if not serverhostname:
268 268 raise error.Abort(_(b'serverhostname argument is required'))
269 269
270 270 if b'SSLKEYLOGFILE' in encoding.environ:
271 271 try:
272 272 import sslkeylog # pytype: disable=import-error
273 273
274 274 sslkeylog.set_keylog(
275 275 pycompat.fsdecode(encoding.environ[b'SSLKEYLOGFILE'])
276 276 )
277 277 ui.warnnoi18n(
278 278 b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n'
279 279 )
280 280 except ImportError:
281 281 ui.warnnoi18n(
282 282 b'sslkeylog module missing, '
283 283 b'but SSLKEYLOGFILE set in environment\n'
284 284 )
285 285
286 286 for f in (keyfile, certfile):
287 287 if f and not os.path.exists(f):
288 288 raise error.Abort(
289 289 _(b'certificate file (%s) does not exist; cannot connect to %s')
290 290 % (f, pycompat.bytesurl(serverhostname)),
291 291 hint=_(
292 292 b'restore missing file or fix references '
293 293 b'in Mercurial config'
294 294 ),
295 295 )
296 296
297 297 settings = _hostsettings(ui, serverhostname)
298 298
299 299 # We can't use ssl.create_default_context() because it calls
300 300 # load_default_certs() unless CA arguments are passed to it. We want to
301 301 # have explicit control over CA loading because implicitly loading
302 302 # CAs may undermine the user's intent. For example, a user may define a CA
303 303 # bundle with a specific CA cert removed. If the system/default CA bundle
304 304 # is loaded and contains that removed CA, you've just undone the user's
305 305 # choice.
306 306 #
307 307 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
308 308 # ends support, including TLS protocols. commonssloptions() restricts the
309 309 # set of allowed protocols.
310 310 sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
311 311 sslcontext.options |= commonssloptions(settings[b'minimumprotocol'])
312 312 sslcontext.verify_mode = settings[b'verifymode']
313 313
314 314 if settings[b'ciphers']:
315 315 try:
316 316 sslcontext.set_ciphers(pycompat.sysstr(settings[b'ciphers']))
317 317 except ssl.SSLError as e:
318 318 raise error.Abort(
319 319 _(b'could not set ciphers: %s')
320 320 % stringutil.forcebytestr(e.args[0]),
321 321 hint=_(b'change cipher string (%s) in config')
322 322 % settings[b'ciphers'],
323 323 )
324 324
325 325 if certfile is not None:
326 326
327 327 def password():
328 328 f = keyfile or certfile
329 329 return ui.getpass(_(b'passphrase for %s: ') % f, b'')
330 330
331 331 sslcontext.load_cert_chain(certfile, keyfile, password)
332 332
333 333 if settings[b'cafile'] is not None:
334 334 try:
335 335 sslcontext.load_verify_locations(cafile=settings[b'cafile'])
336 336 except ssl.SSLError as e:
337 337 if len(e.args) == 1: # pypy has different SSLError args
338 338 msg = e.args[0]
339 339 else:
340 340 msg = e.args[1]
341 341 raise error.Abort(
342 342 _(b'error loading CA file %s: %s')
343 343 % (settings[b'cafile'], stringutil.forcebytestr(msg)),
344 344 hint=_(b'file is empty or malformed?'),
345 345 )
346 346 caloaded = True
347 347 elif settings[b'allowloaddefaultcerts']:
348 348 # This is a no-op on old Python.
349 349 sslcontext.load_default_certs()
350 350 caloaded = True
351 351 else:
352 352 caloaded = False
353 353
354 354 try:
355 355 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
356 356 except ssl.SSLError as e:
357 357 # If we're doing certificate verification and no CA certs are loaded,
358 358 # that is almost certainly the reason why verification failed. Provide
359 359 # a hint to the user.
360 360 # The exception handler is here to handle bugs around cert attributes:
361 361 # https://bugs.python.org/issue20916#msg213479. (See issues5313.)
362 362 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a
363 363 # non-empty list, but the following conditional is otherwise True.
364 364 try:
365 365 if (
366 366 caloaded
367 367 and settings[b'verifymode'] == ssl.CERT_REQUIRED
368 368 and not sslcontext.get_ca_certs()
369 369 ):
370 370 ui.warn(
371 371 _(
372 372 b'(an attempt was made to load CA certificates but '
373 373 b'none were loaded; see '
374 374 b'https://mercurial-scm.org/wiki/SecureConnections '
375 375 b'for how to configure Mercurial to avoid this '
376 376 b'error)\n'
377 377 )
378 378 )
379 379 except ssl.SSLError:
380 380 pass
381 381
382 382 # Try to print more helpful error messages for known failures.
383 383 if util.safehasattr(e, b'reason'):
384 384 # This error occurs when the client and server don't share a
385 385 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
386 386 # outright. Hopefully the reason for this error is that we require
387 387 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
388 388 # reason, try to emit an actionable warning.
389 389 if e.reason == 'UNSUPPORTED_PROTOCOL':
390 390 # We attempted TLS 1.0+.
391 391 if settings[b'minimumprotocol'] == b'tls1.0':
392 392 # We support more than just TLS 1.0+. If this happens,
393 393 # the likely scenario is either the client or the server
394 394 # is really old. (e.g. server doesn't support TLS 1.0+ or
395 395 # client doesn't support modern TLS versions introduced
396 396 # several years from when this comment was written).
397 397 if supportedprotocols != {b'tls1.0'}:
398 398 ui.warn(
399 399 _(
400 400 b'(could not communicate with %s using security '
401 401 b'protocols %s; if you are using a modern Mercurial '
402 402 b'version, consider contacting the operator of this '
403 403 b'server; see '
404 404 b'https://mercurial-scm.org/wiki/SecureConnections '
405 405 b'for more info)\n'
406 406 )
407 407 % (
408 408 pycompat.bytesurl(serverhostname),
409 409 b', '.join(sorted(supportedprotocols)),
410 410 )
411 411 )
412 412 else:
413 413 ui.warn(
414 414 _(
415 415 b'(could not communicate with %s using TLS 1.0; the '
416 416 b'likely cause of this is the server no longer '
417 417 b'supports TLS 1.0 because it has known security '
418 418 b'vulnerabilities; see '
419 419 b'https://mercurial-scm.org/wiki/SecureConnections '
420 420 b'for more info)\n'
421 421 )
422 422 % pycompat.bytesurl(serverhostname)
423 423 )
424 424 else:
425 425 # We attempted TLS 1.1+. We can only get here if the client
426 426 # supports the configured protocol. So the likely reason is
427 427 # the client wants better security than the server can
428 428 # offer.
429 429 ui.warn(
430 430 _(
431 431 b'(could not negotiate a common security protocol (%s+) '
432 432 b'with %s; the likely cause is Mercurial is configured '
433 433 b'to be more secure than the server can support)\n'
434 434 )
435 435 % (
436 436 settings[b'minimumprotocol'],
437 437 pycompat.bytesurl(serverhostname),
438 438 )
439 439 )
440 440 ui.warn(
441 441 _(
442 442 b'(consider contacting the operator of this '
443 443 b'server and ask them to support modern TLS '
444 444 b'protocol versions; or, set '
445 445 b'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
446 446 b'use of legacy, less secure protocols when '
447 447 b'communicating with this server)\n'
448 448 )
449 449 % pycompat.bytesurl(serverhostname)
450 450 )
451 451 ui.warn(
452 452 _(
453 453 b'(see https://mercurial-scm.org/wiki/SecureConnections '
454 454 b'for more info)\n'
455 455 )
456 456 )
457 457
458 458 elif e.reason == 'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows:
459 459
460 460 ui.warn(
461 461 _(
462 462 b'(the full certificate chain may not be available '
463 463 b'locally; see "hg help debugssl")\n'
464 464 )
465 465 )
466 466 raise
467 467
468 468 # check if wrap_socket failed silently because socket had been
469 469 # closed
470 470 # - see http://bugs.python.org/issue13721
471 471 if not sslsocket.cipher():
472 472 raise error.SecurityError(_(b'ssl connection failed'))
473 473
474 474 sslsocket._hgstate = {
475 475 b'caloaded': caloaded,
476 476 b'hostname': serverhostname,
477 477 b'settings': settings,
478 478 b'ui': ui,
479 479 }
480 480
481 481 return sslsocket
482 482
483 483
484 484 def wrapserversocket(
485 485 sock, ui, certfile=None, keyfile=None, cafile=None, requireclientcert=False
486 486 ):
487 487 """Wrap a socket for use by servers.
488 488
489 489 ``certfile`` and ``keyfile`` specify the files containing the certificate's
490 490 public and private keys, respectively. Both keys can be defined in the same
491 491 file via ``certfile`` (the private key must come first in the file).
492 492
493 493 ``cafile`` defines the path to certificate authorities.
494 494
495 495 ``requireclientcert`` specifies whether to require client certificates.
496 496
497 497 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
498 498 """
499 499 # This function is not used much by core Mercurial, so the error messaging
500 500 # doesn't have to be as detailed as for wrapsocket().
501 501 for f in (certfile, keyfile, cafile):
502 502 if f and not os.path.exists(f):
503 503 raise error.Abort(
504 504 _(b'referenced certificate file (%s) does not exist') % f
505 505 )
506 506
507 507 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
508 508 # ends support, including TLS protocols. commonssloptions() restricts the
509 509 # set of allowed protocols.
510 510 protocol = ssl.PROTOCOL_SSLv23
511 511 options = commonssloptions(b'tls1.0')
512 512
513 513 # This config option is intended for use in tests only. It is a giant
514 514 # footgun to kill security. Don't define it.
515 515 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
516 516 if exactprotocol == b'tls1.0':
517 517 if b'tls1.0' not in supportedprotocols:
518 518 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
519 519 protocol = ssl.PROTOCOL_TLSv1
520 520 elif exactprotocol == b'tls1.1':
521 521 if b'tls1.1' not in supportedprotocols:
522 522 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
523 523 protocol = ssl.PROTOCOL_TLSv1_1
524 524 elif exactprotocol == b'tls1.2':
525 525 if b'tls1.2' not in supportedprotocols:
526 526 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
527 527 protocol = ssl.PROTOCOL_TLSv1_2
528 528 elif exactprotocol:
529 529 raise error.Abort(
530 530 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
531 531 )
532 532
533 533 # We /could/ use create_default_context() here since it doesn't load
534 534 # CAs when configured for client auth. However, it is hard-coded to
535 535 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
536 536 sslcontext = ssl.SSLContext(protocol)
537 537 sslcontext.options |= options
538 538
539 539 # Improve forward secrecy.
540 540 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
541 541 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
542 542
543 543 # Use the list of more secure ciphers if found in the ssl module.
544 544 if util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
545 545 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
546 # pytype: disable=module-attr
546 547 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
548 # pytype: enable=module-attr
547 549
548 550 if requireclientcert:
549 551 sslcontext.verify_mode = ssl.CERT_REQUIRED
550 552 else:
551 553 sslcontext.verify_mode = ssl.CERT_NONE
552 554
553 555 if certfile or keyfile:
554 556 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
555 557
556 558 if cafile:
557 559 sslcontext.load_verify_locations(cafile=cafile)
558 560
559 561 return sslcontext.wrap_socket(sock, server_side=True)
560 562
561 563
562 564 class wildcarderror(Exception):
563 565 """Represents an error parsing wildcards in DNS name."""
564 566
565 567
566 568 def _dnsnamematch(dn, hostname, maxwildcards=1):
567 569 """Match DNS names according RFC 6125 section 6.4.3.
568 570
569 571 This code is effectively copied from CPython's ssl._dnsname_match.
570 572
571 573 Returns a bool indicating whether the expected hostname matches
572 574 the value in ``dn``.
573 575 """
574 576 pats = []
575 577 if not dn:
576 578 return False
577 579 dn = pycompat.bytesurl(dn)
578 580 hostname = pycompat.bytesurl(hostname)
579 581
580 582 pieces = dn.split(b'.')
581 583 leftmost = pieces[0]
582 584 remainder = pieces[1:]
583 585 wildcards = leftmost.count(b'*')
584 586 if wildcards > maxwildcards:
585 587 raise wildcarderror(
586 588 _(b'too many wildcards in certificate DNS name: %s') % dn
587 589 )
588 590
589 591 # speed up common case w/o wildcards
590 592 if not wildcards:
591 593 return dn.lower() == hostname.lower()
592 594
593 595 # RFC 6125, section 6.4.3, subitem 1.
594 596 # The client SHOULD NOT attempt to match a presented identifier in which
595 597 # the wildcard character comprises a label other than the left-most label.
596 598 if leftmost == b'*':
597 599 # When '*' is a fragment by itself, it matches a non-empty dotless
598 600 # fragment.
599 601 pats.append(b'[^.]+')
600 602 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
601 603 # RFC 6125, section 6.4.3, subitem 3.
602 604 # The client SHOULD NOT attempt to match a presented identifier
603 605 # where the wildcard character is embedded within an A-label or
604 606 # U-label of an internationalized domain name.
605 607 pats.append(stringutil.reescape(leftmost))
606 608 else:
607 609 # Otherwise, '*' matches any dotless string, e.g. www*
608 610 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
609 611
610 612 # add the remaining fragments, ignore any wildcards
611 613 for frag in remainder:
612 614 pats.append(stringutil.reescape(frag))
613 615
614 616 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
615 617 return pat.match(hostname) is not None
616 618
617 619
618 620 def _verifycert(cert, hostname):
619 621 """Verify that cert (in socket.getpeercert() format) matches hostname.
620 622 CRLs is not handled.
621 623
622 624 Returns error message if any problems are found and None on success.
623 625 """
624 626 if not cert:
625 627 return _(b'no certificate received')
626 628
627 629 dnsnames = []
628 630 san = cert.get('subjectAltName', [])
629 631 for key, value in san:
630 632 if key == 'DNS':
631 633 try:
632 634 if _dnsnamematch(value, hostname):
633 635 return
634 636 except wildcarderror as e:
635 637 return stringutil.forcebytestr(e.args[0])
636 638
637 639 dnsnames.append(value)
638 640
639 641 if not dnsnames:
640 642 # The subject is only checked when there is no DNS in subjectAltName.
641 643 for sub in cert.get('subject', []):
642 644 for key, value in sub:
643 645 # According to RFC 2818 the most specific Common Name must
644 646 # be used.
645 647 if key == 'commonName':
646 648 # 'subject' entries are unicode.
647 649 try:
648 650 value = value.encode('ascii')
649 651 except UnicodeEncodeError:
650 652 return _(b'IDN in certificate not supported')
651 653
652 654 try:
653 655 if _dnsnamematch(value, hostname):
654 656 return
655 657 except wildcarderror as e:
656 658 return stringutil.forcebytestr(e.args[0])
657 659
658 660 dnsnames.append(value)
659 661
660 662 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
661 663 if len(dnsnames) > 1:
662 664 return _(b'certificate is for %s') % b', '.join(dnsnames)
663 665 elif len(dnsnames) == 1:
664 666 return _(b'certificate is for %s') % dnsnames[0]
665 667 else:
666 668 return _(b'no commonName or subjectAltName found in certificate')
667 669
668 670
669 671 def _plainapplepython():
670 672 """return true if this seems to be a pure Apple Python that
671 673 * is unfrozen and presumably has the whole mercurial module in the file
672 674 system
673 675 * presumably is an Apple Python that uses Apple OpenSSL which has patches
674 676 for using system certificate store CAs in addition to the provided
675 677 cacerts file
676 678 """
677 679 if (
678 680 not pycompat.isdarwin
679 681 or resourceutil.mainfrozen()
680 682 or not pycompat.sysexecutable
681 683 ):
682 684 return False
683 685 exe = os.path.realpath(pycompat.sysexecutable).lower()
684 686 return exe.startswith(b'/usr/bin/python') or exe.startswith(
685 687 b'/system/library/frameworks/python.framework/'
686 688 )
687 689
688 690
689 691 def _defaultcacerts(ui):
690 692 """return path to default CA certificates or None.
691 693
692 694 It is assumed this function is called when the returned certificates
693 695 file will actually be used to validate connections. Therefore this
694 696 function may print warnings or debug messages assuming this usage.
695 697
696 698 We don't print a message when the Python is able to load default
697 699 CA certs because this scenario is detected at socket connect time.
698 700 """
699 701 # The "certifi" Python package provides certificates. If it is installed
700 702 # and usable, assume the user intends it to be used and use it.
701 703 try:
702 704 import certifi
703 705
704 706 certs = certifi.where()
705 707 if os.path.exists(certs):
706 708 ui.debug(b'using ca certificates from certifi\n')
707 709 return pycompat.fsencode(certs)
708 710 except (ImportError, AttributeError):
709 711 pass
710 712
711 713 # Apple's OpenSSL has patches that allow a specially constructed certificate
712 714 # to load the system CA store. If we're running on Apple Python, use this
713 715 # trick.
714 716 if _plainapplepython():
715 717 dummycert = os.path.join(
716 718 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
717 719 )
718 720 if os.path.exists(dummycert):
719 721 return dummycert
720 722
721 723 return None
722 724
723 725
724 726 def validatesocket(sock):
725 727 """Validate a socket meets security requirements.
726 728
727 729 The passed socket must have been created with ``wrapsocket()``.
728 730 """
729 731 shost = sock._hgstate[b'hostname']
730 732 host = pycompat.bytesurl(shost)
731 733 ui = sock._hgstate[b'ui']
732 734 settings = sock._hgstate[b'settings']
733 735
734 736 try:
735 737 peercert = sock.getpeercert(True)
736 738 peercert2 = sock.getpeercert()
737 739 except AttributeError:
738 740 raise error.SecurityError(_(b'%s ssl connection error') % host)
739 741
740 742 if not peercert:
741 743 raise error.SecurityError(
742 744 _(b'%s certificate error: no certificate received') % host
743 745 )
744 746
745 747 if settings[b'disablecertverification']:
746 748 # We don't print the certificate fingerprint because it shouldn't
747 749 # be necessary: if the user requested certificate verification be
748 750 # disabled, they presumably already saw a message about the inability
749 751 # to verify the certificate and this message would have printed the
750 752 # fingerprint. So printing the fingerprint here adds little to no
751 753 # value.
752 754 ui.warn(
753 755 _(
754 756 b'warning: connection security to %s is disabled per current '
755 757 b'settings; communication is susceptible to eavesdropping '
756 758 b'and tampering\n'
757 759 )
758 760 % host
759 761 )
760 762 return
761 763
762 764 # If a certificate fingerprint is pinned, use it and only it to
763 765 # validate the remote cert.
764 766 peerfingerprints = {
765 767 b'sha1': hex(hashutil.sha1(peercert).digest()),
766 768 b'sha256': hex(hashlib.sha256(peercert).digest()),
767 769 b'sha512': hex(hashlib.sha512(peercert).digest()),
768 770 }
769 771
770 772 def fmtfingerprint(s):
771 773 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
772 774
773 775 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
774 776
775 777 if settings[b'certfingerprints']:
776 778 for hash, fingerprint in settings[b'certfingerprints']:
777 779 if peerfingerprints[hash].lower() == fingerprint:
778 780 ui.debug(
779 781 b'%s certificate matched fingerprint %s:%s\n'
780 782 % (host, hash, fmtfingerprint(fingerprint))
781 783 )
782 784 if settings[b'legacyfingerprint']:
783 785 ui.warn(
784 786 _(
785 787 b'(SHA-1 fingerprint for %s found in legacy '
786 788 b'[hostfingerprints] section; '
787 789 b'if you trust this fingerprint, remove the old '
788 790 b'SHA-1 fingerprint from [hostfingerprints] and '
789 791 b'add the following entry to the new '
790 792 b'[hostsecurity] section: %s:fingerprints=%s)\n'
791 793 )
792 794 % (host, host, nicefingerprint)
793 795 )
794 796 return
795 797
796 798 # Pinned fingerprint didn't match. This is a fatal error.
797 799 if settings[b'legacyfingerprint']:
798 800 section = b'hostfingerprint'
799 801 nice = fmtfingerprint(peerfingerprints[b'sha1'])
800 802 else:
801 803 section = b'hostsecurity'
802 804 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
803 805 raise error.SecurityError(
804 806 _(b'certificate for %s has unexpected fingerprint %s')
805 807 % (host, nice),
806 808 hint=_(b'check %s configuration') % section,
807 809 )
808 810
809 811 # Security is enabled but no CAs are loaded. We can't establish trust
810 812 # for the cert so abort.
811 813 if not sock._hgstate[b'caloaded']:
812 814 raise error.SecurityError(
813 815 _(
814 816 b'unable to verify security of %s (no loaded CA certificates); '
815 817 b'refusing to connect'
816 818 )
817 819 % host,
818 820 hint=_(
819 821 b'see https://mercurial-scm.org/wiki/SecureConnections for '
820 822 b'how to configure Mercurial to avoid this error or set '
821 823 b'hostsecurity.%s:fingerprints=%s to trust this server'
822 824 )
823 825 % (host, nicefingerprint),
824 826 )
825 827
826 828 msg = _verifycert(peercert2, shost)
827 829 if msg:
828 830 raise error.SecurityError(
829 831 _(b'%s certificate error: %s') % (host, msg),
830 832 hint=_(
831 833 b'set hostsecurity.%s:certfingerprints=%s '
832 834 b'config setting or use --insecure to connect '
833 835 b'insecurely'
834 836 )
835 837 % (host, nicefingerprint),
836 838 )
General Comments 0
You need to be logged in to leave comments. Login now