ssh kitten: match hostnames against both remote hostname and hostname used on the command line to connect to it

This commit is contained in:
Kovid Goyal 2022-03-07 20:32:02 +05:30
parent d037c0b0fc
commit 855718b179
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 34 additions and 25 deletions

View File

@ -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

View File

@ -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 <kitten-ssh.hostname>` as
the first setting, otherwise the last :opt:`hostname <kitten-ssh.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

View File

@ -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({})

View File

@ -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?')

View File

@ -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 <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='''

View File

@ -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',