From ffc6100660d26dcb7606566425c2cd0d176a6db8 Mon Sep 17 00:00:00 2001 From: zenaku Date: Sat, 30 May 2026 08:25:00 -0500 Subject: [PATCH] add commands --- cli/__main__.py | 501 +++--------------------------------- cli/commands/__init__.py | 9 + cli/commands/application.py | 73 ++++++ cli/commands/get_set.py | 40 +++ cli/commands/operator.py | 32 +++ cli/commands/policy.py | 53 ++++ cli/commands/profile.py | 201 +++++++++++++++ cli/commands/sync_update.py | 22 ++ cli/helpers.py | 33 +++ cli/observers.py | 37 +++ pyproject.toml | 2 +- 11 files changed, 530 insertions(+), 473 deletions(-) create mode 100644 cli/commands/__init__.py create mode 100644 cli/commands/application.py create mode 100644 cli/commands/get_set.py create mode 100644 cli/commands/operator.py create mode 100644 cli/commands/policy.py create mode 100644 cli/commands/profile.py create mode 100644 cli/commands/sync_update.py create mode 100644 cli/helpers.py create mode 100644 cli/observers.py diff --git a/cli/__main__.py b/cli/__main__.py index fcd7f73..34dd563 100644 --- a/cli/__main__.py +++ b/cli/__main__.py @@ -1,491 +1,48 @@ 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 core.Errors import UnknownConnectionTypeError +from cli.helpers import get_distribution +from cli.commands import all_commands +from cli.commands import get_set, sync_update from pathlib import Path -from typing import Optional, Union import argparse -import pprint import sys + +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) + + 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() + sys.excepthook = _handle_exception - 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')) + distribution = get_distribution() - 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')) + parser = argparse.ArgumentParser(prog=distribution.name) + parser.add_argument('--version', '-v', action='version', version=f'{distribution.name} v{distribution.version}') + subparsers = parser.add_subparsers(title='commands', dest='command') - 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')) + for command in all_commands: + command.register(subparsers) - 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') - - 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) + arguments = parser.parse_args() if arguments.command is None: - main_parser.print_help() + parser.print_help() - elif arguments.command == 'profile': + elif arguments.command in (get_set.NAME, get_set.NAME_SET): + get_set.handle(arguments, parser) - if arguments.subcommand is None: - profile_parser.print_help() + elif arguments.command in (sync_update.NAME_SYNC, sync_update.NAME_UPDATE): + sync_update.handle(arguments, parser) - 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) + else: + command = next((c for c in all_commands if getattr(c, 'NAME', None) == arguments.command), None) + if command: + command.handle(arguments, parser) \ No newline at end of file diff --git a/cli/commands/__init__.py b/cli/commands/__init__.py new file mode 100644 index 0000000..71a6d9a --- /dev/null +++ b/cli/commands/__init__.py @@ -0,0 +1,9 @@ +from cli.commands import profile +from cli.commands import application +from cli.commands import policy +from cli.commands import get_set +from cli.commands import sync_update +import importlib +operator_command = importlib.import_module('cli.commands.operator') + +all_commands = [profile, application, operator_command, policy, get_set, sync_update] diff --git a/cli/commands/application.py b/cli/commands/application.py new file mode 100644 index 0000000..3ad0139 --- /dev/null +++ b/cli/commands/application.py @@ -0,0 +1,73 @@ +from core.controllers.ApplicationVersionController import ApplicationVersionController +from core.models.session.Application import Application +from cli.helpers import parse_application_argument +from cli.observers import application_version_observer, connection_observer +import pprint + +NAME = 'application' + + +def register(subparsers): + parser = subparsers.add_parser(NAME) + subs = parser.add_subparsers(title='subcommands', dest='subcommand') + + base = _base_parser() + + list_parser = subs.add_parser('list') + list_parser.add_argument('--code', '-c') + + subs.add_parser('show', parents=[base]) + + install_parser = subs.add_parser('install', parents=[base]) + install_parser.add_argument('--reinstall', '-r', action='store_true') + + subs.add_parser('uninstall', parents=[base]) + + return parser + + +def handle(arguments, main_parser): + if arguments.subcommand is None: + main_parser.parse_args(['application', '--help']) + return + + if 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': + details = parse_application_argument(arguments.application) + app_version = ApplicationVersionController.get(details.get('application_code'), details.get('version_number')) + if app_version is not None: + pprint.pp(app_version) + else: + main_parser.error('the following argument should be a valid reference: --application/-a') + + elif arguments.subcommand == 'install': + details = parse_application_argument(arguments.application) + app_version = ApplicationVersionController.get(details.get('application_code'), details.get('version_number')) + if app_version is not None: + ApplicationVersionController.install(app_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': + details = parse_application_argument(arguments.application) + app_version = ApplicationVersionController.get(details.get('application_code'), details.get('version_number')) + if app_version is not None: + ApplicationVersionController.uninstall(app_version) + else: + main_parser.error('the following argument should be a valid reference: --application/-a') + + +def _base_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--application', '-a', required=True) + return p \ No newline at end of file diff --git a/cli/commands/get_set.py b/cli/commands/get_set.py new file mode 100644 index 0000000..d01d91f --- /dev/null +++ b/cli/commands/get_set.py @@ -0,0 +1,40 @@ +from core.controllers.ConfigurationController import ConfigurationController + +NAME = 'get' +NAME_SET = 'set' + + +def register(subparsers): + get_parser = subparsers.add_parser(NAME) + get_subs = get_parser.add_subparsers(title='subcommands', dest='subcommand') + get_subs.add_parser('connection') + get_subs.add_parser('endpoint_verification') + + set_parser = subparsers.add_parser(NAME_SET) + set_subs = set_parser.add_subparsers(title='subcommands', dest='subcommand') + + set_connection = set_subs.add_parser('connection') + set_connection.add_argument('connection_type', choices=['system', 'tor']) + + set_endpoint = set_subs.add_parser('endpoint_verification') + set_endpoint.add_argument('endpoint_verification_state', choices=['enabled', 'disabled']) + + return get_parser + + +def handle(arguments, main_parser): + if arguments.command == NAME: + if arguments.subcommand is None: + main_parser.parse_args(['get', '--help']) + elif arguments.subcommand == 'connection': + print(ConfigurationController.get_connection()) + elif arguments.subcommand == 'endpoint_verification': + print('enabled' if ConfigurationController.get_endpoint_verification_enabled() else 'disabled') + + elif arguments.command == NAME_SET: + if arguments.subcommand is None: + main_parser.parse_args(['set', '--help']) + elif arguments.subcommand == 'connection': + ConfigurationController.set_connection(arguments.connection_type) + elif arguments.subcommand == 'endpoint_verification': + ConfigurationController.set_endpoint_verification_enabled(arguments.endpoint_verification_state == 'enabled') \ No newline at end of file diff --git a/cli/commands/operator.py b/cli/commands/operator.py new file mode 100644 index 0000000..33453b1 --- /dev/null +++ b/cli/commands/operator.py @@ -0,0 +1,32 @@ +from core.controllers.OperatorController import OperatorController +import pprint + +NAME = 'operator' + + +def register(subparsers): + parser = subparsers.add_parser(NAME) + subs = parser.add_subparsers(title='subcommands', dest='subcommand') + + subs.add_parser('list') + + show_parser = subs.add_parser('show') + show_parser.add_argument('--id', '-i', type=int, required=True) + + return parser + + +def handle(arguments, main_parser): + if arguments.subcommand is None: + main_parser.parse_args(['operator', '--help']) + return + + if arguments.subcommand == 'list': + pprint.pp(OperatorController.get_all()) + + elif arguments.subcommand == 'show': + operator = OperatorController.get(arguments.id) + if operator is not None: + pprint.pp(operator) + else: + main_parser.error('the following argument should be a valid reference: --id/-i') \ No newline at end of file diff --git a/cli/commands/policy.py b/cli/commands/policy.py new file mode 100644 index 0000000..f5635a8 --- /dev/null +++ b/cli/commands/policy.py @@ -0,0 +1,53 @@ +from core.controllers.PolicyController import PolicyController +import pprint + +NAME = 'policy' + + +def register(subparsers): + parser = subparsers.add_parser(NAME) + subs = parser.add_subparsers(title='subcommands', dest='subcommand') + + base = _base_parser() + + subs.add_parser('preview', parents=[base]) + subs.add_parser('instate', parents=[base]) + subs.add_parser('inspect', parents=[base]) + subs.add_parser('revoke', parents=[base]) + + return parser + + +def handle(arguments, main_parser): + if arguments.subcommand is None: + main_parser.parse_args(['policy', '--help']) + return + + policy = PolicyController.get(arguments.policy) + + if policy is None: + main_parser.error('the following argument should be a valid reference: --policy/-p') + + 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) + + +def _base_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--policy', '-p', choices=['capability', 'privilege'], required=True) + return p \ No newline at end of file diff --git a/cli/commands/profile.py b/cli/commands/profile.py new file mode 100644 index 0000000..3ac7184 --- /dev/null +++ b/cli/commands/profile.py @@ -0,0 +1,201 @@ +from core.Errors import MissingSubscriptionError, InvalidSubscriptionError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError +from core.controllers.ApplicationVersionController import ApplicationVersionController +from core.controllers.InvoiceController import InvoiceController +from core.controllers.LocationController import LocationController +from core.controllers.ProfileController import ProfileController +from core.controllers.SubscriptionController import SubscriptionController +from core.controllers.SubscriptionPlanController import SubscriptionPlanController +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 cli.helpers import sanitize_profile, parse_application_argument, parse_location_argument +from cli.observers import profile_observer, application_version_observer, connection_observer, invoice_observer +import pprint + +NAME = 'profile' + + +def register(subparsers): + parser = subparsers.add_parser(NAME) + subs = parser.add_subparsers(title='subcommands', dest='subcommand') + + base = _base_parser() + pristine = _pristine_parser() + connection_protection = _connection_protection_parser() + endpoint_verification = _endpoint_verification_parser() + profile_state_protection = _profile_state_protection_parser() + + subs.add_parser('list') + subs.add_parser('show', parents=[base]) + subs.add_parser('destroy', parents=[base]) + subs.add_parser('enable', parents=[base, pristine, connection_protection, endpoint_verification, profile_state_protection]) + subs.add_parser('disable', parents=[base, connection_protection]) + + create_parser = subs.add_parser('create') + create_subs = create_parser.add_subparsers(title='profile_types', dest='profile_type') + + session_parser = create_subs.add_parser('session', parents=[base]) + session_parser.add_argument('--name', '-n', default='') + session_parser.add_argument('--location', '-l', default='') + session_parser.add_argument('--application', '-a', required=True) + session_parser.add_argument('--connection', '-c', dest='connection_type', choices=['system', 'tor', 'wireguard'], default='system') + session_parser.add_argument('--mask-connection', '-m', action='store_true') + session_parser.add_argument('--resolution', '-r', default='1280x720') + + system_parser = create_subs.add_parser('system', parents=[base]) + system_parser.add_argument('--name', '-n', default='') + system_parser.add_argument('--location', '-l', default='') + system_parser.add_argument('--connection', '-c', dest='connection_type', choices=['wireguard'], default='wireguard') + + return parser + + +def handle(arguments, main_parser): + if arguments.subcommand is None: + main_parser.parse_args(['profile', '--help']) + return + + ignore = _build_ignore(arguments) + + if 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': + app_details = parse_application_argument(arguments.application) + app_version = ApplicationVersionController.get(app_details.get('application_code'), app_details.get('version_number')) + if app_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, app_version, connection) + 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 None: + main_parser.error('the following argument should be a valid reference: --id/-i') + 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: + _handle_subscription_error(exception, profile, ignore, arguments, main_parser) + + 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') + + +def _handle_subscription_error(exception, profile, ignore, arguments, main_parser): + 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 in ('1', ''): + 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 None: + raise RuntimeError('The subscription could not be created. Please try again later.') + ProfileController.attach_subscription(profile, potential_subscription) + subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer) + if subscription is None: + raise RuntimeError('The subscription could not be activated. Please try again later.') + 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) + + 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') + + +def _build_ignore(arguments): + 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) + return tuple(ignore) + + +def _base_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--id', '-i', type=int, required=True) + return p + +def _pristine_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--pristine', '-p', action='store_true') + return p + +def _connection_protection_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--without-connection-protection', action='store_true') + return p + +def _endpoint_verification_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--without-endpoint-verification', action='store_true') + return p + +def _profile_state_protection_parser(): + import argparse + p = argparse.ArgumentParser(add_help=False) + p.add_argument('--without-profile-state-protection', action='store_true') + return p \ No newline at end of file diff --git a/cli/commands/sync_update.py b/cli/commands/sync_update.py new file mode 100644 index 0000000..dba61c7 --- /dev/null +++ b/cli/commands/sync_update.py @@ -0,0 +1,22 @@ +from core.controllers.ClientController import ClientController +from core.controllers.ClientVersionController import ClientVersionController +from cli.observers import client_observer, connection_observer + +NAME_SYNC = 'sync' +NAME_UPDATE = 'update' + + +def register(subparsers): + subparsers.add_parser(NAME_SYNC) + subparsers.add_parser(NAME_UPDATE) + + +def handle(arguments, main_parser): + if arguments.command == NAME_SYNC: + ClientController.sync(client_observer=client_observer, connection_observer=connection_observer) + + elif arguments.command == NAME_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) \ No newline at end of file diff --git a/cli/helpers.py b/cli/helpers.py new file mode 100644 index 0000000..ce05250 --- /dev/null +++ b/cli/helpers.py @@ -0,0 +1,33 @@ +from importlib import metadata + + +def sanitize_profile(candidate): + if candidate is not None and candidate.has_subscription(): + candidate.subscription.billing_code = candidate.subscription.get_sanitized_billing_code() + return candidate + + +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 + has_source_files = any(f.parts[0] == 'cli' for f in candidate_files) + is_editable = any('direct_url.json' in str(f) for f in candidate_files) + if has_source_files or is_editable: + 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') diff --git a/cli/observers.py b/cli/observers.py new file mode 100644 index 0000000..a03d151 --- /dev/null +++ b/cli/observers.py @@ -0,0 +1,37 @@ +from core.controllers.ApplicationController import ApplicationController +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 cli.helpers import sanitize_profile +import pprint + +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'))) diff --git a/pyproject.toml b/pyproject.toml index 3d79418..acd12c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", ] dependencies = [ - "sp-hydra-veil-core == 2.2.1", + "sp-hydra-veil-core == 2.3.0", ] [project.urls]