from core.Constants import Constants from core.errors.logger import logger from core.models.manage.session_management import init_session, get_session, close_session, create_ALL_tables, create_ONLY_db_version_table, get_path, does_it_exist, does_db_version_table_exist from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message from core.services.helpers.manage_assets import assets_folder_setup import os import sys # ============================================================================ # DISPLAY CHOICES AND RECOVERY UI # ============================================================================ def recovery_dialog(custom_error, db_version_you_have): """This ends the app by forcing them to delete the database, move it, or just quit.""" print(custom_error) print(""" 1) Delete old database (won't harm existing profiles.) 2) Do nothing. Exit. """ ) try: choice = int(input("Enter your choice (1 or 2):")) except: print("must enter a number") sys.exit() logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}") if choice == 1: if os.path.exists(database_path): os.remove(database_path) logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}") logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..") sys.exit() else: logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.") sys.exit() # ============================================================================ # ASSETS FOLDER # ============================================================================ if not assets_folder_setup(): custom_error = "Unable to Setup your Assets Folder. Please create an assets directory at ~/.local/share/hydra-veil/assets" print(custom_error) sys.exit() # ============================================================================ # INITIALIZE DATABASE # ============================================================================ # Does the database exist? system_path = get_path() database_path = system_path / "storage.db" main_db_exists = does_it_exist(database_path) # Setup operations on the main DB which create it init_session() # (engine, Session, _session all initialized from session_management) session = get_session() # does the version checker table exist? version_table_exists = does_db_version_table_exist() # are they upgrading from a legacy version? # that would mean the table existed, but not the version table, if not version_table_exists and main_db_exists: logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.") # shut down db connection close_session() # THEN DISPLAY CHOICES AND RECOVERY UI custom_error = "Upgrade Time! We transitioned to a new Database format for new features! Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions." db_version_you_have = "Old System" recovery_dialog(custom_error, db_version_you_have) sys.exit() # If they're still here, then if it's NOT a legacy version, # and the new version table doesn't exist, then create it: if not version_table_exists: create_ONLY_db_version_table() # ============================================================================ # COMPARE DB VERISONS # ============================================================================ compatability_dict = check_database_compatibility(session) reason = compatability_dict.get("reason", "error") is_compatable = compatability_dict.get("result", False) logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}") # ============================================================================ # IF THE VERSIONS MATCH # ============================================================================ if is_compatable: made_tables = create_ALL_tables() # SCREEN FAILED TABLES if not made_tables: custom_error = "Critical Failure with starting the models of the database." logger.error(f"[DB MANAGEMENT] {custom_error}") close_session() choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"}) logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}") sys.exit() # ASSUME TABLES CREATED logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing") # UPDATE DATA did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS) if not did_it_insert: logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.") # Load GUI either way: # If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles, if reason == "no_database": logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..") force_sync = True else: logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..") force_sync = False # ============================================================================ # but IF the versions do NOT match # ============================================================================ else: logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}") db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version") # WHAT IS THE REASON? custom_error = get_custom_message(reason, compatability_dict) # shut down db connection close_session() # THEN DISPLAY CHOICES AND RECOVERY UI recovery_dialog(custom_error, db_version_you_have) if force_sync: print("You have to sync to avoid errors. Please run ./hydra-veil-x86_64.AppImage --cli sync") # continue with old flow: # from core.Constants import Constants from core.Errors import MissingSubscriptionError, InvalidSubscriptionError, UnknownConnectionTypeError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ClientController import ClientController from core.controllers.ClientVersionController import ClientVersionController from core.controllers.ConfigurationController import ConfigurationController from core.controllers.InvoiceController import InvoiceController from core.controllers.LocationController import LocationController from core.controllers.PolicyController import PolicyController from core.controllers.ProfileController import ProfileController from core.controllers.SubscriptionController import SubscriptionController from core.controllers.SubscriptionPlanController import SubscriptionPlanController from core.models.session.Application import Application from core.models.session.SessionConnection import SessionConnection from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemConnection import SystemConnection from core.models.system.SystemProfile import SystemProfile from core.observers.ApplicationVersionObserver import ApplicationVersionObserver from core.observers.ClientObserver import ClientObserver from core.observers.ConnectionObserver import ConnectionObserver from core.observers.InvoiceObserver import InvoiceObserver from core.observers.ProfileObserver import ProfileObserver from importlib import metadata from pathlib import Path from typing import Optional, Union import argparse import pprint import sys if __name__ == '__main__': Path(Constants.HV_CONFIG_HOME).mkdir(parents=True, exist_ok=True) Path(Constants.HV_DATA_HOME).mkdir(parents=True, exist_ok=True) application_version_observer = ApplicationVersionObserver() client_observer = ClientObserver() connection_observer = ConnectionObserver() invoice_observer = InvoiceObserver() profile_observer = ProfileObserver() application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...')) application_version_observer.subscribe('download_progressing', lambda event: print(f'Current progress: {event.meta.get('progress'):.2f}%', flush=True, end='\r')) application_version_observer.subscribe('downloaded', lambda event: print('\n')) client_observer.subscribe('synchronizing', lambda event: print('Synchronizing...\n')) client_observer.subscribe('updating', lambda event: print('Updating client...')) client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get('progress'):.2f}%', flush=True, end='\r')) client_observer.subscribe('updated', lambda event: print('\n')) connection_observer.subscribe('connecting', lambda event: print(f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...\n')) connection_observer.subscribe('tor_bootstrapping', lambda event: print('Bootstrapping Tor...')) connection_observer.subscribe('tor_bootstrap_progressing', lambda event: print(f'Current progress: {event.meta.get('progress'):.2f}%', flush=True, end='\r')) connection_observer.subscribe('tor_bootstrapped', lambda event: print('\n')) invoice_observer.subscribe('retrieved', lambda event: print(f'\n{pprint.pp(event.subject)}\n')) invoice_observer.subscribe('processing', lambda event: print('A payment has been detected and is being verified...\n')) invoice_observer.subscribe('settled', lambda event: print('The payment has been successfully verified.\n')) profile_observer.subscribe('created', lambda event: pprint.pp((__sanitize_profile(event.subject), 'Created'))) profile_observer.subscribe('destroyed', lambda event: pprint.pp((__sanitize_profile(event.subject), 'Destroyed'))) profile_observer.subscribe('disabled', lambda event: pprint.pp((__sanitize_profile(event.subject), 'Disabled')) if event.meta.get('explicitly') else None) profile_observer.subscribe('enabled', lambda event: pprint.pp((__sanitize_profile(event.subject), 'Enabled'))) def __get_distribution(): for candidate in metadata.distributions(): if 'cli' not in candidate.name: continue candidate_files = candidate.files if candidate_files is None: continue for distribution_file in candidate_files: if distribution_file.parts[0] == 'cli': return candidate return None def __parse_composite_argument(argument: str, first_key: str, second_key: str, separator: str = ':'): return dict(zip([first_key, second_key], argument.split(separator) + [''])) def __parse_application_argument(application_argument: str): return __parse_composite_argument(application_argument, 'application_code', 'version_number') def __parse_location_argument(location_argument: str): return __parse_composite_argument(location_argument, 'country_code', 'code') def __sanitize_profile(candidate: Optional[Union[SessionProfile, SystemProfile]]): if candidate is not None and candidate.has_subscription(): sanitized_billing_code = candidate.subscription.get_sanitized_billing_code() candidate.subscription.billing_code = sanitized_billing_code return candidate def __handle_exception(identifier, message, traceback): if issubclass(identifier, UnknownConnectionTypeError): print('Please specify the desired connection method and try again.\n') else: sys.__excepthook__(identifier, message, traceback) sys.excepthook = __handle_exception distribution = __get_distribution() pristine_parser = argparse.ArgumentParser(add_help=False) pristine_parser.add_argument('--pristine', '-p', action='store_true') connection_protection_parser = argparse.ArgumentParser(add_help=False) connection_protection_parser.add_argument('--without-connection-protection', action='store_true') endpoint_verification_parser = argparse.ArgumentParser(add_help=False) endpoint_verification_parser.add_argument('--without-endpoint-verification', action='store_true') profile_state_protection_parser = argparse.ArgumentParser(add_help=False) profile_state_protection_parser.add_argument('--without-profile-state-protection', action='store_true') # Get the distribution object distribution = metadata.distribution('sp-hydra-veil-cli') main_parser = argparse.ArgumentParser(prog=distribution.name) main_parser.add_argument('--version', '-v', action='version', version=f'{distribution.name} v{distribution.version}') main_subparsers = main_parser.add_subparsers(title='commands', dest='command') # main_parser = argparse.ArgumentParser(prog=distribution.name) # main_parser.add_argument('--version', '-v', action='version', version=f'{distribution.name} v{distribution.version}') # main_subparsers = main_parser.add_subparsers(title='commands', dest='command') profile_parser = main_subparsers.add_parser('profile') profile_subparsers = profile_parser.add_subparsers(title='subcommands', dest='subcommand') profile_base_parser = argparse.ArgumentParser(add_help=False) profile_base_parser.add_argument('--id', '-i', type=int, required=True) profile_subparsers.add_parser('list') profile_subparsers.add_parser('show', parents=[profile_base_parser]) profile_create_parser = profile_subparsers.add_parser('create') profile_create_subparsers = profile_create_parser.add_subparsers(title='profile_types', dest='profile_type') session_profile_create_parser = profile_create_subparsers.add_parser('session', parents=[profile_base_parser]) session_profile_create_parser.add_argument('--name', '-n', default='') session_profile_create_parser.add_argument('--location', '-l', default='') session_profile_create_parser.add_argument('--application', '-a', required=True) session_profile_create_parser.add_argument('--connection', '-c', dest='connection_type', choices=['system', 'tor', 'wireguard'], default='system') session_profile_create_parser.add_argument('--mask-connection', '-m', action='store_true') session_profile_create_parser.add_argument('--resolution', '-r', default='1280x720') system_profile_create_parser = profile_create_subparsers.add_parser('system', parents=[profile_base_parser]) system_profile_create_parser.add_argument('--name', '-n', default='') system_profile_create_parser.add_argument('--location', '-l', default='') system_profile_create_parser.add_argument('--connection', '-c', dest='connection_type', choices=['wireguard'], default='wireguard') profile_subparsers.add_parser('destroy', parents=[profile_base_parser]) profile_subparsers.add_parser('enable', parents=[profile_base_parser, pristine_parser, connection_protection_parser, endpoint_verification_parser, profile_state_protection_parser]) profile_subparsers.add_parser('disable', parents=[profile_base_parser, connection_protection_parser]) application_parser = main_subparsers.add_parser('application') application_subparsers = application_parser.add_subparsers(title='subcommands', dest='subcommand') application_base_parser = argparse.ArgumentParser(add_help=False) application_base_parser.add_argument('--application', '-a', required=True) application_list_parser = application_subparsers.add_parser('list') application_list_parser.add_argument('--code', '-c') application_show_parser = application_subparsers.add_parser('show', parents=[application_base_parser]) application_install_parser = application_subparsers.add_parser('install', parents=[application_base_parser]) application_install_parser.add_argument('--reinstall', '-r', action='store_true') application_uninstall_parser = application_subparsers.add_parser('uninstall', parents=[application_base_parser]) policy_parser = main_subparsers.add_parser('policy') policy_subparsers = policy_parser.add_subparsers(title='subcommands', dest='subcommand') policy_base_parser = argparse.ArgumentParser(add_help=False) policy_base_parser.add_argument('--policy', '-p', choices=['capability', 'privilege'], required=True) policy_preview_parser = policy_subparsers.add_parser('preview', parents=[policy_base_parser]) policy_instate_parser = policy_subparsers.add_parser('instate', parents=[policy_base_parser]) policy_inspect_parser = policy_subparsers.add_parser('inspect', parents=[policy_base_parser]) policy_revoke_parser = policy_subparsers.add_parser('revoke', parents=[policy_base_parser]) get_parser = main_subparsers.add_parser('get') get_subparsers = get_parser.add_subparsers(title='subcommands', dest='subcommand') get_connection_parser = get_subparsers.add_parser('connection') get_endpoint_verification_parser = get_subparsers.add_parser('endpoint_verification') set_parser = main_subparsers.add_parser('set') set_subparsers = set_parser.add_subparsers(title='subcommands', dest='subcommand') set_connection_parser = set_subparsers.add_parser('connection') set_connection_parser.add_argument('connection_type', choices=['system', 'tor']) set_endpoint_verification_parser = set_subparsers.add_parser('endpoint_verification') set_endpoint_verification_parser.add_argument('endpoint_verification_state', choices=['enabled', 'disabled']) sync_parser = main_subparsers.add_parser('sync') update_parser = main_subparsers.add_parser('update') arguments = main_parser.parse_args() ignore = [] if getattr(arguments, 'without_connection_protection', False): ignore.append(ConnectionUnprotectedError) if getattr(arguments, 'without_endpoint_verification', False): ignore.append(EndpointVerificationError) if getattr(arguments, 'without_profile_state_protection', False): ignore.append(ProfileStateConflictError) ignore = tuple(ignore) if arguments.command is None: main_parser.print_help() elif arguments.command == 'profile': if arguments.subcommand is None: profile_parser.print_help() elif arguments.subcommand == 'list': profiles = ProfileController.get_all() for key, value in profiles.items(): profiles[key] = __sanitize_profile(value) pprint.pp(profiles) elif arguments.subcommand == 'show': pprint.pp(ProfileController.get(arguments.id)) elif arguments.subcommand == 'create': location_details = __parse_location_argument(arguments.location) location = LocationController.get(location_details.get('country_code'), location_details.get('code')) if location is None: main_parser.error('the following argument should be a valid reference: --location/-l') if arguments.profile_type == 'session': application_version_details = __parse_application_argument(arguments.application) application_version = ApplicationVersionController.get(application_version_details.get('application_code'), application_version_details.get('version_number')) if application_version is None: main_parser.error('the following argument should be a valid reference: --application/-a') connection = SessionConnection(arguments.connection_type, arguments.mask_connection) profile = SessionProfile(arguments.id, arguments.name, None, location, arguments.resolution, application_version, connection) ProfileController.create(profile, profile_observer=profile_observer) else: connection = SystemConnection(arguments.connection_type) profile = SystemProfile(arguments.id, arguments.name, None, location, connection) ProfileController.create(profile, profile_observer=profile_observer) elif arguments.subcommand == 'destroy': profile = ProfileController.get(arguments.id) if profile is not None: ProfileController.destroy(profile, profile_observer=profile_observer) else: main_parser.error('the following argument should be a valid reference: --id/-i') elif arguments.subcommand == 'enable': profile = ProfileController.get(arguments.id) if profile is not None: try: ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer) except (InvalidSubscriptionError, MissingSubscriptionError) as exception: if type(exception).__name__ == 'InvalidSubscriptionError': print('The profile\'s subscription appears to be invalid.\n') elif type(exception).__name__ == 'MissingSubscriptionError': print('The profile is not tied to a subscription.\n') manage_subscription_input = None while manage_subscription_input not in ('1', '2', '3', ''): print('Please select from the following:\n') print(' 1) Request new subscription') print(' 2) Enter billing code') print('\n 3) Exit') manage_subscription_input = input('\nEnter your choice [1]: ') if manage_subscription_input == '1' or manage_subscription_input == '': print('\nCreating subscription...\n') subscription_plan = SubscriptionPlanController.get(profile.connection, 720) if subscription_plan is None: raise RuntimeError('No compatible subscription plan was found. Please contact support.') potential_subscription = SubscriptionController.create(subscription_plan, profile, connection_observer=connection_observer) if potential_subscription is not None: ProfileController.attach_subscription(profile, potential_subscription) else: raise RuntimeError('The subscription could not be created. Please try again later.') subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer) if subscription is not None: ProfileController.attach_subscription(profile, subscription) else: raise RuntimeError('The subscription could not be activated. Please try again later.') ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer) elif manage_subscription_input == '2': billing_code = input('\nEnter your billing code: ') print() subscription = SubscriptionController.get(billing_code, connection_observer=connection_observer) if subscription is not None: ProfileController.attach_subscription(profile, subscription) ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer) else: print('\nThe billing code appears to be invalid.\n') manage_subscription_input = None elif manage_subscription_input == '3': pass else: print('\nInput appears to be invalid. Please try again.\n') else: main_parser.error('the following argument should be a valid reference: --id/-i') elif arguments.subcommand == 'disable': profile = ProfileController.get(arguments.id) if profile is not None: ProfileController.disable(profile, ignore=ignore, profile_observer=profile_observer) else: main_parser.error('the following argument should be a valid reference: --id/-i') elif arguments.command == 'application': if arguments.subcommand is None: application_parser.print_help() elif arguments.subcommand == 'list': if arguments.code: application = Application.find(arguments.code) if application is not None: pprint.pp(ApplicationVersionController.get_all(application)) else: main_parser.error('the following argument should be a valid reference: --code/-c') else: pprint.pp(ApplicationVersionController.get_all()) elif arguments.subcommand == 'show': application_version_details = __parse_application_argument(arguments.application) application_version = ApplicationVersionController.get(application_version_details.get('application_code'), application_version_details.get('version_number')) if application_version is not None: pprint.pp(application_version) else: main_parser.error('the following argument should be a valid reference: --application/-a') elif arguments.subcommand == 'install': application_version_details = __parse_application_argument(arguments.application) application_version = ApplicationVersionController.get(application_version_details.get('application_code'), application_version_details.get('version_number')) if application_version is not None: ApplicationVersionController.install(application_version, arguments.reinstall, application_version_observer=application_version_observer, connection_observer=connection_observer) else: main_parser.error('the following argument should be a valid reference: --application/-a') elif arguments.subcommand == 'uninstall': application_version_details = __parse_application_argument(arguments.application) application_version = ApplicationVersionController.get(application_version_details.get('application_code'), application_version_details.get('version_number')) if application_version is not None: ApplicationVersionController.uninstall(application_version) else: main_parser.error('the following argument should be a valid reference: --application/-a') elif arguments.command == 'policy': if arguments.subcommand is None: policy_parser.print_help() else: policy = PolicyController.get(arguments.policy) if policy is not None: if arguments.subcommand == 'preview': print(PolicyController.preview(policy)) elif arguments.subcommand == 'instate': PolicyController.instate(policy) elif arguments.subcommand == 'inspect': if PolicyController.is_instated(policy): pprint.pp({'status': 'Instated'}) elif PolicyController.is_suggestible(policy): pprint.pp({'status': 'Suggested'}) else: pprint.pp({'status': 'Uninstated'}) elif arguments.subcommand == 'revoke': PolicyController.revoke(policy) elif arguments.command == 'get': if arguments.subcommand is None: get_parser.print_help() elif arguments.subcommand == 'connection': print(ConfigurationController.get_connection()) elif arguments.subcommand == 'endpoint_verification': if ConfigurationController.get_endpoint_verification_enabled(): print('enabled') else: print('disabled') elif arguments.command == 'set': if arguments.subcommand is None: set_parser.print_help() elif arguments.subcommand == 'connection': ConfigurationController.set_connection(arguments.connection_type) elif arguments.subcommand == 'endpoint_verification': if arguments.endpoint_verification_state == 'enabled': ConfigurationController.set_endpoint_verification_enabled(True) elif arguments.endpoint_verification_state == 'disabled': ConfigurationController.set_endpoint_verification_enabled(False) elif arguments.command == 'sync': ClientController.sync(client_observer=client_observer, connection_observer=connection_observer) elif arguments.command == 'update': client_version = ClientController.get_version() if ClientVersionController.is_latest(client_version): print('The client is already up to date.\n') ClientController.update(client_observer=client_observer, connection_observer=connection_observer)