Allow controlling where on the remote computer the ssh kitten installs its data

This commit is contained in:
Kovid Goyal 2022-02-27 13:06:36 +05:30
parent 12658c4756
commit 59f656e3ca
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 81 additions and 51 deletions

View File

@ -19,14 +19,14 @@ from typing import (
from kitty.constants import cache_dir, shell_integration_dir, terminfo_dir from kitty.constants import cache_dir, shell_integration_dir, terminfo_dir
from kitty.shell_integration import get_effective_ksi_env_var from kitty.shell_integration import get_effective_ksi_env_var
from kitty.short_uuid import uuid4 from kitty.short_uuid import uuid4
from kitty.types import run_once
from kitty.utils import SSHConnectionData from kitty.utils import SSHConnectionData
from .completion import complete, ssh_options from .completion import complete, ssh_options
from .options.types import Options as SSHOptions
DEFAULT_SHELL_INTEGRATION_DEST = '.local/share/kitty-ssh-kitten/shell-integration'
def make_tarfile(hostname: str = '', shell_integration_dest: str = DEFAULT_SHELL_INTEGRATION_DEST) -> bytes: def make_tarfile(ssh_opts: SSHOptions) -> bytes:
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo: def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
tarinfo.uname = tarinfo.gname = 'kitty' tarinfo.uname = tarinfo.gname = 'kitty'
@ -51,17 +51,32 @@ def make_tarfile(hostname: str = '', shell_integration_dest: str = DEFAULT_SHELL
buf = io.BytesIO() buf = io.BytesIO()
with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf: with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf:
tf.add(shell_integration_dir, arcname=shell_integration_dest, filter=filter_files) rd = ssh_opts.remote_dir.rstrip('/')
ksi = get_effective_ksi_env_var()
if ksi:
tf.add(shell_integration_dir, arcname=rd + '/shell-integration', filter=filter_files)
add_data_as_file(tf, rd + '/settings/ksi_env_var', ksi)
tf.add(terminfo_dir, arcname='.terminfo', filter=filter_files) tf.add(terminfo_dir, arcname='.terminfo', filter=filter_files)
add_data_as_file(tf, shell_integration_dest.rstrip('/') + '/settings/ksi_env_var', get_effective_ksi_env_var())
return buf.getvalue() return buf.getvalue()
def get_ssh_data(msg: str, shell_integration_dest: str = DEFAULT_SHELL_INTEGRATION_DEST) -> Iterator[bytes]: @run_once
def load_ssh_options() -> Dict[str, SSHOptions]:
from .config import init_config
return init_config()
def get_ssh_data(msg: str, ssh_opts: Optional[Dict[str, SSHOptions]] = None) -> Iterator[bytes]:
from .config import options_for_host
record_sep = b'\036'
if ssh_opts is None:
ssh_opts = load_ssh_options()
def fmt_prefix(msg: Any) -> bytes: def fmt_prefix(msg: Any) -> bytes:
return f'\036{msg}:'.encode('ascii') return str(msg).encode('ascii') + record_sep
yield record_sep # to discard leading data
try: try:
msg = standard_b64decode(msg).decode('utf-8') msg = standard_b64decode(msg).decode('utf-8')
md = dict(x.split('=', 1) for x in msg.split(':')) md = dict(x.split('=', 1) for x in msg.split(':'))
@ -70,6 +85,7 @@ def get_ssh_data(msg: str, shell_integration_dest: str = DEFAULT_SHELL_INTEGRATI
pwfilename = md['pwfile'] pwfilename = md['pwfile']
except Exception: except Exception:
yield fmt_prefix('!invalid ssh data request message') yield fmt_prefix('!invalid ssh data request message')
else:
try: try:
with open(os.path.join(cache_dir(), pwfilename)) as f: with open(os.path.join(cache_dir(), pwfilename)) as f:
os.unlink(f.name) os.unlink(f.name)
@ -78,14 +94,16 @@ def get_ssh_data(msg: str, shell_integration_dest: str = DEFAULT_SHELL_INTEGRATI
except Exception: except Exception:
yield fmt_prefix('!incorrect ssh data password') yield fmt_prefix('!incorrect ssh data password')
else: else:
resolved_ssh_opts = options_for_host(hostname, ssh_opts)
try: try:
data = make_tarfile(hostname, shell_integration_dest) data = make_tarfile(resolved_ssh_opts)
except Exception: except Exception:
yield fmt_prefix('!error while gathering ssh data') yield fmt_prefix('!error while gathering ssh data')
else: else:
from base64 import standard_b64encode from base64 import standard_b64encode
encoded_data = standard_b64encode(data) encoded_data = standard_b64encode(data)
yield fmt_prefix(len(encoded_data)) yield fmt_prefix(len(encoded_data))
yield fmt_prefix(resolved_ssh_opts.remote_dir)
yield encoded_data yield encoded_data
@ -103,7 +121,6 @@ def prepare_script(ans: str, replacements: Dict[str, str]) -> str:
replacements['PASSWORD_FILENAME'] = os.path.basename(tf.name) replacements['PASSWORD_FILENAME'] = os.path.basename(tf.name)
for k in ('EXEC_CMD', 'OVERRIDE_LOGIN_SHELL'): for k in ('EXEC_CMD', 'OVERRIDE_LOGIN_SHELL'):
replacements[k] = replacements.get(k, '') replacements[k] = replacements.get(k, '')
replacements['SHELL_INTEGRATION_DIR'] = replacements.get('SHELL_INTEGRATION_DIR', DEFAULT_SHELL_INTEGRATION_DEST)
def sub(m: 're.Match[str]') -> str: def sub(m: 're.Match[str]') -> str:
return replacements[m.group()] return replacements[m.group()]

View File

@ -27,10 +27,13 @@ to SSH to connect to it.
''' '''
) )
opt('+env', '', opt('remote_dir', '.local/share/kitty-ssh-kitten', long_text='''
option_type='env', The location on the remote computer where the files needed for this kitten
add_to_default=False, are installed. The location is relative to the HOME directory.
long_text=''' '''
)
opt('+env', '', option_type='env', add_to_default=False, long_text='''
Specify environment variables to set on the remote host. Note that Specify environment variables to set on the remote host. Note that
environment variables can refer to each other, so if you use:: environment variables can refer to each other, so if you use::
@ -41,8 +44,5 @@ The value of MYVAR2 will be :code:`a/<path to home directory>/b`. Using
:code:`VAR=` will set it to the empty string and using just :code:`VAR` :code:`VAR=` will set it to the empty string and using just :code:`VAR`
will delete the variable from the child process' environment. The definitions will delete the variable from the child process' environment. The definitions
are processed alphabetically. are processed alphabetically.
''' ''')
)
egr() # }}} egr() # }}}

View File

@ -14,6 +14,9 @@ class Parser:
def hostname(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: def hostname(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
hostname(val, ans) hostname(val, ans)
def remote_dir(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['remote_dir'] = str(val)
def create_result_dict() -> typing.Dict[str, typing.Any]: def create_result_dict() -> typing.Dict[str, typing.Any]:
return { return {

View File

@ -4,11 +4,12 @@ import typing
option_names = ( # {{{ option_names = ( # {{{
'env', 'hostname') # }}} 'env', 'hostname', 'remote_dir') # }}}
class Options: class Options:
hostname: str = '*' hostname: str = '*'
remote_dir: str = '.local/share/kitty-ssh-kitten'
env: typing.Dict[str, str] = {} env: typing.Dict[str, str] = {}
config_paths: typing.Tuple[str, ...] = () config_paths: typing.Tuple[str, ...] = ()
config_overrides: typing.Tuple[str, ...] = () config_overrides: typing.Tuple[str, ...] = ()

View File

@ -36,12 +36,13 @@ leading_data=""
dsc_to_kitty "ssh" "hostname=$hostname:pwfile=$password_filename:pw=$data_password" dsc_to_kitty "ssh" "hostname=$hostname:pwfile=$password_filename:pw=$data_password"
size="" size=""
record_separator=$(printf "\036")
untar() { untar() {
command base64 -d | command tar xjf - --no-same-owner -C "$HOME" command base64 -d | command tar xjf - --no-same-owner -C "$HOME"
} }
get_data() { read_record() {
# We need a way to read a single byte at a time and to read a specified number of bytes in one invocation. # We need a way to read a single byte at a time and to read a specified number of bytes in one invocation.
# The options are head -c, read -N and dd # The options are head -c, read -N and dd
# #
@ -52,45 +53,49 @@ get_data() {
# #
# POSIX dd works for one byte at a time but for reading X bytes it needs the GNU iflag=count_bytes # POSIX dd works for one byte at a time but for reading X bytes it needs the GNU iflag=count_bytes
# extension, and is anyway unsafe as it can lead to corrupt output when the read syscall is interrupted. # extension, and is anyway unsafe as it can lead to corrupt output when the read syscall is interrupted.
record_started=0 record=""
record_separator=$(printf "\036")
while :; do while :; do
n=$(command dd bs=1 count=1 2> /dev/null) n=$(command dd bs=1 count=1 2> /dev/null < /dev/tty)
if [ $record_started = 1 ]; then [ "$n" = "$record_separator" ] && break
if [ "$n" = ":" ]; then break; fi record="$record$n"
size="$size$n"
else
if [ "$n" = "$record_separator" ]; then
record_started=1;
else
leading_data="$leading_data$n"
fi
fi
done done
printf "%s" "$record"
}
get_data() {
leading_data=$(read_record)
size=$(read_record)
case "$size" in case "$size" in
("!"*) ("!"*)
die "$size" die "$size"
;; ;;
esac esac
data_dir=$(read_record)
case "$data_dir" in
("/"*)
;;
(*)
data_dir="$HOME/$data_dir"
;;
esac
# using dd with bs=1 is very slow on Linux, so use head # using dd with bs=1 is very slow on Linux, so use head
command head -c "$size" | untar command head -c "$size" < /dev/tty | untar
# command dd bs=1 "count=$size" 2> /dev/null | untar
rc="$?"; rc="$?";
} }
get_data get_data
command stty "$saved_tty_settings" command stty "$saved_tty_settings"
saved_tty_settings="" saved_tty_settings=""
if [ "$rc" != "0" ]; then die "Failed to extract data transmitted by ssh kitten over the TTY device"; fi
shell_integration_dir="$HOME/SHELL_INTEGRATION_DIR"
shell_integration_settings_file="$shell_integration_dir/settings/ksi_env_var"
if [ ! -f "$shell_integration_settings_file" ]; then die "Extracted data transmitted by ssh kitten is incomplete"; fi
if [ -n "$leading_data" ]; then if [ -n "$leading_data" ]; then
# clear current line as it might have things echoed on it from leading_data # clear current line as it might have things echoed on it from leading_data
# because we only turn off echo in this script whereas the leading bytes could # because we only turn off echo in this script whereas the leading bytes could
# have been sent before the script had a chance to run # have been sent before the script had a chance to run
printf "\r\033[K" printf "\r\033[K"
fi fi
if [ "$rc" != "0" ]; then die "Failed to extract data transmitted by ssh kitten over the TTY device"; fi
[ -f "$HOME/.terminfo/kitty.terminfo" ] || die "Incomplete extraction of ssh data, no kitty.terminfo found";
shell_integration_dir="$data_dir/shell-integration"
shell_integration_settings_file="$data_dir/settings/ksi_env_var"
# export TERMINFO # export TERMINFO
tname=".terminfo" tname=".terminfo"
@ -194,7 +199,11 @@ fi
shell_name=$(basename $login_shell) shell_name=$(basename $login_shell)
# read the variable and remove all leading and trailing spaces and collapse multiple spaces using xargs # read the variable and remove all leading and trailing spaces and collapse multiple spaces using xargs
if [ -f "$shell_integration_settings_file" ]; then
export KITTY_SHELL_INTEGRATION="$(cat $shell_integration_settings_file | xargs echo)" export KITTY_SHELL_INTEGRATION="$(cat $shell_integration_settings_file | xargs echo)"
else
unset KITTY_SHELL_INTEGRATION
fi
exec_bash_with_integration() { exec_bash_with_integration() {
export ENV="$shell_integration_dir/bash/kitty.bash" export ENV="$shell_integration_dir/bash/kitty.bash"