Compare commits

..

8 commits

34 changed files with 143 additions and 766 deletions

6
.env
View file

@ -1,6 +0,0 @@
# Environment: local | production
APP_ENV=local
# API URLs
SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
SP_API_BASE_URL_PRODUCTION=https://api.hydraveil.net/api/v1

View file

@ -1,49 +1,57 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Final from typing import Final
from dotenv import load_dotenv
import os import os
load_dotenv(os.path.join(os.path.dirname(__file__), '../.env'))
_env = os.environ.get('APP_ENV', 'production')
_sp_api_base_url = (
os.environ.get('SP_API_BASE_URL_LOCAL', 'http://simplifiedprivacy.test/api/v1')
if _env == 'local'
else os.environ.get('SP_API_BASE_URL_PRODUCTION', 'https://api.hydraveil.net/api/v1')
)
@dataclass(frozen=True) @dataclass(frozen=True)
class Constants: class Constants:
# ticketing group:
TICKET_API_BASE_URL: Final[str] = os.environ.get( TICKET_API_BASE_URL: Final[str] = os.environ.get(
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net" "TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
) )
SP_API_BASE_URL: Final[str] = _sp_api_base_url
PING_URL: Final[str] = os.environ.get('PING_URL', f'{_sp_api_base_url}/health') SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')
PING_URL: Final[str] = os.environ.get('PING_URL', 'https://api.hydraveil.net/api/v1/health')
CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5')) CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5'))
MAX_CONNECTION_ATTEMPTS: Final[int] = int(os.environ.get('MAX_CONNECTION_ATTEMPTS', '2')) MAX_CONNECTION_ATTEMPTS: Final[int] = int(os.environ.get('MAX_CONNECTION_ATTEMPTS', '2'))
HV_CLIENT_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH') HV_CLIENT_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH')
HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER') HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER')
HOME: Final[str] = os.path.expanduser('~') HOME: Final[str] = os.path.expanduser('~')
SYSTEM_CONFIG_PATH: Final[str] = '/etc' SYSTEM_CONFIG_PATH: Final[str] = '/etc'
CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', os.path.join(HOME, '.cache')) CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', os.path.join(HOME, '.cache'))
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', os.path.join(HOME, '.local/share')) DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', os.path.join(HOME, '.local/share'))
STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', os.path.join(HOME, '.local/state')) STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', os.path.join(HOME, '.local/state'))
HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil' HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil'
HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil' HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil'
HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil' HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil'
HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil' HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil'
HV_STATE_HOME: Final[str] = f'{STATE_HOME}/hydra-veil' HV_STATE_HOME: Final[str] = f'{STATE_HOME}/hydra-veil'
HV_SYSTEM_PROFILE_CONFIG_PATH: Final[str] = f'{HV_SYSTEM_CONFIG_PATH}/profiles' HV_SYSTEM_PROFILE_CONFIG_PATH: Final[str] = f'{HV_SYSTEM_CONFIG_PATH}/profiles'
HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles' HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles'
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles' HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles'
# ticketing group:
HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing" HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data" HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data"
HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications' HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents' HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'
HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime' HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime'
HV_STORAGE_DATABASE_PATH: Final[str] = f'{HV_DATA_HOME}/storage.db' HV_STORAGE_DATABASE_PATH: Final[str] = f'{HV_DATA_HOME}/storage.db'
HV_CAPABILITY_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/apparmor.d/hydra-veil' HV_CAPABILITY_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/apparmor.d/hydra-veil'
HV_PRIVILEGE_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/sudoers.d/hydra-veil' HV_PRIVILEGE_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/sudoers.d/hydra-veil'
HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions'
HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor' HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'

View file

@ -25,7 +25,7 @@ class ConfigurationController:
configuration = ConfigurationController.get() configuration = ConfigurationController.get()
if configuration is None or configuration.connection not in ('system', 'tor', 'vless', 'hysteria2'): if configuration is None or configuration.connection not in ('system', 'tor'):
raise UnknownConnectionTypeError('The preferred connection type could not be determined.') raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
return configuration.connection return configuration.connection

View file

@ -23,7 +23,6 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from core.models.OperatorProxySession import OperatorProxySession
class ConnectionController: class ConnectionController:
@ -66,32 +65,6 @@ class ConnectionController:
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
if connection.needs_operator_proxy():
if profile.has_subscription():
if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
operator_proxy_session = ConnectionController.with_preferred_connection(
profile.subscription.billing_code,
profile.subscription.operator_id,
connection.get_protocol(),
task=WebServiceApiService.post_operator_proxy,
connection_observer=connection_observer
)
if operator_proxy_session is None:
raise InvalidSubscriptionError()
profile.attach_operator_proxy_session(operator_proxy_session)
else:
raise MissingSubscriptionError()
if profile.has_subscription(): if profile.has_subscription():
if not profile.subscription.has_been_activated(): if not profile.subscription.has_been_activated():
@ -177,24 +150,6 @@ class ConnectionController:
ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number) ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number)
session_state.network_port_numbers.wireguard.append(port_number) session_state.network_port_numbers.wireguard.append(port_number)
elif profile.connection.code in ('vless', 'hysteria2'):
if not profile.has_operator_proxy_session():
raise MissingSubscriptionError()
operator_proxy_session = profile.get_operator_proxy_session()
port_number = ConnectionService.get_random_available_port_number()
if profile.connection.code == 'vless':
from core.controllers.encrypted_proxy.VlessController import VlessController
VlessController.enable(operator_proxy_session, port_number)
session_state.network_port_numbers.vless.append(port_number)
elif profile.connection.code == 'hysteria2':
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
HysteriaController.enable(operator_proxy_session, port_number)
session_state.network_port_numbers.hysteria2.append(port_number)
if profile.connection.masked: if profile.connection.masked:
while proxy_port_number is None or proxy_port_number == port_number: while proxy_port_number is None or proxy_port_number == port_number:
@ -209,6 +164,7 @@ class ConnectionController:
SessionStateController.update_or_create(session_state) SessionStateController.update_or_create(session_state)
return proxy_port_number or port_number return proxy_port_number or port_number
@staticmethod @staticmethod
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):

View file

@ -1,17 +0,0 @@
from pathlib import Path
from core.services.encrypted_proxy.disable_service import disable_proxy
class DisableController:
def __init__(self, tmp_dir: Path, wrapper: str, unit: str):
self.tmp_dir = tmp_dir
self.wrapper = wrapper
self.unit = unit
def disable(self, observer=None) -> bool:
return disable_proxy(
tmp_dir=self.tmp_dir,
wrapper=self.wrapper,
unit=self.unit,
observer=observer,
)

View file

@ -1,26 +0,0 @@
from core.services.encrypted_proxy.hysteria_service import enable_hysteria, disable_hysteria
class HysteriaController:
def __init__(self, socks5_port: int):
self.socks5_port = socks5_port
def enable(self, username: str, password: str,
server_host: str, observer=None) -> bool:
if not username or not isinstance(username, str):
if observer:
observer.notify("error", "Invalid username")
return False
if not password or not server_host:
if observer:
observer.notify("error", "Missing password or server_host")
return False
return enable_hysteria(
username=username,
password=password,
server_host=server_host,
socks5_port=self.socks5_port,
observer=observer,
)
def disable(self, observer=None) -> bool:
return disable_hysteria(observer=observer)

View file

@ -1,24 +0,0 @@
from core.services.encrypted_proxy.vless_service import enable_vless, disable_vless
class VlessController:
def __init__(self, socks5_port: int):
self.socks5_port = socks5_port
def enable(self, vless_link: str, username: str, observer=None) -> bool:
if not username or not isinstance(username, str):
if observer:
observer.notify("error", "Invalid username")
return False
if not vless_link or not vless_link.startswith("vless://"):
if observer:
observer.notify("error", "Invalid vless link")
return False
return enable_vless(
vless_link=vless_link,
username=username,
socks5_port=self.socks5_port,
observer=observer,
)
def disable(self, observer=None) -> bool:
return disable_vless(observer=observer)

View file

@ -13,12 +13,14 @@ from core.services.payment_phase.check_if_paid import _check_if_paid
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.services.networking.send_data_to_server import send_data_to_server from core.services.networking.send_data_to_server import send_data_to_server
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice # from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice
from core.services.helpers.valid_profile_quantity import valid_profile_quantity from core.services.helpers.valid_profile_quantity import valid_profile_quantity
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
from core.controllers.tickets.TicketSyncController import sync_ticket_prices from core.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
""" """
Inputs: Which plan (key), which crypto, and how many profiles Inputs: Which plan (key), which crypto, and how many profiles
@ -47,6 +49,12 @@ def initiate_payment(
invoice_data_object.add_error_code("already_exists") invoice_data_object.add_error_code("already_exists")
return invoice_data_object return invoice_data_object
billing_id = do_we_have_billing_id()
if billing_id:
invoice_data_object.add_error_code("billing_code_exists")
invoice_data_object.temp_billing_code = billing_id
return invoice_data_object
rejected_choices = [None, "", False] rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices: if how_many_profiles in rejected_choices:
@ -67,18 +75,6 @@ def initiate_payment(
invoice_data_object.add_error_code("no_keyplan") invoice_data_object.add_error_code("no_keyplan")
return invoice_data_object return invoice_data_object
# confirm the key choice is among the choices from their sync file,
# and if not, then sync again, and try the results from that new file,
is_valid_key = confirm_its_a_valid_key_choice(which_key, ticket_observer)
if not is_valid_key:
sync_results = sync_ticket_prices(ticket_observer, connection_observer)
second_try_to_match = confirm_its_a_valid_key_choice(
which_key, ticket_observer
)
if not second_try_to_match:
invoice_data_object.add_error_code("invalid_key")
return invoice_data_object
# get & save the public key: # get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local") public_key_results = get_pub_key(which_key, connection_observer, "local")

View file

@ -51,6 +51,9 @@ def prepare_tickets(
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
notification = "Preparing Cryptography Locally"
ticket_observer.notify("preparing", subject=notification)
# ok now we have the pre-reqs, let's use this high level orchestrator, # ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_orchestrator( prep_results = ticket_prep_orchestrator(
how_many_profiles, ticket_observer, connection_observer how_many_profiles, ticket_observer, connection_observer

View file

@ -8,7 +8,6 @@ if TYPE_CHECKING:
from core.Constants import Constants from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.networking.get_data_from_server import get_data_from_server from core.services.networking.get_data_from_server import get_data_from_server
from core.services.helpers.save_sync_results import save_sync_results
from core.errors.logger import logger from core.errors.logger import logger
@ -38,7 +37,4 @@ def sync_ticket_prices(
except: except:
return {"valid": False, "error_code": "sync_failed"} return {"valid": False, "error_code": "sync_failed"}
did_it_save = save_sync_results(sync_results)
logger.debug(f"Inside the sync controller, did_it_save is {did_it_save}")
return sync_results return sync_results

View file

@ -15,5 +15,3 @@ class BaseConnection:
def is_system_connection(self): def is_system_connection(self):
return type(self).__name__ == 'SystemConnection' return type(self).__name__ == 'SystemConnection'
def needs_operator_proxy(self):
return False

View file

@ -6,7 +6,6 @@ _table_name: str = 'operators'
_table_definition: str = """ _table_definition: str = """
'id' int UNIQUE, 'id' int UNIQUE,
'name' varchar, 'name' varchar,
'type' varchar,
'public_key' varchar, 'public_key' varchar,
'nostr_public_key' varchar, 'nostr_public_key' varchar,
'nostr_profile_reference' varchar, 'nostr_profile_reference' varchar,
@ -18,18 +17,11 @@ _table_definition: str = """
class Operator(Model): class Operator(Model):
id: int id: int
name: str name: str
type: str
public_key: str public_key: str
nostr_public_key: str nostr_public_key: str
nostr_profile_reference: str nostr_profile_reference: str
nostr_attestation_event_reference: str nostr_attestation_event_reference: str
def is_external(self) -> bool:
return self.type == 'external'
def is_internal(self) -> bool:
return self.type == 'internal'
@staticmethod @staticmethod
def find_by_id(id: int): def find_by_id(id: int):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition) Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
@ -52,7 +44,7 @@ class Operator(Model):
@staticmethod @staticmethod
def save_many(operators): def save_many(operators):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition) Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
Model._insert_many('INSERT INTO operators VALUES(?, ?, ?, ?, ?, ?, ?)', Operator.tuple_factory, operators) Model._insert_many('INSERT INTO operators VALUES(?, ?, ?, ?, ?, ?)', Operator.tuple_factory, operators)
@staticmethod @staticmethod
def factory(cursor, row): def factory(cursor, row):
@ -61,4 +53,4 @@ class Operator(Model):
@staticmethod @staticmethod
def tuple_factory(operator): def tuple_factory(operator):
return operator.id, operator.name, operator.type, operator.public_key, operator.nostr_public_key, operator.nostr_profile_reference, operator.nostr_attestation_event_reference return operator.id, operator.name, operator.public_key, operator.nostr_public_key, operator.nostr_profile_reference, operator.nostr_attestation_event_reference

View file

@ -1,19 +0,0 @@
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from typing import Optional
@dataclass_json
@dataclass
class OperatorProxySession:
id: int
type: str
username: Optional[str]
password: Optional[str]
links: Optional[list]
subscription_url: Optional[str]
operator_id: int
operator_name: str
operator_domain: Optional[str] = None
operator_hysteria2_host: Optional[str] = None
operator_vless_host: Optional[str] = None

View file

@ -10,13 +10,6 @@ import dataclasses_json
@dataclass @dataclass
class Subscription: class Subscription:
billing_code: str billing_code: str
operator_id: Optional[int] = field(
default=None,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
expires_at: Optional[datetime] = field( expires_at: Optional[datetime] = field(
default=None, default=None,
metadata=config( metadata=config(
@ -44,4 +37,4 @@ class Subscription:
@staticmethod @staticmethod
def _iso_format(datetime_instance: datetime): def _iso_format(datetime_instance: datetime):
return datetime.isoformat(datetime_instance).replace('+00:00', 'Z') return datetime.isoformat(datetime_instance).replace('+00:00', 'Z')

View file

@ -6,13 +6,11 @@ class NetworkPortNumbers:
proxy: list[int] = field(default_factory=list) proxy: list[int] = field(default_factory=list)
wireguard: list[int] = field(default_factory=list) wireguard: list[int] = field(default_factory=list)
tor: list[int] = field(default_factory=list) tor: list[int] = field(default_factory=list)
vless: list[int] = field(default_factory=list)
hysteria2: list[int] = field(default_factory=list)
@property @property
def all(self): def all(self):
return self.proxy + self.wireguard + self.tor + self.vless + self.hysteria2 return self.proxy + self.wireguard + self.tor
@property @property
def isolated(self): def isolated(self):
return self.proxy + self.wireguard + self.vless + self.hysteria2 return self.proxy + self.wireguard

View file

@ -1,12 +1,14 @@
from core.models.BaseConnection import BaseConnection from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass from dataclasses import dataclass
@dataclass @dataclass
class SessionConnection(BaseConnection): class SessionConnection(BaseConnection):
masked: bool = False masked: bool = False
def __post_init__(self): def __post_init__(self):
if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'):
if self.code not in ('system', 'tor', 'wireguard'):
raise ValueError('Invalid connection code.') raise ValueError('Invalid connection code.')
def is_unprotected(self): def is_unprotected(self):
@ -14,9 +16,3 @@ class SessionConnection(BaseConnection):
def needs_proxy_configuration(self): def needs_proxy_configuration(self):
return self.masked is True return self.masked is True
def needs_operator_proxy(self):
return self.code in ('vless', 'hysteria2')
def get_protocol(self):
return self.code if self.needs_operator_proxy() else None

View file

@ -22,24 +22,35 @@ class SessionProfile(BaseProfile):
return self.connection is not None return self.connection is not None
def save(self): def save(self):
if 'application_version' in self._get_dirty_keys(): if 'application_version' in self._get_dirty_keys():
persistent_state_path = f'{self.get_data_path()}/persistent-state' persistent_state_path = f'{self.get_data_path()}/persistent-state'
if os.path.isdir(persistent_state_path): if os.path.isdir(persistent_state_path):
shutil.rmtree(persistent_state_path, ignore_errors=True) shutil.rmtree(persistent_state_path, ignore_errors=True)
if 'location' in self._get_dirty_keys(): if 'location' in self._get_dirty_keys():
self.__delete_proxy_configuration() self.__delete_proxy_configuration()
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
super().save() super().save()
def attach_proxy_configuration(self, proxy_configuration): def attach_proxy_configuration(self, proxy_configuration):
proxy_configuration_file_contents = f'{proxy_configuration.to_json(indent=4)}\n' proxy_configuration_file_contents = f'{proxy_configuration.to_json(indent=4)}\n'
os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True) os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
proxy_configuration_file_path = self.get_proxy_configuration_path() proxy_configuration_file_path = self.get_proxy_configuration_path()
with open(proxy_configuration_file_path, 'w') as proxy_configuration_file: with open(proxy_configuration_file_path, 'w') as proxy_configuration_file:
proxy_configuration_file.write(proxy_configuration_file_contents) proxy_configuration_file.write(proxy_configuration_file_contents)
def attach_wireguard_configuration(self, wireguard_configuration): def attach_wireguard_configuration(self, wireguard_configuration):
wireguard_configuration_file_path = self.get_wireguard_configuration_path() wireguard_configuration_file_path = self.get_wireguard_configuration_path()
with open(wireguard_configuration_file_path, 'w') as wireguard_configuration_file: with open(wireguard_configuration_file_path, 'w') as wireguard_configuration_file:
wireguard_configuration_file.write(wireguard_configuration) wireguard_configuration_file.write(wireguard_configuration)
@ -50,15 +61,19 @@ class SessionProfile(BaseProfile):
return f'{self.get_config_path()}/wg.conf' return f'{self.get_config_path()}/wg.conf'
def get_proxy_configuration(self): def get_proxy_configuration(self):
try: try:
config_file_contents = open(self.get_proxy_configuration_path(), 'r').read() config_file_contents = open(self.get_proxy_configuration_path(), 'r').read()
except FileNotFoundError: except FileNotFoundError:
return None return None
try: try:
proxy_configuration = json.loads(config_file_contents) proxy_configuration = json.loads(config_file_contents)
except ValueError: except ValueError:
return None return None
proxy_configuration = ProxyConfiguration.from_dict(proxy_configuration) proxy_configuration = ProxyConfiguration.from_dict(proxy_configuration)
return proxy_configuration return proxy_configuration
def has_proxy_configuration(self): def has_proxy_configuration(self):
@ -68,26 +83,36 @@ class SessionProfile(BaseProfile):
return os.path.isfile(f'{self.get_config_path()}/wg.conf') return os.path.isfile(f'{self.get_config_path()}/wg.conf')
def address_security_incident(self): def address_security_incident(self):
super().address_security_incident() super().address_security_incident()
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
def determine_timezone(self): def determine_timezone(self):
time_zone = None time_zone = None
if self.has_connection(): if self.has_connection():
if self.connection.needs_proxy_configuration(): if self.connection.needs_proxy_configuration():
if self.has_proxy_configuration(): if self.has_proxy_configuration():
time_zone = self.get_proxy_configuration().time_zone time_zone = self.get_proxy_configuration().time_zone
elif self.connection.needs_wireguard_configuration(): elif self.connection.needs_wireguard_configuration():
if self.has_wireguard_configuration(): if self.has_wireguard_configuration():
time_zone = self.get_wireguard_configuration_metadata('TZ') time_zone = self.get_wireguard_configuration_metadata('TZ')
if time_zone is None and self.has_location(): if time_zone is None and self.has_location():
time_zone = self.location.time_zone time_zone = self.location.time_zone
if time_zone is None: if time_zone is None:
raise UnknownTimeZoneError('The preferred time zone could not be determined.') raise UnknownTimeZoneError('The preferred time zone could not be determined.')
return time_zone return time_zone
def __delete_proxy_configuration(self): def __delete_proxy_configuration(self):
Path(self.get_proxy_configuration_path()).unlink(missing_ok=True) Path(self.get_proxy_configuration_path()).unlink(missing_ok=True)
def __delete_wireguard_configuration(self): def __delete_wireguard_configuration(self):
Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True) Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True)

View file

@ -4,46 +4,64 @@ from core.models.BaseProfile import BaseProfile
from core.models.system.SystemConnection import SystemConnection from core.models.system.SystemConnection import SystemConnection
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import json
import os import os
import shutil import shutil
import subprocess import subprocess
@dataclass @dataclass
class SystemProfile(BaseProfile): class SystemProfile(BaseProfile):
connection: Optional[SystemConnection] connection: Optional[SystemConnection]
def get_system_config_path(self): def get_system_config_path(self):
return self.__get_system_config_path(self.id) filepath = self.__get_system_config_path(self.id)
the_id = self.id
return filepath
def save(self): def save(self):
if 'location' in self._get_dirty_keys(): if 'location' in self._get_dirty_keys():
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
super().save() super().save()
def attach_wireguard_configuration(self, wireguard_configuration): def attach_wireguard_configuration(self, wireguard_configuration):
if shutil.which('pkexec') is None: if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec') raise CommandNotFoundError('pkexec')
wireguard_configuration_file_backup_path = f'{self.get_config_path()}/wg.conf.bak' wireguard_configuration_file_backup_path = f'{self.get_config_path()}/wg.conf.bak'
with open(wireguard_configuration_file_backup_path, 'w') as wireguard_configuration_file: with open(wireguard_configuration_file_backup_path, 'w') as wireguard_configuration_file:
wireguard_configuration_file.write(wireguard_configuration) wireguard_configuration_file.write(wireguard_configuration)
wireguard_configuration_is_attached = False wireguard_configuration_is_attached = False
failed_attempt_count = 0 failed_attempt_count = 0
while not wireguard_configuration_is_attached and failed_attempt_count < 3: while not wireguard_configuration_is_attached and failed_attempt_count < 3:
process = subprocess.Popen(('pkexec', 'install', '-D', wireguard_configuration_file_backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744')) process = subprocess.Popen(('pkexec', 'install', '-D', wireguard_configuration_file_backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744'))
wireguard_configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8) wireguard_configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not wireguard_configuration_is_attached: if not wireguard_configuration_is_attached:
failed_attempt_count += 1 failed_attempt_count += 1
if not wireguard_configuration_is_attached: if not wireguard_configuration_is_attached:
raise ProfileModificationError('The WireGuard configuration could not be attached.') raise ProfileModificationError('The WireGuard configuration could not be attached.')
def get_wireguard_configuration_path(self): def get_wireguard_configuration_path(self):
return f'{self.get_system_config_path()}/wg.conf' filepath = f'{self.get_system_config_path()}/wg.conf'
return filepath
def has_wireguard_configuration(self): def has_wireguard_configuration(self):
return os.path.isfile(f'{self.get_system_config_path()}/wg.conf') filepath = f'{self.get_system_config_path()}/wg.conf'
if os.path.isdir(os.path.dirname(filepath)):
return os.path.isfile(filepath)
else:
return False
def address_security_incident(self): def address_security_incident(self):
super().address_security_incident() super().address_security_incident()
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
@ -52,49 +70,40 @@ class SystemProfile(BaseProfile):
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
except ProfileModificationError: except ProfileModificationError:
raise ProfileDeletionError('The WireGuard configuration could not be deleted.') raise ProfileDeletionError('The WireGuard configuration could not be deleted.')
if shutil.which('pkexec') is None: if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec') raise CommandNotFoundError('pkexec')
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_system_config_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) try:
if not completed_successfully: process = subprocess.run(('pkexec', 'rm', '-rf', self.get_system_config_path()))
raise ProfileDeletionError('The profile could not be deleted.') completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not completed_successfully:
raise ProfileDeletionError('The profile could not be deleted.')
except:
print("skipping the delete of the WG folder.")
super().delete() super().delete()
def attach_operator_proxy_session(self, operator_proxy_session):
from core.models.OperatorProxySession import OperatorProxySession
operator_proxy_session_file_contents = f'{operator_proxy_session.to_json(indent=4)}\n'
os.makedirs(self.get_config_path(), exist_ok=True)
operator_proxy_session_file_path = self.get_operator_proxy_session_path()
with open(operator_proxy_session_file_path, 'w') as operator_proxy_session_file:
operator_proxy_session_file.write(operator_proxy_session_file_contents)
def get_operator_proxy_session_path(self):
return f'{self.get_config_path()}/operator_proxy_session.json'
def get_operator_proxy_session(self):
try:
config_file_contents = open(self.get_operator_proxy_session_path(), 'r').read()
except FileNotFoundError:
return None
try:
data = json.loads(config_file_contents)
except ValueError:
return None
from core.models.OperatorProxySession import OperatorProxySession
return OperatorProxySession.from_dict(data)
def has_operator_proxy_session(self):
return os.path.isfile(self.get_operator_proxy_session_path())
def __delete_wireguard_configuration(self): def __delete_wireguard_configuration(self):
if self.has_wireguard_configuration(): if self.has_wireguard_configuration():
if shutil.which('pkexec') is None: if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec') raise CommandNotFoundError('pkexec')
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_wireguard_configuration_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) try:
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_wireguard_configuration_path()), check=True)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
except subprocess.CalledProcessError as e:
completed_successfully = True
except:
completed_successfully = True
if not completed_successfully: if not completed_successfully:
raise ProfileModificationError('The WireGuard configuration could not be deleted.') raise ProfileModificationError('The WireGuard configuration could not be deleted.')
@staticmethod @staticmethod
def __get_system_config_path(id: int): def __get_system_config_path(id: int):
return f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}' config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}'
return config_path

View file

@ -1,8 +0,0 @@
from core.observers.BaseObserver import BaseObserver
class EncryptedProxyObserver(BaseObserver):
def __init__(self):
self.on_connected = []
self.on_disconnected = []
self.on_error = []

View file

@ -178,8 +178,6 @@ class WebServiceApiService:
data['operator']['id'], data['operator']['id'],
data['operator']['name'], data['operator']['name'],
data['operator'].get('domain'), data['operator'].get('domain'),
data['operator'].get('hysteria2_host'),
data['operator'].get('vless_host'),
) )
return None return None

View file

@ -1,33 +0,0 @@
import ssl
import time
import urllib.request
from pathlib import Path
from core.utils.encrypted_proxy.singbox import SingboxRunner
def get_public_ip(timeout: int = 8) -> str:
ctx = ssl.create_default_context()
try:
req = urllib.request.Request(
"https://ifconfig.me/ip",
headers={"User-Agent": "curl/7.0"}
)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return resp.read().decode().strip()
except Exception as e:
return f"unknown ({e})"
def disable_proxy(tmp_dir: Path, wrapper: str, unit: str, observer=None) -> bool:
runner = SingboxRunner(wrapper, unit)
runner.stop()
for f in tmp_dir.glob("*-sing-box.json"):
f.unlink(missing_ok=True)
time.sleep(1)
ip = get_public_ip()
if observer:
observer.notify("disconnected", {"ip": ip})
return True

View file

@ -1,102 +0,0 @@
import socket
from pathlib import Path
from core.Constants import Constants
from core.utils.encrypted_proxy.net import get_real_ip, verify_ip
from core.utils.encrypted_proxy.singbox import SingboxRunner
def _resolve(host: str) -> str:
return socket.gethostbyname(host)
def build_hysteria_config(username: str, password: str,
server_host: str, socks5_port: int) -> dict:
server_ip = _resolve(server_host)
return {
"dns": {
"servers": [{"tag": "local", "type": "udp", "server": "9.9.9.9"}],
"final": "local",
"strategy": "ipv4_only",
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"address": ["172.19.0.1/30"],
"mtu": 9000,
"auto_route": True,
"stack": "system",
},
{
"type": "socks",
"tag": "socks-in",
"listen": "127.0.0.1",
"listen_port": socks5_port,
},
],
"outbounds": [
{"type": "direct", "tag": "direct"},
{"type": "block", "tag": "block"},
{
"type": "hysteria2",
"tag": "proxy",
"server": server_ip,
"server_port": 443,
"password": f"{username}:{password}",
"tls": {
"enabled": True,
"server_name": server_host,
"insecure": False,
},
},
],
"route": {
"rules": [
{"protocol": "dns", "action": "hijack-dns"},
{"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"},
{"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"},
{"ip_is_private": True, "outbound": "direct"},
{"ip_version": 6, "outbound": "block"},
{"inbound": ["tun-in", "socks-in"], "outbound": "proxy"},
],
"final": "proxy",
"default_domain_resolver": "local",
"auto_detect_interface": True,
},
}
def enable_hysteria(username: str, password: str, server_host: str,
socks5_port: int, observer=None) -> bool:
real_ip = get_real_ip()
runner = SingboxRunner()
runner.stop()
config_path = Path(Constants.HV_DATA_HOME) / f"{username}-sing-box.json"
config = build_hysteria_config(username, password, server_host, socks5_port)
try:
runner.write_config(config_path, config)
ok = runner.start(config_path)
except Exception as e:
if observer:
observer.notify("error", str(e))
return False
if not ok:
if observer:
observer.notify("error", "sing-box not active after start")
return False
proxy_ip = verify_ip(socks5_port)
if observer:
observer.notify("connected", {
"real_ip": real_ip,
"proxy_ip": proxy_ip,
"socks5_port": socks5_port,
})
return True
def disable_hysteria(observer=None) -> bool:
SingboxRunner().stop()
if observer:
observer.notify("disconnected", {})
return True

View file

@ -1,126 +0,0 @@
from urllib.parse import unquote
from pathlib import Path
from core.Constants import Constants
from core.utils.encrypted_proxy.net import get_real_ip, verify_ip
from core.utils.encrypted_proxy.singbox import SingboxRunner
def parse_vless_link(link: str) -> dict:
link = link.replace("vless://", "")
uuid, rest = link.split("@", 1)
hostport, qs = rest.split("?", 1)
query = qs.split("#")[0]
host, port = hostport.rsplit(":", 1)
params = {}
for part in query.split("&"):
if "=" in part:
k, v = part.split("=", 1)
params[k] = v
sni = params.get("sni", host)
ws_host = params.get("host", "").strip() or sni
return {
"uuid": uuid,
"host": host,
"port": int(port),
"path": unquote(params.get("path", "/vless")),
"sni": sni,
"ws_host": ws_host,
"security": params.get("security", "tls"),
"network": params.get("type", "ws"),
}
def build_vless_config(vless: dict, socks5_port: int) -> dict:
return {
"dns": {
"servers": [{"tag": "local", "type": "udp", "server": "9.9.9.9"}],
"final": "local",
"strategy": "ipv4_only",
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"address": ["172.19.0.1/30"],
"mtu": 9000,
"auto_route": True,
"stack": "system",
},
{
"type": "socks",
"tag": "socks-in",
"listen": "127.0.0.1",
"listen_port": socks5_port,
},
],
"outbounds": [
{"type": "direct", "tag": "direct"},
{"type": "block", "tag": "block"},
{
"type": "vless",
"tag": "proxy",
"server": vless["sni"],
"server_port": 443,
"uuid": vless["uuid"],
"tls": {
"enabled": True,
"server_name": vless["sni"],
"insecure": False,
},
"transport": {
"type": "ws",
"path": vless["path"],
"headers": {"Host": vless["sni"]},
},
},
],
"route": {
"rules": [
{"protocol": "dns", "action": "hijack-dns"},
{"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"},
{"ip_is_private": True, "outbound": "direct"},
{"ip_version": 6, "outbound": "block"},
{"inbound": ["tun-in", "socks-in"], "outbound": "proxy"},
],
"final": "proxy",
"default_domain_resolver": "local",
"auto_detect_interface": True,
},
}
def enable_vless(vless_link: str, username: str,
socks5_port: int, observer=None) -> bool:
vless = parse_vless_link(vless_link)
real_ip = get_real_ip()
runner = SingboxRunner()
runner.stop()
config_path = Path(Constants.HV_DATA_HOME) / f"{username}-sing-box.json"
config = build_vless_config(vless, socks5_port)
try:
runner.write_config(config_path, config)
ok = runner.start(config_path)
except Exception as e:
if observer:
observer.notify("error", str(e))
return False
if not ok:
if observer:
observer.notify("error", "sing-box not active after start")
return False
proxy_ip = verify_ip(socks5_port)
if observer:
observer.notify("connected", {
"real_ip": real_ip,
"proxy_ip": proxy_ip,
"socks5_port": socks5_port,
})
return True
def disable_vless(observer=None) -> bool:
SingboxRunner().stop()
if observer:
observer.notify("disconnected", {})
return True

View file

@ -96,7 +96,7 @@ def is_the_key_to_blame(
# invalid key # invalid key
notification = f"Invalid Numbers on the public_key" notification = f"Invalid Numbers on the public_key"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return {"valid": True, "message": "invalid_key"} return {"valid": False, "message": "invalid_key"}
new_public_key = get_new_pubkey_from_api(connection_observer) new_public_key = get_new_pubkey_from_api(connection_observer)
@ -121,7 +121,7 @@ def is_the_key_to_blame(
if not result_of_comparison: if not result_of_comparison:
error_msg = "New key is the SAME as the old one." error_msg = "New key is the SAME as the old one."
ticket_observer.notify("preparing", subject=error_msg) ticket_observer.notify("preparing", subject=error_msg)
return {"valid": False, "message": "same"} return {"valid": False, "comparison": "same"}
status_update = "New key is DIFFERENT from the old one!" status_update = "New key is DIFFERENT from the old one!"
ticket_observer.notify("preparing", subject=status_update) ticket_observer.notify("preparing", subject=status_update)
@ -142,9 +142,9 @@ def is_the_key_to_blame(
if quantity_results > 0: if quantity_results > 0:
logger.debug("Therefore, the new key works.") logger.debug("Therefore, the new key works.")
return {"valid": True, "comparison": "different", "matters": False} return {"valid": True, "comparison": "different"}
else: else:
logger.debug( logger.debug(
"Therefore, the new key doesn't help. It is different, but also produces invalid blind signatures." "Therefore, the new key doesn't help. It is different, but also produces invalid blind signatures."
) )
return {"valid": False, "comparison": "different", "matters": False} return {"valid": False, "comparison": "different"}

View file

@ -0,0 +1,13 @@
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.Constants import Constants
def do_we_have_billing_id() -> str | None:
try:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
billing_id = get_value_from_json_file(filepath, "temp_billing_code")
return billing_id
except:
return None

View file

@ -37,7 +37,7 @@ def get_from_server_and_save(
if status == True: if status == True:
# extract: # extract:
public_key = public_key_results.get("valid", False) public_key = public_key_results.get("data", False)
# save it: # save it:
did_it_save = write_string_to_text_file(public_key, file_path) did_it_save = write_string_to_text_file(public_key, file_path)

View file

@ -37,6 +37,8 @@ def ticket_prep_orchestrator(
} }
# assuming we actually saved the unblinding factors, # assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.."
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign: # then send the entire blinded list to the server to sign:
blind_signatures = send_blind_commitments( blind_signatures = send_blind_commitments(
@ -56,9 +58,13 @@ def ticket_prep_orchestrator(
else: else:
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again, # regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again,
notification = "Saving the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures) did_they_ALL_save = save_ALL_blind_sigs(blind_signatures)
# verify the server's blind signatures against the public key, # verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures( failed_validations = validate_blind_signatures(
blind_signatures, ticket_observer, connection_observer blind_signatures, ticket_observer, connection_observer
) )
@ -68,6 +74,8 @@ def ticket_prep_orchestrator(
# did verification of any of the blind sigs fail? # did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations) how_many_failed = len(failed_validations)
if how_many_failed >= 1: if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures."
ticket_observer.notify("preparing", subject=notification)
logger.debug( logger.debug(
f"Verification failed for {how_many_failed} blind signatures." f"Verification failed for {how_many_failed} blind signatures."
) )
@ -79,6 +87,8 @@ def ticket_prep_orchestrator(
} }
# Unblind the signatures & combine with unblinded commitment: # Unblind the signatures & combine with unblinded commitment:
notification = f"Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets( did_prep_work = unblind_ALL_tickets(
blind_signatures, ticket_observer, connection_observer blind_signatures, ticket_observer, connection_observer
) )

View file

@ -1,44 +0,0 @@
import socket
import subprocess
import time
def resolve(host: str) -> str | None:
try:
return socket.gethostbyname(host)
except Exception:
return None
def get_real_ip(timeout: int = 10) -> str:
try:
r = subprocess.run(
["curl", "-s", "--max-time", str(timeout), "https://ifconfig.me/ip"],
capture_output=True, timeout=timeout + 2,
)
return r.stdout.decode().strip() or "unknown (empty)"
except Exception as e:
return f"unknown ({e})"
def get_proxied_ip(socks5_port: int, timeout: int = 10) -> str:
try:
r = subprocess.run(
["curl", "-s", "--max-time", str(timeout),
"-x", f"socks5h://127.0.0.1:{socks5_port}",
"https://ifconfig.me/ip"],
capture_output=True, timeout=timeout + 2,
)
return r.stdout.decode().strip() or "unknown (empty)"
except Exception as e:
return f"unknown ({e})"
def verify_ip(socks5_port: int, retries: int = 3, delay: float = 2.0) -> str:
for attempt in range(1, retries + 1):
ip = get_proxied_ip(socks5_port)
if not ip.startswith("unknown"):
return ip
if attempt < retries:
time.sleep(delay)
return "unknown"

View file

@ -1,35 +0,0 @@
import json
import subprocess
import time
from pathlib import Path
class SingboxRunner:
WRAPPER = "/usr/local/bin/hydraveil-singbox"
def start(self, config_path: Path) -> bool:
self.stop()
time.sleep(1)
self._process = subprocess.Popen(
["sudo", self.WRAPPER, str(config_path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(2)
return self.is_running()
def stop(self) -> str:
subprocess.run(["sudo", self.WRAPPER, "", "stop"], capture_output=True)
subprocess.run(["sudo", "ip", "link", "delete", "tun0"],
capture_output=True)
self._process = None
return "stopped"
def is_running(self) -> bool:
r = subprocess.run(["pgrep", "-f", "sing-box"], capture_output=True)
return r.returncode == 0
def write_config(self, config_path: Path, config: dict) -> None:
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w") as f:
json.dump(config, f, indent=2)

View file

@ -1,173 +0,0 @@
#!/bin/bash
set -e
REPO_URL="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-core"
BRANCH="core-laravel-proxyTEST"
INSTALL_DIR="$HOME/sp-hydra-veil-core"
SINGBOX_VERSION="1.13.5"
SINGBOX_BIN="/usr/bin/sing-box"
WRAPPER_PATH="/usr/local/bin/hydraveil-singbox"
SUDOERS_PATH="/etc/sudoers.d/hydraveil"
HV_DATA_HOME="$HOME/.local/share/hydra-veil"
echo "=== sp-hydra-veil-core installer ==="
# 1. Clone repo
echo ""
echo "[1/6] Cloning repository (branch: $BRANCH)..."
if [ -d "$INSTALL_DIR" ]; then
echo "Directory already exists, removing..."
rm -rf "$INSTALL_DIR"
fi
git clone --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR"
# 2. .env
echo ""
echo "[2/6] Creating .env..."
cat > "$INSTALL_DIR/.env" << 'DOTENV'
SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
SP_API_BASE_URL_PRODUCTION=https://fake.simplifiedprivacy.org/api/v1
APP_ENV=production
DOTENV
echo ".env created."
# 3. venv + dependencies
echo ""
echo "[3/6] Setting up virtual environment..."
python3 -m venv "$INSTALL_DIR/.venv"
source "$INSTALL_DIR/.venv/bin/activate"
pip install --quiet --upgrade pip
pip install \
python-dotenv \
"cryptography~=46.0.3" \
"dataclasses-json~=0.6.7" \
"marshmallow~=3.26.1" \
"psutil~=7.1.3" \
"pysocks~=1.7.1" \
"python-dateutil~=2.9.0.post0" \
"requests~=2.32.5" \
"annotated-types==0.7.0" \
"certifi==2026.4.22" \
"charset-normalizer==3.4.7" \
"click==8.3.3" \
"cytoolz==1.1.0" \
"eth-hash==0.8.0" \
"eth-typing==6.0.0" \
"eth-utils==6.0.0" \
"idna==3.13" \
"packaging==26.2" \
"pathspec==1.1.1" \
"platformdirs==4.9.6" \
"py-ecc==8.0.0" \
"pydantic==2.13.3" \
"pydantic_core==2.46.3" \
"pydeps==3.0.6" \
"pytokens==0.4.1" \
"stdlib-list==0.12.0" \
"toolz==1.1.0" \
"typing-inspect==0.9.0" \
"typing-inspection==0.4.2" \
"typing_extensions==4.15.0" \
"urllib3==2.6.3"
pip install -e "$INSTALL_DIR" --no-deps
echo "Dependencies installed."
# 4. sing-box
echo ""
echo "[4/6] Checking sing-box..."
# Find sing-box in any common location
FOUND_SINGBOX=""
for candidate in /usr/bin/sing-box /usr/local/bin/sing-box /bin/sing-box /usr/local/sbin/sing-box; do
if [ -x "$candidate" ]; then
FOUND_SINGBOX="$candidate"
break
fi
done
# Also check PATH
if [ -z "$FOUND_SINGBOX" ]; then
FOUND_SINGBOX=$(command -v sing-box 2>/dev/null || true)
fi
if [ -n "$FOUND_SINGBOX" ]; then
echo "sing-box found at: $FOUND_SINGBOX"
echo "Version: $($FOUND_SINGBOX version | head -1)"
# If not at /usr/bin/sing-box, create symlink
if [ "$FOUND_SINGBOX" != "$SINGBOX_BIN" ]; then
echo "Creating symlink: $FOUND_SINGBOX -> $SINGBOX_BIN"
sudo ln -sf "$FOUND_SINGBOX" "$SINGBOX_BIN"
fi
else
echo "sing-box not found, installing v$SINGBOX_VERSION..."
wget -q "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz" -O /tmp/sing-box.tar.gz
tar -xzf /tmp/sing-box.tar.gz -C /tmp
sudo cp "/tmp/sing-box-${SINGBOX_VERSION}-linux-amd64/sing-box" "$SINGBOX_BIN"
sudo chmod 755 "$SINGBOX_BIN"
rm -rf /tmp/sing-box.tar.gz "/tmp/sing-box-${SINGBOX_VERSION}-linux-amd64"
echo "sing-box installed: $($SINGBOX_BIN version | head -1)"
fi
# Final check
if [ ! -x "$SINGBOX_BIN" ]; then
echo "ERROR: sing-box not found at $SINGBOX_BIN after install. Aborting."
exit 1
fi
echo "sing-box OK at $SINGBOX_BIN"
# 5. wrapper + sudoers + HV_DATA_HOME
echo ""
echo "[5/6] Installing wrapper..."
sudo tee "$WRAPPER_PATH" > /dev/null << WRAPPER
#!/bin/bash
CONFIG=\$1
ACTION=\$2
if [[ "\$ACTION" == "stop" ]]; then
pkill -9 -f sing-box
exit 0
fi
if [[ "\$CONFIG" != /tmp/*.json ]] && \\
[[ "\$CONFIG" != /home/*/.local/share/hydra-veil/*.json ]]; then
echo "Error: config path not allowed"
exit 1
fi
exec $SINGBOX_BIN run -c "\$CONFIG"
WRAPPER
sudo chmod 755 "$WRAPPER_PATH"
# Verify wrapper points to correct sing-box
if grep -q "$SINGBOX_BIN" "$WRAPPER_PATH"; then
echo "Wrapper OK — points to $SINGBOX_BIN"
else
echo "ERROR: wrapper does not reference $SINGBOX_BIN"
exit 1
fi
# 6. sudoers
echo ""
echo "[6/6] Configuring sudoers..."
if [ ! -f "$SUDOERS_PATH" ]; then
echo "$USER ALL=(ALL) NOPASSWD: $WRAPPER_PATH" | sudo tee "$SUDOERS_PATH" > /dev/null
sudo chmod 440 "$SUDOERS_PATH"
echo "Sudoers configured."
else
echo "Sudoers already exists, skipping."
fi
# Create HV_DATA_HOME directory
mkdir -p "$HV_DATA_HOME"
echo "HV_DATA_HOME created: $HV_DATA_HOME"
echo ""
echo "=== Installation complete ==="
echo "Activate the environment with:"
echo " source $INSTALL_DIR/.venv/bin/activate"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-core" name = "sp-hydra-veil-core"
version = "2.3.0" version = "2.3.4"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]
@ -15,7 +15,6 @@ dependencies = [
"cryptography ~= 46.0.3", "cryptography ~= 46.0.3",
"dataclasses-json ~= 0.6.7", "dataclasses-json ~= 0.6.7",
"marshmallow ~= 3.26.1", "marshmallow ~= 3.26.1",
"psutil ~= 7.1.3",
"pysocks ~= 1.7.1", "pysocks ~= 1.7.1",
"python-dateutil ~= 2.9.0.post0", "python-dateutil ~= 2.9.0.post0",
"requests ~= 2.32.5", "requests ~= 2.32.5",