sp-hydra-veil-gui/gui/v2/workers/worker.py
2026-06-16 07:26:10 -04:00

239 lines
10 KiB
Python
Executable file

from datetime import datetime, timezone
from PyQt6.QtCore import QObject, pyqtSignal
from core.controllers.ProfileController import ProfileController
from core.controllers.SubscriptionController import SubscriptionController
from core.controllers.tickets.UseTicketController import (
use_ticket,
do_we_use_a_random_ticket,
)
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import (
CommandNotFoundError,
EndpointVerificationError,
FileIntegrityError,
InvalidSubscriptionError,
MissingSubscriptionError,
ProfileActivationError,
ProfileModificationError,
ProfileStateConflictError,
UnsupportedApplicationVersionError,
)
from gui.v2.actions.locations import location_candidates
from gui.v2.infrastructure.setup_observers import (
application_version_observer,
connection_observer,
profile_observer,
ticket_observer,
)
class Worker(QObject):
update_signal = pyqtSignal(str, bool, int, int, str)
change_page = pyqtSignal(str, bool)
ticket_data_loss = pyqtSignal(str, str)
def __init__(self, profile_data):
self.profile_data = profile_data
super().__init__()
profile_observer.subscribe(
'disabled', lambda event: self.handle_profile_status(event.subject, False))
profile_observer.subscribe(
'enabled', lambda event: self.handle_profile_status(event.subject, True))
self.profile_type = None
self._ticket_error_emitted = False
self._consumed_ticket = None
def run(self):
self.profile = ProfileController.get(int(self.profile_data['id']))
if 'use_ticket' in self.profile_data:
ticket_billing_code = self._consume_ticket(
self.profile_data['use_ticket'],
self.profile_data.get('ticket_location'))
if ticket_billing_code is None:
return
self.profile_data['billing_code'] = ticket_billing_code
elif 'billing_code' not in self.profile_data and self.profile is not None:
if not self._profile_has_valid_subscription():
ticket_billing_code = self._maybe_auto_use_ticket()
if ticket_billing_code is None and self._ticket_error_emitted:
return
if ticket_billing_code:
self.profile_data['billing_code'] = ticket_billing_code
if 'billing_code' in self.profile_data:
subscription = SubscriptionController.get(
self.profile_data['billing_code'], connection_observer=connection_observer)
if subscription is not None:
ProfileController.attach_subscription(
self.profile, subscription)
else:
if self._consumed_ticket is not None:
self._ticket_error_emitted = True
self.ticket_data_loss.emit(
self._consumed_ticket,
str(self.profile_data.get('billing_code', '')),
)
return
self.change_page.emit('The billing code is invalid.', True)
return
if self.profile:
try:
ignore_exceptions = []
if self.profile_data.get('ignore_endpoint_verification', False):
ignore_exceptions.append(EndpointVerificationError)
if self.profile_data.get('ignore_profile_state_conflict', False):
ignore_exceptions.append(ProfileStateConflictError)
ignore_tuple = tuple(ignore_exceptions)
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
application_version_observer=application_version_observer,
connection_observer=connection_observer)
except EndpointVerificationError:
self.update_signal.emit(
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
except (InvalidSubscriptionError, MissingSubscriptionError) as e:
self.change_page.emit(
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
except ProfileActivationError:
self.update_signal.emit(
"The profile could not be enabled", False, None, None, None)
except UnsupportedApplicationVersionError:
self.update_signal.emit(
"The application version in question is not supported", False, None, None, None)
except FileIntegrityError:
self.update_signal.emit(
"Application version file integrity could not be verified.", False, None, None, None)
except ProfileModificationError:
self.update_signal.emit(
"WireGuard configuration could not be attached.", False, None, None, None)
except ProfileStateConflictError:
self.update_signal.emit(
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
except CommandNotFoundError as e:
self.update_signal.emit(str(e.subject), False, -1, None, None)
except Exception as e:
print(e)
self.update_signal.emit(
"An unknown error occurred", False, None, None, None)
else:
self.update_signal.emit(
f"No profile found with ID: {self.profile_data['id']}", False, None, None, None)
def _location_candidates(self, preferred=None):
return location_candidates(self.profile, preferred)
def _profile_has_valid_subscription(self):
try:
sub = getattr(self.profile, 'subscription', None)
if not sub:
return False
expires_at = getattr(sub, 'expires_at', None)
if not expires_at:
return False
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
return expires_at > datetime.now(timezone.utc)
except Exception:
return False
def _maybe_auto_use_ticket(self):
profile_connection_type = self.profile.connection.code
if profile_connection_type != "wireguard":
error_msg = "Tickets are not yet supported for Tor. We are very sorry, but we're not able to develop all features for all products."
print(error_msg)
return None
try:
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
except Exception:
return None
if error_msg:
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {error_msg}', True)
return None
if which_ticket is None or which_ticket == 'error':
return None
return self._consume_ticket(which_ticket, None)
def _consume_ticket(self, which_ticket, which_location):
candidates = self._location_candidates(preferred=which_location)
if not candidates:
self._ticket_error_emitted = True
self.change_page.emit('Could not determine profile location for ticket use.', True)
return None
last_msg = None
for cand in candidates:
try:
outcome = use_ticket(which_ticket, cand, ticket_observer, connection_observer)
except Exception as e:
last_msg = str(e)
continue
if not isinstance(outcome, dict):
last_msg = 'invalid_response'
continue
billing_code = outcome.get('billing_code')
if outcome.get('valid') and billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
if billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
msg = outcome.get('message', 'failed')
last_msg = msg
if msg != 'invalid_location':
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {msg}', True)
return None
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True)
return None
def handle_profile_status(self, profile, is_enabled):
profile_id = profile.id
profile_connection = str(profile.connection.code)
message = self.generate_profile_message(profile, is_enabled)
if isinstance(profile, SessionProfile):
self.profile_type = 1
elif isinstance(profile, SystemProfile):
self.profile_type = 2
else:
self.profile_type = None
self.update_signal.emit(
message, is_enabled, profile_id, self.profile_type, profile_connection)
@staticmethod
def generate_profile_message(profile, is_enabled, idle=False):
profile_id = profile.id
if not profile.subscription or not profile.subscription.expires_at:
return f"Offline. No subscription found."
profile_date = profile.subscription.expires_at
status = 'enabled' if is_enabled else 'disabled'
expiration_date = profile_date.replace(tzinfo=timezone.utc)
time_left = expiration_date - datetime.now(timezone.utc)
days_left = time_left.days
hours_left, remainder = divmod(time_left.seconds, 3600)
formatted_expiration = expiration_date.strftime("%Y-%m-%d %H:%M:%S")
if expiration_date < datetime.now(timezone.utc):
return "Offline. Subscription has expired."
if idle:
return f"Offline. Expires in {days_left} days."
if is_enabled:
return f"Profile {int(profile_id)} {status}. Expires on {formatted_expiration}. Time left: {days_left} days, {hours_left} hours."
else:
return f"Profile {int(profile_id)} {status}"