sp-hydra-veil-cli/cli/__main__.py

644 lines
30 KiB
Python

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)