From bdedc984944f17da4c2e55d02ace6493f88f8f63 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sat, 13 May 2023 19:11:38 -0400 Subject: [PATCH] Switched to .INI config format, merged config files, finished adding functionality to new config menus Former-commit-id: b5e877be47a49e967ced792156deb4f61841fb5e Former-commit-id: c60df212fe964f089e246d39b3dbeaf28c006174 --- lib/sshync.py | 28 ++++---- lib/sshyp.py | 63 ++++++++++-------- lib/stweak.py | 177 +++++++++++++++++++++++++++++++------------------- 3 files changed, 157 insertions(+), 111 deletions(-) diff --git a/lib/sshync.py b/lib/sshync.py index ddd945e..732fee4 100644 --- a/lib/sshync.py +++ b/lib/sshync.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from os import listdir, remove, walk -from os.path import expanduser, isdir, getmtime, join +from os.path import expanduser, isdir, isfile, getmtime, join from subprocess import CalledProcessError, PIPE, run from sys import exit as s_exit home = expanduser("~") @@ -118,25 +118,21 @@ def sort_titles_mods(_list_1, _list_2): return _title_list_2_sorted, _mod_list_2_sorted -# creates a sshync job profile -def make_profile(_profile_dir, _local_dir, _remote_dir, _identity, _ip, _port, _user): - open(_profile_dir, 'w').write(f"{_user}\n{_ip}\n{_port}\n{_local_dir}\n{_remote_dir}\n{_identity}\n") - - # returns a list of data read from a sshync job profile def get_profile(_profile_dir): - try: - _profile_data = open(_profile_dir).readlines() - except (FileNotFoundError, IndexError): + from configparser import ConfigParser + _profile_data = ConfigParser() + if isfile(_profile_dir): + _profile_data.read(_profile_dir) + else: print('\n\u001b[38;5;9merror: the profile does not exist or is corrupted\u001b[0m\n') - _profile_data = None s_exit(2) - _user = _profile_data[0].rstrip() - _ip = _profile_data[1].rstrip() - _port = _profile_data[2].rstrip() - _local_dir = _profile_data[3].rstrip() - _remote_dir = _profile_data[4].rstrip() - _identity = _profile_data[5].rstrip() + _user = _profile_data.get('SSHYNC', 'user') + _ip = _profile_data.get('SSHYNC', 'ip') + _port = _profile_data.get('SSHYNC', 'port') + _local_dir = _profile_data.get('SSHYNC', 'local_dir') + _remote_dir = _profile_data.get('SSHYNC', 'remote_dir') + _identity = _profile_data.get('SSHYNC', 'identity_file') _client_device_id = listdir(f"{home}/.config/sshyp/devices")[0].rstrip() return _user, _ip, _port, _local_dir, _remote_dir, _identity, _client_device_id diff --git a/lib/sshyp.py b/lib/sshyp.py index dc7fa40..301fac5 100755 --- a/lib/sshyp.py +++ b/lib/sshyp.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 +from configparser import ConfigParser, NoSectionError from os import chmod, environ, listdir, remove, walk from os.path import exists, expanduser, isdir, isfile, realpath from pathlib import Path from random import randint from shutil import move, rmtree -from sshync import delete as offline_delete, run_profile, get_profile +from sshync import delete as offline_delete, run_profile from subprocess import CalledProcessError, DEVNULL, PIPE, run from sys import argv, exit as s_exit # PORT START UNAME-IMPORT @@ -162,7 +163,7 @@ def decrypt(_entry_dir, _shm_folder, _shm_entry, _quick_pass, # call decrypt() based on quick-unlock status def determine_decrypt(_entry_dir, _shm_folder, _shm_entry): - if quick_unlock_enabled == 'yes': + if quick_unlock_enabled == 'true': decrypt(_entry_dir, _shm_folder, _shm_entry, whitelist_verify(port, username_ssh, ip, client_device_id)) else: decrypt(_entry_dir, _shm_folder, _shm_entry, False) @@ -201,7 +202,10 @@ def edit_note(_shm_folder, _shm_entry, _lines): # attempts to connect to the user's server via ssh to register the device for syncing -def copy_id_check(_port, _username_ssh, _ip, _client_device_id): +def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _sshyp_data): + from stweak import write_config + if not _sshyp_data.has_section('CLIENT-ONLINE'): + _sshyp_data.add_section('CLIENT-ONLINE') try: run(['ssh', '-o', 'ConnectTimeout=3', '-i', f"{home}/.ssh/sshyp", '-p', _port, f"{_username_ssh}@{_ip}", f'python3 -c \'from pathlib import Path; Path("/home/{_username_ssh}/.config/sshyp/devices/' @@ -210,9 +214,11 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id): print('\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key (~/.ssh/sshyp.pub) is ' 'registered on the remote server and that the entered ip, port, and username are correct\n\nsyncing ' 'functionality will be disabled until this is addressed\u001b[0m\n') - open(f"{home}/.config/sshyp/ssh-error", 'w').write('1') + _sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1') + write_config(_sshyp_data) return True - open(f"{home}/.config/sshyp/ssh-error", 'w').write('0') + _sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '0') + write_config(_sshyp_data) return False @@ -365,7 +371,7 @@ def sync(): chmod(_path, 0o700) for _file in _files: chmod(_root + '/' + _file, 0o600) - run_profile(f"{home}/.config/sshyp/sshyp.sshync", silent_sync) + run_profile(f"{home}/.config/sshyp/sshyp.ini", silent_sync) # PORT START WHITELIST-SERVER @@ -674,7 +680,6 @@ def remove_data(): # checks extension config files for matches to argument, runs extensions def extension_runner(): - from configparser import ConfigParser _output_com, _extension_dir = None, realpath(__file__).rsplit('/', 1)[0] + '/extensions/' if isdir(_extension_dir): for _extension in listdir(_extension_dir): @@ -722,31 +727,30 @@ if __name__ == "__main__": # import saved userdata tmp_dir = f"{home}/.config/sshyp/tmp/" try: - sshyp_data = open(f"{home}/.config/sshyp/sshyp-data").readlines() - device_type = sshyp_data[0].rstrip() + sshyp_data = ConfigParser() + sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") + device_type = sshyp_data.get('GENERAL', 'device_type') if device_type == 'client': directory = f"{home}/.local/share/sshyp/" - gpg_id = sshyp_data[1].rstrip() - editor = sshyp_data[2].rstrip() - quick_unlock_enabled = sshyp_data[3].rstrip() - if isfile(f"{home}/.config/sshyp/sshyp.sshync"): - ssh_info = get_profile(f"{home}/.config/sshyp/sshyp.sshync") - username_ssh = ssh_info[0].rstrip() - ip = ssh_info[1].rstrip() - port = ssh_info[2].rstrip() - directory_ssh = str(ssh_info[4].rstrip()) - client_device_id = listdir(f"{home}/.config/sshyp/devices")[0].rstrip() - ssh_error = int(open(f"{home}/.config/sshyp/ssh-error").read().rstrip()) - if ssh_error == 1: - ssh_error = copy_id_check(port, username_ssh, ip, client_device_id) - else: - ssh_error = False - else: + gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id') + editor = sshyp_data.get('CLIENT-GENERAL', 'text_editor') + offline_mode_enabled = sshyp_data.get('CLIENT-GENERAL', 'offline_mode_enabled') + if offline_mode_enabled == 'true': ssh_error = True - except (FileNotFoundError, IndexError): - print('\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + else: + quick_unlock_enabled = sshyp_data.get('CLIENT-ONLINE', 'quick_unlock_enabled') + username_ssh = sshyp_data.get('SSHYNC', 'user') + ip = sshyp_data.get('SSHYNC', 'ip') + port = sshyp_data.get('SSHYNC', 'port') + directory_ssh = sshyp_data.get('SSHYNC', 'remote_dir') + client_device_id = listdir(f"{home}/.config/sshyp/devices")[0] + ssh_error = int(sshyp_data.get('CLIENT-ONLINE', 'ssh_error')) + if ssh_error == 1: + ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, sshyp_data) + except (FileNotFoundError, NoSectionError): + print(f"\n{73*'!'}") print("not all necessary configurations have been made - please run 'sshyp init'") - print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n') + print(f"{73*'!'}\n") s_exit(1) else: from stweak import initial_setup @@ -814,6 +818,9 @@ if __name__ == "__main__": elif arguments[1] == 'setup': success_flag = True whitelist_setup() + elif arg_count == 1 and arguments[0] == 'tweak': + from stweak import global_menu + global_menu(False) elif arg_count > 2 and arguments[1] in ('add', 'del'): success_flag = True whitelist_manage(arguments[2]) diff --git a/lib/stweak.py b/lib/stweak.py index 5358e42..00a1e36 100644 --- a/lib/stweak.py +++ b/lib/stweak.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -from curses import A_REVERSE, KEY_DOWN, KEY_UP, cbreak, curs_set, endwin, initscr, newwin, noecho, nocbreak +from configparser import ConfigParser +from curses import A_REVERSE, echo, KEY_DOWN, KEY_UP, cbreak, curs_set, endwin, initscr, newwin, noecho, nocbreak from curses.textpad import rectangle, Textbox from os import environ, listdir, remove, symlink from os.path import exists, expanduser, isfile @@ -7,13 +8,23 @@ from pathlib import Path from random import randint from re import sub from shutil import get_terminal_size, which -from sshync import make_profile from sshyp import copy_id_check, string_gen from subprocess import PIPE, run # PORT START UNAME-IMPORT from os import uname # PORT END UNAME-IMPORT -home, stdscr, sshyp_data = expanduser("~"), initscr(), [] +home, stdscr, sshyp_data = expanduser("~"), initscr(), ConfigParser() +if isfile(f"{home}/.config/sshyp/sshyp.ini"): + _exists_flag = True + sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") +else: + _exists_flag = False + + +# writes data stored in ConfigParser to the correct config file +def write_config(_sshyp_data=sshyp_data): + with open(f"{home}/.config/sshyp/sshyp.ini", 'w') as configfile: + _sshyp_data.write(configfile) # creates a radio selection between the provided options @@ -58,15 +69,18 @@ def curses_text(_pretext): return _box.gather().strip() -# cleanly exit curses -def curses_terminate(): +# 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 + _offline_mode = 'false' # PORT START TWEAK-DEVTYPE _install_type = curses_radio(('server', 'client (ssh-synchronized)', 'client (offline)'), 'device + sync type configuration') @@ -74,15 +88,18 @@ 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() - print(f"\nmake sure the ssh service is running and properly configured") + curses_terminate('\nmake sure the ssh service is running and properly configured') else: _dev_type = 'client' if _install_type == 2: - _offline_mode = True - if isfile(f"{home}/.config/sshyp/sshyp.sshync"): - remove(f"{home}/.config/sshyp/sshyp.sshync") - Path(f"{home}/.local/share/sshyp").mkdir(mode=0o700, parents=True, exist_ok=True) + _offline_mode = 'true' + if not sshyp_data.has_section('CLIENT-GENERAL'): + sshyp_data.add_section('CLIENT-GENERAL') + sshyp_data.set('CLIENT-GENERAL', 'offline_mode_enabled', _offline_mode) + if not sshyp_data.has_section('GENERAL'): + sshyp_data.add_section('GENERAL') + sshyp_data.set('GENERAL', 'device_type', _dev_type) + write_config() # PORT END TWEAK-DEVTYPE return _dev_type, _offline_mode @@ -108,14 +125,35 @@ def gpg_config(): run(['gpg', '--batch', '--generate-key', f"{home}/.config/sshyp/gpg-gen"]) remove(f"{home}/.config/sshyp/gpg-gen") _gpg_id = run(['gpg', '-k'], stdout=PIPE, text=True).stdout.splitlines()[-3].strip() - return _gpg_id + + # lock file generation + if isfile(f"{home}/.config/sshyp/lock.gpg"): + remove(f"{home}/.config/sshyp/lock.gpg") + open(f"{home}/.config/sshyp/lock", 'w') + run(['gpg', '-qr', _gpg_id, '-e', f"{home}/.config/sshyp/lock"]) + remove(f"{home}/.config/sshyp/lock") + + if not sshyp_data.has_section('CLIENT-GENERAL'): + sshyp_data.add_section('CLIENT-GENERAL') + sshyp_data.set('CLIENT-GENERAL', 'gpg_id', _gpg_id) + write_config() # text editor configuration -def editor_config(): - _editor = curses_text('enter the name of your preferred text editor:\n\n\n\n\n' - '(ctrl+g/enter to confirm)\n\nexample input: vim') - return _editor +def editor_config(_env_mode): + if _env_mode: + # set default text editor to value of EDITOR environment variable, otherwise default to nano + if 'EDITOR' in environ: + _editor = environ['EDITOR'] + else: + _editor = 'nano' + else: + _editor = curses_text('enter the name of your preferred text editor:\n\n\n\n\n' + '(ctrl+g/enter to confirm)\n\nexample input: vim') + if not sshyp_data.has_section('CLIENT-GENERAL'): + sshyp_data.add_section('CLIENT-GENERAL') + sshyp_data.set('CLIENT-GENERAL', 'text_editor', _editor) + write_config() # ssh+sshync configuration @@ -129,10 +167,15 @@ def ssh_config(): _username_ssh = _uiport_split[0] _iport = _uiport_split[1].rsplit(':', 1) - # sshync profile generation - make_profile(f"{home}/.config/sshyp/sshyp.sshync", - f"{home}/.local/share/sshyp/", f"/home/{_username_ssh}/.local/share/sshyp/", - f"{home}/.ssh/sshyp", _iport[0], _iport[1], _username_ssh) + if not sshyp_data.has_section('SSHYNC'): + sshyp_data.add_section('SSHYNC') + sshyp_data.set('SSHYNC', 'user', _username_ssh) + sshyp_data.set('SSHYNC', 'ip', _iport[0]) + sshyp_data.set('SSHYNC', 'port', _iport[1]) + sshyp_data.set('SSHYNC', 'local_dir', f"{home}/.local/share/sshyp/") + sshyp_data.set('SSHYNC', 'remote_dir', f"/home/{_username_ssh}/.local/share/sshyp/") + sshyp_data.set('SSHYNC', 'identity_file', f"{home}/.ssh/sshyp") + write_config() return _iport[1], _username_ssh, _iport[0] @@ -148,31 +191,36 @@ def dev_id_config(_ip, _username_ssh, _port): _device_id = _device_id_prefix + '-' + _device_id_suffix open(f"{home}/.config/sshyp/devices/{_device_id}", 'w') # test server connection and attempt to register device id - copy_id_check(_ip, _username_ssh, _port, _device_id) - return _device_id + copy_id_check(_ip, _username_ssh, _port, _device_id, sshyp_data) # quick-unlock configuration -def quick_unlock_config(): - _quick_unlock_sel = curses_radio(('yes', 'no'), 'enable quick-unlock?') - if _quick_unlock_sel == 0: - _enabled = 'yes' +def quick_unlock_config(_default): + if _default: + _enabled = 'false' else: - _enabled = 'no' - return _enabled + _quick_unlock_sel = curses_radio(('yes', 'no'), 'enable quick-unlock?') + if _quick_unlock_sel == 0: + _enabled = 'true' + else: + _enabled = 'false' + if not sshyp_data.has_section('CLIENT-ONLINE'): + sshyp_data.add_section('CLIENT-ONLINE') + sshyp_data.set('CLIENT-ONLINE', 'quick_unlock_enabled', _enabled) + write_config() -# runs secondary configuration menu - clients only +# runs secondary configuration menu def global_menu(_post_setup): # curses initialization noecho() cbreak() stdscr.keypad(True) - _options, _choice = [], 4 + _options, _choice, _term_message = [], 4, False try: if not _post_setup: - _options.extend(['change device/synchronization types', 'change gpg key', 're-configure ssh', + _options.extend(['change device/synchronization types', 'change gpg key', 're-configure ssh(ync)', 'change device name']) _message, _choice = 'all configuration options:', 0 _options.extend(['[OPTIONAL, RECOMMENDED] set custom text editor', @@ -183,28 +231,40 @@ def global_menu(_post_setup): _choice += curses_radio(_options, _message) if _choice == 0: - install_type() + _dev_sync_types = install_type() + # if not running in server or offline mode and a sshync config has not been made + if _dev_sync_types[0] != 'server' and _dev_sync_types[1] != 'true' and not sshyp_data.has_section('SSHYNC'): + _ip, _username_ssh, _port = ssh_config() + # if no device id is set + if not listdir(f"{home}/.config/sshyp/devices"): + dev_id_config(_ip, _username_ssh, _port) elif _choice == 1: gpg_config() elif _choice == 2: ssh_config() elif _choice == 3: - dev_id_config() + dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'), + sshyp_data.get('SSHYNC', 'port')) elif _choice == 4: - editor_config() + editor_config(False) elif _choice == 5: - quick_unlock_config() + quick_unlock_config(False) else: pass - curses_terminate() + curses_terminate(_term_message) except KeyboardInterrupt: - curses_terminate() + curses_terminate(False) # runs initial configuration wizard def initial_setup(): - # config directory creation + # 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) + + # removal of old config files + if _exists_flag: + sshyp_data.clear() # temporary file symlink creation if not exists(f"{home}/.config/sshyp/tmp"): @@ -218,6 +278,7 @@ def initial_setup(): # PORT END UNAME-TMP # curses initialization + noecho() cbreak() stdscr.keypad(True) @@ -225,41 +286,33 @@ def initial_setup(): try: # device+sync type selection _dev_sync_types = install_type() - sshyp_data.append(_dev_sync_types[0]) if _dev_sync_types[0] == 'client': # gpg configuration - sshyp_data.append(gpg_config()) + gpg_config() - # lock file generation (requires gpg configuration) - if isfile(f"{home}/.config/sshyp/lock.gpg"): - remove(f"{home}/.config/sshyp/lock.gpg") - open(f"{home}/.config/sshyp/lock", 'w') - run(['gpg', '-qr', str(sshyp_data[1]), '-e', f"{home}/.config/sshyp/lock"]) - remove(f"{home}/.config/sshyp/lock") + # text editor configuration (automated) + editor_config(True) - # set default text editor to value of EDITOR environment variable, otherwise default to nano - if 'EDITOR' in environ: - sshyp_data.append(environ['EDITOR']) - else: - sshyp_data.append('nano') + # quick-unlock configuration (disabled by default) + quick_unlock_config(True) # online (synchronized mode) configuration - if not _dev_sync_types[1]: + if _dev_sync_types[1] != 'true': # ssh+sshync configuration _ip, _username_ssh, _port = ssh_config() # device id configuration - _device_id = dev_id_config(_ip, _username_ssh, _port) + dev_id_config(_ip, _username_ssh, _port) # cleanly exit curses - curses_terminate() + curses_terminate(False) else: # cleanly exit curses - curses_terminate() + curses_terminate(False) # PORT START CLIPTOOL # check for clipboard tool and display warning if missing @@ -274,18 +327,8 @@ def initial_setup(): f'"{_clipboard_package}" is installed\u001b[0m') # PORT END CLIPTOOL - # write main config file (sshyp-data) - with open(f"{home}/.config/sshyp/sshyp-data", 'w') as _config_file: - _lines = 0 - for _item in sshyp_data: - _lines += 1 - _config_file.write(str(_item) + '\n') - while _lines < 4: - _lines += 1 - _config_file.write('n') - global_menu(True) except KeyboardInterrupt: # cleanly exit curses - curses_terminate() + curses_terminate(False)