diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 4e3c8722d..e86d78b1d 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -34,7 +34,7 @@ from kitty.constants import ( from kitty.options.types import Options from kitty.shm import SharedMemory from kitty.types import run_once -from kitty.utils import SSHConnectionData, no_echo +from kitty.utils import SSHConnectionData, set_echo as turn_off_echo from .completion import complete, ssh_options from .config import init_config, options_for_host @@ -435,15 +435,15 @@ def wrap_bootstrap_script(sh_script: str, interpreter: str) -> List[str]: def get_remote_command( - remote_args: List[str], ssh_opts: SSHOptions, - hostname: str = 'localhost', cli_hostname: str = '', cli_uname: str = '', echo_on: bool = True, + remote_args: List[str], ssh_opts: SSHOptions, hostname: str = 'localhost', cli_hostname: str = '', cli_uname: str = '', + echo_on: bool = True, request_data: bool = False ) -> Tuple[List[str], Dict[str, str], str]: interpreter = ssh_opts.interpreter q = os.path.basename(interpreter).lower() is_python = 'python' in q sh_script, replacements, shm = bootstrap_script( ssh_opts, script_type='py' if is_python else 'sh', remote_args=remote_args, - cli_hostname=cli_hostname, cli_uname=cli_uname, echo_on=echo_on) + cli_hostname=cli_hostname, cli_uname=cli_uname, echo_on=echo_on, request_data=request_data) return wrap_bootstrap_script(sh_script, interpreter), replacements, shm.name @@ -503,8 +503,10 @@ def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes: def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str) -> Iterator[None]: ssh_started_at = time.monotonic() with open(os.open(os.ctermid(), os.O_CLOEXEC | os.O_RDWR | os.O_NOCTTY), 'wb') as tty: - tty.write(dcs_to_kitty(data_request)) - tty.flush() + if data_request: + turn_off_echo(tty.fileno()) + tty.write(dcs_to_kitty(data_request)) + tty.flush() try: yield finally: @@ -556,25 +558,26 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple use_control_master = host_opts.share_connections if use_control_master: cmd[insertion_point:insertion_point] = connection_sharing_args(host_opts, int(os.environ['KITTY_PID'])) - with restore_terminal_state() as echo_on: - rcmd, replacements, shm_name = get_remote_command(remote_args, host_opts, hostname, hostname_for_match, uname, echo_on) - cmd += rcmd - # We force use of askpass so that OpenSSH does not use the tty leaving - # it free for us to use + use_kitty_askpass = host_opts.askpass == 'native' or (host_opts.askpass == 'unless-set' and 'SSH_ASKPASS' not in os.environ) + need_to_request_data = not use_kitty_askpass + if use_kitty_askpass: os.environ['SSH_ASKPASS_REQUIRE'] = 'force' - if not os.environ.get('SSH_ASKPASS'): - os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py') - with no_echo(sys.stdin.fileno()): - try: - p = subprocess.Popen(cmd) - except FileNotFoundError: - raise SystemExit('Could not find the ssh executable, is it in your PATH?') - else: - with drain_potential_tty_garbage(p, 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements)): - try: - raise SystemExit(p.wait()) - except KeyboardInterrupt: - raise SystemExit(1) + os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py') + with restore_terminal_state() as echo_on: + rcmd, replacements, shm_name = get_remote_command( + remote_args, host_opts, hostname, hostname_for_match, uname, echo_on, request_data=need_to_request_data) + cmd += rcmd + try: + p = subprocess.Popen(cmd) + except FileNotFoundError: + raise SystemExit('Could not find the ssh executable, is it in your PATH?') + else: + rq = '' if need_to_request_data else 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements) + with drain_potential_tty_garbage(p, rq): + try: + raise SystemExit(p.wait()) + except KeyboardInterrupt: + raise SystemExit(1) def main(args: List[str]) -> NoReturn: diff --git a/kittens/ssh/options/definition.py b/kittens/ssh/options/definition.py index 67320f7ba..a2f706605 100644 --- a/kittens/ssh/options/definition.py +++ b/kittens/ssh/options/definition.py @@ -107,4 +107,11 @@ opt('login_shell', '', long_text=''' The login shell to execute on the remote host. By default, the remote user account's login shell is used.''') +opt('askpass', 'unless-set', long_text=''' +Control the program SSH uses to ask for passwords or confirmation of host keys etc. +The default is to use kitty's native askpass, unless the SSH_ASKPASS environment variable +is set. Set it to :code:`ssh` to not interfere with the normal ssh askpass mechanism at all, +which typically means that ssh will prompt at the terminal. Set it to :code:`native` to always use +kitty's native, built-in askpass implementation. +''') egr() # }}} diff --git a/kittens/ssh/options/parse.py b/kittens/ssh/options/parse.py index de21110a9..855479538 100644 --- a/kittens/ssh/options/parse.py +++ b/kittens/ssh/options/parse.py @@ -7,6 +7,9 @@ from kitty.conf.utils import merge_dicts, to_bool class Parser: + def askpass(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['askpass'] = str(val) + def copy(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: for k, v in copy(val, ans["copy"]): ans["copy"][k] = v diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py index b46f49d65..7f18aa4d7 100644 --- a/kittens/ssh/options/types.py +++ b/kittens/ssh/options/types.py @@ -5,6 +5,7 @@ import kittens.ssh.copy option_names = ( # {{{ + 'askpass', 'copy', 'cwd', 'env', @@ -17,6 +18,7 @@ option_names = ( # {{{ class Options: + askpass: str = 'unless-set' cwd: str = '' hostname: str = '*' interpreter: str = 'sh' diff --git a/kitty/utils.py b/kitty/utils.py index 3b818d8f9..0e7d5bab9 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -508,16 +508,25 @@ class TTYIO: break -@contextmanager -def no_echo(fd: int = -1) -> Generator[None, None, None]: +def set_echo(fd: int = -1, on: bool = False) -> Tuple[int, List[Union[int, List[Union[bytes, int]]]]]: import termios if fd < 0: fd = sys.stdin.fileno() old = termios.tcgetattr(fd) new = termios.tcgetattr(fd) - new[3] = new[3] & ~termios.ECHO + if on: + new[3] |= termios.ECHO + else: + new[3] &= ~termios.ECHO + termios.tcsetattr(fd, termios.TCSADRAIN, new) + return fd, old + + +@contextmanager +def no_echo(fd: int = -1) -> Generator[None, None, None]: + import termios + fd, old = set_echo(fd) try: - termios.tcsetattr(fd, termios.TCSADRAIN, new) yield finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) diff --git a/shell-integration/ssh/bootstrap.py b/shell-integration/ssh/bootstrap.py index d85e45715..0937c48e2 100644 --- a/shell-integration/ssh/bootstrap.py +++ b/shell-integration/ssh/bootstrap.py @@ -23,13 +23,24 @@ HOME = os.path.expanduser('~') login_shell = pwd.getpwuid(os.geteuid()).pw_shell or 'sh' +def set_echo(fd, on=False): + if fd < 0: + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + if on: + new[3] |= termios.ECHO + else: + new[3] &= ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, new) + return fd, old + + def cleanup(): global tty_fd if tty_fd > -1: if echo_on: - s = termios.tcgetattr(tty_fd) - s[3] |= termios.ECHO - termios.tcsetattr(tty_fd, termios.TCSANOW, s) + set_echo(tty_fd, True) os.close(tty_fd) tty_fd = -1 @@ -218,6 +229,7 @@ def main(): tty_fd = os.open(os.ctermid(), os.O_RDWR | getattr(os, 'O_CLOEXEC', 16777216)) try: if request_data: + set_echo(tty_fd, on=False) send_data_request() get_data() finally: diff --git a/shell-integration/ssh/bootstrap.sh b/shell-integration/ssh/bootstrap.sh index cb7a761ea..505efc0a9 100644 --- a/shell-integration/ssh/bootstrap.sh +++ b/shell-integration/ssh/bootstrap.sh @@ -73,7 +73,10 @@ login_cwd="" request_data="REQUEST_DATA" trap "cleanup_on_bootstrap_exit" EXIT -[ "$request_data" = "1" ] && dcs_to_kitty "ssh" "id="REQUEST_ID":pwfile="PASSWORD_FILENAME":pw="DATA_PASSWORD"" +[ "$request_data" = "1" ] && { + command stty "-echo" < /dev/tty + dcs_to_kitty "ssh" "id="REQUEST_ID":pwfile="PASSWORD_FILENAME":pw="DATA_PASSWORD"" +} mv_files_and_dirs() { cwd="$PWD"