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:
parent
81002cd76d
commit
732b17fb4a
11 changed files with 471 additions and 254 deletions
|
|
@ -35,6 +35,7 @@ class ApplicationController:
|
|||
@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):
|
||||
|
||||
from core.services.profiles.profile_connections import enable_profile, disable_profile
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
|
||||
persistent_state_path = f'{profile.get_data_path()}/persistent-state'
|
||||
|
|
@ -99,7 +100,8 @@ class ApplicationController:
|
|||
if not fork_process_id:
|
||||
|
||||
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)
|
||||
sys.exit()
|
||||
|
|
@ -107,7 +109,8 @@ class ApplicationController:
|
|||
else:
|
||||
|
||||
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
|
||||
def _sync(proxies: Optional[dict] = None):
|
||||
|
|
|
|||
|
|
@ -77,8 +77,11 @@ class ConnectionController:
|
|||
if not ConnectionUnprotectedError in ignore:
|
||||
raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.')
|
||||
else:
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
|
||||
from core.services.profiles.profile_connections import disable_profile
|
||||
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':
|
||||
|
||||
|
|
|
|||
|
|
@ -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.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
|
||||
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.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.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
|
||||
|
|
@ -32,230 +33,55 @@ import time
|
|||
|
||||
|
||||
class ProfileController:
|
||||
# Initialize the singleton repository
|
||||
_repo = ProfileRepository()
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
def get_all():
|
||||
return Profile.all()
|
||||
return ProfileController._repo.get_all()
|
||||
|
||||
@staticmethod
|
||||
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()
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('created', profile)
|
||||
# 2. update cache to prevent drift
|
||||
ProfileController._repo.update_cache(profile)
|
||||
|
||||
# 3. Notify observer
|
||||
if observer is not None:
|
||||
observer.notify(event_type, 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,
|
||||
ticket_observer: TicketObserver = None,
|
||||
max_resolution: Optional[str] = None
|
||||
) -> Result:
|
||||
def delete(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
|
||||
"""Remove the profile from both disk and cache"""
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
# 1. Save to temp variable:
|
||||
profile_id = profile.id
|
||||
|
||||
# =========== 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():
|
||||
|
||||
# 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)
|
||||
# 2. Permanently delete from disk
|
||||
profile.delete()
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('destroyed', profile)
|
||||
# 3. update cache with the temp variable to prevent drift
|
||||
ProfileController._repo.invalidate(profile_id=profile_id)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
|
||||
|
|
@ -289,24 +115,6 @@ class ProfileController:
|
|||
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]):
|
||||
|
|
@ -347,12 +155,3 @@ class ProfileController:
|
|||
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from core.errors.logger import logger
|
|||
from core.models.Result import Result, ResultError
|
||||
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 ProfileType
|
||||
|
||||
|
|
@ -110,7 +111,7 @@ def update_profile(
|
|||
|
||||
logger.info("[UPDATE PROFILE] Passing to Profile model to save..")
|
||||
try:
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
|
||||
return Result(valid=True)
|
||||
except Exception as e:
|
||||
|
|
@ -181,5 +182,5 @@ def change_systemwide_connection(profile: Profile, new_value: str) -> Result:
|
|||
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:
|
||||
|
||||
|
|
@ -109,6 +109,37 @@ class BaseProfile(ABC):
|
|||
|
||||
|
||||
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 ===
|
||||
config_dict = self.to_dict()
|
||||
|
||||
|
|
@ -337,3 +368,8 @@ class BaseProfile(ABC):
|
|||
# if profile['location'].get('time_zone') is not None:
|
||||
# location.time_zone = profile['location']['time_zone']
|
||||
# profile['location'] = location
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ def use_ticket_get_sub(
|
|||
assassins_subscription = Subscription(billing_code=billing_code)
|
||||
profile.subscription = assassins_subscription
|
||||
profile.ticket = which_ticket
|
||||
profile.save()
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
ProfileController.update(profile)
|
||||
return Result(valid=True, data=profile)
|
||||
|
||||
|
||||
|
|
@ -157,7 +158,7 @@ def wipe(
|
|||
profile.name = "assassin"
|
||||
profile.subscription = None
|
||||
profile.ticket = None
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
|
||||
# DELETE the pre-existing WG config:
|
||||
profile.delete_wireguard_configuration()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# from core.controllers.ProfileController import ProfileController
|
||||
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 get_all_unused_tickets
|
||||
|
|
@ -46,7 +47,9 @@ def respawn_profile(
|
|||
|
||||
# Regardless of how it was acquired,
|
||||
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}"
|
||||
logger.info(notification)
|
||||
|
|
@ -78,7 +81,8 @@ def respawn_profile(
|
|||
|
||||
# save the 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.")
|
||||
|
||||
|
||||
|
|
|
|||
54
core/services/profiles/ProfileRepository.py
Normal file
54
core/services/profiles/ProfileRepository.py
Normal 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)
|
||||
|
||||
314
core/services/profiles/profile_connections.py
Normal file
314
core/services/profiles/profile_connections.py
Normal 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")
|
||||
|
|
@ -88,7 +88,8 @@ def activate_subscription(
|
|||
raise InvalidSubscriptionError()
|
||||
|
||||
profile.subscription = subscription
|
||||
profile.save()
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
ProfileController.update(profile)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData
|
||||
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}")
|
||||
|
||||
# Get profile object:
|
||||
profile = Profile.find_by_id(profile_id)
|
||||
profile = ProfileController.get(profile_id)
|
||||
if not profile:
|
||||
error_msg = f"Invalid profile id of {profile_id}"
|
||||
logger.error(error_msg)
|
||||
|
|
@ -315,7 +316,7 @@ def create_individual_invoice(payment_cycle: OverallPaymentCycle, connection_obs
|
|||
expires_at=expires_at,
|
||||
billing_pending=True
|
||||
)
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
payment_cycle.got_invoice = True
|
||||
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)
|
||||
"""
|
||||
print(f"starting lookup of profile id {profile_id}")
|
||||
profile = Profile.find_by_id(profile_id)
|
||||
profile = ProfileController.get(profile_id)
|
||||
if not profile:
|
||||
error_msg = f"Invalid profile id of {profile_id}"
|
||||
logger.error(error_msg)
|
||||
|
|
@ -440,7 +441,7 @@ def get_profile_subscription(profile_id: int, save_pending: Optional[bool] = Fal
|
|||
# Optional save pending flag:
|
||||
if save_pending:
|
||||
profile.billing_pending = True
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
|
||||
print(f"returning profile {profile}")
|
||||
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..")
|
||||
|
||||
# Get real profile object:
|
||||
profile = Profile.find_by_id(profile_id)
|
||||
profile = ProfileController.get(profile_id)
|
||||
if not profile:
|
||||
error_msg = f"Invalid profile id of {profile_id}"
|
||||
logger.error(error_msg)
|
||||
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
|
||||
|
||||
profile.subscription.billing_pending = False
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
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)
|
||||
|
||||
# Get profile object:
|
||||
profile = Profile.find_by_id(profile_id)
|
||||
profile = ProfileController.get(profile_id)
|
||||
if not profile:
|
||||
error_msg = f"Invalid profile id of {profile_id}"
|
||||
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,
|
||||
billing_pending=True
|
||||
)
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
return Result(valid=True)
|
||||
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)}"
|
||||
|
|
@ -542,7 +543,7 @@ def save_subscription(profile: Profile, subscription: Subscription) -> bool:
|
|||
return False
|
||||
|
||||
profile.subscription = subscription
|
||||
profile.save()
|
||||
ProfileController.update(profile)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue