from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface from core.services.networking.general_connection_tools.connection_enable import establish_connection from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting from core.services.networking.systemwide.encrypted_proxy.singbox_runner import end_singbox from core.services.networking.systemwide import killswitch from core.services.subscriptions import subscriptions from core.errors.exceptions import FirewallError from core.models.Result import Result, ResultError from core.observers.TicketObserver import TicketObserver from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket from core.services.prepare_tickets.ticket_tracker import get_tickets_with from core.errors.logger import logger from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.SessionStateController import SessionStateController from core.controllers.SystemStateController import SystemStateController from core.models.BaseProfile import BaseProfile as Profile from core.models.Subscription import Subscription from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.observers.ApplicationVersionObserver import ApplicationVersionObserver from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ProfileObserver import ProfileObserver from core.services.WebServiceApiService import WebServiceApiService from typing import Union, Optional import base64 import re import time class ProfileController: @staticmethod def get(id: int) -> Union[SessionProfile, SystemProfile, None]: return Profile.find_by_id(id) @staticmethod def get_all(): return Profile.all() @staticmethod def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None): profile.save() if profile_observer is not None: profile_observer.notify('created', profile) @staticmethod def enable( profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None): from core.controllers.ConnectionController import ConnectionController # =========== ALREADY ENABLED ============ if ProfileController.is_enabled(profile): if not ProfileStateConflictError in ignore: raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.') else: ProfileController.disable(profile) # =========== PRISTINE ============ if pristine: profile.delete_data() # ============================================================================ # SESSION # ============================================================================ if profile.is_session_profile(): application_version = profile.application_version if not application_version.is_installed(): ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer) try: port_number = establish_connection(profile, ignore=ignore, connection_observer=connection_observer) except ConnectionError: raise ProfileActivationError('The profile could not be enabled.') except ValueError: raise ProfileActivationError('The profile could not be enabled.') if profile_observer is not None: profile_observer.notify('enabled', profile) ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer) # ============================================================================ # SYSTEMWIDE # ============================================================================ if profile.is_system_profile(): try: establish_connection(profile, ignore=ignore, connection_observer=connection_observer) if profile_observer is not None: profile_observer.notify('enabled', profile) except FirewallError: raise except ConnectionError: raise ProfileActivationError('The profile could not be enabled.') except ValueError: raise ProfileActivationError('The profile could not be enabled.') @staticmethod def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, ignore: tuple[type[Exception]] = (), profile_observer: ProfileObserver = None): from core.controllers.ConnectionController import ConnectionController if profile.is_session_profile(): if SessionStateController.exists(profile.id): session_state = SessionStateController.get(profile.id) if session_state is not None: for port_number in session_state.network_port_numbers.tor: ConnectionController.terminate_tor_session_connection(port_number) session_state.dissolve(session_state.id) if profile_observer is not None: profile_observer.notify('disabled', profile, dict( explicitly=explicitly, )) # ============================================================================ # SYSTEMWIDE # ============================================================================ if profile.is_system_profile(): subjects = ProfileController.get_all().values() for subject in subjects: if subject.is_session_profile(): if subject.connection.is_unprotected() and ProfileController.is_enabled(subject) and not ConnectionUnprotectedError in ignore: raise ConnectionUnprotectedError('Disabling this system connection would leave one or more sessions exposed.') if SystemStateController.exists(): system_state = SystemStateController.get() if profile.id != system_state.profile_id: raise ProfileDeactivationError('The profile could not be disabled.') try: # ================= SETTINGS ================= firewall_setting = get_firewall_setting() dns_setting = get_dns_setting() # ======= KILL SYSTEMWIDE ============= if profile.connection.code == "wireguard": terminate_system_connection( firewall_setting=firewall_setting, dns_setting=dns_setting ) elif profile.connection.code in ("hysteria2", "vless"): end_singbox() else: raise ProfileDeactivationError('Unsupported protocol.') # ================= UPDATE UI ================ # if it made it this far, it worked in theory. if profile_observer is not None: profile_observer.notify('disabled', profile, dict( explicitly=explicitly, )) except ConnectionTerminationError: raise ProfileDeactivationError('The profile could not be disabled.') except ValueError: raise ProfileDeactivationError('The profile could not be disabled.') except FirewallError: raise time.sleep(1.0) @staticmethod def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None, ticket_observer: TicketObserver = None, connection_observer: ConnectionObserver = None): #################################### # DESTROY TICKET #################################### which_ticket = profile.ticket # try via lookup: if not which_ticket: logger.info(f"Unable to find the ticket # in the profile's native config for {profile.id}. Checking the ticket tracker JSON") target_subscription = profile.subscription.billing_code ticket_list = get_tickets_with(target_subscription) if len(ticket_list) >= 1: which_ticket = ticket_list[0] logger.info(f"We got the ticket {which_ticket} for profile {profile.id} from the ticket tracker JSON") else: logger.info(f"We were UNABLE to find a ticket with the subscription for profile {profile.id} from the ticket tracker JSON. Proceeding with delete regardless..") # Regardless of how it was acquired, if which_ticket: notification = f"Respawn Started for Ticket {which_ticket}" logger.info(notification) ticket_observer.notify("preparing", subject=notification) respawn_result = respawn_billing_code_into_ticket( profile=profile, which_ticket=which_ticket, ticket_observer=ticket_observer, connection_observer=connection_observer ) if respawn_result.valid: notification = f"Ticket Respawned!" logger.info(notification) ticket_observer.notify("preparing", subject=notification) else: notification = f"Error with Ticket Respawn!" logger.error(f"{notification} {respawn_result.error_type} with message: {respawn_result.message}") ticket_observer.notify("preparing", subject=notification) #################################### # DESTROY PROFILE #################################### ProfileController.disable(profile) profile.delete() if profile_observer is not None: profile_observer.notify('destroyed', profile) @staticmethod def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription): profile.subscription = subscription profile.save() @staticmethod def activate_subscription(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None): from core.controllers.ConnectionController import ConnectionController if profile.has_subscription(): subscription = subscriptions.get_subscription( billing_code=profile.subscription.billing_code, connection_observer=connection_observer ) # legacy: # subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer) if subscription is not None: profile.subscription = subscription profile.save() else: raise InvalidSubscriptionError() else: raise MissingSubscriptionError() @staticmethod def is_enabled(profile: Union[SessionProfile, SystemProfile]): from core.controllers.ConnectionController import ConnectionController if profile.is_session_profile(): session_state = SessionStateController.get_or_new(profile.id) return len(session_state.network_port_numbers.all) > 0 or len(session_state.process_ids) > 0 if profile.is_system_profile(): system_state = SystemStateController.get() if system_state is not None and system_state.profile_id is profile.id: return system_uses_wireguard_interface() return False @staticmethod def get_invoice(profile: Union[SessionProfile, SystemProfile]): if profile.has_subscription(): # return subscriptions.get_invoice(billing_code=profile.subscription.billing_code) return WebServiceApiService.get_invoice(profile.subscription.billing_code) else: return None @staticmethod def attach_proxy_configuration(profile: Union[SessionProfile, SystemProfile]): if profile.is_session_profile() and profile.has_subscription(): proxy_configuration = WebServiceApiService.get_proxy_configuration(profile.subscription.billing_code) if proxy_configuration is not None: profile.attach_proxy_configuration(proxy_configuration) @staticmethod def get_proxy_configuration(profile: Union[SessionProfile, SystemProfile]): if profile.is_session_profile(): return profile.get_proxy_configuration() else: return None @staticmethod def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]): profile.has_proxy_configuration() # @staticmethod # def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None): # from core.controllers.ConnectionController import ConnectionController # if not profile.has_subscription(): # raise MissingSubscriptionError() # if not profile.has_location(): # raise MissingLocationError() # wireguard_keys = ProfileController.__generate_wireguard_keys() # wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.country_code, profile.location.code, profile.subscription.billing_code, wireguard_keys.get('public'), task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer) # if wireguard_configuration is None: # raise InvalidSubscriptionError() # expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE) # wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration) # profile.attach_wireguard_configuration(wireguard_configuration) @staticmethod def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]): return profile.get_wireguard_configuration_path() @staticmethod def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]): return profile.has_wireguard_configuration() # @staticmethod # def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()): # try: # ProfileController.__verify_wireguard_endpoint(profile) # except EndpointVerificationError as error: # if not EndpointVerificationError in ignore: # profile.address_security_incident() # raise error # @staticmethod # def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]): # from cryptography.hazmat.primitives.asymmetric import ed25519 # import base64 # signature = profile.get_wireguard_configuration_metadata('Signature') # wireguard_public_keys = profile.get_wireguard_public_keys() # operator = profile.location.operator # if signature is None: # raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.') # if not wireguard_public_keys: # raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.') # if operator is None: # raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.') # try: # operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key)) # for wireguard_public_key in wireguard_public_keys: # operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8')) # except Exception: # raise EndpointVerificationError('The WireGuard endpoint could not be verified.') # @staticmethod # def __generate_wireguard_keys(): # from cryptography.hazmat.primitives import serialization # from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey # raw_private_key = X25519PrivateKey.generate() # public_key = raw_private_key.public_key().public_bytes( # encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw # ) # private_key = raw_private_key.private_bytes( # encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption() # ) # return dict( # private=base64.b64encode(private_key).decode(), # public=base64.b64encode(public_key).decode() # )