From 24255be0bd7812b95beb722704d1d30fa9dac7a6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 10 Sep 2021 18:09:59 +0530 Subject: [PATCH] Add a permission password to the transfer kitten --- kittens/transfer/main.py | 38 ++++++++++++++++++++++++++++++++++--- kitty/file_transmission.py | 15 +++++++++++---- kitty/options/definition.py | 9 ++++++++- kitty/options/parse.py | 3 +++ kitty/options/types.py | 2 ++ 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/kittens/transfer/main.py b/kittens/transfer/main.py index 1a80afcf7..de382603c 100644 --- a/kittens/transfer/main.py +++ b/kittens/transfer/main.py @@ -81,6 +81,15 @@ receiving computer. In :code:`normal` mode the last argument is assumed to be a destination path on the receiving computer. +--permissions-password -p +The password to use to skip the transfer confirmation popup in kitty. Must match the +password set for the :opt:`file_transfer_password` option in kitty.conf. Note that +leading and trailing whitespace is removed from the password. A password starting with +., / or ~ characters is assumed to be a file name to read the password from. A value +of - means read the password from STDIN. A password that is purely a number less than 256 +is assumed to be the number of a file descriptor from which to read the actual password. + + --confirm-paths type=bool-set Before actually transferring files, show a mapping of local file names to remote file names @@ -288,8 +297,9 @@ class SendState(NameReprEnum): class SendManager: - def __init__(self, request_id: str, files: Tuple[File, ...]): + def __init__(self, request_id: str, files: Tuple[File, ...], pw: Optional[str] = None): self.files = files + self.password = pw or '' self.fid_map = {f.file_id: f for f in self.files} self.request_id = request_id self.state = SendState.waiting_for_permission @@ -323,7 +333,7 @@ class SendManager: self.all_started = not found_not_started def start_transfer(self) -> str: - return FileTransmissionCommand(action=Action.send).serialize() + return FileTransmissionCommand(action=Action.send, password=self.password).serialize() def next_chunk(self) -> str: if self.active_file is None: @@ -389,10 +399,12 @@ class Send(Handler): if before == SendState.waiting_for_permission: if self.manager.state == SendState.permission_denied: self.cmd.styled('Permission denied for this transfer', fg='red') + self.print() self.quit_loop(1) return if self.manager.state == SendState.permission_granted: self.cmd.styled('Permission granted for this transfer', fg='green') + self.print() self.loop_tick() def check_for_transmit_ok(self) -> None: @@ -435,6 +447,7 @@ class Send(Handler): def on_interrupt(self) -> None: self.cmd.styled('Interrupt requested, cancelling transfer, transferred files are in undefined state', fg='red') + self.print() self.abort_transfer() def abort_transfer(self) -> None: @@ -447,7 +460,7 @@ def send_main(cli_opts: TransferCLIOptions, args: List[str]) -> None: files = files_for_send(cli_opts, args) print(f'Found {len(files)} files and directories, requesting transfer permission…') loop = Loop() - handler = Send(cli_opts, SendManager(random_id(), files)) + handler = Send(cli_opts, SendManager(random_id(), files, cli_opts.permissions_password)) loop.loop(handler) raise SystemExit(loop.return_code) @@ -459,8 +472,27 @@ def parse_transfer_args(args: List[str]) -> Tuple[TransferCLIOptions, List[str]] ) +def read_password(loc: str) -> str: + if not loc: + return '' + if loc.isdigit() and int(loc) >= 0 and int(loc) < 256: + with open(int(loc), 'rb') as f: + return f.read().decode('utf-8') + if loc[0] in ('.', '~', '/'): + if loc[0] == '~': + loc = os.path.expanduser(loc) + with open(loc, 'rb') as f: + return f.read().decode('utf-8') + if loc == '-': + return sys.stdin.read() + return loc + + def main(args: List[str]) -> None: cli_opts, items = parse_transfer_args(args) + if cli_opts.permissions_password: + cli_opts.permissions_password = read_password(cli_opts.permissions_password).strip() + if not items: raise SystemExit('Usage: kitty +kitten transfer file_or_directory ...') if cli_opts.direction == 'send': diff --git a/kitty/file_transmission.py b/kitty/file_transmission.py index 5265394cd..a10f9f9ed 100644 --- a/kitty/file_transmission.py +++ b/kitty/file_transmission.py @@ -16,7 +16,9 @@ from gettext import gettext as _ from time import monotonic from typing import IO, Any, Callable, Deque, Dict, List, Optional, Tuple, Union -from kitty.fast_data_types import FILE_TRANSFER_CODE, OSC, add_timer, get_boss +from kitty.fast_data_types import ( + FILE_TRANSFER_CODE, OSC, add_timer, get_boss, get_options +) from .utils import log_error, sanitize_control_codes @@ -97,7 +99,7 @@ class FileTransmissionCommand: ttype: TransmissionType = TransmissionType.simple id: str = '' file_id: str = '' - secret: str = '' + password: str = field(default='', metadata={'base64': True}) quiet: int = 0 mtime: int = -1 permissions: int = -1 @@ -313,8 +315,9 @@ class ActiveReceive: files: Dict[str, DestFile] accepted: bool = False - def __init__(self, id: str, quiet: int) -> None: + def __init__(self, id: str, quiet: int, password: str) -> None: self.id = id + self.password = password self.files = {} self.last_activity_at = monotonic() self.send_acknowledgements = quiet < 1 @@ -432,7 +435,7 @@ class FileTransmission: if cmd.action is not Action.send: log_error(f'File transmission command received for unknown or rejected id: {cmd.id}, ignoring') return - ar = self.active_receives[cmd.id] = ActiveReceive(cmd.id, cmd.quiet) + ar = self.active_receives[cmd.id] = ActiveReceive(cmd.id, cmd.quiet, cmd.password) self.start_receive(ar.id) return @@ -518,6 +521,10 @@ class FileTransmission: return False def start_receive(self, ar_id: str) -> None: + ar = self.active_receives[ar_id] + if ar.password: + self.handle_send_confirmation(ar_id, {'response': 'y' if ar.password == get_options().file_transfer_password else 'n'}) + return boss = get_boss() window = boss.window_id_map.get(self.window_id) if window is not None: diff --git a/kitty/options/definition.py b/kitty/options/definition.py index e75d45d04..1d92fd554 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -2458,7 +2458,7 @@ opt('editor', '.', The terminal editor (such as ``vim`` or ``nano``) to use when editing the kitty config file or similar tasks. -The default value of . means to use the environment variables :envvar:`VISUAL` +The default value of :code:`.` means to use the environment variables :envvar:`VISUAL` and :envvar:`EDITOR` in that order. If these variables aren't set, kitty will run your :opt:`shell` (``$SHELL -l -i -c env``) to see if your shell config files set :envvar:`VISUAL` or :envvar:`EDITOR`. If that doesn't work, kitty @@ -2563,6 +2563,13 @@ stored for writing to the system clipboard. See also :opt:`clipboard_control`. A value of zero means no size limit is applied. ''') +opt('file_transfer_password', '', long_text=''' +A password, that can be supplied to the file transfer kitten to skip +the transfer confirmation dialog. This should only be used +when initiating transfers from trusted computers, over trusted networks +or encrypted transports. +''') + opt('allow_hyperlinks', 'yes', option_type='allow_hyperlinks', ctype='bool', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 405060529..b6b1a1c22 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -942,6 +942,9 @@ class Parser: for k, v in env(val, ans["env"]): ans["env"][k] = v + def file_transfer_password(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['file_transfer_password'] = str(val) + def focus_follows_mouse(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['focus_follows_mouse'] = to_bool(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 193de8b61..16fbec8bd 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -345,6 +345,7 @@ option_names = ( # {{{ 'enable_audio_bell', 'enabled_layouts', 'env', + 'file_transfer_password', 'focus_follows_mouse', 'font_family', 'font_features', @@ -484,6 +485,7 @@ class Options: editor: str = '.' enable_audio_bell: bool = True enabled_layouts: typing.List[str] = ['fat', 'grid', 'horizontal', 'splits', 'stack', 'tall', 'vertical'] + file_transfer_password: str = '' focus_follows_mouse: bool = False font_family: str = 'monospace' font_size: float = 11.0