Compare commits

..

21 commits

Author SHA1 Message Date
693c3404ad Actualizar pyproject.toml 2026-05-29 13:05:19 +00:00
f38aa3b6f1 26-05-16-6:29 2026-05-26 18:29:24 -05:00
6849f1a46f Eliminar .env 2026-05-26 21:41:10 +00:00
c5c0e9837a add .env 2026-05-26 21:40:33 +00:00
86621021bc add installer 2026-05-26 16:31:16 -05:00
474a6961b3 fix connection 2026-05-25 01:09:38 +00:00
9aa3518fec fix connection 2026-05-25 01:08:27 +00:00
29bd0e713b fix conection 2026-05-25 01:07:30 +00:00
c93a8572a1 ffi 2026-05-25 01:06:53 +00:00
db8a0f0c74 fix 2xx and subdomain 2026-05-25 00:40:11 +00:00
3077e8d525 fix add subdomain 2026-05-25 00:39:16 +00:00
2e7fce9c1c add subdomain operator 2026-05-25 00:02:57 +00:00
0d5fdf2d66 fix 2xx 2026-05-25 00:01:52 +00:00
866dda72e7 fix system-profile 2026-05-23 15:15:15 +00:00
918c070f73 fix system-profile 2026-05-23 15:14:09 +00:00
a6d412b589 fix subdomain 2026-05-23 04:40:13 +00:00
93fc03c6ca fix subdomain 2026-05-23 04:39:37 +00:00
811b285fe6 add install.sh 2026-05-23 03:12:33 +00:00
8ef8f9e1c9 feat: use SystemProfile path for sing-box config storage 2026-05-23 03:03:23 +00:00
89705bbab7 feat: use SystemProfile path for sing-box config storage 2026-05-23 03:02:36 +00:00
a3cf476af4 core-laravel-proxyTEST 2026-05-16 00:36:11 -05:00
212 changed files with 3424 additions and 7064 deletions

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
APP_ENV=
SP_API_BASE_URL_LOCAL=
SP_API_BASE_URL_PRODUCTION=
# SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
# SP_API_BASE_URL_PRODUCTION=https://api.hydraveil.net/api/v1

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
.dev
prototype_client.py prototype_client.py
.idea .idea
.venv .venv
__pycache__ __pycache__
dist dist
.env

View file

@ -1,34 +1,34 @@
# sp-hydra-veil-core python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
subscription = WebServiceApiService.post_subscription(2, operator_id=3)
print('billing code:', subscription.billing_code)
"
The `sp-hydra-veil-core` library exposes core logic to higher-level components. parchear
sudo docker compose exec laravel.test php artisan tinker --execute="
\$sub = \App\Models\Subscription::orderBy('id', 'desc')->first();
\$sub->expires_at = '2027-12-31 23:59:59';
\$sub->duration = 720;
\$sub->payment_reference = 'bypass_' . uniqid();
\$sub->save();
echo 'OK' . PHP_EOL;
"
conectar
python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.encrypted_proxy.VlessController import VlessController
from core.observers.EncryptedProxyObserver import EncryptedProxyObserver
## Build Instructions observer = EncryptedProxyObserver()
observer.subscribe('connected', lambda e: print('connected:', e.subject))
observer.subscribe('error', lambda e: print('error:', e.subject))
observer.subscribe('disconnected', lambda e: print('disconnected:', e.subject))
### Presumptions session = WebServiceApiService.post_operator_proxy('617N-LPKB-MVPT-YRQM', 3, 'vless')
print('session:', session)
* Your system is configured to use the Simplified Privacy package registry [1]. controller = VlessController(1080)
controller.enable(session.links[0], session.username, observer)
"
### Prerequisites
* `build`
* `twine`
To install them, activate your `venv`, if necessary, and run:
```bash
pip install build twine
```
### Build Steps
* Activate your `venv`, if necessary, and run:
```bash
rm -rf ./dist/*
python3 -m build
python3 -m twine upload --repository forgejo ./dist/*
```
---
[1] https://forgejo.org/docs/v14.0/user/packages/pypi/#configuring-the-package-registry

View file

@ -1,72 +1,50 @@
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:
DB_VERSION_THIS_APP_WANTS = 1
# Fallback for development (running outside AppImage)
fallback_non_appimage = os.path.dirname(os.path.abspath(__file__))
# appimage home
APPDIR_HOME: Final[str] = os.environ.get('APPDIR', fallback_non_appimage)
print(f"APPDIR_HOME is {APPDIR_HOME}")
# 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
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', f'{_sp_api_base_url}/health')
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'
INSTALL_DIR: Final[str] = os.environ.get('INSTALL_DIR', os.path.expanduser('~'))
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', f'{INSTALL_DIR}/.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', f'{INSTALL_DIR}/.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', f'{INSTALL_DIR}/data')
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', f'{INSTALL_DIR}/.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'
# ── killswitch / dns wrappers ─────────────────────────────────────────────
KILLSWITCH_WRAPPER: Final[str] = os.environ.get(
'KILLSWITCH_WRAPPER', '/opt/hydra-veil/killswitch'
)

View file

@ -0,0 +1,124 @@
from core.Constants import Constants
from core.Errors import UnknownClientPathError, UnknownClientVersionError, CommandNotFoundError
from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientVersionController import ClientVersionController
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.LocationController import LocationController
from core.controllers.OperatorController import OperatorController
from core.controllers.SubscriptionPlanController import SubscriptionPlanController
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from typing import Optional
import os
import pathlib
import re
import shutil
import subprocess
class ClientController:
@staticmethod
def get_working_directory():
return str(pathlib.Path(__file__).resolve().parent.parent.parent)
@staticmethod
def get_version():
if (version_number := Constants.HV_CLIENT_VERSION_NUMBER) is None:
raise UnknownClientVersionError('The client version could not be determined.')
return ClientVersionController.get_or_new(version_number)
@staticmethod
def can_be_updated():
try:
version = ClientController.get_version()
except UnknownClientVersionError:
return False
return not ClientVersionController.is_latest(version)
@staticmethod
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(task=ClientController.__sync, client_observer=client_observer, connection_observer=connection_observer)
@staticmethod
def update(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(task=ClientController.__update, client_observer=client_observer, connection_observer=connection_observer)
@staticmethod
def __get_path():
if (path := Constants.HV_CLIENT_PATH) is None:
raise UnknownClientPathError('The client path could not be determined.')
return path
@staticmethod
def __sync(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
if client_observer is not None:
client_observer.notify('synchronizing')
# noinspection PyProtectedMember
ApplicationController._sync(proxies=proxies)
# noinspection PyProtectedMember
ApplicationVersionController._sync(proxies=proxies)
# noinspection PyProtectedMember
ClientVersionController._sync(proxies=proxies)
# noinspection PyProtectedMember
OperatorController._sync(proxies=proxies)
# noinspection PyProtectedMember
LocationController._sync(proxies=proxies)
# noinspection PyProtectedMember
SubscriptionPlanController._sync(proxies=proxies)
ConfigurationController.update_last_synced_at()
if client_observer is not None:
client_observer.notify('synchronized')
@staticmethod
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
if ClientController.can_be_updated():
if client_observer is not None:
client_observer.notify('updating')
client_path = ClientController.__get_path()
update_environment = os.environ.copy()
if proxies is not None:
update_environment | dict(http_proxy=proxies['http'], https_proxy=proxies['https'])
if shutil.which('hv-updater') is None:
raise CommandNotFoundError('hv-updater')
process = subprocess.Popen(('hv-updater', '--overwrite', '--remove-old', client_path), env=update_environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
highest_reported_progress = 0
for line in iter(process.stdout.readline, ''):
match = re.search(r'(\d+\.\d+)%', line)
if match and (progress := float(match.group(1))) > highest_reported_progress:
highest_reported_progress = progress
if client_observer is not None:
client_observer.notify('update_progressing', None, dict(
progress=progress
))
if client_observer is not None:
client_observer.notify('updated')

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'): if configuration is None or configuration.connection not in ('system', 'tor', 'vless', 'hysteria2'):
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
@ -91,15 +91,3 @@ class ConfigurationController:
@staticmethod @staticmethod
def update_or_create(configuration): def update_or_create(configuration):
configuration.save() configuration.save()
@staticmethod
def change_firewall(new_value):
configuration = ConfigurationController.get_or_new()
configuration.firewall = new_value
configuration.save()
@staticmethod
def change_dns(new_value):
configuration = ConfigurationController.get_or_new()
configuration.dns = new_value
configuration.save()

View file

@ -0,0 +1,529 @@
from collections.abc import Callable
from core.Constants import Constants
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError, CommandNotFoundError
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController
from core.controllers.SessionStateController import SessionStateController
from core.controllers.SystemStateController import SystemStateController
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.system.SystemState import SystemState
from core.observers.ConnectionObserver import ConnectionObserver
from core.services.WebServiceApiService import WebServiceApiService
from essentials.modules.TorModule import TorModule
from essentials.services.ConnectionService import ConnectionService
from pathlib import Path
from subprocess import CalledProcessError
from typing import Union, Optional, Any
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
import time
from core.models.OperatorProxySession import OperatorProxySession
class ConnectionController:
@staticmethod
def with_preferred_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
connection = ConfigurationController.get_connection()
if connection == 'system':
return task(*args, **kwargs)
elif connection == 'tor':
return ConnectionController.__with_tor_connection(*args, task=task, connection_observer=connection_observer, **kwargs)
else:
return None
@staticmethod
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
connection = profile.connection
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
if profile.has_subscription():
if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
proxy_configuration = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_proxy_configuration, connection_observer=connection_observer)
if proxy_configuration is None:
raise InvalidSubscriptionError()
profile.attach_proxy_configuration(proxy_configuration)
else:
raise MissingSubscriptionError()
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 not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
else:
if profile.is_system_profile():
if ConnectionController.system_uses_wireguard_interface() and SystemStateController.exists():
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise MissingSubscriptionError()
if profile.is_session_profile():
try:
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
else:
raise ConnectionError('The connection could not be established.')
if profile.is_system_profile():
try:
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
else:
raise ConnectionError('The connection could not be established.')
return None
@staticmethod
def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
session_directory = tempfile.mkdtemp(prefix='hv-')
session_state = SessionStateController.get_or_new(profile.id)
port_number = None
proxy_port_number = None
if profile.connection.is_unprotected():
if not ConnectionController.system_uses_wireguard_interface():
if not ConnectionUnprotectedError in ignore:
raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.')
else:
ProfileController.disable(profile)
if profile.connection.code == 'tor':
port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer)
session_state.network_port_numbers.tor.append(port_number)
elif profile.connection.code == 'wireguard':
if ConfigurationController.get_endpoint_verification_enabled():
ProfileController.verify_wireguard_endpoint(profile, ignore=ignore)
port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_wireguard_session_connection(profile, session_directory, 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:
while proxy_port_number is None or proxy_port_number == port_number:
proxy_port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_proxy_session_connection(profile, session_directory, port_number, proxy_port_number)
session_state.network_port_numbers.proxy.append(proxy_port_number)
if not profile.connection.is_unprotected():
ConnectionController.await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
SessionStateController.update_or_create(session_state)
return proxy_port_number or port_number
@staticmethod
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
if ConfigurationController.get_endpoint_verification_enabled():
ProfileController.verify_wireguard_endpoint(profile, ignore=ignore)
try:
ConnectionController.__establish_system_connection(profile, connection_observer)
except ConnectionError:
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
except CalledProcessError:
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
try:
ConnectionController.__establish_system_connection(profile, connection_observer)
except (ConnectionError, CalledProcessError):
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
ConnectionController.terminate_tor_connection()
time.sleep(1.0)
@staticmethod
def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None):
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.start_service(connection_observer)
for session_state in SessionStateController.all():
for port_number in session_state.network_port_numbers.tor:
tor_module.create_session(port_number)
@staticmethod
def terminate_tor_connection():
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.stop_service()
@staticmethod
def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None):
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
@staticmethod
def terminate_tor_session_connection(port_number: int):
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.destroy_session(port_number)
@staticmethod
def establish_wireguard_session_connection(profile: SessionProfile, session_directory: str, port_number: int):
if not profile.has_wireguard_configuration():
raise FileNotFoundError('No valid WireGuard configuration file detected.')
wireguard_session_directory = f'{session_directory}/wireguard'
Path(wireguard_session_directory).mkdir(exist_ok=True, mode=0o700)
wireproxy_configuration_file_path = f'{wireguard_session_directory}/wireproxy.conf'
Path(wireproxy_configuration_file_path).touch(exist_ok=True, mode=0o600)
with open(wireproxy_configuration_file_path, 'w') as wireproxy_configuration_file:
wireproxy_configuration_file.write(f'WGConfig = {profile.get_wireguard_configuration_path()}\n\n[Socks5]\nBindAddress = 127.0.0.1:{str(port_number)}\n')
return subprocess.Popen((f'{Constants.HV_RUNTIME_DATA_HOME}/wireproxy/wireproxy', '-c', wireproxy_configuration_file_path), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
@staticmethod
def establish_proxy_session_connection(profile: SessionProfile, session_directory: str, port_number: int, proxy_port_number: int):
if shutil.which('proxychains4') is None:
raise CommandNotFoundError('proxychains4')
if shutil.which('microsocks') is None:
raise CommandNotFoundError('microsocks')
if profile.has_proxy_configuration():
proxy_configuration = profile.get_proxy_configuration()
else:
raise FileNotFoundError('No valid proxy configuration file detected.')
proxy_session_directory = f'{session_directory}/proxy'
Path(proxy_session_directory).mkdir(parents=True, exist_ok=True, mode=0o700)
proxychains_proxy_list = ''
if port_number is not None:
proxychains_proxy_list = f'socks5 127.0.0.1 {port_number}\n'
proxychains_proxy_list = proxychains_proxy_list + f'socks5 {proxy_configuration.ip_address} {proxy_configuration.port_number} {proxy_configuration.username} {proxy_configuration.password}'
proxychains_template_file_path = f'{Constants.HV_RUNTIME_DATA_HOME}/proxychains.ptpl'
with open(proxychains_template_file_path, 'r') as proxychains_template_file:
proxychains_configuration_file_path = f'{proxy_session_directory}/proxychains.conf'
Path(proxychains_configuration_file_path).touch(exist_ok=True, mode=0o600)
proxychains_configuration_file_contents = proxychains_template_file.read().format(proxy_list=proxychains_proxy_list)
with open(proxychains_configuration_file_path, 'w') as proxychains_configuration_file:
proxychains_configuration_file.write(proxychains_configuration_file_contents)
return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
@staticmethod
def terminate_system_connection():
if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli')
if SystemStateController.exists():
process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if completed_successfully or not ConnectionController.system_uses_wireguard_interface():
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ConnectionController.terminate_tor_connection()
SystemState.dissolve()
else:
raise ConnectionTerminationError('The connection could not be terminated.')
@staticmethod
def get_proxies(port_number: int):
return dict(
http=f'socks5h://127.0.0.1:{port_number}',
https=f'socks5h://127.0.0.1:{port_number}'
)
@staticmethod
def await_connection(port_number: Optional[int] = None, connection_observer: Optional[ConnectionObserver] = None):
if port_number is None:
ConnectionController.await_network_interface()
for retry_count in range(Constants.MAX_CONNECTION_ATTEMPTS):
if connection_observer is not None:
connection_observer.notify('connecting', dict(
retry_interval=Constants.CONNECTION_RETRY_INTERVAL,
maximum_number_of_attempts=Constants.MAX_CONNECTION_ATTEMPTS,
attempt_count=retry_count + 1
))
try:
ConnectionController.__test_connection(port_number)
return
except ConnectionError:
time.sleep(Constants.CONNECTION_RETRY_INTERVAL)
retry_count += 1
raise ConnectionError('The connection could not be established.')
@staticmethod
def await_network_interface():
network_interface_is_activated = False
retry_interval = .5
maximum_number_of_attempts = 10
attempt = 0
while not network_interface_is_activated and attempt < maximum_number_of_attempts:
time.sleep(retry_interval)
network_interface_is_activated = ConnectionController.system_uses_wireguard_interface()
attempt += 1
if not network_interface_is_activated:
raise ConnectionError('The network interface could not be activated.')
@staticmethod
def system_uses_wireguard_interface():
if shutil.which('ip') is None:
raise CommandNotFoundError('ip')
process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE)
process_output = str(process.stdout.read())
return bool(re.search('dev wg', str(process_output)))
@staticmethod
def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None):
if shutil.which('dbus-send') is None:
raise CommandNotFoundError('dbus-send')
if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli')
ConnectionController.terminate_system_connection()
try:
process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', profile.get_wireguard_configuration_path()), text=True)
except CalledProcessError:
raise ConnectionError('The connection could not be established.')
try:
connection_id = (m := re.search(r'(?<=\()([a-f0-9-]+?)(?=\))', process_output)) and m.group(1)
ipv6_method = subprocess.check_output(('nmcli', '-g', 'ipv6.method', 'connection', 'show', connection_id), text=True).strip()
except CalledProcessError:
raise ConnectionError('The connection could not be established.')
if ipv6_method in ('disabled', 'ignore'):
try:
subprocess.run(('dbus-send', '--system', '--print-reply', '--dest=org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager', 'org.freedesktop.DBus.Properties.Set', 'string:org.freedesktop.NetworkManager', 'string:ConnectivityCheckEnabled', 'variant:boolean:false'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
except CalledProcessError:
raise ConnectionError('The connection could not be established.')
try:
subprocess.run(('nmcli', 'connection', 'add', 'type', 'dummy', 'save', 'no', 'con-name', 'hv-ipv6-sink', 'ifname', 'hvipv6sink0', 'ipv6.method', 'manual', 'ipv6.addresses', 'fd7a:fd4b:54e3:077c::/64', 'ipv6.gateway', 'fd7a:fd4b:54e3:077c::1', 'ipv6.dns', '::1', 'ipv6.route-metric', '72'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
except CalledProcessError:
raise ConnectionError('The connection could not be established.')
SystemStateController.create(profile.id)
try:
ConnectionController.await_connection(connection_observer=connection_observer)
except ConnectionError:
raise ConnectionError('The connection could not be established.')
@staticmethod
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer)
ConnectionController.await_connection(port_number, connection_observer=connection_observer)
task_output = task(*args, proxies=ConnectionController.get_proxies(port_number), **kwargs)
ConnectionController.terminate_tor_session_connection(port_number)
return task_output
@staticmethod
def __test_connection(port_number: Optional[int] = None, timeout: float = 4.0):
request_urls = [Constants.PING_URL]
proxies = None
if os.environ.get('PING_URL') is None:
request_urls.extend([
'https://hc1.simplifiedprivacy.net',
'https://hc2.simplifiedprivacy.org',
'https://hc3.hydraveil.net'
])
random.shuffle(request_urls)
if port_number is not None:
proxies = ConnectionController.get_proxies(port_number)
for request_url in request_urls:
command = [
sys.executable, '-u', '-c', 'import requests, sys\n'
'try:\n'
f' response = requests.get(\'{request_url}\', proxies={proxies}, timeout={timeout})\n'
' response.raise_for_status(); print(response.text)\n'
'except requests.exceptions.RequestException:\n'
' sys.exit(1)'
]
try:
_response = subprocess.check_output(command, text=True, timeout=timeout)
return None
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass
raise ConnectionError('The connection could not be established.')
@staticmethod
def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
if not profile.has_subscription():
raise MissingSubscriptionError()
if profile.connection.needs_wireguard_configuration() and profile.has_wireguard_configuration():
if profile.subscription.has_been_activated():
return True
return False

View file

@ -0,0 +1,22 @@
from core.models.Location import Location
from core.services.WebServiceApiService import WebServiceApiService
from typing import Optional
class LocationController:
@staticmethod
def get(country_code: str, code: str):
return Location.find(country_code, code)
@staticmethod
def get_all():
return Location.all()
@staticmethod
def _sync(proxies: Optional[dict] = None):
locations = WebServiceApiService.get_locations(proxies)
Location.truncate()
Location.save_many(locations)

View file

@ -0,0 +1,22 @@
from core.models.Operator import Operator
from core.services.WebServiceApiService import WebServiceApiService
from typing import Optional
class OperatorController:
@staticmethod
def get(id: int):
return Operator.find_by_id(id)
@staticmethod
def get_all():
return Operator.all()
@staticmethod
def _sync(proxies: Optional[dict] = None):
operators = WebServiceApiService.get_operators(proxies)
Operator.truncate()
Operator.save_many(operators)

View file

@ -1,8 +1,3 @@
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.general_connection_tools.connection_enable import establish_connection
# from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ApplicationVersionController import ApplicationVersionController
@ -40,33 +35,29 @@ class ProfileController:
if profile_observer is not None: if profile_observer is not None:
profile_observer.notify('created', profile) profile_observer.notify('created', profile)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile.save()
if profile_observer is not None:
profile_observer.notify('updated', profile)
@staticmethod @staticmethod
def enable( def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None):
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
pristine: bool = False,
asynchronous: bool = False,
profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
# =========== ALREADY ENABLED ============
if ProfileController.is_enabled(profile): if ProfileController.is_enabled(profile):
if not ProfileStateConflictError in ignore: if not ProfileStateConflictError in ignore:
raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.') raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.')
else: else:
ProfileController.disable(profile) ProfileController.disable(profile)
# =========== PRISTINE ============
if pristine: if pristine:
profile.delete_data() profile.delete_data()
# ============================================================================
# SESSION
# ============================================================================
if profile.is_session_profile(): if profile.is_session_profile():
application_version = profile.application_version application_version = profile.application_version
@ -75,7 +66,7 @@ class ProfileController:
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer) ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
try: try:
port_number = establish_connection(profile, ignore=ignore, connection_observer=connection_observer) port_number = ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.') raise ProfileActivationError('The profile could not be enabled.')
@ -84,12 +75,10 @@ class ProfileController:
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer) ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer)
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile(): if profile.is_system_profile():
try: try:
establish_connection(profile, ignore=ignore, connection_observer=connection_observer) ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.') raise ProfileActivationError('The profile could not be enabled.')
@ -133,7 +122,7 @@ class ProfileController:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
try: try:
terminate_system_connection() ConnectionController.terminate_system_connection()
except ConnectionTerminationError: except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
@ -195,7 +184,7 @@ class ProfileController:
system_state = SystemStateController.get() system_state = SystemStateController.get()
if system_state is not None and system_state.profile_id is profile.id: if system_state is not None and system_state.profile_id is profile.id:
return system_uses_wireguard_interface() return ConnectionController.system_uses_wireguard_interface()
return False return False
@ -229,28 +218,28 @@ class ProfileController:
def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]): def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
profile.has_proxy_configuration() profile.has_proxy_configuration()
# @staticmethod @staticmethod
# def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None): def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
# from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
# if not profile.has_subscription(): if not profile.has_subscription():
# raise MissingSubscriptionError() raise MissingSubscriptionError()
# if not profile.has_location(): if not profile.has_location():
# raise MissingLocationError() raise MissingLocationError()
# wireguard_keys = ProfileController.__generate_wireguard_keys() wireguard_keys = ProfileController.__generate_wireguard_keys()
# wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.country_code, profile.location.code, profile.subscription.billing_code, wireguard_keys.get('public'), task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer) wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.country_code, profile.location.code, profile.subscription.billing_code, wireguard_keys.get('public'), task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer)
# if wireguard_configuration is None: if wireguard_configuration is None:
# raise InvalidSubscriptionError() raise InvalidSubscriptionError()
# expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE) expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE)
# wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration) wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration)
# profile.attach_wireguard_configuration(wireguard_configuration) profile.attach_wireguard_configuration(wireguard_configuration)
@staticmethod @staticmethod
def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]): def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]):
@ -260,65 +249,65 @@ class ProfileController:
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]): def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
return profile.has_wireguard_configuration() return profile.has_wireguard_configuration()
# @staticmethod @staticmethod
# def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()): def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
# try: try:
# ProfileController.__verify_wireguard_endpoint(profile) ProfileController.__verify_wireguard_endpoint(profile)
# except EndpointVerificationError as error: except EndpointVerificationError as error:
# if not EndpointVerificationError in ignore: if not EndpointVerificationError in ignore:
# profile.address_security_incident() profile.address_security_incident()
# raise error raise error
# @staticmethod @staticmethod
# def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]): def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
# from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import ed25519
# import base64 import base64
# signature = profile.get_wireguard_configuration_metadata('Signature') signature = profile.get_wireguard_configuration_metadata('Signature')
# wireguard_public_keys = profile.get_wireguard_public_keys() wireguard_public_keys = profile.get_wireguard_public_keys()
# operator = profile.location.operator operator = profile.location.operator
# if signature is None: if signature is None:
# raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.') raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
# if not wireguard_public_keys: if not wireguard_public_keys:
# raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.') raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
# if operator is None: if operator is None:
# raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.') raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
# try: try:
# operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key)) operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
# for wireguard_public_key in wireguard_public_keys: for wireguard_public_key in wireguard_public_keys:
# operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8')) operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
# except Exception: except Exception:
# raise EndpointVerificationError('The WireGuard endpoint could not be verified.') raise EndpointVerificationError('The WireGuard endpoint could not be verified.')
# @staticmethod @staticmethod
# def __generate_wireguard_keys(): def __generate_wireguard_keys():
# from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
# from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
# raw_private_key = X25519PrivateKey.generate() raw_private_key = X25519PrivateKey.generate()
# public_key = raw_private_key.public_key().public_bytes( public_key = raw_private_key.public_key().public_bytes(
# encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
# ) )
# private_key = raw_private_key.private_bytes( private_key = raw_private_key.private_bytes(
# encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption() encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
# ) )
# return dict( return dict(
# private=base64.b64encode(private_key).decode(), private=base64.b64encode(private_key).decode(),
# public=base64.b64encode(public_key).decode() public=base64.b64encode(public_key).decode()
# ) )

View file

@ -9,23 +9,10 @@ class SubscriptionPlanController:
@staticmethod @staticmethod
def get(connection: Union[SessionConnection, SystemConnection], duration: int): def get(connection: Union[SessionConnection, SystemConnection], duration: int):
"""
Called by:
GUI in worker.py
Purpose:
confirm the subscription's length is valid
"""
return SubscriptionPlan.find(connection, duration) return SubscriptionPlan.find(connection, duration)
@staticmethod @staticmethod
def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None): def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None):
"""
Not used. Good candidate to be cut.
GUI's create_interface_elements
inside duration selection page actually has hardcoded amounts
"""
return SubscriptionPlan.all(connection) return SubscriptionPlan.all(connection)
@staticmethod @staticmethod

View file

@ -12,10 +12,8 @@ class SystemStateController:
return SystemState.exists() return SystemState.exists()
@staticmethod @staticmethod
def create(profile_id: int, firewalled: bool, dns_set: bool) -> SystemState: def create(profile_id):
current_state = SystemState(profile_id, firewalled, dns_set) return SystemState(profile_id).save()
current_state.save()
return current_state
@staticmethod @staticmethod
def update_or_create(system_state): def update_or_create(system_state):

View file

@ -0,0 +1,17 @@
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

@ -0,0 +1,26 @@
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

@ -0,0 +1,24 @@
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

@ -9,25 +9,16 @@ from core.models.invoice.TicketInvoice import TicketInvoice
from core.services.prepare_tickets.get_pub_key import get_pub_key from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.services.payment_phase.check_if_paid import _check_if_paid
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
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.api_requests.step1_get_or_post 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
@ -56,12 +47,6 @@ 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:
@ -82,6 +67,18 @@ 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")
@ -119,14 +116,6 @@ def initiate_payment(
payload, connection_observer, invoice_data_object payload, connection_observer, invoice_data_object
) )
if isinstance(result, ApiResponse):
logger.error(result.message)
if not result.valid:
invoice_data_object.add_error_code("connection_error")
return invoice_data_object
else:
logger.error("Critical Error with TicketPayController recieving a valid ApiResponse object.")
if result == False or result == None: if result == False or result == None:
invoice_data_object.add_error_code("failed_save") invoice_data_object.add_error_code("failed_save")
@ -184,25 +173,8 @@ def check_if_paid(
url = make_url(which_endpoint) url = make_url(which_endpoint)
# literally send: # literally send:
api_reply_object = send_data_to_server(payload, url, connection_observer) reply = send_data_to_server(payload, url, connection_observer)
if not api_reply_object.valid: logger.debug(f"inside ticketpay controller the reply is {reply}")
api_reply_object = solve_api_problems(
api_reply_object=api_reply_object,
get_or_post="post",
url=url,
payload=payload,
connection_observer=connection_observer,
client_observer=None
)
if not api_reply_object.valid:
# 2nd try, return the error of why we have no payload:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return {"valid": False, "message": error_msg}
# return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
return reply_dict
return reply

View file

@ -51,9 +51,6 @@ 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

@ -0,0 +1,44 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver
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
def sync_ticket_prices(
ticket_observer: TicketObserver, connection_observer: ConnectionObserver
) -> dict:
notification = f"Connecting to get Ticket Pricing..."
ticket_observer.notify("connecting", subject=notification)
rejected_list = [None, False, ""]
base_url = Constants.TICKET_API_BASE_URL
if base_url in rejected_list:
notification = "Base URL is Empty, so it can't fetch prices.."
ticket_observer.notify("failed_input", subject=notification)
return {"valid": False, "error_code": "invalid_url"}
url = f"{base_url}/sync"
try:
sync_results = get_data_from_server(url, connection_observer)
if sync_results in rejected_list:
return {"valid": False, "error_code": "sync_failed"}
logger.debug(f"Inside the sync controller, sync_results is: {sync_results}")
except:
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

View file

@ -8,8 +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.using_tickets.use_ticket_orchestrator import use_ticket_orchestrator from core.services.using_tickets.use_ticket_orchestrator import use_ticket_orchestrator
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.prepare_tickets.ticket_tracker import ( from core.services.prepare_tickets.ticket_tracker import (
get_data_for_a_single_ticket, get_data_for_a_single_ticket,
does_ticket_tracker_exist, does_ticket_tracker_exist,
@ -113,7 +111,7 @@ def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
does_the_file_exist, path_of_file = does_ticket_tracker_exist() does_the_file_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False: if does_the_file_exist == False:
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}" error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
# ticket_observer.notify("failed_input", subject=error_msg) ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg} return {"valid": False, "message": error_msg}
# Use the Model: # Use the Model:
@ -170,12 +168,7 @@ def use_ticket(
ticket_observer.notify("connecting", "Connecting..") ticket_observer.notify("connecting", "Connecting..")
reply = use_ticket_orchestrator(which_ticket, which_location, connection_observer) reply = use_ticket_orchestrator(which_ticket, which_location, connection_observer)
if isinstance(reply, ApiResponse): return reply
error_msg = reply.message
reply_as_dict = {"valid": False, "message": f"API Failed: {error_msg}"}
return reply_as_dict
else:
return reply
def pick_a_random_ticket(ticket_observer: TicketObserver) -> dict: def pick_a_random_ticket(ticket_observer: TicketObserver) -> dict:

30
app/errors/exceptions.py Normal file
View file

@ -0,0 +1,30 @@
class ApplicationError(Exception):
pass
class MathError(ApplicationError):
pass
class NetworkingError(ApplicationError):
pass
class CriticalFailure(ApplicationError):
pass
class InvalidData(ApplicationError):
pass
class ServerSideError(ApplicationError):
pass
class MissingData(ApplicationError):
pass
class FailedToSave(ApplicationError):
pass

View file

@ -1,21 +1,19 @@
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses_json import dataclass_json from dataclasses_json import dataclass_json
@dataclass_json @dataclass_json
@dataclass @dataclass
class BaseConnection: class BaseConnection:
code: str code: str
# Called by: ConnectionController
# it uses it for basic type checks on wireguard code
def needs_wireguard_configuration(self): def needs_wireguard_configuration(self):
return self.code == 'wireguard' return self.code == 'wireguard'
# Called By SubscriptionPlan
def is_session_connection(self): def is_session_connection(self):
return type(self).__name__ == 'SessionConnection' return type(self).__name__ == 'SessionConnection'
# Not called. Dead code
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

@ -1,29 +1,11 @@
from core.models.manage.session_management import get_session
from core.errors.logger import logger
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from core.Constants import Constants from core.Constants import Constants
from core.Helpers import write_atomically from core.Helpers import write_atomically
from core.models.Location import Location
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
# If switching to enums for Connection:
# from core.models.system.SystemConnection import SystemConnection, SystemConnectionTypes
# from core.models.session.SessionConnection import SessionConnection, SessionConnectionTypes
from core.controllers.LocationController import LocationController
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.Subscription import Subscription from core.models.Subscription import Subscription
from core.models.session.ApplicationVersion import ApplicationVersion from core.models.session.ApplicationVersion import ApplicationVersion
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
from dataclasses_json import config, Exclude, dataclass_json from dataclasses_json import config, Exclude, dataclass_json
from pathlib import Path from pathlib import Path
from typing import Optional, Self from typing import Optional, Self
import json import json
@ -31,34 +13,7 @@ import os
import re import re
import shutil import shutil
import tempfile import tempfile
from sqlalchemy.orm import Session
from enum import Enum
@safe_db_operation
def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation:
location_object = session.execute(
select(Location)
.where((Location.country_code == country_code) & (Location.code == city_code))
.options(joinedload(Location.operator))
).scalar_one_or_none()
return location_object
def get_profile_location_data(country_code: str, city_code: str) -> Location:
# with get_session() as session:
api_reply_object = execute_location_sql(country_code, city_code)
if api_reply_object.valid:
data = api_reply_object.data
location_obj = api_reply_object.data
return location_obj
else:
logger.error(f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}")
return None
class ProfileType(str, Enum):
SESSION = "session"
SYSTEM = "system"
@dataclass_json @dataclass_json
@dataclass @dataclass
@ -68,11 +23,7 @@ class BaseProfile(ABC):
) )
name: str name: str
subscription: Optional[Subscription] subscription: Optional[Subscription]
type: ProfileType location: Optional[Location]
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
# legacy version included it to be serialized, but now it's an SQLAlchemy object.
# location: Optional[Location]
@abstractmethod @abstractmethod
def get_wireguard_configuration_path(self): def get_wireguard_configuration_path(self):
@ -100,21 +51,9 @@ class BaseProfile(ABC):
def is_system_profile(self): def is_system_profile(self):
return type(self).__name__ == 'SystemProfile' return type(self).__name__ == 'SystemProfile'
def save(self: Self): def save(self: Self):
# === SERIALIZATION ===
config_dict = self.to_dict()
# === LOCATION === config_file_contents = f'{self.to_json(indent=4)}\n'
location_dict = self.location.convert_to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
if self.location:
config_dict["location"] = location_dict
# === FILE I/O ===
config_file_contents = json.dumps(config_dict, indent=4) + '\n'
# legacy version:
# config_file_contents = f'{self.to_json(indent=4)}\n'
os.makedirs(self.get_config_path(), exist_ok=True) os.makedirs(self.get_config_path(), exist_ok=True)
os.makedirs(self.get_data_path(), exist_ok=True) os.makedirs(self.get_data_path(), exist_ok=True)
@ -207,7 +146,6 @@ class BaseProfile(ABC):
return list([key for key, value in asdict(self).items() if value != asdict(reference).get(key)]) return list([key for key, value in asdict(self).items() if value != asdict(reference).get(key)])
@staticmethod @staticmethod
def find_by_id(id: int): def find_by_id(id: int):
@ -223,34 +161,18 @@ class BaseProfile(ABC):
profile['id'] = id profile['id'] = id
profiles_location = profile['location']
if profile['location'] is not None: if profile['location'] is not None:
# =========== GET COUNTRY & LOCATION ===========
if isinstance(profiles_location, dict):
try:
country_code = profile['location']['country_code']
city_code = profile['location']['code']
except:
logger.error(f"CRITICAL ERROR! Can't find country code or city for profile id {profile['id']} inside BaseProfile")
return
else:
# potentially coming from SQLAlchemy ALREADY:
country_code = profile.location.country_code
city_code = profile.location.code
# =========== GET DATA USING THAT COUNTRY & LOCATION =========== location = Location.find(profile['location']['country_code'] or None, profile['location']['code'] or None)
location_dict = get_profile_location_data(country_code, city_code)
if location_dict: if location is not None:
profile['location'] = location_dict
# this needs error handling if there's no location or malconformed config. if profile['location'].get('time_zone') is not None:
location.time_zone = profile['location']['time_zone']
profile['location'] = location
# =========== SESSION ===========
if 'application_version' in profile: if 'application_version' in profile:
profile['type'] = ProfileType.SESSION
if profile['application_version'] is not None: if profile['application_version'] is not None:
application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None) application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None)
@ -262,10 +184,7 @@ class BaseProfile(ABC):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
profile = SessionProfile.from_dict(profile) profile = SessionProfile.from_dict(profile)
# =========== SYSTEM ===========
else: else:
profile['type'] = ProfileType.SYSTEM
from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemProfile import SystemProfile
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -273,7 +192,6 @@ class BaseProfile(ABC):
return profile return profile
@staticmethod @staticmethod
def exists(id: int): def exists(id: int):
return re.match(r'^\d{1,2}$', str(id)) and os.path.isfile(f'{BaseProfile.__get_config_path(id)}/config.json') return re.match(r'^\d{1,2}$', str(id)) and os.path.isfile(f'{BaseProfile.__get_config_path(id)}/config.json')
@ -306,10 +224,3 @@ class BaseProfile(ABC):
@staticmethod @staticmethod
def __get_data_path(id: int): def __get_data_path(id: int):
return f'{Constants.HV_PROFILE_DATA_HOME}/{str(id)}' return f'{Constants.HV_PROFILE_DATA_HOME}/{str(id)}'
# legacy phased out since SQLAlchemy handles the time_zone.
# if location is not None:
# if profile['location'].get('time_zone') is not None:
# location.time_zone = profile['location']['time_zone']
# profile['location'] = location

View file

@ -1,5 +1,3 @@
from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
from core.Helpers import write_atomically from core.Helpers import write_atomically
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -49,30 +47,6 @@ class Configuration:
) )
) )
firewall: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
dns: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
did_sudo_setup: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
def save(self: Self): def save(self: Self):
config_file_contents = f'{self.to_json(indent=4)}\n' config_file_contents = f'{self.to_json(indent=4)}\n'
@ -108,31 +82,3 @@ class Configuration:
def _from_iso_format(datetime_string: str): def _from_iso_format(datetime_string: str):
date_string = datetime_string.replace('Z', '+00:00') date_string = datetime_string.replace('Z', '+00:00')
return datetime.fromisoformat(date_string) return datetime.fromisoformat(date_string)
def read_config():
try:
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
except FileNotFoundError:
return None
try:
configuration = json.loads(config_file_contents)
except ValueError:
logger.error(f"[CONFIG] Can't load JSON config")
return configuration
def get_setting(looking_for):
config = read_config()
if not config:
logger.error(f"[CONFIG] Can't load the entire config")
return None
if looking_for not in config:
logger.error(f"[CONFIG] What you want isn't in the config")
return None
result = config[looking_for]
return result

115
app/models/Location.py Normal file
View file

@ -0,0 +1,115 @@
from core.models.Model import Model
from core.models.Operator import Operator
from dataclasses import dataclass, field
from dataclasses_json import config, Exclude
from typing import Optional
_table_name: str = 'locations'
_table_definition: str = """
'id' int UNIQUE,
'country_code' varchar,
'country_name' varchar,
'code' varchar,
'name' varchar,
'time_zone' varchar,
'operator_id' int,
'provider_name' varchar,
'is_proxy_capable' bool,
'is_wireguard_capable' bool,
UNIQUE(code, country_code)
"""
@dataclass
class Location(Model):
country_code: str
code: str
id: Optional[int] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
country_name: Optional[str] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
name: Optional[str] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
time_zone: Optional[str] = None
operator_id: Optional[int] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
provider_name: Optional[str] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
is_proxy_capable: Optional[bool] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
is_wireguard_capable: Optional[bool] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
operator: Optional[Operator] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
available: Optional[bool] = field(
default=False,
metadata=config(exclude=Exclude.ALWAYS)
)
def __post_init__(self):
self.operator = Operator.find_by_id(self.operator_id)
self.available = self.exists(self.country_code, self.code)
if isinstance(self.is_proxy_capable, int):
self.is_proxy_capable = bool(self.is_proxy_capable)
if isinstance(self.is_wireguard_capable, int):
self.is_wireguard_capable = bool(self.is_wireguard_capable)
def is_available(self):
return self.exists(self.country_code, self.code)
@staticmethod
def find_by_id(id: int):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_one('SELECT * FROM locations WHERE id = ? LIMIT 1', Location.factory, [id])
@staticmethod
def find(country_code: str, code: str):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_one('SELECT * FROM locations WHERE country_code = ? AND code = ? LIMIT 1', Location.factory, [country_code, code])
@staticmethod
def exists(country_code: str, code: str):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_exists('SELECT * FROM locations WHERE country_code = ? AND code = ?', [country_code, code])
@staticmethod
def all():
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_all('SELECT * FROM locations', Location.factory)
@staticmethod
def truncate():
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
@staticmethod
def save_many(locations):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
Model._insert_many('INSERT INTO locations VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', Location.tuple_factory, locations)
@staticmethod
def factory(cursor, row):
local_fields = [column[0] for column in cursor.description]
return Location(**{key: value for key, value in zip(local_fields, row)})
@staticmethod
def tuple_factory(location):
return location.id, location.country_code, location.country_name, location.code, location.name, location.time_zone, location.operator_id, location.provider_name, location.is_proxy_capable, location.is_wireguard_capable

64
app/models/Operator.py Normal file
View file

@ -0,0 +1,64 @@
from core.models.Model import Model
from dataclasses import dataclass
_table_name: str = 'operators'
_table_definition: str = """
'id' int UNIQUE,
'name' varchar,
'type' varchar,
'public_key' varchar,
'nostr_public_key' varchar,
'nostr_profile_reference' varchar,
'nostr_attestation_event_reference' varchar
"""
@dataclass
class Operator(Model):
id: int
name: str
type: str
public_key: str
nostr_public_key: str
nostr_profile_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
def find_by_id(id: int):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_one('SELECT * FROM operators WHERE id = ? LIMIT 1', Operator.factory, [id])
@staticmethod
def exists(id: int):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_exists('SELECT * FROM operators WHERE id = ?', [id])
@staticmethod
def all():
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_all('SELECT * FROM operators', Operator.factory)
@staticmethod
def truncate():
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
@staticmethod
def save_many(operators):
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)
@staticmethod
def factory(cursor, row):
local_fields = [column[0] for column in cursor.description]
return Operator(**{key: value for key, value in zip(local_fields, row)})
@staticmethod
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

View file

@ -0,0 +1,19 @@
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,6 +10,13 @@ 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(
@ -37,4 +44,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,11 +6,13 @@ 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 return self.proxy + self.wireguard + self.tor + self.vless + self.hysteria2
@property @property
def isolated(self): def isolated(self):
return self.proxy + self.wireguard return self.proxy + self.wireguard + self.vless + self.hysteria2

View file

@ -0,0 +1,22 @@
from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass
@dataclass
class SessionConnection(BaseConnection):
masked: bool = False
def __post_init__(self):
if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'):
raise ValueError('Invalid connection code.')
def is_unprotected(self):
return self.code == 'system' and self.masked is False
def needs_proxy_configuration(self):
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

@ -16,41 +16,30 @@ import shutil
class SessionProfile(BaseProfile): class SessionProfile(BaseProfile):
resolution: str resolution: str
application_version: Optional[ApplicationVersion] application_version: Optional[ApplicationVersion]
connection: Optional[SessionConnection] = None connection: Optional[SessionConnection]
def has_connection(self): def has_connection(self):
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)
@ -61,19 +50,15 @@ 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):
@ -83,36 +68,26 @@ 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

@ -0,0 +1,15 @@
from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass
@dataclass
class SystemConnection (BaseConnection):
def __post_init__(self):
if self.code != 'wireguard':
raise ValueError('Invalid connection code.')
@staticmethod
def needs_proxy_configuration():
return False

View file

@ -4,64 +4,46 @@ 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):
filepath = self.__get_system_config_path(self.id) return 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):
filepath = f'{self.get_system_config_path()}/wg.conf' return f'{self.get_system_config_path()}/wg.conf'
return filepath
def has_wireguard_configuration(self): def has_wireguard_configuration(self):
filepath = f'{self.get_system_config_path()}/wg.conf' return os.path.isfile(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()
@ -70,40 +52,49 @@ 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()))
try: completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_system_config_path())) if not completed_successfully:
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) raise ProfileDeletionError('The profile could not be deleted.')
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()))
try: completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
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):
config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}' return f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}'
return config_path

View file

@ -11,8 +11,6 @@ import pathlib
@dataclass @dataclass
class SystemState: class SystemState:
profile_id: int profile_id: int
firewalled: bool
dns_set: bool
def save(self: Self): def save(self: Self):

View file

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

View file

@ -1,7 +1,8 @@
from core.Constants import Constants from core.Constants import Constants
from core.models.ClientVersion import ClientVersion from core.models.ClientVersion import ClientVersion
# from core.models.Location import Location from core.models.Location import Location
# from core.models.Operator import Operator from core.models.Operator import Operator
from core.models.OperatorProxySession import OperatorProxySession
from core.models.Subscription import Subscription from core.models.Subscription import Subscription
from core.models.SubscriptionPlan import SubscriptionPlan from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.invoice.Invoice import Invoice from core.models.invoice.Invoice import Invoice
@ -18,12 +19,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_applications(proxies: Optional[dict] = None): def get_applications(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/platforms/linux-x86_64/applications', None, proxies) response = WebServiceApiService.__get('/platforms/linux-x86_64/applications', None, proxies)
applications = [] applications = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for application in response.json()['data']: for application in response.json()['data']:
applications.append(Application(application['code'], application['name'], application['id'])) applications.append(Application(application['code'], application['name'], application['id']))
@ -32,12 +31,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_application_versions(code: str, proxies: Optional[dict] = None): def get_application_versions(code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get(f'/platforms/linux-x86_64/applications/{code}/application-versions', None, proxies) response = WebServiceApiService.__get(f'/platforms/linux-x86_64/applications/{code}/application-versions', None, proxies)
application_versions = [] application_versions = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for application_version in response.json()['data']: for application_version in response.json()['data']:
application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['format_revision'], application_version['id'], application_version['download_path'], application_version['released_at'], application_version['file_hash'])) application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['format_revision'], application_version['id'], application_version['download_path'], application_version['released_at'], application_version['file_hash']))
@ -46,12 +43,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_client_versions(proxies: Optional[dict] = None): def get_client_versions(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/platforms/linux-x86_64/appimage/client-versions', None, proxies) response = WebServiceApiService.__get('/platforms/linux-x86_64/appimage/client-versions', None, proxies)
client_versions = [] client_versions = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for client_version in response.json()['data']: for client_version in response.json()['data']:
client_versions.append(ClientVersion(client_version['version_number'], client_version['released_at'], client_version['id'], client_version['download_path'])) client_versions.append(ClientVersion(client_version['version_number'], client_version['released_at'], client_version['id'], client_version['download_path']))
@ -60,26 +55,22 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_operators(proxies: Optional[dict] = None): def get_operators(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/operators', None, proxies) response = WebServiceApiService.__get('/operators', None, proxies)
operators = [] operators = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for operator in response.json()['data']: for operator in response.json()['data']:
operators.append(Operator(operator['id'], operator['name'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference'])) operators.append(Operator(operator['id'], operator['name'], operator['type'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference']))
return operators return operators
@staticmethod @staticmethod
def get_locations(proxies: Optional[dict] = None): def get_locations(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/locations', None, proxies) response = WebServiceApiService.__get('/locations', None, proxies)
locations = [] locations = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for location in response.json()['data']: for location in response.json()['data']:
locations.append(Location(location['country']['code'], location['code'], location['id'], location['country']['name'], location['name'], location['time_zone']['code'], location['operator_id'], location['provider']['name'], location['is_proxy_capable'], location['is_wireguard_capable'])) locations.append(Location(location['country']['code'], location['code'], location['id'], location['country']['name'], location['name'], location['time_zone']['code'], location['operator_id'], location['provider']['name'], location['is_proxy_capable'], location['is_wireguard_capable']))
@ -88,60 +79,57 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_subscription_plans(proxies: Optional[dict] = None): def get_subscription_plans(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/subscription-plans', None, proxies) response = WebServiceApiService.__get('/subscription-plans', None, proxies)
subscription_plans = [] subscription_plans = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for subscription_plan in response.json()['data']: for subscription_plan in response.json()['data']:
subscription_plans.append(SubscriptionPlan(subscription_plan['id'], subscription_plan['code'], subscription_plan['wireguard_session_limit'], subscription_plan['duration'], subscription_plan['price'], subscription_plan['features_proxy'], subscription_plan['features_wireguard'])) subscription_plans.append(SubscriptionPlan(subscription_plan['id'], subscription_plan['code'], subscription_plan['wireguard_session_limit'], subscription_plan['duration'], subscription_plan['price'], subscription_plan['features_proxy'], subscription_plan['features_wireguard']))
return subscription_plans return subscription_plans
@staticmethod @staticmethod
def post_subscription(subscription_plan_id, location_id, proxies: Optional[dict] = None): def post_subscription(subscription_plan_id, location_id=None, operator_id=None, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes body = {'subscription_plan_id': subscription_plan_id}
response = WebServiceApiService.__post('/subscriptions', None, {
'subscription_plan_id': subscription_plan_id,
'location_id': location_id
}, proxies)
if response.status_code == status_codes.CREATED:
return Subscription(response.headers['X-Billing-Code'])
if operator_id is not None:
body['operator_id'] = operator_id
else: else:
return None body['location_id'] = location_id
response = WebServiceApiService.__post('/subscriptions', None, body, proxies)
if 200 <= response.status_code < 300:
return Subscription(response.headers['X-Billing-Code'], operator_id=operator_id)
return None
@staticmethod @staticmethod
def get_subscription(billing_code: str, proxies: Optional[dict] = None): def get_subscription(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
billing_code = billing_code.replace('-', '').upper() billing_code = billing_code.replace('-', '').upper()
billing_code_fragments = re.findall('....?', billing_code) billing_code_fragments = re.findall('....?', billing_code)
billing_code = '-'.join(billing_code_fragments) billing_code = '-'.join(billing_code_fragments)
response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies) response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
subscription = response.json()['data'] subscription = response.json()['data']
return Subscription(billing_code, Subscription.from_iso_format(subscription['expires_at'])) return Subscription(
billing_code,
operator_id=subscription.get('operator_id'),
expires_at=Subscription.from_iso_format(subscription['expires_at'])
)
else: return None
return None
@staticmethod @staticmethod
def get_invoice(billing_code: str, proxies: Optional[dict] = None): def get_invoice(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/invoices/current', billing_code, proxies) response = WebServiceApiService.__get('/invoices/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
response_data = response.json()['data'] response_data = response.json()['data']
@ -157,37 +145,56 @@ class WebServiceApiService:
return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods)) return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods))
else: return None
return None
@staticmethod @staticmethod
def get_proxy_configuration(billing_code: str, proxies: Optional[dict] = None): def get_proxy_configuration(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies) response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
proxy_configuration = response.json()['data'] proxy_configuration = response.json()['data']
return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code']) return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code'])
else: return None
return None
@staticmethod
def post_operator_proxy(billing_code: str, operator_id: int, protocol: str, proxies: Optional[dict] = None):
response = WebServiceApiService.__post('/subscriptions/current/operator-proxies', billing_code, {
'operator_id': operator_id,
'protocol': protocol,
}, proxies)
if 200 <= response.status_code < 300:
data = response.json()['data']
return OperatorProxySession(
data['id'],
data['type'],
data['username'],
data.get('password'),
data.get('links'),
data.get('subscription_url'),
data['operator']['id'],
data['operator']['name'],
data['operator'].get('domain'),
data['operator'].get('hysteria2_host'),
data['operator'].get('vless_host'),
)
return None
@staticmethod @staticmethod
def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None): def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__post(f'/countries/{country_code}/locations/{location_code}/wireguard-sessions', billing_code, { response = WebServiceApiService.__post(f'/countries/{country_code}/locations/{location_code}/wireguard-sessions', billing_code, {
'public_key': public_key, 'public_key': public_key,
}, proxies) }, proxies)
if response.status_code == status_codes.CREATED: if 200 <= response.status_code < 300:
return response.text return response.text
else:
return None return None
@staticmethod @staticmethod
def __get(path, billing_code: Optional[str] = None, proxies: Optional[dict] = None): def __get(path, billing_code: Optional[str] = None, proxies: Optional[dict] = None):
@ -211,16 +218,4 @@ class WebServiceApiService:
else: else:
headers = None headers = None
return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies) return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies)
@staticmethod
def get_cached_sync(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/cachedsync', None, proxies)
if response.status_code == status_codes.OK:
return response.json()
else:
return None

View file

View file

@ -0,0 +1,33 @@
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

@ -0,0 +1,102 @@
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.SINGBOX_CONFIG_DIR) / 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

@ -0,0 +1,130 @@
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
import socket
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:
server_ip = socket.gethostbyname(vless["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": "vless",
"tag": "proxy",
"server": server_ip,
"server_port": vless["port"],
"uuid": vless["uuid"],
"tls": {
"enabled": vless["security"] == "tls",
"server_name": vless["sni"],
"insecure": False,
},
"transport": {
"type": "ws",
"path": vless["path"],
"headers": {"Host": vless["ws_host"]},
},
},
],
"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_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.SINGBOX_CONFIG_DIR) / 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, retries=5, delay=3.0)
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

@ -11,9 +11,7 @@ from core.services.prepare_tickets.get_public_key_by_config import get_public_ke
from core.services.prepare_tickets.get_pub_key import key_is_in_valid_format, get_pub_key from core.services.prepare_tickets.get_pub_key import key_is_in_valid_format, get_pub_key
from core.services.failed_verification.test_if_new_key_works import test_if_new_key_works from core.services.failed_verification.test_if_new_key_works import test_if_new_key_works
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.services.networking.api_requests.step1_get_or_post import get_data_from_api from core.services.networking.get_data_from_server import get_data_from_server
# utils # utils
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
@ -72,7 +70,7 @@ def get_new_pubkey_from_api(connection_observer: ConnectionObserver) -> dict | N
url = make_url(which_key_plan) url = make_url(which_key_plan)
# the result of this is a python dictionary with single ' # the result of this is a python dictionary with single '
api_results = get_data_from_server(url, None, connection_observer) api_results = get_data_from_server(url, connection_observer)
if "data" in api_results: if "data" in api_results:
new_public_key = api_results["data"] new_public_key = api_results["data"]
@ -98,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": False, "message": "invalid_key"} return {"valid": True, "message": "invalid_key"}
new_public_key = get_new_pubkey_from_api(connection_observer) new_public_key = get_new_pubkey_from_api(connection_observer)
@ -123,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, "comparison": "same"} return {"valid": False, "message": "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)
@ -144,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"} return {"valid": True, "comparison": "different", "matters": False}
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"} return {"valid": False, "comparison": "different", "matters": False}

View file

@ -5,15 +5,13 @@ if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
# services # services
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
# 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.api_requests.step1_get_or_post import send_data_to_server
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# use temp billing to get the plan details # use temp billing to get the plan details
def get_plan_data( def get_plan_data(
temp_billing_code: str, connection_observer: ConnectionObserver temp_billing_code: str, connection_observer: ConnectionObserver
) -> dict | ApiResponse: ) -> dict:
which_endpoint = "/plan" which_endpoint = "/plan"
url = make_url(which_endpoint) url = make_url(which_endpoint)
payload = {"temp_billing_code": temp_billing_code} payload = {"temp_billing_code": temp_billing_code}

View file

@ -0,0 +1,76 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
# services
from core.services.networking.internet_test import do_we_have_internet
from core.services.networking.is_dns_problem import is_dns_problem, is_dns_problem_via_tor
from core.services.networking.extract_domain import extract_domain
from core.services.networking.is_tor_working import is_tor_working
# errors
from core.errors.exceptions import *
from core.errors.logger import logger
#######################################################
############### START : REGULAR SYSTEM ###############
#######################################################
def evaluate_regular_networking_problem(
url: str,
connection_observer: ConnectionObserver,
port_number: int | None = None,
proxies: dict | None = None,
) -> dict:
domain_only = extract_domain(url)
if not do_we_have_internet():
return {"valid": False, "error_code": "no_internet"}
if is_dns_problem(domain_only):
logger.debug("This is a DNS issue.")
return {"valid": False, "error_code": "dns_issue"}
return {"valid": False, "error_code": "unknown"}
#######################################################
############### END: REGULAR SYSTEM ###############
#######################################################
#######################################################
############### START: TOR ###############
#######################################################
def evaluate_tor_networking_problem(
url: str,
connection_observer: ConnectionObserver,
) -> dict:
domain_only = extract_domain(url)
logger.debug(f"Evaluating a networking problem for a Tor connection")
logger.debug("checking if Tor works against the official Tor Project API..")
if not is_tor_working(connection_observer):
return {"valid": False, "error_code": "tor_issue"}
else:
tor_fine = f"While there were connection issues, but Tor is working fine!"
logger.error(tor_fine, exc_info=True)
logger.debug(tor_fine)
logger.debug("Check the original DNS via Tor...")
if is_dns_problem_via_tor(domain_only, connection_observer):
return {"valid": False, "error_code": "dns_issue"}
else:
dns_fine = f"There were connection issues, but the DNS for {domain_only} via Tor is working fine!"
logger.error(dns_fine, exc_info=True)
logger.debug(dns_fine)
# can't solve it:
return {"valid": False, "error_code": "unknown"}
#######################################################
############### END: TOR ###############
#######################################################

View file

@ -0,0 +1,31 @@
"""
Extract domain from URL, stripping protocol and path.
If exact TLD match isn't found, tries to return the best guess.
"""
def extract_domain(url: str) -> str:
common_domains = [".com", ".net", ".org", ".is"]
# Remove protocol
if url.startswith("https://"):
url = url[8:]
elif url.startswith("http://"):
url = url[7:]
# Find the earliest TLD in the string
earliest_pos = len(url)
earliest_tld_len = 0
for tld in common_domains:
pos = url.find(tld)
if pos != -1 and pos < earliest_pos:
earliest_pos = pos
earliest_tld_len = len(tld)
# If a valid TLD was found, return domain + TLD
if earliest_tld_len > 0:
return url[: earliest_pos + earliest_tld_len]
# Fallback: return everything before the first slash (best guess)
return url.split("/")[0]

View file

@ -1,13 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
# if TYPE_CHECKING: if TYPE_CHECKING:
from core.observers.ClientObserver import ClientObserver from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver
from core.services.networking.evaluate_networking_problem import evaluate_tor_networking_problem
# services # services
from core.services.networking.get_connection_type import get_connection_type from core.services.networking.get_connection_type import get_connection_type
from core.services.networking.use_tor import tor_get_request from core.services.networking.use_tor import tor_get_request
@ -19,13 +14,13 @@ import traceback
# Generic GET request to an endpoint, filtered by the user's preference of connection type (Tor or Not) # Generic GET request to an endpoint, filtered by the user's preference of connection type (Tor or Not)
def get_data_from_server(url: str, connection_observer: ConnectionObserver, client_observer: Optional[ClientObserver] = None) -> dict: def get_data_from_server(url: str, connection_observer: ConnectionObserver) -> dict:
try: try:
connection_type = get_connection_type() connection_type = get_connection_type()
logger.debug(f"connection type is: {connection_type}") logger.debug(f"connection type is: {connection_type}")
if connection_type == "tor": if connection_type == "tor":
response = tor_get_request(url, connection_observer, client_observer) response = tor_get_request(url, connection_observer)
else: else:
response = regular_get_request(url, connection_observer) response = regular_get_request(url, connection_observer)

View file

@ -0,0 +1,56 @@
# for tor:
from essentials.modules.TorModule import TorModule
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
import json, os
# for both:
from core.services.networking.extract_domain import extract_domain
from core.errors.logger import logger
import socket
import socks
"""
Check if there's a DNS problem with a domain.
Returns True if DNS fails (problem exists), False if DNS resolves successfully.
"""
def is_dns_problem(url: str) -> bool:
domain = extract_domain(url)
try:
socket.gethostbyname(domain)
return False # DNS resolved successfully, no problem
except socket.gaierror:
return True # DNS resolution failed, problem exists
except Exception:
return True # Any other error is treated as a DNS problem
def is_dns_problem_via_tor(
domain: str,
connection_observer: ConnectionObserver | None = None,
) -> bool:
logger.debug("We've triggered EVALUATING IF it's a DNS problem via Tor")
port_number = ConnectionService.get_random_available_port_number()
tor_module = TorModule(os.path.expanduser("~/sp-tor-test"))
tor_module.create_session(port_number, connection_observer)
# Set up SOCKS5 proxy using the same port as the existing Tor proxy,
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", port_number)
socket.socket = socks.socksocket
# DNS query check
try:
logger.debug("checking dns via socket..")
socket.gethostbyname(domain)
tor_module.destroy_session(port_number)
return False # DNS resolved successfully, no problem
except socket.gaierror:
return True # DNS resolution failed, problem exists
except Exception:
return True # Any other error is treated as a DNS problem

View file

@ -5,5 +5,8 @@ from core.Constants import Constants
def make_url(which_endpoint: str) -> str: def make_url(which_endpoint: str) -> str:
domain = Constants.TICKET_API_BASE_URL domain = Constants.TICKET_API_BASE_URL
# hardcode option:
# domain = "https://ticket.hydraveil.net"
final_url = f"{domain}/{which_endpoint}" final_url = f"{domain}/{which_endpoint}"
return final_url return final_url

View file

@ -15,7 +15,6 @@ def regular_get_request(url: str, connection_observer: ConnectionObserver):
import requests import requests
try: try:
print(f"we are doing a regular GET request to: {url}")
response = requests.get(url) response = requests.get(url)
# Raises HTTPError for 4xx/5xx # Raises HTTPError for 4xx/5xx

View file

@ -0,0 +1,131 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import requests
# tor
from essentials.modules.TorModule import TorModule
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
# services
from core.services.networking.evaluate_networking_problem import evaluate_tor_networking_problem
# errors
from core.errors.exceptions import *
from core.errors.logger import logger
# generic
import json, os
from typing import Any
def tor_get_request(custom_url: str, connection_observer: ConnectionObserver) -> dict:
import requests
port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"Using Tor on port number {port_number}")
try:
tor_module = TorModule(os.path.expanduser("~/sp-tor-test"))
tor_module.create_session(port_number, connection_observer)
proxies = {
"http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}",
}
logger.debug(f"Doing Request through Tor via port {port_number}")
response = requests.get(custom_url, proxies=proxies)
logger.debug("Request through Tor successful.")
# if it crashes from the above error check, then it won't destroy the proxy,
tor_module.destroy_session(port_number)
# return response
dictonary_of_response = response.json()
final_reply = {"valid": True, "data": dictonary_of_response}
return final_reply
except requests.exceptions.ConnectionError:
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number, proxies
)
return problem_results
except requests.exceptions.Timeout:
logger.debug("Connection timed out")
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number
)
return problem_results
except requests.exceptions.HTTPError:
logger.debug(f"HTTP error: {response.status_code}")
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number
)
return problem_results
def tor_post_request(
custom_url: str,
payload: dict,
connection_observer: ConnectionObserver,
) -> requests.Response | dict:
import requests # bulky import
response = {
"status": "unable_to_get"
} # incase this goes to the except block, it's "defined"
port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"Using Tor on port number {port_number}")
try:
tor_module = TorModule(os.path.expanduser("~/sp-tor-test"))
tor_module.create_session(port_number, connection_observer)
proxies = {
"http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}",
}
logger.debug(f"Sending data through Tor via port {port_number}")
response = requests.post(custom_url, json=payload, proxies=proxies)
logger.debug("Sending data through Tor successful.")
tor_module.destroy_session(port_number)
return response
except requests.exceptions.ConnectionError:
error_msg = "There was a Connection Error with the Tor Module."
logger.error(error_msg, exc_info=True)
logger.debug(error_msg)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except requests.exceptions.Timeout:
error_msg = "Connection timed out"
logger.error(error_msg, exc_info=True)
logger.debug(error_msg)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except requests.exceptions.HTTPError:
if isinstance(response, requests.Response):
error_msg = f"HTTP error: {response.status_code}"
logger.error(error_msg, exc_info=True)
logger.debug(error_msg)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
# services:
from core.services.networking.send_data_to_server import send_data_to_server
from core.services.networking.make_url import make_url
# errors:
from core.errors.exceptions import *
from core.errors.logger import logger
# it's private because it assumes it's being called from the controller, (with a JSON)
def _check_if_paid(payload: dict, connection_observer: ConnectionObserver) -> str:
# prep endpoint:
which_endpoint = "check_paid"
url = make_url(which_endpoint)
# literally send:
reply = send_data_to_server(payload, url, connection_observer)
if "valid" in reply:
valid_status = reply.get("valid")
if valid_status == True:
return "paid"
else:
return "not_paid"
else:
error_msg = f"When checking if paid, the Server returned invalid data or it never sent. The reply is {reply}"
raise InvalidData(error_msg)

Some files were not shown because too many files have changed in this diff Show more