Compare commits
8 commits
core-larav
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a83d4bec07 | |||
| 8e2ea64b6b | |||
| dfdcb78fff | |||
| d602f251e0 | |||
| cf0f8cd4ea | |||
| 998f0dc883 | |||
| 38226f149a | |||
| 026b18b396 |
34 changed files with 143 additions and 766 deletions
6
.env
6
.env
|
|
@ -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
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
13
core/services/payment_phase/do_we_have_billing_id.py
Normal file
13
core/services/payment_phase/do_we_have_billing_id.py
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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)
|
|
||||||
173
install.sh
173
install.sh
|
|
@ -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"
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue