Fixed many instances where the curses menus could break the terminal session

Former-commit-id: df73403b4a8bc3ffb238486516d0f1e5a1f601c6
Former-commit-id: 649503907f391ff318fc0d3247b965ed3b2aacba
This commit is contained in:
2023-07-07 15:06:57 -04:00
parent 4ad516e156
commit ee56a7a919
3 changed files with 154 additions and 169 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ from os import listdir, remove, walk
from os.path import expanduser, isdir, isfile, getmtime, join from os.path import expanduser, isdir, isfile, getmtime, join
from subprocess import CalledProcessError, PIPE, run from subprocess import CalledProcessError, PIPE, run
from sys import exit as s_exit from sys import exit as s_exit
home = expanduser("~") home = expanduser('~')
# PORT START SSHYNC-REMOTE # PORT START SSHYNC-REMOTE
+7 -7
View File
@@ -10,7 +10,7 @@ from sys import argv, exit as s_exit
# PORT START UNAME-IMPORT-SSHYP # PORT START UNAME-IMPORT-SSHYP
from os import uname from os import uname
# PORT END UNAME-IMPORT-SSHYP # PORT END UNAME-IMPORT-SSHYP
home = expanduser("~") home = expanduser('~')
# UTILITY FUNCTIONS # UTILITY FUNCTIONS
@@ -652,8 +652,8 @@ if __name__ == "__main__":
print(f"{73*'!'}\n") print(f"{73*'!'}\n")
s_exit(1) s_exit(1)
else: else:
from stweak import initial_setup from stweak import wrapped_entry
initial_setup() wrapped_entry(False)
s_exit() s_exit()
# run function based on arguments # run function based on arguments
@@ -702,8 +702,8 @@ if __name__ == "__main__":
read_shortcut() read_shortcut()
elif arguments[0] == 'tweak': elif arguments[0] == 'tweak':
success_flag = True success_flag = True
from stweak import global_menu from stweak import wrapped_entry
global_menu('client', 'configuration options:') wrapped_entry(device_type)
# PORT START ARGS-SERVER # PORT START ARGS-SERVER
# server arguments # server arguments
@@ -713,8 +713,8 @@ if __name__ == "__main__":
print_info() print_info()
elif arg_count == 1 and arguments[0] == 'tweak': elif arg_count == 1 and arguments[0] == 'tweak':
success_flag = True success_flag = True
from stweak import global_menu from stweak import wrapped_entry
global_menu('server', 'configuration options:') wrapped_entry(device_type)
# PORT END ARGS-SERVER # PORT END ARGS-SERVER
if arg_count > 0 and success_flag == 0 and arguments[0] != 'sync': if arg_count > 0 and success_flag == 0 and arguments[0] != 'sync':
+146 -161
View File
@@ -10,7 +10,7 @@ from subprocess import PIPE, run
# PORT START UNAME-IMPORT-STWEAK # PORT START UNAME-IMPORT-STWEAK
from os import uname from os import uname
# PORT END UNAME-IMPORT-STWEAK # 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"): if isfile(f"{home}/.config/sshyp/sshyp.ini"):
_exists_flag = True _exists_flag = True
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
@@ -66,15 +66,6 @@ def curses_text(_pretext):
return _box.gather().strip() 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 # device+sync type selection
def install_type(): def install_type():
_offline_mode = 'false' _offline_mode = 'false'
@@ -86,7 +77,7 @@ def install_type():
_dev_type = 'server' _dev_type = 'server'
Path(f"{home}/.config/sshyp/deleted").mkdir(mode=0o700, exist_ok=True) Path(f"{home}/.config/sshyp/deleted").mkdir(mode=0o700, exist_ok=True)
Path(f"{home}/.config/sshyp/whitelist").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: else:
_dev_type = 'client' _dev_type = 'client'
if _install_type == 1: if _install_type == 1:
@@ -260,7 +251,6 @@ def refresh_encryption():
# decrypt, optimize, and re-encrypt each entry with the newly selected key # decrypt, optimize, and re-encrypt each entry with the newly selected key
if isdir(_directory): 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') _gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id')
for _root, _dirs, _files in sorted(walk(_directory, topdown=True)): for _root, _dirs, _files in sorted(walk(_directory, topdown=True)):
for _filename in _files: for _filename in _files:
@@ -369,7 +359,7 @@ def extension_downloader():
_extensions.append('CANCEL') _extensions.append('CANCEL')
_choice = curses_radio(_extensions, 'select an extension for more info') _choice = curses_radio(_extensions, 'select an extension for more info')
if _choice == len(_extensions)-1: if _choice == len(_extensions)-1:
return False, False return False
_selected = _extensions[_choice] _selected = _extensions[_choice]
_choice = curses_radio(('no', 'yes'), f"install {_selected}?\n\n\n\n\ndescription: " _choice = curses_radio(('no', 'yes'), f"install {_selected}?\n\n\n\n\ndescription: "
f"{_pointer.get(_selected, 'desc')}\n\nusage: " f"{_pointer.get(_selected, 'desc')}\n\nusage: "
@@ -383,8 +373,8 @@ def extension_downloader():
# set permissions under active user # set permissions under active user
chmod(_exe_dir, 0o755) chmod(_exe_dir, 0o755)
chmod(_ini_dir, 0o644) chmod(_ini_dir, 0o644)
return False, _selected return _selected
return False, False return False
# uninstalls/deletes selected extensions # uninstalls/deletes selected extensions
@@ -395,16 +385,16 @@ def extension_remover():
_installed.append('CANCEL') _installed.append('CANCEL')
_choice = curses_radio(_installed, 'select an extension to uninstall') _choice = curses_radio(_installed, 'select an extension to uninstall')
if _choice == len(_installed)-1: 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]}?") _sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?")
if _sure == 0: if _sure == 0:
return False, False return False
return False, _installed[_choice] return _installed[_choice]
# provides options for managing extensions # provides options for managing extensions
def extension_menu(): 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 # determine which of the supported privilege escalation utilities is installed
if which('doas') is not None: if which('doas') is not None:
_escalator = 'doas' _escalator = 'doas'
@@ -417,126 +407,124 @@ def extension_menu():
while True: while True:
_choice = curses_radio(('download/update extensions', 'remove extensions', 'BACK'), 'extension management') _choice = curses_radio(('download/update extensions', 'remove extensions', 'BACK'), 'extension management')
if _choice == 0: if _choice == 0:
_term_message, _ext_name = extension_downloader() _ext_name = extension_downloader()
if _ext_name: if _ext_name:
# True represents installation # True represents installation
_action = True _action = True
break break
elif _choice == 1: elif _choice == 1:
_term_message, _ext_name = extension_remover() _ext_name = extension_remover()
if _ext_name: if _ext_name:
break break
else: else:
break break
return _term_message, _ext_name, _escalator, _action return _ext_name, _escalator, _action
# runs secondary configuration menu # 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: while True:
# curses initialization _options, _choice, _exit_signal = ['change device/synchronization types'], 0, False
noecho() if _device_type == 'client':
cbreak() _options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name',
stdscr.keypad(True) '[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 if _choice == 0:
try: _dev_sync_types = install_type()
if _device_type == 'client': # if switching to client mode...
_options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name', if _dev_sync_types[0] == 'client':
'[OPTIONAL, RECOMMENDED] set custom text editor', # ...and gpg settings are missing
'[OPTIONAL] enable/disable quick-unlock', if not sshyp_data.has_option('CLIENT-GENERAL', 'gpg_id'):
'[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':
gpg_config() gpg_config()
else: # ...and text editor settings are missing
whitelist_menu() if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'):
elif _choice == 2: editor_config(True)
if _device_type == 'client': # ...and online (synced) mode is enabled...
ssh_config() if _dev_sync_types[1] == 'false':
else: # ...and quick-unlock settings are missing
_exit_signal = True if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'):
elif _choice == 3: quick_unlock_config(True)
if not sshyp_data.has_section('SSHYNC'): # ...and there is no sshync config present
ssh_config() if not sshyp_data.has_section('SSHYNC'):
dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'), _ip, _username_ssh, _port = ssh_config()
sshyp_data.get('SSHYNC', 'port')) # ...and there is no device ID present
elif _choice == 4: if not listdir(f"{home}/.config/sshyp/devices"):
editor_config(False) dev_id_config(_ip, _username_ssh, _port)
elif _choice == 5: # ...or ssh_error is missing
_enabled = quick_unlock_config(False) elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'):
if _enabled == 'true': sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1')
_term_message = ('\nquick-unlock has been enabled client-side - in order for this feature to ' write_config()
'function,\nyou must first log in to the sshyp server and run:\n\nsshyp tweak\n\n' _device_type = _dev_sync_types[0]
'from there you can create a quick-unlock pin and add this device to the ' elif _choice == 1:
'whitelist\n') if _device_type == 'client':
elif _choice == 6: gpg_config()
_success = refresh_encryption() else:
if _success == 1: whitelist_menu()
_term_message = "\na backup of your previous entry directory has been created:\n\n" \ elif _choice == 2:
f"{home}/.local/share/sshyp.old\n" if _device_type == 'client':
elif _success == 2: ssh_config()
_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()
else: else:
_exit_signal = True _exit_signal = True
curses_terminate(_term_message) elif _choice == 3:
# if root is needed for extension management... if not sshyp_data.has_section('SSHYNC'):
if _choice == 7 and _ext_name: ssh_config()
if _action: dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'),
# install with privilege escalation (outside of curses) sshyp_data.get('SSHYNC', 'port'))
from tempfile import gettempdir elif _choice == 4:
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini" editor_config(False)
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir)) elif _choice == 5:
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}")) _enabled = quick_unlock_config(False)
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) if _enabled == 'true':
else: term_messages.append('quick-unlock has been enabled client-side - in order for this feature to '
# uninstall with privilege escalation (outside of curses) 'function,\nyou must first log in to the sshyp server and run:\n\nsshyp tweak\n\n'
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}", f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) 'from there you can create a quick-unlock pin and add this device to the '
break 'whitelist')
except KeyboardInterrupt: elif _choice == 6:
curses_terminate(False) _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 _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: if _exit_signal:
break break
# runs initial configuration wizard # runs initial configuration wizard
def initial_setup(): def initial_setup(_scr):
global stdscr
stdscr = _scr
# required directory creation # required directory creation
Path(f"{home}/.config/sshyp/devices").mkdir(mode=0o700, parents=True, exist_ok=True) 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) Path(f"{home}/.local/share/sshyp").mkdir(mode=0o700, parents=True, exist_ok=True)
@@ -545,60 +533,57 @@ def initial_setup():
if _exists_flag: if _exists_flag:
sshyp_data.clear() sshyp_data.clear()
# curses initialization
noecho()
cbreak()
stdscr.keypad(True)
# curses menu tree # curses menu tree
try: # device+sync type selection
# device+sync type selection _dev_sync_types = install_type()
_dev_sync_types = install_type()
if _dev_sync_types[0] == 'client': if _dev_sync_types[0] == 'client':
# gpg configuration # gpg configuration
gpg_config() gpg_config()
# text editor configuration (automated) # text editor configuration (automated)
editor_config(True) editor_config(True)
# quick-unlock configuration (disabled by default) # quick-unlock configuration (disabled by default)
quick_unlock_config(True) quick_unlock_config(True)
# online (synchronized mode) configuration # online (synchronized mode) configuration
if _dev_sync_types[1] != 'true': if _dev_sync_types[1] != 'true':
# ssh+sshync configuration # ssh+sshync configuration
_ip, _username_ssh, _port = ssh_config() _ip, _username_ssh, _port = ssh_config()
# device id configuration # device id configuration
dev_id_config(_ip, _username_ssh, _port) dev_id_config(_ip, _username_ssh, _port)
# cleanly exit curses # PORT START CLIPTOOL TODO fix printing with new curses wrapper
curses_terminate(False) # 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: # run optional configuration menu
# cleanly exit curses wrapped_entry(_dev_sync_types[0], 'additional configuration options:')
curses_terminate(False)
# 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:') # runs the specified entry function (menu start point) within a curses wrapper
def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
except KeyboardInterrupt: from curses import wrapper, use_default_colors
# cleanly exit curses # a boolean value represents init
curses_terminate(False) 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")