From 855718b17975cf5067439424997db4bd65953fd5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 7 Mar 2022 20:32:02 +0530 Subject: [PATCH] ssh kitten: match hostnames against both remote hostname and hostname used on the command line to connect to it --- docs/conf.rst | 2 ++ docs/kittens/ssh.rst | 13 ++++++------- kittens/ssh/config.py | 4 ++-- kittens/ssh/main.py | 28 +++++++++++++++++----------- kittens/ssh/options/definition.py | 11 ++++++----- kitty/conf/types.py | 1 + 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/docs/conf.rst b/docs/conf.rst index 2f6086552..bac4c4e0b 100644 --- a/docs/conf.rst +++ b/docs/conf.rst @@ -24,6 +24,8 @@ Comments can be added to the config file as lines starting with the ``#`` character. This works only if the ``#`` character is the first character in the line. +.. _include: + You can include secondary config files via the :code:`include` directive. If you use a relative path for :code:`include`, it is resolved with respect to the location of the current config file. Note that environment variables are diff --git a/docs/kittens/ssh.rst b/docs/kittens/ssh.rst index 0e69ffe12..497ae4ca4 100644 --- a/docs/kittens/ssh.rst +++ b/docs/kittens/ssh.rst @@ -56,19 +56,18 @@ Additionally, you can pass config options on the command line: .. code-block:: sh - kitty +kitten ssh --kitten hostname=somehost --kitten interpreter=python ... + kitty +kitten ssh --kitten interpreter=python servername The :code:`--kitten` argument can be specified multiple times, with directives from :file:`ssh.conf`. These are merged with :file:`ssh.conf` as if they were -appended to the end of that file. It is important to specify the :opt:`hostname ` as -the first setting, otherwise the last :opt:`hostname ` from :file:`ssh.conf` will be -used. +appended to the end of that file. They apply only to the host being SSHed to +by this invocation. .. note:: - Because of the poor design of SSH, any typing you do before the shell prompt - appears may be lost. So ideally dont start typing till you see the shell - prompt :) + Because of limitations of the design of SSH, any typing you do before the + shell prompt appears may be lost. So ideally dont start typing till you see + the shell prompt 😇. A real world example diff --git a/kittens/ssh/config.py b/kittens/ssh/config.py index 0b8e48a84..6ec437dd2 100644 --- a/kittens/ssh/config.py +++ b/kittens/ssh/config.py @@ -24,11 +24,11 @@ def host_matches(pat: str, hostname: str, username: str) -> bool: return fnmatch.fnmatchcase(hostname, pat) and fnmatch.fnmatchcase(username, upat) -def options_for_host(hostname: str, username: str, per_host_opts: Dict[str, SSHOptions]) -> SSHOptions: +def options_for_host(hostname: str, username: str, per_host_opts: Dict[str, SSHOptions], cli_hostname: str = '', cli_uname: str = '') -> SSHOptions: matches = [] for spat, opts in per_host_opts.items(): for pat in spat.split(): - if host_matches(pat, hostname, username): + if host_matches(pat, hostname, username) or (cli_hostname and host_matches(pat, cli_hostname, cli_uname)): matches.append(opts) if not matches: return SSHOptions({}) diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 79a5f5199..7da9e6d22 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -159,12 +159,14 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]: raise ValueError('Incorrect password') if rq_id != request_id: raise ValueError('Incorrect request id') + cli_hostname = env_data['cli_hostname'] + cli_uname = env_data['cli_uname'] except Exception as e: traceback.print_exc() yield fmt_prefix(f'!{e}') else: ssh_opts = {k: SSHOptions(v) for k, v in env_data['opts'].items()} - resolved_ssh_opts = options_for_host(hostname, username, ssh_opts) + resolved_ssh_opts = options_for_host(hostname, username, ssh_opts, cli_hostname, cli_uname) resolved_ssh_opts.copy = {k: CopyInstruction(*v) for k, v in resolved_ssh_opts.copy.items()} try: data = make_tarfile(resolved_ssh_opts, env_data['env']) @@ -205,7 +207,7 @@ def prepare_exec_cmd(remote_args: Sequence[str], is_python: bool) -> str: def bootstrap_script( script_type: str = 'sh', remote_args: Sequence[str] = (), ssh_opts_dict: Dict[str, Dict[str, Any]] = {}, - test_script: str = '', request_id: Optional[str] = None + test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = '' ) -> str: if request_id is None: request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID'] @@ -214,7 +216,7 @@ def bootstrap_script( ans = f.read() pw = uuid4() with tempfile.NamedTemporaryFile(prefix='ssh-kitten-pw-', suffix='.json', dir=cache_dir(), delete=False) as tf: - data = {'pw': pw, 'env': dict(os.environ), 'opts': ssh_opts_dict} + data = {'pw': pw, 'env': dict(os.environ), 'opts': ssh_opts_dict, 'cli_hostname': cli_hostname, 'cli_uname': cli_uname} tf.write(json.dumps(data).encode('utf-8')) atexit.register(safe_remove, tf.name) replacements = { @@ -224,10 +226,6 @@ def bootstrap_script( return prepare_script(ans, replacements) -def load_script(script_type: str = 'sh', remote_args: Sequence[str] = (), ssh_opts_dict: Dict[str, Dict[str, Any]] = {}) -> str: - return bootstrap_script(script_type, remote_args, ssh_opts_dict=ssh_opts_dict) - - def get_ssh_cli() -> Tuple[Set[str], Set[str]]: other_ssh_args: Set[str] = set() boolean_ssh_args: Set[str] = set() @@ -393,11 +391,14 @@ def parse_ssh_args(args: List[str], extra_args: Tuple[str, ...] = ()) -> Tuple[L def get_remote_command( - remote_args: List[str], hostname: str = 'localhost', interpreter: str = 'sh', + remote_args: List[str], hostname: str = 'localhost', cli_hostname: str = '', cli_uname: str = '', + interpreter: str = 'sh', ssh_opts_dict: Dict[str, Dict[str, Any]] = {} ) -> List[str]: is_python = 'python' in interpreter.lower() - sh_script = load_script(script_type='py' if is_python else 'sh', remote_args=remote_args, ssh_opts_dict=ssh_opts_dict) + sh_script = bootstrap_script( + script_type='py' if is_python else 'sh', remote_args=remote_args, ssh_opts_dict=ssh_opts_dict, + cli_hostname=cli_hostname, cli_uname=cli_uname) return [f'{interpreter} -c {shlex.quote(sh_script)}'] @@ -436,12 +437,17 @@ def main(args: List[str]) -> NoReturn: for i, a in enumerate(found_extra_args): if i % 2 == 1: overrides.append(pat.sub(r'\1 ', a.lstrip())) + if overrides: + overrides.insert(0, f'hostname {uname}@{hostname_for_match}') so = init_config(overrides) sod = {k: v._asdict() for k, v in so.items()} - cmd += get_remote_command(remote_args, hostname, options_for_host(hostname_for_match, uname, so).interpreter, sod) + cmd += get_remote_command(remote_args, hostname, hostname_for_match, uname, options_for_host(hostname_for_match, uname, so).interpreter, sod) import subprocess with suppress(FileNotFoundError): - raise SystemExit(subprocess.run(cmd).returncode) + try: + raise SystemExit(subprocess.run(cmd).returncode) + except KeyboardInterrupt: + raise SystemExit(1) raise SystemExit('Could not find the ssh executable, is it in your PATH?') diff --git a/kittens/ssh/options/definition.py b/kittens/ssh/options/definition.py index 34bcabe0e..8fe12be44 100644 --- a/kittens/ssh/options/definition.py +++ b/kittens/ssh/options/definition.py @@ -29,10 +29,11 @@ The hostname the following options apply to. A glob pattern to match multiple hosts can be used. Multiple hostnames can also be specified separated by spaces. The hostname can include an optional username in the form :code:`user@host`. When not specified options apply to all hosts, until the -first hostname specification is found. Note that the hostname this matches -against is the hostname used by the remote computer, not the name you pass -to SSH to connect to it. If you wish to include the same basic configuration for many -different hosts, you can do so with the :code:`include` directive (see :doc:`/conf`). +first hostname specification is found. Note that matching of hostname is done against +both the hostname used by the remote computer, and the name you pass +to SSH to connect to it. If either matches, it is considered a match. +If you wish to include the same basic configuration for many +different hosts, you can do so with the :ref:`include ` directive. ''') opt('+copy', '', option_type='copy', add_to_default=False, long_text=f''' @@ -84,7 +85,7 @@ The interpreter to use on the remote host. Must be either a POSIX complaint shel or a python executable. If the default sh is not available for broken, using an alternate interpreter can be useful. Note that as the interpreter is used for bootstrapping, hostname specific values are matched again the hostname from the -command line args rather than the actual remote hostname. +command line args only. ''') opt('remote_dir', '.local/share/kitty-ssh-kitten', option_type='relative_dir', long_text=''' diff --git a/kitty/conf/types.py b/kitty/conf/types.py index defe2af53..60f9a83ef 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -49,6 +49,7 @@ def ref_map() -> Dict[str, str]: from kitty.actions import get_all_actions ref_map = { 'layouts': f'{website_url("overview")}#layouts', + 'include': f'{website_url("conf")}#include', 'watchers': f'{website_url("launch")}#watchers', 'sessions': f'{website_url("overview")}#startup-sessions', 'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',