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 character. This works only if the ``#`` character is the first character
in the line. in the line.
.. _include:
You can include secondary config files via the :code:`include` directive. If 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 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 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 .. 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 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 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 appended to the end of that file. They apply only to the host being SSHed to
the first setting, otherwise the last :opt:`hostname <kitten-ssh.hostname>` from :file:`ssh.conf` will be by this invocation.
used.
.. note:: .. note::
Because of the poor design of SSH, any typing you do before the shell prompt Because of limitations of the design of SSH, any typing you do before the
appears may be lost. So ideally dont start typing till you see the shell shell prompt appears may be lost. So ideally dont start typing till you see
prompt :) the shell prompt 😇.
A real world example 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) 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 = [] matches = []
for spat, opts in per_host_opts.items(): for spat, opts in per_host_opts.items():
for pat in spat.split(): 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) matches.append(opts)
if not matches: if not matches:
return SSHOptions({}) return SSHOptions({})

View File

@ -159,12 +159,14 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
raise ValueError('Incorrect password') raise ValueError('Incorrect password')
if rq_id != request_id: if rq_id != request_id:
raise ValueError('Incorrect request id') raise ValueError('Incorrect request id')
cli_hostname = env_data['cli_hostname']
cli_uname = env_data['cli_uname']
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield fmt_prefix(f'!{e}') yield fmt_prefix(f'!{e}')
else: else:
ssh_opts = {k: SSHOptions(v) for k, v in env_data['opts'].items()} 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()} resolved_ssh_opts.copy = {k: CopyInstruction(*v) for k, v in resolved_ssh_opts.copy.items()}
try: try:
data = make_tarfile(resolved_ssh_opts, env_data['env']) 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( def bootstrap_script(
script_type: str = 'sh', remote_args: Sequence[str] = (), script_type: str = 'sh', remote_args: Sequence[str] = (),
ssh_opts_dict: Dict[str, Dict[str, Any]] = {}, 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: ) -> str:
if request_id is None: if request_id is None:
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID'] request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
@ -214,7 +216,7 @@ def bootstrap_script(
ans = f.read() ans = f.read()
pw = uuid4() pw = uuid4()
with tempfile.NamedTemporaryFile(prefix='ssh-kitten-pw-', suffix='.json', dir=cache_dir(), delete=False) as tf: 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')) tf.write(json.dumps(data).encode('utf-8'))
atexit.register(safe_remove, tf.name) atexit.register(safe_remove, tf.name)
replacements = { replacements = {
@ -224,10 +226,6 @@ def bootstrap_script(
return prepare_script(ans, replacements) 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]]: def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
other_ssh_args: Set[str] = set() other_ssh_args: Set[str] = set()
boolean_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( 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]] = {} ssh_opts_dict: Dict[str, Dict[str, Any]] = {}
) -> List[str]: ) -> List[str]:
is_python = 'python' in interpreter.lower() 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)}'] 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): for i, a in enumerate(found_extra_args):
if i % 2 == 1: if i % 2 == 1:
overrides.append(pat.sub(r'\1 ', a.lstrip())) overrides.append(pat.sub(r'\1 ', a.lstrip()))
if overrides:
overrides.insert(0, f'hostname {uname}@{hostname_for_match}')
so = init_config(overrides) so = init_config(overrides)
sod = {k: v._asdict() for k, v in so.items()} 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 import subprocess
with suppress(FileNotFoundError): 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?') 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. 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`. The hostname can include an optional username in the form :code:`user@host`.
When not specified options apply to all hosts, until the When not specified options apply to all hosts, until the
first hostname specification is found. Note that the hostname this matches first hostname specification is found. Note that matching of hostname is done against
against is the hostname used by the remote computer, not the name you pass both the hostname used by the remote computer, and the name you pass
to SSH to connect to it. If you wish to include the same basic configuration for many to SSH to connect to it. If either matches, it is considered a match.
different hosts, you can do so with the :code:`include` directive (see :doc:`/conf`). 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''' 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 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 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 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=''' 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 from kitty.actions import get_all_actions
ref_map = { ref_map = {
'layouts': f'{website_url("overview")}#layouts', 'layouts': f'{website_url("overview")}#layouts',
'include': f'{website_url("conf")}#include',
'watchers': f'{website_url("launch")}#watchers', 'watchers': f'{website_url("launch")}#watchers',
'sessions': f'{website_url("overview")}#startup-sessions', 'sessions': f'{website_url("overview")}#startup-sessions',
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions', 'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',