sp-hydra-veil-core/core/controllers/ProfileController.py

356 lines
15 KiB
Python

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
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,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = 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():
# 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)
# ============================================================================
# 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)
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()
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 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 turn_on_assassin(profile: SessionProfile):
profile.assassin = True
profile.save()
@staticmethod
def turn_off_assassin(profile: SessionProfile):
profile.assassin = False
profile.save()