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
287 changed files with 4838 additions and 16394 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

10
.gitignore vendored
View file

@ -1,14 +1,6 @@
.env
.dev
prototype_client.py prototype_client.py
.idea .idea
.venv .venv
__pycache__ __pycache__
__pycache__/
dist dist
.mypy_cache .env
*.c
*.so
*.o
*.egg-info/
core/services/crypto/cython/build

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

50
app/Constants.py Normal file
View file

@ -0,0 +1,50 @@
from dataclasses import dataclass
from typing import Final
from dotenv import load_dotenv
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)
class Constants:
TICKET_API_BASE_URL: Final[str] = os.environ.get(
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
)
SP_API_BASE_URL: Final[str] = _sp_api_base_url
PING_URL: Final[str] = os.environ.get('PING_URL', f'{_sp_api_base_url}/health')
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'))
HV_CLIENT_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH')
HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER')
HOME: Final[str] = os.path.expanduser('~')
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', f'{INSTALL_DIR}/.cache')
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', f'{INSTALL_DIR}/.config')
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', f'{INSTALL_DIR}/data')
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_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil'
HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil'
HV_DATA_HOME: Final[str] = f'{DATA_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_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles'
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles'
HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data"
HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'
HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime'
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_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_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'

View file

@ -2,10 +2,7 @@ from core.Constants import Constants
from core.Errors import CommandNotFoundError from core.Errors import CommandNotFoundError
from core.controllers.SessionStateController import SessionStateController from core.controllers.SessionStateController import SessionStateController
from core.models.session.Application import Application from core.models.session.Application import Application
from core.models.orm_models.ApplicationVersion import ApplicationVersion from core.models.session.ApplicationVersion import ApplicationVersion
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.models.session.SessionProfile import SessionProfile from core.models.session.SessionProfile import SessionProfile
from core.models.session.SessionState import SessionState from core.models.session.SessionState import SessionState
from core.observers.ProfileObserver import ProfileObserver from core.observers.ProfileObserver import ProfileObserver
@ -33,7 +30,7 @@ class ApplicationController:
return Application.all() return Application.all()
@staticmethod @staticmethod
def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None, connection_observer: ConnectionObserver = None, ticket_observer: TicketObserver = None): def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None):
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
@ -99,7 +96,7 @@ class ApplicationController:
if not fork_process_id: if not fork_process_id:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer) ProfileController.disable(profile, False, profile_observer=profile_observer)
time.sleep(1.0) time.sleep(1.0)
sys.exit() sys.exit()
@ -107,7 +104,7 @@ class ApplicationController:
else: else:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer) ProfileController.disable(profile, False, profile_observer=profile_observer)
@staticmethod @staticmethod
def _sync(proxies: Optional[dict] = None): def _sync(proxies: Optional[dict] = None):

View file

@ -0,0 +1,123 @@
from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError, ApplicationAlreadyInstalledError
from core.controllers.ApplicationController import ApplicationController
from core.models.session.Application import Application
from core.models.session.ApplicationVersion import ApplicationVersion
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
from core.observers.ConnectionObserver import ConnectionObserver
from core.services.WebServiceApiService import WebServiceApiService
from io import BytesIO
from typing import Optional
import hashlib
import shutil
import tarfile
class ApplicationVersionController:
@staticmethod
def get(application_code: str, version_number: str):
return ApplicationVersion.find(application_code, version_number)
@staticmethod
def get_all(application: Optional[Application] = None):
return ApplicationVersion.all(application)
@staticmethod
def install(application_version: ApplicationVersion, reinstall: bool = False, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None):
if not application_version.is_supported():
raise UnsupportedApplicationVersionError('The application version in question is not supported.')
if reinstall:
ApplicationVersionController.uninstall(application_version)
if application_version.is_installed():
raise ApplicationAlreadyInstalledError('The application in question is already installed.')
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController.__install, application_version_observer=application_version_observer, connection_observer=connection_observer)
@staticmethod
def uninstall(application_version: ApplicationVersion):
shutil.rmtree(application_version.get_installation_path(), ignore_errors=True)
@staticmethod
def _sync(proxies: Optional[dict] = None):
applications = ApplicationController.get_all()
application_versions = []
for application_code in (application.code for application in applications):
application_version_subset = WebServiceApiService.get_application_versions(application_code, proxies)
for application_version in application_version_subset:
application_versions.append(application_version)
ApplicationVersion.truncate()
ApplicationVersion.save_many(application_versions)
@staticmethod
def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, proxies: Optional[dict] = None):
import requests
if application_version_observer is not None:
application_version_observer.notify('downloading', application_version)
if proxies is not None:
response = requests.get(application_version.download_path, stream=True, proxies=proxies)
else:
response = requests.get(application_version.download_path, stream=True)
if response.status_code == 200:
response_size = int(response.headers.get('Content-Length', 0))
response_buffer = BytesIO()
block_size = 1024
bytes_written = 0
for data in response.iter_content(block_size):
bytes_written += len(data)
response_buffer.write(data)
progress = (bytes_written / response_size) * 100 if response_size > 0 else 0
if application_version_observer is not None:
application_version_observer.notify('download_progressing', application_version, dict(
progress=progress
))
application_version_observer.notify('downloaded', application_version)
response_buffer.seek(0)
file_hash = ApplicationVersionController.__calculate_file_hash(response_buffer)
if file_hash != application_version.file_hash:
raise FileIntegrityError('Application version file integrity could not be verified.')
with tarfile.open(fileobj=response_buffer, mode = 'r:gz') as tar_file:
tar_file.extractall(application_version.get_installation_path())
with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file:
hash_file.write(f'{file_hash}\n')
else:
raise ConnectionError('The application version could not be downloaded.')
@staticmethod
def __calculate_file_hash(file):
hasher = hashlib.sha3_512()
buffer = file.read(65536)
while len(buffer) > 0:
hasher.update(buffer)
buffer = file.read(65536)
file.seek(0)
return hasher.hexdigest()

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

@ -1,24 +1,14 @@
from core.Errors import UnknownConnectionTypeError from core.Errors import UnknownConnectionTypeError
from core.models.Configuration import Configuration, ConnectionChoice from core.models.Configuration import Configuration
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
class ConfigurationController: class ConfigurationController:
_config: Optional[Configuration] = None
@staticmethod @staticmethod
def get(): def get():
if ConfigurationController._config is None: return Configuration.get()
ConfigurationController._config = Configuration.get()
return ConfigurationController._config
# return Configuration.get()
@staticmethod
def reload_from_disk():
ConfigurationController._config = None
@staticmethod @staticmethod
def get_or_new(): def get_or_new():
@ -34,35 +24,19 @@ class ConfigurationController:
def get_connection(): def get_connection():
configuration = ConfigurationController.get() configuration = ConfigurationController.get()
return configuration.connection.value
@staticmethod if configuration is None or configuration.connection not in ('system', 'tor', 'vless', 'hysteria2'):
def get_connection_enum(): raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
configuration = ConfigurationController.get()
return configuration.connection return configuration.connection
@staticmethod @staticmethod
def set_connection(connection_string: Optional[str] = None): def set_connection(connection: Optional[str] = None):
configuration = ConfigurationController.get_or_new() configuration = ConfigurationController.get_or_new()
if connection_string == "tor":
connection = ConnectionChoice.TOR
elif connection_string == "system":
connection = ConnectionChoice.SYSTEM
else:
raise UnknownConnectionTypeError(f'The choice of {connection_string} is not valid.')
configuration.connection = connection configuration.connection = connection
configuration.save() configuration.save()
@staticmethod
def set_connection_enum(connection_enum: ConnectionChoice) -> bool:
configuration = ConfigurationController.get_or_new()
configuration.connection = connection_enum
configuration.save()
@staticmethod @staticmethod
def get_auto_sync_enabled(): def get_auto_sync_enabled():
@ -110,45 +84,10 @@ class ConfigurationController:
@staticmethod @staticmethod
def update_last_synced_at(): def update_last_synced_at():
configuration = ConfigurationController.get() configuration = ConfigurationController.get_or_new()
configuration.last_synced_at = datetime.now(timezone.utc) configuration.last_synced_at = datetime.now(timezone.utc)
configuration.save() configuration.save()
@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()
configuration.firewall = new_value
configuration.save()
@staticmethod
def get_firewall_setting():
configuration = ConfigurationController.get()
return configuration.firewall
@staticmethod
def change_dns(new_value):
configuration = ConfigurationController.get()
configuration.dns = new_value
configuration.save()
@staticmethod
def get_dns_setting():
configuration = ConfigurationController.get()
return configuration.dns
@staticmethod
def get_singbox_version():
configuration = ConfigurationController.get()
return configuration.singbox
@staticmethod
def update_singbox_version(new_value) -> bool:
configuration = ConfigurationController.get()
configuration.singbox = new_value
configuration.save()
return True

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

@ -0,0 +1,313 @@
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.SessionStateController import SessionStateController
from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import BaseProfile as Profile
from core.models.Subscription import Subscription
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.ProfileObserver import ProfileObserver
from core.services.WebServiceApiService import WebServiceApiService
from typing import Union, Optional
import base64
import re
import time
class ProfileController:
@staticmethod
def get(id: int) -> Union[SessionProfile, SystemProfile, None]:
return Profile.find_by_id(id)
@staticmethod
def get_all():
return Profile.all()
@staticmethod
def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile.save()
if profile_observer is not None:
profile_observer.notify('created', profile)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile.save()
if profile_observer is not None:
profile_observer.notify('updated', profile)
@staticmethod
def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
if ProfileController.is_enabled(profile):
if not ProfileStateConflictError in ignore:
raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.')
else:
ProfileController.disable(profile)
if pristine:
profile.delete_data()
if profile.is_session_profile():
application_version = profile.application_version
if not application_version.is_installed():
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
try:
port_number = ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.')
if profile_observer is not None:
profile_observer.notify('enabled', profile)
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer)
if profile.is_system_profile():
try:
ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.')
if profile_observer is not None:
profile_observer.notify('enabled', profile)
@staticmethod
def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, ignore: tuple[type[Exception]] = (), profile_observer: ProfileObserver = None):
from core.controllers.ConnectionController import ConnectionController
if profile.is_session_profile():
if SessionStateController.exists(profile.id):
session_state = SessionStateController.get(profile.id)
if session_state is not None:
for port_number in session_state.network_port_numbers.tor:
ConnectionController.terminate_tor_session_connection(port_number)
session_state.dissolve(session_state.id)
if profile.is_system_profile():
subjects = ProfileController.get_all().values()
for subject in subjects:
if subject.is_session_profile():
if subject.connection.is_unprotected() and ProfileController.is_enabled(subject) and not ConnectionUnprotectedError in ignore:
raise ConnectionUnprotectedError('Disabling this system connection would leave one or more sessions exposed.')
if SystemStateController.exists():
system_state = SystemStateController.get()
if profile.id != system_state.profile_id:
raise ProfileDeactivationError('The profile could not be disabled.')
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.')
if profile_observer is not None:
profile_observer.notify('disabled', profile, dict(
explicitly=explicitly,
))
time.sleep(1.0)
@staticmethod
def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
ProfileController.disable(profile)
profile.delete()
if profile_observer is not None:
profile_observer.notify('destroyed', profile)
@staticmethod
def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
profile.subscription = subscription
profile.save()
@staticmethod
def activate_subscription(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
from core.controllers.ConnectionController import ConnectionController
if profile.has_subscription():
subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
if subscription is not None:
profile.subscription = subscription
profile.save()
else:
raise InvalidSubscriptionError()
else:
raise MissingSubscriptionError()
@staticmethod
def is_enabled(profile: Union[SessionProfile, SystemProfile]):
from core.controllers.ConnectionController import ConnectionController
if profile.is_session_profile():
session_state = SessionStateController.get_or_new(profile.id)
return len(session_state.network_port_numbers.all) > 0 or len(session_state.process_ids) > 0
if profile.is_system_profile():
system_state = SystemStateController.get()
if system_state is not None and system_state.profile_id is profile.id:
return ConnectionController.system_uses_wireguard_interface()
return False
@staticmethod
def get_invoice(profile: Union[SessionProfile, SystemProfile]):
if profile.has_subscription():
return WebServiceApiService.get_invoice(profile.subscription.billing_code)
else:
return None
@staticmethod
def attach_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
if profile.is_session_profile() and profile.has_subscription():
proxy_configuration = WebServiceApiService.get_proxy_configuration(profile.subscription.billing_code)
if proxy_configuration is not None:
profile.attach_proxy_configuration(proxy_configuration)
@staticmethod
def get_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
if profile.is_session_profile():
return profile.get_proxy_configuration()
else:
return None
@staticmethod
def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
profile.has_proxy_configuration()
@staticmethod
def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
from core.controllers.ConnectionController import ConnectionController
if not profile.has_subscription():
raise MissingSubscriptionError()
if not profile.has_location():
raise MissingLocationError()
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)
if wireguard_configuration is None:
raise InvalidSubscriptionError()
expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE)
wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration)
profile.attach_wireguard_configuration(wireguard_configuration)
@staticmethod
def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]):
return profile.get_wireguard_configuration_path()
@staticmethod
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
return profile.has_wireguard_configuration()
@staticmethod
def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
try:
ProfileController.__verify_wireguard_endpoint(profile)
except EndpointVerificationError as error:
if not EndpointVerificationError in ignore:
profile.address_security_incident()
raise error
@staticmethod
def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
from cryptography.hazmat.primitives.asymmetric import ed25519
import base64
signature = profile.get_wireguard_configuration_metadata('Signature')
wireguard_public_keys = profile.get_wireguard_public_keys()
operator = profile.location.operator
if signature is None:
raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
if not wireguard_public_keys:
raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
if operator is None:
raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
try:
operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
for wireguard_public_key in wireguard_public_keys:
operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
except Exception:
raise EndpointVerificationError('The WireGuard endpoint could not be verified.')
@staticmethod
def __generate_wireguard_keys():
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
raw_private_key = X25519PrivateKey.generate()
public_key = raw_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
)
private_key = raw_private_key.private_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
)
return dict(
private=base64.b64encode(private_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,13 +12,8 @@ class SystemStateController:
return SystemState.exists() return SystemState.exists()
@staticmethod @staticmethod
def create(profile_id: int, firewalled: bool, dns_set: bool, process_id: int = None) -> SystemState: def create(profile_id):
if process_id: return SystemState(profile_id).save()
current_state = SystemState(profile_id, firewalled, dns_set, process_id)
else:
current_state = SystemState(profile_id, firewalled, dns_set)
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

@ -0,0 +1,180 @@
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.models.invoice.TicketInvoice import TicketInvoice
from core.services.prepare_tickets.get_pub_key import get_pub_key
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.check_if_paid import _check_if_paid
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.make_url import make_url
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.errors.exceptions import *
from core.errors.logger import logger
from core.controllers.tickets.TicketSyncController import sync_ticket_prices
"""
Inputs: Which plan (key), which crypto, and how many profiles
Outputs: a temp billing code & crypto address. (or "error")
"""
def initiate_payment(
how_many_profiles: int,
which_key: str,
which_cryptocurrency: str,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
bypass_existing: bool,
) -> TicketInvoice:
###############
invoice_data_object = TicketInvoice()
try:
if bypass_existing == False:
tickets_exist_already, path = does_ticket_tracker_exist()
logger.debug(f"tickets_exist_already is {tickets_exist_already}")
if tickets_exist_already:
invoice_data_object.add_error_code("already_exists")
return invoice_data_object
rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if not valid_profile_quantity(how_many_profiles):
notification = "Invalid profile quantity"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if which_key in rejected_choices:
notification = "Missing key plan, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_keyplan")
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:
public_key_results = get_pub_key(which_key, connection_observer, "local")
if isinstance(public_key_results, dict):
status = public_key_results.get("valid", False)
if status == False:
message = public_key_results.get(
"message", "Please fix before you continue"
)
error_code = public_key_results.get("error_code", "No error_code")
logger.debug(f"error_code: {error_code}")
invoice_data_object.add_error_code(error_code)
notification = f"Connection Issues or Invalid Key chosen. {message}"
ticket_observer.notify("failed_input", subject=notification)
return invoice_data_object
else:
notification = f"Invalid Key chosen or the server is down for that key."
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_pub_key")
return invoice_data_object
# In this case, the controller is going to send the whole JSON payload to the service,
# instead of passing 4 values seperately.
payload = {
"which_key": which_key,
"payment_type": "crypto",
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
# controller sends to the service:
result = save_and_send_intitial_billing(
payload, connection_observer, invoice_data_object
)
if result == False or result == None:
invoice_data_object.add_error_code("failed_save")
return invoice_data_object
except InvalidData as e:
error_msg = "Invalid Data."
ticket_observer.notify("failed_input", subject=error_msg)
invoice_data_object.add_error_code("invalid_data")
return invoice_data_object
except NetworkingError as e:
error_msg = f"NetworkingError: {e}"
logger.error(error_msg, exc_info=True)
ticket_observer.notify("connection_error", subject=error_msg)
invoice_data_object.add_error_code("connection_error")
return invoice_data_object
except ServerSideError as e:
error_msg = f"ServerSideError: {e}"
logger.error(error_msg, exc_info=True)
ticket_observer.notify("failed_output", subject=error_msg)
invoice_data_object.add_error_code("server_error")
return invoice_data_object
except Exception as e:
error_msg = f"Error: {e}"
logger.error(error_msg, exc_info=True)
ticket_observer.notify("unknown_error", subject=error_msg)
invoice_data_object.add_error_code("unknown_error")
return invoice_data_object
###############
def check_if_paid(
temp_billing_code: str,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
) -> dict:
rejected_reasons = [None, "", False]
if temp_billing_code in rejected_reasons:
error_msg = "Invalid Temp Billing Code"
logger.error(f"{error_msg} inside the check_if_paid function", exc_info=True)
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "error_code": "rejected"}
else:
# prep the JSON payload:
payload = {"temp_billing_code": temp_billing_code}
# prep endpoint:
which_endpoint = "check_paid"
url = make_url(which_endpoint)
# literally send:
reply = send_data_to_server(payload, url, connection_observer)
logger.debug(f"inside ticketpay controller the reply is {reply}")
return reply

View file

@ -0,0 +1,82 @@
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.services.prepare_tickets.ticket_prep_orchestrator import ticket_prep_orchestrator
from core.services.prepare_tickets.make_sure_pub_key_exists import make_sure_pub_key_exists
from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered,
)
from core.observers.BaseObserver import BaseObserver
"""
Goal:
this is the controller for the view to speak with the high level "ticket_prep_orchestrator" service function,
which makes commitments, sends to the server, and then unblinds them, and preps the ticket JSONs.
Requires:
a temp billing ID
having paid already
Doesn't Require:
If it doesn't have the public key, then it checks the config file.
If it doesn't have a config file, then it uses the temp billing id to get the public key from the server.
"""
def prepare_tickets(
how_many_profiles: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
) -> dict:
# make sure it's a number:
if not isinstance(how_many_profiles, int):
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# make sure that "how_many_profiles" is actually the number of profiles ordered
# (which is based on locally saved data from the previous step):
how_many_ordered = get_how_many_profiles_were_ordered()
if how_many_profiles != how_many_ordered:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# make sure this guy has a public key to verify against:
does_he_have_public_key = make_sure_pub_key_exists(connection_observer)
if does_he_have_public_key == False:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_orchestrator(
how_many_profiles, ticket_observer, connection_observer
)
# rest of this function is evaluating the results:
if prep_results == False or prep_results == None:
return {"valid": False, "message": "error"}
if "valid" not in prep_results:
return {"valid": False, "message": "error"}
if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!"
ticket_observer.notify("preparing", subject=notification)
return prep_results
if "how_many_failed" in prep_results:
how_many_failed = prep_results.get("how_many_failed", 0)
failed_validations = prep_results.get("failed_validations", None)
if failed_validations:
notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification)
return prep_results
notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification)
return prep_results

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

@ -1,30 +1,23 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from core.essentials.observers.ConnectionObserver import ConnectionObserver from core.essentials.observers.ConnectionObserver import ConnectionObserver
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.prepare_tickets.ticket_tracker import (
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist get_data_for_a_single_ticket,
does_ticket_tracker_exist,
# from core.services.prepare_tickets import setup_ticket_tracker )
from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist
from core.services.helpers.get_value_from_config import get_value_from_config from core.services.helpers.get_value_from_config import get_value_from_config
from core.utils.basic_operations.does_file_exist import does_file_exist from core.utils.basic_operations.does_file_exist import does_file_exist
from core.utils.basic_operations.write_or_read_from_json import update_json from core.utils.basic_operations.write_or_read_from_json import update_json
from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.errors.logger import logger
# generic
import random import random
from typing import Optional
def modify_random_tickets_setting( def modify_random_tickets_setting(
@ -35,7 +28,7 @@ def modify_random_tickets_setting(
choices = ["on", "off"] choices = ["on", "off"]
if on_or_off not in choices: if on_or_off not in choices:
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "message": "Invalid choice for turning on or off"} return {"valid": False, "message": f"Invalid choice for turning on or off"}
billing_folder = Constants.HV_TICKETING_CONFIG_HOME billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json" filepath = f"{billing_folder}/billing_choices.json"
@ -45,28 +38,24 @@ def modify_random_tickets_setting(
notification = "First setup the Tickets before picking use" notification = "First setup the Tickets before picking use"
ticket_observer.notify("failed_input", subject=notification) ticket_observer.notify("failed_input", subject=notification)
return {"valid": False, "message": notification} return {"valid": False, "message": notification}
if on_or_off == "on":
update_result = update_json(filepath, "use_random", True)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
elif on_or_off == "off":
update_result = update_json(filepath, "use_random", False)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
else: else:
ticket_observer.notify("failed_input", None) try:
return { if on_or_off == "on":
"valid": False, update_json(filepath, "use_random", True)
"message": "Invalid choice for turning on or off", return {"valid": True}
} elif on_or_off == "off":
update_json(filepath, "use_random", False)
def conclude_and_return(update_result: bool, filepath: str, ticket_observer: TicketObserver) -> dict: return {"valid": True}
if update_result: else:
return {"valid": True} ticket_observer.notify("failed_input", None)
else: return {
notification = f"Error with modifying config file. Check the filepath {filepath}" "valid": False,
logger.error(notification) "message": f"Invalid choice for turning on or off",
ticket_observer.notify("error", subject=notification) }
return {"valid": False, "message": notification} except:
notification = f"Error with modifying config file. Check {filepath}"
ticket_observer.notify("error", subject=notification)
return {"valid": False, "message": notification}
""" """
@ -82,47 +71,47 @@ def do_we_use_a_random_ticket(ticket_observer: TicketObserver) -> tuple:
config_data = get_value_from_config("use_random") config_data = get_value_from_config("use_random")
# if the 'value' key is in the config, that means it successfully read the config. # if the 'value' key is in the config, that means it successfully read the config.
if "value" not in config_data: if "value" in config_data:
# this is a problem with reading the config itself: random_setting = config_data["value"]
# they want a random ticket
if random_setting == True:
data_results = pick_a_random_ticket(ticket_observer)
if "random_ticket" in data_results:
which_ticket = data_results["random_ticket"]
error_msg = None
return which_ticket, error_msg
else:
which_ticket = "error"
if "message" in data_results:
error_msg = data_results["message"]
else:
error_msg = (
"Missing or Invalid Data. Unable to get unused ticket list."
)
return which_ticket, error_msg
# if it read the config, but the value is false:
else:
which_ticket = None
error_msg = None
return which_ticket, error_msg
# this is a problem with reading the config itself:
else:
which_ticket = "error" which_ticket = "error"
error_msg = "There is an error with the config file, or no config. Are you sure you have tickets?" error_msg = "There is an error with the config file, or no config. Are you sure you have tickets?"
ticket_observer.notify("failed_input", subject=error_msg) ticket_observer.notify("failed_input", subject=error_msg)
return which_ticket, error_msg return which_ticket, error_msg
random_setting = config_data["value"]
# if it read the config, but the value is false:
if not random_setting:
which_ticket = None
error_msg = None
return which_ticket, error_msg
# they want a random ticket
data_results = pick_a_random_ticket(ticket_observer)
# invalid format:
if "random_ticket" not in data_results:
which_ticket = "error"
if "message" in data_results:
error_msg = data_results["message"]
else:
error_msg = (
"Missing or Invalid Data. Unable to get unused ticket list."
)
return which_ticket, error_msg
# finally get the result:
which_ticket = data_results["random_ticket"]
error_msg = None
return which_ticket, error_msg
def get_unused_tickets(ticket_observer: TicketObserver) -> dict: def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
# does the file keeping track of ALL tickets exist: # does the file keeping track of ALL tickets exist:
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:
@ -143,10 +132,9 @@ use_ticket function requires:
def use_ticket( def use_ticket(
which_ticket: int, which_ticket: int,
which_location: int, # Location.id which_location: str,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
profile: Optional[Union[SessionProfile, SystemProfile]] = None, # only for assassin or single profiles.
) -> dict: ) -> dict:
which_ticket = str(which_ticket) # type: ignore which_ticket = str(which_ticket) # type: ignore
@ -154,44 +142,46 @@ def use_ticket(
# does the ticket's file exist: # does the ticket's file exist:
ticket_exists = does_ticket_file_exist(which_ticket) ticket_exists = does_ticket_file_exist(which_ticket)
if ticket_exists == False: if ticket_exists == False:
error_msg = "The ticket file does not exist in the correct folder." error_msg = f"The ticket file does not exist in the correct folder."
ticket_observer.notify("failed_input", subject=error_msg) ticket_observer.notify("failed_input", subject=error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) return {"valid": False, "message": error_msg}
# does the file keeping track of ALL tickets exist: # does the file keeping track of ALL tickets exist:
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 Result(valid=False, error_type=ResultError.MISSING_FILE, message=error_msg) return {"valid": False, "message": error_msg}
# is the ticket used?
try:
status, location, subscription = get_data_for_a_single_ticket(which_ticket)
if status == "used":
error_msg = f"Ticket is already tied to {location} with the subscription {subscription}"
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg}
except:
error_msg = f"Your local ticket tracker has no value for ticket {which_ticket}"
return {"valid": False, "message": error_msg}
# the actual work here, everything else is just handling: # the actual work here, everything else is just handling:
ticket_observer.notify("connecting", "Connecting..") ticket_observer.notify("connecting", "Connecting..")
reply_object = use_ticket_orchestrator(which_ticket, which_location, connection_observer) reply = use_ticket_orchestrator(which_ticket, which_location, connection_observer)
return reply_object return reply
# TRANSLATE FOR UI
# if reply_object.valid:
# return {"valid": True, "billing_code": reply_object.data}
# error_msg = reply_object.message
# error_type = reply_object.error_type
# logger.error(f"Error Type: {error_type}")
# return {"valid": False, "message": error_msg, "error_type": error_type}
def pick_a_random_ticket(ticket_observer: TicketObserver) -> dict: def pick_a_random_ticket(ticket_observer: TicketObserver) -> dict:
ticket_data = get_unused_tickets(ticket_observer) ticket_data = get_unused_tickets(ticket_observer)
if "valid" not in ticket_data: if "valid" in ticket_data:
if ticket_data["valid"] == True:
list_of_unused_tickets = ticket_data["data"]
random_ticket = random.choice(list_of_unused_tickets)
return {"valid": True, "random_ticket": random_ticket}
else:
return ticket_data
else:
error_msg = "Missing or Invalid Data. Unable to get unused ticket list." error_msg = "Missing or Invalid Data. Unable to get unused ticket list."
return {"valid": False, "message": error_msg} return {"valid": False, "message": error_msg}
if ticket_data["valid"] != True:
return ticket_data
# it's valid, get the list & pick one:
list_of_unused_tickets = ticket_data["data"]
random_ticket = random.choice(list_of_unused_tickets)
return {"valid": True, "random_ticket": random_ticket}

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

@ -18,7 +18,7 @@ debug_mode = os.getenv("DEBUG", "").lower() in ("1", "true", "yes")
console_handler = logging.StreamHandler(sys.stdout) console_handler = logging.StreamHandler(sys.stdout)
console_level = logging.DEBUG if debug_mode else logging.WARNING console_level = logging.DEBUG if debug_mode else logging.WARNING
console_handler.setLevel(console_level) console_handler.setLevel(console_level)
console_formatter = logging.Formatter("[%(funcName)s] %(message)s") console_formatter = logging.Formatter("%(message)s")
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler) logger.addHandler(console_handler)

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,32 +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 core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.orm_calls.application_version_calls import get_application_version
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
@ -34,40 +13,8 @@ import os
import re import re
import shutil import shutil
import tempfile import tempfile
from sqlalchemy.orm import Session
from enum import Enum
from core.models.manage.session_management import init_session, get_session
@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"[get_profile_location_data] Got invalid SQL Query")
return None
class ProfileType(str, Enum):
SESSION = "session"
SYSTEM = "system"
@dataclass_json @dataclass_json
@dataclass @dataclass
class BaseProfile(ABC): class BaseProfile(ABC):
@ -76,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):
@ -108,25 +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, app_version_dict: dict = None): config_file_contents = f'{self.to_json(indent=4)}\n'
# === SERIALIZATION ===
config_dict = self.to_dict()
# === LOCATION ===
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
# === APPLICATION ===
if app_version_dict:
config_dict["application_version"] = app_version_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)
@ -219,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):
@ -235,62 +161,30 @@ 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 = get_application_version( application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None)
profile['application_version']['application_code'] or None,
profile['application_version']['version_number'] or None
)
if application_version is not None: if application_version is not None:
profile['application_version'] = application_version profile['application_version'] = application_version
if application_version is None:
profile['application_version'] = None
# legacy
# 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)
# if application_version is not None:
# profile['application_version'] = application_version
from core.models.session.SessionProfile import SessionProfile from core.models.session.SessionProfile import SessionProfile
# 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
@ -298,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')
@ -331,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

@ -0,0 +1,84 @@
from core.Constants import Constants
from core.Helpers import write_atomically
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config
from datetime import datetime
from marshmallow import fields
from typing import Optional, Self
from zoneinfo import ZoneInfo
import dataclasses_json
import json
import os
import sys
@dataclass_json
@dataclass
class Configuration:
connection: Optional[str] = field(
default=None,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
auto_sync_enabled: Optional[bool] = field(
default=None,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
endpoint_verification_enabled: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
last_synced_at: Optional[datetime] = field(
default=None,
metadata=config(
encoder=lambda datetime_instance: Configuration._iso_format(datetime_instance),
decoder=lambda datetime_string: Configuration._from_iso_format(datetime_string),
mm_field=fields.DateTime(format='iso'),
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
def save(self: Self):
config_file_contents = f'{self.to_json(indent=4)}\n'
os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
config_file_path = f'{Constants.HV_CONFIG_HOME}/config.json'
write_atomically(config_file_path, config_file_contents)
@staticmethod
def get():
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:
sys.exit(1)
# noinspection PyUnresolvedReferences
configuration = Configuration.from_dict(configuration)
return configuration
@staticmethod
def _iso_format(datetime_instance: datetime):
datetime_instance = datetime_instance.replace(tzinfo=ZoneInfo('UTC'))
return datetime.isoformat(datetime_instance).replace('+00:00', 'Z')
@staticmethod
def _from_iso_format(datetime_string: str):
date_string = datetime_string.replace('Z', '+00:00')
return datetime.fromisoformat(date_string)

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(

View file

@ -1,13 +1,13 @@
from pydantic import BaseModel from pydantic import BaseModel
from core.errors.get_error_msg import get_error_msg from core.errors.get_error_msg import get_error_msg
class TicketInvoice(BaseModel): class TicketInvoice(BaseModel):
valid: bool = True temp_billing_code: str | None = None
temp_billing_code: str payment_type: str | None = None
payment_type: str = "crypto" selected_currency: str | None = None
selected_currency: str = None due_amount: float | None = None
due_amount: float address: str | None = None
address: str
final_error_msg: str | None = None final_error_msg: str | None = None
error_code: str | None = None error_code: str | None = None

View file

@ -0,0 +1,134 @@
from core.Constants import Constants
from core.models.Model import Model
from core.models.session.Application import Application
from dataclasses import dataclass, field
from dataclasses_json import config, Exclude
from datetime import datetime
from dateutil.parser import isoparse
from marshmallow import fields
from typing import Optional
import os
_table_name: str = 'application_versions'
_table_definition: str = """
'id' int UNIQUE,
'application_code' varchar,
'version_number' varchar,
'format_revision' int,
'download_path' varchar UNIQUE,
'released_at' varchar,
'file_hash' varchar,
UNIQUE(application_code, version_number)
"""
@dataclass
class ApplicationVersion(Model):
application_code: str
version_number: str
format_revision: Optional[int] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
id: Optional[int] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
download_path: Optional[str] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
released_at: Optional[datetime] = field(
default=None,
metadata=config(
encoder=datetime.isoformat,
decoder=datetime.fromisoformat,
mm_field=fields.DateTime(format='iso'),
exclude=Exclude.ALWAYS
)
)
file_hash: Optional[str] = field(
default=None,
metadata=config(exclude=Exclude.ALWAYS)
)
installed: Optional[bool] = field(
default=False,
metadata=config(exclude=Exclude.ALWAYS)
)
supported: Optional[bool] = field(
default=False,
metadata=config(exclude=Exclude.ALWAYS)
)
def __post_init__(self):
self.installed = self.is_installed()
self.supported = self.is_supported()
def get_installation_path(self):
return f'{Constants.HV_APPLICATION_DATA_HOME}/{self.application_code}/{self.version_number}'
def is_installed(self):
return os.path.isdir(self.get_installation_path()) and len(os.listdir(self.get_installation_path())) > 0
def is_supported(self):
return self.exists(self.application_code, self.version_number) and self.format_revision == 2
def get_installed_file_hash(self):
try:
return open(f'{self.get_installation_path()}/.sha3-512').readline().strip()
except FileNotFoundError:
return None
def is_fresh(self):
return self.is_installed() and (not self.is_supported() or self.file_hash == self.get_installed_file_hash())
@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 application_versions WHERE id = ? LIMIT 1', ApplicationVersion.factory, [id])
@staticmethod
def find(application_code: str, version_number: str):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_one('SELECT * FROM application_versions WHERE application_code = ? AND version_number = ? LIMIT 1', ApplicationVersion.factory, [application_code, version_number])
@staticmethod
def all(application: Optional[Application] = None):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
if application is None:
return Model._query_all('SELECT * FROM application_versions', ApplicationVersion.factory)
else:
return Model._query_all('SELECT * FROM application_versions WHERE application_code = ?', ApplicationVersion.factory, [application.code])
@staticmethod
def exists(application_code: str, version_number: str):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_exists('SELECT * FROM application_versions WHERE application_code = ? AND version_number = ?', [application_code, version_number])
@staticmethod
def truncate():
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
@staticmethod
def save_many(application_versions):
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
Model._insert_many('INSERT INTO application_versions VALUES(?, ?, ?, ?, ?, ?, ?)', ApplicationVersion.tuple_factory, application_versions)
@staticmethod
def factory(cursor, row):
database_fields = [column[0] for column in cursor.description]
application_version = ApplicationVersion(**{key: value for key, value in zip(database_fields, row)})
application_version.released_at = isoparse(str(application_version.released_at))
return application_version
@staticmethod
def tuple_factory(application_version):
return application_version.id, application_version.application_code, application_version.version_number, application_version.format_revision, application_version.download_path, application_version.released_at, application_version.file_hash

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

@ -1,11 +1,7 @@
from core.Constants import Constants from core.Constants import Constants
from core.Errors import UnknownTimeZoneError from core.Errors import UnknownTimeZoneError
from core.models.BaseProfile import BaseProfile from core.models.BaseProfile import BaseProfile
# from core.models.session.ApplicationVersion import ApplicationVersion from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.session.ProxyConfiguration import ProxyConfiguration from core.models.session.ProxyConfiguration import ProxyConfiguration
from core.models.session.SessionConnection import SessionConnection from core.models.session.SessionConnection import SessionConnection
from dataclasses import dataclass from dataclasses import dataclass
@ -20,48 +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]
ticket: Optional[int] = None
assassin: Optional[bool] = False
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):
print("We are able to trigger save on the child session")
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()
# === APPLICATION ===
app_version_dict = self.application_version.convert_to_dict()
print(f"session child got {app_version_dict} as dict..")
super().save(app_version_dict=app_version_dict)
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)
@ -72,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):
@ -94,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,66 +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]
ticket: Optional[int] = None
assassin: Optional[bool] = False
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()
@ -72,66 +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
def attach_encrypted_proxy_config(self, config_data: dict):
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
backup_path = f'{self.get_config_path()}/proxy.conf.bak'
with open(backup_path, 'w') as configuration_file:
configuration_file.write(config_data)
configuration_is_attached = False
failed_attempt_count = 0
while not configuration_is_attached and failed_attempt_count < 3:
process = subprocess.Popen(('pkexec', 'install', '-D', backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744'))
configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not configuration_is_attached:
failed_attempt_count += 1
if not configuration_is_attached:
raise ProfileModificationError('The WireGuard configuration could not be attached.')

View file

@ -6,15 +6,11 @@ from typing import Self
import json import json
import os import os
import pathlib import pathlib
from typing import Optional
@dataclass_json @dataclass_json
@dataclass @dataclass
class SystemState: class SystemState:
profile_id: int profile_id: int
firewalled: bool
dns_set: bool
process_id: Optional[int] = None
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,15 +1,14 @@
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.orm_models.Location import Location from core.models.Location import Location
from core.models.orm_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
from core.models.invoice.PaymentMethod import PaymentMethod from core.models.invoice.PaymentMethod import PaymentMethod
from core.models.session.Application import Application from core.models.session.Application import Application
# from core.models.session.ApplicationVersion import ApplicationVersion from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.session.ProxyConfiguration import ProxyConfiguration from core.models.session.ProxyConfiguration import ProxyConfiguration
from typing import Optional from typing import Optional
import re import re
@ -20,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']))
@ -34,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']))
@ -48,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']))
@ -62,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']))
@ -90,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']
@ -159,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):
@ -214,15 +219,3 @@ class WebServiceApiService:
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

@ -15,19 +15,13 @@ from py_ecc.optimized_bls12_381 import (
) )
from py_ecc.optimized_bls12_381.optimized_curve import FQ, FQ2 from py_ecc.optimized_bls12_381.optimized_curve import FQ, FQ2
# for the (optional) validity tests:
from py_ecc.optimized_bls12_381 import pairing
# errors: # errors:
from core.errors.logger import logger from core.errors.logger import logger
import traceback import traceback
# for the (optional) validity tests:
try:
from core.services.crypto.cython.bls12_381_pairing import pairing
logger.info("Imported Cython pairings")
except ImportError:
# Fallback to py_ecc if not compiled
from py_ecc.optimized_bls12_381 import pairing
logger.error("FAILED to import cython pairings, used python py_ecc")
class TicketCustomer: class TicketCustomer:
""" """
@ -212,7 +206,6 @@ class TicketCustomer:
blinded_signature = self._deserialize_point_g2(blind_signature) blinded_signature = self._deserialize_point_g2(blind_signature)
blinded_commitment_as_dict = get_data(which_ticket, "blinded_commitment_json") blinded_commitment_as_dict = get_data(which_ticket, "blinded_commitment_json")
blinded_commitment = self._deserialize_point_g2(blinded_commitment_as_dict) blinded_commitment = self._deserialize_point_g2(blinded_commitment_as_dict)
projective_public_key = self.load_key(string_public_key) projective_public_key = self.load_key(string_public_key)
@ -221,7 +214,6 @@ class TicketCustomer:
return {"valid": False, "message": "invalid_key"} return {"valid": False, "message": "invalid_key"}
# All of that was to prep the values for this pairing equation, # All of that was to prep the values for this pairing equation,
# Which now uses Cython
try: try:
if pairing(blinded_signature, G1) == pairing( if pairing(blinded_signature, G1) == pairing(
blinded_commitment, projective_public_key blinded_commitment, projective_public_key

View file

@ -1,7 +1,4 @@
from core.services.crypto.TicketCustomer import TicketCustomer from core.services.crypto.TicketCustomer import TicketCustomer
from core.errors.logger import logger
from typing import Optional
""" """
We are making both an unblinded and blinded commitment pair. We are making both an unblinded and blinded commitment pair.
@ -18,14 +15,12 @@ def make_ONE_commitment_pair(
# First, make the original unblinded commitment. it's saved inside the profile object: # First, make the original unblinded commitment. it's saved inside the profile object:
did_unblinded_save = profile_object.make_unblinded_commitment(which_ticket) did_unblinded_save = profile_object.make_unblinded_commitment(which_ticket)
logger.info(f"did_unblinded_save {did_unblinded_save}")
# that `profile_object` object is holding the unblinded commitment, # that `profile_object` object is holding the unblinded commitment,
# so it can be directly used to blind it (without having to serialize then deserialize it). # so it can be directly used to blind it (without having to serialize then deserialize it).
# Then BLIND it, so it can be sent to the billing server: # Then BLIND it, so it can be sent to the billing server:
blind_commitment = profile_object.blind_commitment(which_ticket) blind_commitment = profile_object.blind_commitment(which_ticket)
# logger.info(f"blind_commitment is {blind_commitment}")
# we need to make sure we actually saved the data, # we need to make sure we actually saved the data,
# because it's the only way to unblind it later: # because it's the only way to unblind it later:
@ -38,30 +33,15 @@ def make_ONE_commitment_pair(
return blind_commitment return blind_commitment
def make_ALL_commitments(how_many_profiles_to_make: int, which_ticket: Optional[int] = None) -> list | None: def make_ALL_commitments(how_many_profiles_to_make: int) -> list | None:
# Setup the entire class object of "profile_object" for using all these other functions, # Setup the entire class object of "profile_object" for using all these other functions,
profile_object = TicketCustomer() profile_object = TicketCustomer()
# setup loop: # setup loop:
which_ticket = 0
list_of_all_blinded_data = [] list_of_all_blinded_data = []
failed_to_save = [] failed_to_save = []
##########################################################
# SINGLE TICKET PREP (respawn flow)
##########################################################
if which_ticket and how_many_profiles_to_make == 1:
logger.info("Doing a single ticket prep.")
blinded_string = make_ONE_commitment_pair(profile_object, which_ticket)
profile_object.reset()
list_of_all_blinded_data.append(blinded_string)
return list_of_all_blinded_data
##########################################################
# BULK TICKET PREP (regular flow)
##########################################################
logger.info(f"Doing a regular bulk ticket prep for {how_many_profiles_to_make} tickets")
which_ticket = 0
# loop up to the number of profiles requested: # loop up to the number of profiles requested:
while which_ticket < how_many_profiles_to_make: while which_ticket < how_many_profiles_to_make:
which_ticket = which_ticket + 1 which_ticket = which_ticket + 1

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

@ -1,5 +1,10 @@
from core.Constants import Constants
from urllib.parse import unquote 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: def parse_vless_link(link: str) -> dict:
link = link.replace("vless://", "") link = link.replace("vless://", "")
@ -26,23 +31,24 @@ def parse_vless_link(link: str) -> dict:
} }
def build_vless_config(vless: dict, socks5_port: int, server_ip: str) -> dict: def build_vless_config(vless: dict, socks5_port: int) -> dict:
server_ip = socket.gethostbyname(vless["host"])
return { return {
"dns": { "dns": {
"servers": [{"tag": "tunnel-dns", "type": "udp", "server": "9.9.9.9"}], "servers": [{"tag": "local", "type": "udp", "server": "9.9.9.9"}],
"final": "tunnel-dns", "final": "local",
"strategy": "ipv4_only", "strategy": "ipv4_only",
"independent_cache": True,
}, },
"inbounds": [ "inbounds": [
{ {
"type": "tun", "type": "tun",
"tag": "tun-in", "tag": "tun-in",
"interface_name": Constants.SINGBOX_TUN_IF, "interface_name": "tun0",
"address": [Constants.SINGBOX_INTERNAL_ADDR], "address": ["172.19.0.1/30"],
"mtu": 9000, "mtu": 9000,
"auto_route": True, "auto_route": True,
"stack": "gvisor", "stack": "system",
}, },
{ {
"type": "socks", "type": "socks",
@ -57,7 +63,7 @@ def build_vless_config(vless: dict, socks5_port: int, server_ip: str) -> dict:
{ {
"type": "vless", "type": "vless",
"tag": "proxy", "tag": "proxy",
"server": str(server_ip), "server": server_ip,
"server_port": vless["port"], "server_port": vless["port"],
"uuid": vless["uuid"], "uuid": vless["uuid"],
"tls": { "tls": {
@ -75,15 +81,50 @@ def build_vless_config(vless: dict, socks5_port: int, server_ip: str) -> dict:
"route": { "route": {
"rules": [ "rules": [
{"protocol": "dns", "action": "hijack-dns"}, {"protocol": "dns", "action": "hijack-dns"},
{"ip_cidr": [Constants.SINGBOX_INTERNAL_SUBNET], "action": "hijack-dns"}, {"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"},
{"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"}, {"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"},
{"ip_is_private": True, "outbound": "direct"}, {"ip_is_private": True, "outbound": "direct"},
{"ip_version": 6, "outbound": "block"}, {"ip_version": 6, "outbound": "block"},
{"inbound": ["tun-in", "socks-in"], "outbound": "proxy"}, {"inbound": ["tun-in", "socks-in"], "outbound": "proxy"},
], ],
"final": "proxy", "final": "proxy",
"default_domain_resolver": "tunnel-dns", "default_domain_resolver": "local",
"auto_detect_interface": True, "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,10 +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.get_data_from_server import get_data_from_server
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# 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
@ -73,21 +70,12 @@ 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)
api_results = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
if api_results.valid: if "data" in api_results:
new_public_key = api_results.data new_public_key = api_results["data"]
return new_public_key
else:
logger.error(f"API Results returned were invalid for that key.")
return None
return new_public_key
def are_keys_different(old_public_key, new_public_key) -> bool: def are_keys_different(old_public_key, new_public_key) -> bool:
@ -108,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)
@ -133,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)
@ -154,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

@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
# services
from core.services.networking.make_url import make_url
from core.services.networking.send_data_to_server import send_data_to_server
# use temp billing to get the plan details
def get_plan_data(
temp_billing_code: str, connection_observer: ConnectionObserver
) -> dict:
which_endpoint = "/plan"
url = make_url(which_endpoint)
payload = {"temp_billing_code": temp_billing_code}
reply = send_data_to_server(payload, url, connection_observer)
return reply

View file

@ -0,0 +1,11 @@
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.Constants import Constants
def get_which_billing_key() -> str:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
json_key = "which_key"
which_key = get_value_from_json_file(filepath, json_key)
return which_key

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
@ -16,17 +11,16 @@ from core.services.networking.regular_get_request import regular_get_request
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
import traceback import traceback
from typing import Optional
# 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