Introduced new ProfileRepo to cache profiles. All profile access is now coordinated through the ProfileController. And the ProfileController's previous bloat mission-creep of connections, is now moved to it's own service repo

This commit is contained in:
SimplifiedPrivacy 2026-10-15 12:10:49 -04:00
parent 81002cd76d
commit 732b17fb4a
11 changed files with 471 additions and 254 deletions

View file

@ -35,6 +35,7 @@ class ApplicationController:
@staticmethod @staticmethod
def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None, connection_observer: ConnectionObserver = None, ticket_observer: TicketObserver = None): def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None, connection_observer: ConnectionObserver = None, ticket_observer: TicketObserver = None):
from core.services.profiles.profile_connections import enable_profile, disable_profile
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
persistent_state_path = f'{profile.get_data_path()}/persistent-state' persistent_state_path = f'{profile.get_data_path()}/persistent-state'
@ -99,7 +100,8 @@ class ApplicationController:
if not fork_process_id: if not fork_process_id:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer) # ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
disable_profile(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
time.sleep(1.0) time.sleep(1.0)
sys.exit() sys.exit()
@ -107,7 +109,8 @@ class ApplicationController:
else: else:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer) # ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
disable_profile(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
@staticmethod @staticmethod
def _sync(proxies: Optional[dict] = None): def _sync(proxies: Optional[dict] = None):

View file

@ -77,8 +77,11 @@ class ConnectionController:
if not ConnectionUnprotectedError in ignore: if not ConnectionUnprotectedError in ignore:
raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.') raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.')
else: else:
from core.controllers.ProfileController import ProfileController from core.services.profiles.profile_connections import disable_profile
ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer) disable_profile(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
# from core.controllers.ProfileController import ProfileController
# ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
if profile.connection.code == 'tor': if profile.connection.code == 'tor':

View file

@ -1,3 +1,4 @@
from core.services.profiles.ProfileRepository import ProfileRepository
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection 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.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.general_connection_tools.connection_enable import establish_connection from core.services.networking.general_connection_tools.connection_enable import establish_connection
@ -8,8 +9,8 @@ from core.services.subscriptions import subscriptions
from core.errors.exceptions import FirewallError from core.errors.exceptions import FirewallError
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from core.services.assassin.ticket_respawn import respawn_profile # from core.services.assassin.ticket_respawn import respawn_profile
from core.services.assassin import assassin_tools # from core.services.assassin import assassin_tools
from core.errors.logger import logger from core.errors.logger import logger
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
@ -32,230 +33,55 @@ import time
class ProfileController: class ProfileController:
# Initialize the singleton repository
_repo = ProfileRepository()
@staticmethod @staticmethod
def get(id: int) -> Union[SessionProfile, SystemProfile, None]: def get(id: int) -> Union[SessionProfile, SystemProfile, None]:
return Profile.find_by_id(id) # Controller no longer talks to the Model; it talks to the Repo
return ProfileController._repo.get(id)
@staticmethod @staticmethod
def get_all(): def get_all():
return Profile.all() return ProfileController._repo.get_all()
@staticmethod @staticmethod
def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None): def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
# Call the helper with the 'created' event
ProfileController._save_and_sync(profile, 'created', profile_observer)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
# Call the helper with the 'updated' event
ProfileController._save_and_sync(profile, 'updated', profile_observer)
@staticmethod
def _save_and_sync(profile, event_type: str, observer: ProfileObserver = None):
"""Private helper to handle the actual persistence and cache synchronization."""
# 1. Persist to disk
profile.save() profile.save()
if profile_observer is not None: # 2. update cache to prevent drift
profile_observer.notify('created', profile) ProfileController._repo.update_cache(profile)
# 3. Notify observer
if observer is not None:
observer.notify(event_type, profile)
@staticmethod @staticmethod
def enable( def delete(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile: Union[SessionProfile, SystemProfile], """Remove the profile from both disk and cache"""
ignore: tuple[type[Exception]] = (),
pristine: bool = False,
asynchronous: bool = False,
profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = None
) -> Result:
from core.controllers.ConnectionController import ConnectionController # 1. Save to temp variable:
profile_id = profile.id
# =========== ALREADY ENABLED ============ # 2. Permanently delete from disk
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():
# ASSASSIN MODE
if profile.assassin:
assassin_result = assassin_tools.create(
profile=profile,
max_resolution=max_resolution,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with setting up Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileActivationError(f'Assassin NOT enabled: {assassin_result.message}')
# random assassin values become the main profile:
profile = assassin_result.data
# CONCLUSION OF ASSASSIN CODE
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, ticket_observer=ticket_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, ticket_observer=ticket_observer, connection_observer=connection_observer)
return Result(valid=True)
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile():
try:
connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
if connection_result.valid:
if profile_observer is not None:
profile_observer.notify('enabled', profile)
return Result(valid=True)
else:
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
error_msg = connection_result.message
raise ProfileActivationError(error_msg)
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,
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
wipe_assassin: bool = False
):
from core.controllers.ConnectionController import ConnectionController
if profile.is_session_profile():
# START ASSASSIN SECTION
if profile.assassin and wipe_assassin:
print("triggering to WIPE assassin")
assassin_result = assassin_tools.wipe(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with wiping respawn of Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileDeactivationError(f'Assassin NOT disabled: {assassin_result.message}')
# END ASSASSIN SECTION
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
respawned = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawned:
logger.error("Serious issue with respawning the ticket. We might raise an error here.")
####################################
# DESTROY PROFILE
####################################
ProfileController.disable(profile)
profile.delete() profile.delete()
if profile_observer is not None: # 3. update cache with the temp variable to prevent drift
profile_observer.notify('destroyed', profile) ProfileController._repo.invalidate(profile_id=profile_id)
@staticmethod @staticmethod
def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription): def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
@ -289,24 +115,6 @@ class ProfileController:
else: else:
raise MissingSubscriptionError() 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 @staticmethod
def get_invoice(profile: Union[SessionProfile, SystemProfile]): def get_invoice(profile: Union[SessionProfile, SystemProfile]):
@ -347,12 +155,3 @@ class ProfileController:
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]): def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
return profile.has_wireguard_configuration() return profile.has_wireguard_configuration()
@staticmethod
def turn_on_assassin(profile: SessionProfile):
profile.assassin = True
profile.save()
@staticmethod
def turn_off_assassin(profile: SessionProfile):
profile.assassin = False
profile.save()

View file

@ -3,6 +3,7 @@ from core.errors.logger import logger
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.services.networking.systemwide.general_tools.manage_sudo_configs import remove_sudo_config from core.services.networking.systemwide.general_tools.manage_sudo_configs import remove_sudo_config
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import BaseProfile as Profile from core.models.BaseProfile import BaseProfile as Profile
from core.models.BaseProfile import ProfileType from core.models.BaseProfile import ProfileType
@ -110,7 +111,7 @@ def update_profile(
logger.info("[UPDATE PROFILE] Passing to Profile model to save..") logger.info("[UPDATE PROFILE] Passing to Profile model to save..")
try: try:
profile.save() ProfileController.update(profile)
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..") logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
return Result(valid=True) return Result(valid=True)
except Exception as e: except Exception as e:
@ -181,5 +182,5 @@ def change_systemwide_connection(profile: Profile, new_value: str) -> Result:
return Result(valid=True, data=profile) return Result(valid=True, data=profile)
def save_billing_code(profile: Profile, billing_code: str) -> bool: # def save_billing_code(profile: Profile, billing_code: str) -> bool:

View file

@ -109,6 +109,37 @@ class BaseProfile(ABC):
def save(self: Self, app_version_dict: dict = None): def save(self: Self, app_version_dict: dict = None):
"""
First warn via the logs, if the caller is bypassing the Cache and accessing it directly.
Then persist changes in the profile/model to disk.
"""
#--------------------------
# Cache Drift Protection
#--------------------------
# 1. Get the call stack
# stack[0] is this current 'save' method
# stack[1] is the immediate caller
stack = inspect.stack()
caller_frame = stack[1]
caller_function_name = caller_frame.function
caller_module_name = caller_frame.filename # Full path to the file
# 2. Only the Repo or Controller is allowed to save
allowed_callers = ['create', 'update_cache']
is_allowed_file = "core/controllers/ProfileController.py" in caller_module_name or "core/services/profiles/ProfileRepository.py" in caller_module_name
if caller_function_name not in allowed_callers and not is_allowed_file:
logger.warning(
f"ARCHITECTURE VIOLATION: .save() called directly from "
f"function '{caller_function_name}' in {caller_module_name}. "
f"Please route this through ProfileController to ensure cache integrity."
)
#--------------------------
# Actual Save
#--------------------------
# === SERIALIZATION === # === SERIALIZATION ===
config_dict = self.to_dict() config_dict = self.to_dict()
@ -337,3 +368,8 @@ class BaseProfile(ABC):
# if profile['location'].get('time_zone') is not None: # if profile['location'].get('time_zone') is not None:
# location.time_zone = profile['location']['time_zone'] # location.time_zone = profile['location']['time_zone']
# profile['location'] = location # profile['location'] = location

View file

@ -129,7 +129,8 @@ def use_ticket_get_sub(
assassins_subscription = Subscription(billing_code=billing_code) assassins_subscription = Subscription(billing_code=billing_code)
profile.subscription = assassins_subscription profile.subscription = assassins_subscription
profile.ticket = which_ticket profile.ticket = which_ticket
profile.save() from core.controllers.ProfileController import ProfileController
ProfileController.update(profile)
return Result(valid=True, data=profile) return Result(valid=True, data=profile)
@ -157,7 +158,7 @@ def wipe(
profile.name = "assassin" profile.name = "assassin"
profile.subscription = None profile.subscription = None
profile.ticket = None profile.ticket = None
profile.save() ProfileController.update(profile)
# DELETE the pre-existing WG config: # DELETE the pre-existing WG config:
profile.delete_wireguard_configuration() profile.delete_wireguard_configuration()

View file

@ -1,3 +1,4 @@
# from core.controllers.ProfileController import ProfileController
from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket
from core.services.prepare_tickets.ticket_tracker import find_ticket_by_sub from core.services.prepare_tickets.ticket_tracker import find_ticket_by_sub
from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets
@ -46,7 +47,9 @@ def respawn_profile(
# Regardless of how it was acquired, # Regardless of how it was acquired,
if not which_ticket: if not which_ticket:
return Result(valid=False, error_type=ResultError.TICKET, message="This profile is not associated with a ticket. And/or you lack a ticket slot to respawn.") error_msg = "This profile is not associated with a ticket. And/or you lack a ticket slot to respawn."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.TICKET, message=error_msg)
notification = f"Respawn Started for Ticket {which_ticket}" notification = f"Respawn Started for Ticket {which_ticket}"
logger.info(notification) logger.info(notification)
@ -78,7 +81,8 @@ def respawn_profile(
# save the ticket, # save the ticket,
profile.ticket = which_ticket profile.ticket = which_ticket
profile.save() from core.controllers.ProfileController import ProfileController
ProfileController.update(profile)
return Result(valid=True, message="This profile fully respawned and had it's billing code wiped.") return Result(valid=True, message="This profile fully respawned and had it's billing code wiped.")

View file

@ -0,0 +1,54 @@
from core.models.BaseProfile import BaseProfile
# generic
from typing import Dict, List, Union, Optional
from threading import Lock
class ProfileRepository:
"""
Singleton Repository that mediates between the Controller and the Model.
Handles caching logic to prevent redundant file system hits.
"""
_instance = None
_lock = Lock() # Ensure singleton thread-safety
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super(ProfileRepository, cls).__new__(cls)
# The actual cache: { profile_id: profile_object }
cls._instance._cache: Dict[int, any] = {}
return cls._instance
def get(self, profile_id: int):
# 1. Check cache first
if profile_id in self._cache:
return self._cache[profile_id]
# 2. Fallback to disk via the Model
profile = BaseProfile.find_by_id(profile_id)
# 3. Update cache for next time
if profile:
self._cache[profile_id] = profile
return profile
def get_all(self):
# First, get the current list of IDs from the model, to avoid drift, from new files added to disk
all_profiles = BaseProfile.all()
# Update our internal cache with everything currently on disk
for p in all_profiles:
self._cache[id] = p
return all_profiles
def update_cache(self, profile):
"""Explicitly updates the cache to prevent drift after a save."""
self._cache[profile.id] = profile
def invalidate(self, profile_id: int):
"""Removes an item from cache if it's deleted from disk."""
self._cache.pop(profile_id, None)

View file

@ -0,0 +1,314 @@
from core.controllers.ConnectionController import ConnectionController
from core.controllers.ProfileController import ProfileController
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.services.assassin.ticket_respawn import respawn_profile
from core.services.assassin import assassin_tools
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
# generic
from typing import Union, Optional
import base64
import re
import time
def is_enabled(profile: Union[SessionProfile, SystemProfile]) -> int | None:
"""Return the quantity of profiles enabled, or False if none are."""
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
def enable_session_profile(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
asynchronous: bool = False,
profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = None
) -> Result:
"""
Launch a session profile.
1) by starting a connnection on a port,
2) and then starting the application using that networking port
"""
# ASSASSIN MODE
if profile.assassin:
assassin_result = assassin_tools.create(
profile=profile,
max_resolution=max_resolution,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with setting up Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileActivationError(f'Assassin NOT enabled: {assassin_result.message}')
# random assassin values become the main profile:
profile = assassin_result.data
# CONCLUSION OF ASSASSIN CODE
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, ticket_observer=ticket_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, ticket_observer=ticket_observer, connection_observer=connection_observer)
return Result(valid=True)
def enable_profile(
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,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = None
) -> Result:
"""Orchestrate Enabling both types of profiles"""
# =========== ALREADY ENABLED ============
if 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():
return enable_session_profile(
profile=profile,
ignore=ignore,
asynchronous=asynchronous,
profile_observer=profile_observer,
application_version_observer=application_version_observer,
connection_observer=connection_observer,
ticket_observer=ticket_observer,
max_resolution=max_resolution
)
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile():
try:
connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
if connection_result.valid:
if profile_observer is not None:
profile_observer.notify('enabled', profile)
return Result(valid=True)
else:
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
error_msg = connection_result.message
raise ProfileActivationError(error_msg)
except FirewallError:
raise
except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.')
except ValueError:
raise ProfileActivationError('The profile could not be enabled.')
def disable_profile(
profile: Union[SessionProfile, SystemProfile],
explicitly: bool = True,
ignore: tuple[type[Exception]] = (),
profile_observer: ProfileObserver = None,
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
wipe_assassin: bool = False
) -> Result:
"""Orchestrate disabling a profile."""
if profile.is_session_profile():
# START ASSASSIN SECTION
if profile.assassin and wipe_assassin:
print("triggering to WIPE assassin")
assassin_result = assassin_tools.wipe(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with wiping respawn of Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileDeactivationError(f'Assassin NOT disabled: {assassin_result.message}')
# END ASSASSIN SECTION
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 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)
def turn_on_assassin(profile: SessionProfile):
profile.assassin = True
ProfileController.update(profile)
def turn_off_assassin(profile: SessionProfile):
profile.assassin = False
ProfileController.update(profile)
def destroy_profile(
profile: Union[SessionProfile, SystemProfile],
profile_observer: ProfileObserver = None,
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None
) -> Result:
"""
Permanently end a profile, by:
1) Respawning it's ticket if it has one
2) Disabling it,
3) Destroying it on disk & cache
"""
####################################
# RESPAWN TICKET
####################################
which_ticket = profile.ticket
respawn_result = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawn_result.valid:
logger.error(f"Unable to respawn the ticket. {respawn_result.message} {respawn_result.error_type}")
####################################
# DESTROY PROFILE
####################################
logger.info("Disabling profile")
disable_profile(profile)
logger.info("Wiping profile from disk..")
ProfileController.delete(profile)
if profile_observer is not None:
profile_observer.notify('destroyed', profile)
return Result(valid=True, message="Deleted Profile")

View file

@ -88,7 +88,8 @@ def activate_subscription(
raise InvalidSubscriptionError() raise InvalidSubscriptionError()
profile.subscription = subscription profile.subscription = subscription
profile.save() from core.controllers.ProfileController import ProfileController
ProfileController.update(profile)
return True return True

View file

@ -1,4 +1,5 @@
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import BaseProfile as Profile from core.models.BaseProfile import BaseProfile as Profile
from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData
from core.models.enum_models import ProtocolChoice, PaymentCategory from core.models.enum_models import ProtocolChoice, PaymentCategory
@ -242,7 +243,7 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
logger.info(f"creating an individual invoice for profile {profile_id}") logger.info(f"creating an individual invoice for profile {profile_id}")
# Get profile object: # Get profile object:
profile = Profile.find_by_id(profile_id) profile = ProfileController.get(profile_id)
if not profile: if not profile:
error_msg = f"Invalid profile id of {profile_id}" error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg) logger.error(error_msg)
@ -315,7 +316,7 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
expires_at=expires_at, expires_at=expires_at,
billing_pending=True billing_pending=True
) )
profile.save() ProfileController.update(profile)
payment_cycle.got_invoice = True payment_cycle.got_invoice = True
return Result(valid=True, data=final_invoice) return Result(valid=True, data=final_invoice)
@ -430,7 +431,7 @@ def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = Fal
Optional flag to SAVE that the billing is pending. (metaphor of a POST request) Optional flag to SAVE that the billing is pending. (metaphor of a POST request)
""" """
print(f"starting lookup of profile id {profile_id}") print(f"starting lookup of profile id {profile_id}")
profile = Profile.find_by_id(profile_id) profile = ProfileController.get(profile_id)
if not profile: if not profile:
error_msg = f"Invalid profile id of {profile_id}" error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg) logger.error(error_msg)
@ -440,7 +441,7 @@ def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = Fal
# Optional save pending flag: # Optional save pending flag:
if save_pending: if save_pending:
profile.billing_pending = True profile.billing_pending = True
profile.save() ProfileController.update(profile)
print(f"returning profile {profile}") print(f"returning profile {profile}")
return profile.subscription return profile.subscription
@ -490,14 +491,14 @@ def mark_profile_as_paid(profile_id: int):
logger.info(f"Marking profile_id {str(profile_id)} as paid..") logger.info(f"Marking profile_id {str(profile_id)} as paid..")
# Get real profile object: # Get real profile object:
profile = Profile.find_by_id(profile_id) profile = ProfileController.get(profile_id)
if not profile: if not profile:
error_msg = f"Invalid profile id of {profile_id}" error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg) logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT) return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
profile.subscription.billing_pending = False profile.subscription.billing_pending = False
profile.save() ProfileController.update(profile)
return Result(valid=True) return Result(valid=True)
@ -513,7 +514,7 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
return Result(valid=False, message=error_msg, error_type=ResultError.MISSING_DATA) return Result(valid=False, message=error_msg, error_type=ResultError.MISSING_DATA)
# Get profile object: # Get profile object:
profile = Profile.find_by_id(profile_id) profile = ProfileController.get(profile_id)
if not profile: if not profile:
error_msg = f"Invalid profile id of {profile_id}" error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg) logger.error(error_msg)
@ -525,7 +526,7 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
expires_at=expires_at, expires_at=expires_at,
billing_pending=True billing_pending=True
) )
profile.save() ProfileController.update(profile)
return Result(valid=True) return Result(valid=True)
except Exception as e: except Exception as e:
error_msg = f"Legacy system wouldn't create or save Subscription object with {str(billing_code)} and {str(expires_at)} or type {type(expires_at)}, with error: {str(e)}" error_msg = f"Legacy system wouldn't create or save Subscription object with {str(billing_code)} and {str(expires_at)} or type {type(expires_at)}, with error: {str(e)}"
@ -542,7 +543,7 @@ def save_subscription(profile: Profile, subscription: Subscription) -> bool:
return False return False
profile.subscription = subscription profile.subscription = subscription
profile.save() ProfileController.update(profile)
return True return True