From ee56a7a919307291f57d51cf09069658b527b0f7 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Fri, 7 Jul 2023 15:06:57 -0400 Subject: [PATCH] Fixed many instances where the curses menus could break the terminal session Former-commit-id: df73403b4a8bc3ffb238486516d0f1e5a1f601c6 Former-commit-id: 649503907f391ff318fc0d3247b965ed3b2aacba --- lib/sshync.py | 2 +- lib/sshyp.py | 14 +-- lib/stweak.py | 307 ++++++++++++++++++++++++-------------------------- 3 files changed, 154 insertions(+), 169 deletions(-) diff --git a/lib/sshync.py b/lib/sshync.py index d0d2058..ea1d1c9 100644 --- a/lib/sshync.py +++ b/lib/sshync.py @@ -2,7 +2,7 @@ from os import listdir, remove, walk from os.path import expanduser, isdir, isfile, getmtime, join from subprocess import CalledProcessError, PIPE, run from sys import exit as s_exit -home = expanduser("~") +home = expanduser('~') # PORT START SSHYNC-REMOTE diff --git a/lib/sshyp.py b/lib/sshyp.py index 3ac5868..1256e3b 100755 --- a/lib/sshyp.py +++ b/lib/sshyp.py @@ -10,7 +10,7 @@ from sys import argv, exit as s_exit # PORT START UNAME-IMPORT-SSHYP from os import uname # PORT END UNAME-IMPORT-SSHYP -home = expanduser("~") +home = expanduser('~') # UTILITY FUNCTIONS @@ -652,8 +652,8 @@ if __name__ == "__main__": print(f"{73*'!'}\n") s_exit(1) else: - from stweak import initial_setup - initial_setup() + from stweak import wrapped_entry + wrapped_entry(False) s_exit() # run function based on arguments @@ -702,8 +702,8 @@ if __name__ == "__main__": read_shortcut() elif arguments[0] == 'tweak': success_flag = True - from stweak import global_menu - global_menu('client', 'configuration options:') + from stweak import wrapped_entry + wrapped_entry(device_type) # PORT START ARGS-SERVER # server arguments @@ -713,8 +713,8 @@ if __name__ == "__main__": print_info() elif arg_count == 1 and arguments[0] == 'tweak': success_flag = True - from stweak import global_menu - global_menu('server', 'configuration options:') + from stweak import wrapped_entry + wrapped_entry(device_type) # PORT END ARGS-SERVER if arg_count > 0 and success_flag == 0 and arguments[0] != 'sync': diff --git a/lib/stweak.py b/lib/stweak.py index 1f0a16d..c17f67c 100644 --- a/lib/stweak.py +++ b/lib/stweak.py @@ -10,7 +10,7 @@ from subprocess import PIPE, run # PORT START UNAME-IMPORT-STWEAK from os import uname # PORT END UNAME-IMPORT-STWEAK -home, stdscr, sshyp_data = expanduser("~"), initscr(), ConfigParser() +home, sshyp_data, stdscr, term_messages = expanduser('~'), ConfigParser(), None, [] if isfile(f"{home}/.config/sshyp/sshyp.ini"): _exists_flag = True sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") @@ -66,15 +66,6 @@ def curses_text(_pretext): return _box.gather().strip() -# cleanly exit curses and optionally prints an exit message -def curses_terminate(_term_message): - nocbreak() - echo() - endwin() - if _term_message: - print(_term_message) - - # device+sync type selection def install_type(): _offline_mode = 'false' @@ -86,7 +77,7 @@ def install_type(): _dev_type = 'server' Path(f"{home}/.config/sshyp/deleted").mkdir(mode=0o700, exist_ok=True) Path(f"{home}/.config/sshyp/whitelist").mkdir(mode=0o700, exist_ok=True) - curses_terminate('\nmake sure the ssh service is running and properly configured\n') + term_messages.append('make sure the ssh service is running and properly configured') else: _dev_type = 'client' if _install_type == 1: @@ -260,7 +251,6 @@ def refresh_encryption(): # decrypt, optimize, and re-encrypt each entry with the newly selected key if isdir(_directory): - curses_terminate('\noptimizing and re-encrypting entries... please wait - do not terminate this process') _gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id') for _root, _dirs, _files in sorted(walk(_directory, topdown=True)): for _filename in _files: @@ -369,7 +359,7 @@ def extension_downloader(): _extensions.append('CANCEL') _choice = curses_radio(_extensions, 'select an extension for more info') if _choice == len(_extensions)-1: - return False, False + return False _selected = _extensions[_choice] _choice = curses_radio(('no', 'yes'), f"install {_selected}?\n\n\n\n\ndescription: " f"{_pointer.get(_selected, 'desc')}\n\nusage: " @@ -383,8 +373,8 @@ def extension_downloader(): # set permissions under active user chmod(_exe_dir, 0o755) chmod(_ini_dir, 0o644) - return False, _selected - return False, False + return _selected + return False # uninstalls/deletes selected extensions @@ -395,16 +385,16 @@ def extension_remover(): _installed.append('CANCEL') _choice = curses_radio(_installed, 'select an extension to uninstall') if _choice == len(_installed)-1: - return False, False + return False _sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?") if _sure == 0: - return False, False - return False, _installed[_choice] + return False + return _installed[_choice] # provides options for managing extensions def extension_menu(): - _term_message, _ext_name, _action = False, False, False + _ext_name, _action = False, False # determine which of the supported privilege escalation utilities is installed if which('doas') is not None: _escalator = 'doas' @@ -417,126 +407,124 @@ def extension_menu(): while True: _choice = curses_radio(('download/update extensions', 'remove extensions', 'BACK'), 'extension management') if _choice == 0: - _term_message, _ext_name = extension_downloader() + _ext_name = extension_downloader() if _ext_name: # True represents installation _action = True break elif _choice == 1: - _term_message, _ext_name = extension_remover() + _ext_name = extension_remover() if _ext_name: break else: break - return _term_message, _ext_name, _escalator, _action + return _ext_name, _escalator, _action # runs secondary configuration menu -def global_menu(_device_type, _top_message): +def global_menu(_scr, _device_type, _top_message): + global stdscr + # only set global stdscr if running as entry point + if not isinstance(_scr, bool): + stdscr = _scr + while True: - # curses initialization - noecho() - cbreak() - stdscr.keypad(True) + _options, _choice, _exit_signal = ['change device/synchronization types'], 0, False + if _device_type == 'client': + _options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name', + '[OPTIONAL, RECOMMENDED] set custom text editor', + '[OPTIONAL] enable/disable quick-unlock', + '[OPTIONAL] re-encrypt/optimize entries', + '[OPTIONAL] extension management']) + else: + _options.extend(['manage quick-unlock/whitelist']) + _options.extend(['EXIT/DONE']) + _choice += curses_radio(_options, _top_message) - _options, _choice, _term_message, _exit_signal = ['change device/synchronization types'], 0, False, False - try: - if _device_type == 'client': - _options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name', - '[OPTIONAL, RECOMMENDED] set custom text editor', - '[OPTIONAL] enable/disable quick-unlock', - '[OPTIONAL] re-encrypt/optimize entries', - '[OPTIONAL] extension management']) - else: - _options.extend(['manage quick-unlock/whitelist']) - _options.extend(['EXIT/DONE']) - _choice += curses_radio(_options, _top_message) - - if _choice == 0: - _dev_sync_types = install_type() - # if switching to client mode... - if _dev_sync_types[0] == 'client': - # ...and gpg settings are missing - if not sshyp_data.has_option('CLIENT-GENERAL', 'gpg_id'): - gpg_config() - # ...and text editor settings are missing - if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'): - editor_config(True) - # ...and online (synced) mode is enabled... - if _dev_sync_types[1] == 'false': - # ...and quick-unlock settings are missing - if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'): - quick_unlock_config(True) - # ...and there is no sshync config present - if not sshyp_data.has_section('SSHYNC'): - _ip, _username_ssh, _port = ssh_config() - # ...and there is no device ID present - if not listdir(f"{home}/.config/sshyp/devices"): - dev_id_config(_ip, _username_ssh, _port) - # ...or ssh_error is missing - elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'): - sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1') - write_config() - _device_type = _dev_sync_types[0] - elif _choice == 1: - if _device_type == 'client': + if _choice == 0: + _dev_sync_types = install_type() + # if switching to client mode... + if _dev_sync_types[0] == 'client': + # ...and gpg settings are missing + if not sshyp_data.has_option('CLIENT-GENERAL', 'gpg_id'): gpg_config() - else: - whitelist_menu() - elif _choice == 2: - if _device_type == 'client': - ssh_config() - else: - _exit_signal = True - elif _choice == 3: - if not sshyp_data.has_section('SSHYNC'): - ssh_config() - dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'), - sshyp_data.get('SSHYNC', 'port')) - elif _choice == 4: - editor_config(False) - elif _choice == 5: - _enabled = quick_unlock_config(False) - if _enabled == 'true': - _term_message = ('\nquick-unlock has been enabled client-side - in order for this feature to ' - 'function,\nyou must first log in to the sshyp server and run:\n\nsshyp tweak\n\n' - 'from there you can create a quick-unlock pin and add this device to the ' - 'whitelist\n') - elif _choice == 6: - _success = refresh_encryption() - if _success == 1: - _term_message = "\na backup of your previous entry directory has been created:\n\n" \ - f"{home}/.local/share/sshyp.old\n" - elif _success == 2: - _term_message = '\n\u001b[38;5;9merror: re-encryption failed: ' \ - 'entry directory not found\u001b[0m\n' - elif _choice == 7: - _term_message, _ext_name, _escalator, _action = extension_menu() + # ...and text editor settings are missing + if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'): + editor_config(True) + # ...and online (synced) mode is enabled... + if _dev_sync_types[1] == 'false': + # ...and quick-unlock settings are missing + if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'): + quick_unlock_config(True) + # ...and there is no sshync config present + if not sshyp_data.has_section('SSHYNC'): + _ip, _username_ssh, _port = ssh_config() + # ...and there is no device ID present + if not listdir(f"{home}/.config/sshyp/devices"): + dev_id_config(_ip, _username_ssh, _port) + # ...or ssh_error is missing + elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'): + sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1') + write_config() + _device_type = _dev_sync_types[0] + elif _choice == 1: + if _device_type == 'client': + gpg_config() + else: + whitelist_menu() + elif _choice == 2: + if _device_type == 'client': + ssh_config() else: _exit_signal = True - curses_terminate(_term_message) - # if root is needed for extension management... - if _choice == 7 and _ext_name: - if _action: - # install with privilege escalation (outside of curses) - from tempfile import gettempdir - _exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini" - run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir)) - run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}")) - run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) - else: - # uninstall with privilege escalation (outside of curses) - run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}", f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) - break - except KeyboardInterrupt: - curses_terminate(False) + elif _choice == 3: + if not sshyp_data.has_section('SSHYNC'): + ssh_config() + dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'), + sshyp_data.get('SSHYNC', 'port')) + elif _choice == 4: + editor_config(False) + elif _choice == 5: + _enabled = quick_unlock_config(False) + if _enabled == 'true': + term_messages.append('quick-unlock has been enabled client-side - in order for this feature to ' + 'function,\nyou must first log in to the sshyp server and run:\n\nsshyp tweak\n\n' + 'from there you can create a quick-unlock pin and add this device to the ' + 'whitelist') + elif _choice == 6: + _success = refresh_encryption() + if _success == 1: + term_messages.append("a backup of your previous entry directory has been created:\n\n" + f"{home}/.local/share/sshyp.old") + elif _success == 2: + term_messages.append('\u001b[38;5;9merror: re-encryption failed: ' + 'entry directory not found\u001b[0m') + elif _choice == 7: + _ext_name, _escalator, _action = extension_menu() + else: _exit_signal = True + # if root is needed for extension management... + if _choice == 7 and _ext_name: + if _action: + # install with privilege escalation (outside of curses) + from tempfile import gettempdir + _exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini" + run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir)) + run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}")) + run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) + else: + # uninstall with privilege escalation (outside of curses) + run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}", f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) + break if _exit_signal: break # runs initial configuration wizard -def initial_setup(): +def initial_setup(_scr): + global stdscr + stdscr = _scr + # required directory creation Path(f"{home}/.config/sshyp/devices").mkdir(mode=0o700, parents=True, exist_ok=True) Path(f"{home}/.local/share/sshyp").mkdir(mode=0o700, parents=True, exist_ok=True) @@ -545,60 +533,57 @@ def initial_setup(): if _exists_flag: sshyp_data.clear() - # curses initialization - noecho() - cbreak() - stdscr.keypad(True) - # curses menu tree - try: - # device+sync type selection - _dev_sync_types = install_type() + # device+sync type selection + _dev_sync_types = install_type() - if _dev_sync_types[0] == 'client': + if _dev_sync_types[0] == 'client': - # gpg configuration - gpg_config() + # gpg configuration + gpg_config() - # text editor configuration (automated) - editor_config(True) + # text editor configuration (automated) + editor_config(True) - # quick-unlock configuration (disabled by default) - quick_unlock_config(True) + # quick-unlock configuration (disabled by default) + quick_unlock_config(True) - # online (synchronized mode) configuration - if _dev_sync_types[1] != 'true': + # online (synchronized mode) configuration + if _dev_sync_types[1] != 'true': - # ssh+sshync configuration - _ip, _username_ssh, _port = ssh_config() + # ssh+sshync configuration + _ip, _username_ssh, _port = ssh_config() - # device id configuration - dev_id_config(_ip, _username_ssh, _port) + # device id configuration + dev_id_config(_ip, _username_ssh, _port) - # cleanly exit curses - curses_terminate(False) + # PORT START CLIPTOOL TODO fix printing with new curses wrapper + # check for clipboard tool and display warning if missing + if uname()[0] in ('Linux', 'FreeBSD') and 'WSL_DISTRO_NAME' not in environ \ + and not exists("/data/data/com.termux"): + _display_server, _clipboard_tool, _clipboard_package = None, None, None + if 'WAYLAND_DISPLAY' in environ: + _display_server, _clipboard_tool, _clipboard_package = 'Wayland', 'wl-copy', 'wl-clipboard' + elif 'DISPLAY' in environ: + _display_server, _clipboard_tool, _clipboard_package = 'X11', 'xclip', 'xclip' + if _display_server is not None and which(_clipboard_tool) is None: + print(f'\n\u001b[38;5;9mwarning: you are using {_display_server} and "{_clipboard_tool}" is not ' + f'present - \ncopying entry fields will not function until ' + f'"{_clipboard_package}" is installed\u001b[0m') + # PORT END CLIPTOOL - else: - # cleanly exit curses - curses_terminate(False) + # run optional configuration menu + wrapped_entry(_dev_sync_types[0], 'additional configuration options:') - # PORT START CLIPTOOL - # check for clipboard tool and display warning if missing - if uname()[0] in ('Linux', 'FreeBSD') and 'WSL_DISTRO_NAME' not in environ \ - and not exists("/data/data/com.termux"): - _display_server, _clipboard_tool, _clipboard_package = None, None, None - if 'WAYLAND_DISPLAY' in environ: - _display_server, _clipboard_tool, _clipboard_package = 'Wayland', 'wl-copy', 'wl-clipboard' - elif 'DISPLAY' in environ: - _display_server, _clipboard_tool, _clipboard_package = 'X11', 'xclip', 'xclip' - if _display_server is not None and which(_clipboard_tool) is None: - print(f'\n\u001b[38;5;9mwarning: you are using {_display_server} and "{_clipboard_tool}" is not ' - f'present - \ncopying entry fields will not function until ' - f'"{_clipboard_package}" is installed\u001b[0m') - # PORT END CLIPTOOL - global_menu(_dev_sync_types[0], 'additional configuration options:') - - except KeyboardInterrupt: - # cleanly exit curses - curses_terminate(False) +# runs the specified entry function (menu start point) within a curses wrapper +def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'): + from curses import wrapper, use_default_colors + # a boolean value represents init + if isinstance(_gm_device_type, bool): + wrapper(lambda _wrap_stdscr: (use_default_colors(), initial_setup(_wrap_stdscr))) + # any other vlaue will be provided as the global menu device type + else: + wrapper(lambda _wrap_stdscr: (use_default_colors(), global_menu(_wrap_stdscr, _gm_device_type, _gm_top_message))) + for _message in term_messages: + print(f"\n{_message}\n")