Compare commits
21 commits
master
...
core-larav
| Author | SHA1 | Date | |
|---|---|---|---|
| 693c3404ad | |||
| f38aa3b6f1 | |||
| 6849f1a46f | |||
| c5c0e9837a | |||
| 86621021bc | |||
| 474a6961b3 | |||
| 9aa3518fec | |||
| 29bd0e713b | |||
| c93a8572a1 | |||
| db8a0f0c74 | |||
| 3077e8d525 | |||
| 2e7fce9c1c | |||
| 0d5fdf2d66 | |||
| 866dda72e7 | |||
| 918c070f73 | |||
| a6d412b589 | |||
| 93fc03c6ca | |||
| 811b285fe6 | |||
| 8ef8f9e1c9 | |||
| 89705bbab7 | |||
| a3cf476af4 |
287 changed files with 4838 additions and 16394 deletions
6
.env.example
Normal file
6
.env.example
Normal 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
10
.gitignore
vendored
|
|
@ -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
|
|
||||||
56
README.md
56
README.md
|
|
@ -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
50
app/Constants.py
Normal 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'
|
||||||
|
|
@ -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):
|
||||||
123
app/controllers/ApplicationVersionController.py
Normal file
123
app/controllers/ApplicationVersionController.py
Normal 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()
|
||||||
124
app/controllers/ClientController.py
Normal file
124
app/controllers/ClientController.py
Normal 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')
|
||||||
|
|
@ -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
|
|
||||||
529
app/controllers/ConnectionController.py
Normal file
529
app/controllers/ConnectionController.py
Normal 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
|
||||||
22
app/controllers/LocationController.py
Normal file
22
app/controllers/LocationController.py
Normal 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)
|
||||||
22
app/controllers/OperatorController.py
Normal file
22
app/controllers/OperatorController.py
Normal 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)
|
||||||
313
app/controllers/ProfileController.py
Normal file
313
app/controllers/ProfileController.py
Normal 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()
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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):
|
||||||
17
app/controllers/encrypted_proxy/DisableController.py
Normal file
17
app/controllers/encrypted_proxy/DisableController.py
Normal 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,
|
||||||
|
)
|
||||||
26
app/controllers/encrypted_proxy/HysteriaController.py
Normal file
26
app/controllers/encrypted_proxy/HysteriaController.py
Normal 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)
|
||||||
24
app/controllers/encrypted_proxy/VlessController.py
Normal file
24
app/controllers/encrypted_proxy/VlessController.py
Normal 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)
|
||||||
0
app/controllers/encrypted_proxy/__init__.py
Normal file
0
app/controllers/encrypted_proxy/__init__.py
Normal file
180
app/controllers/tickets/TicketPayController.py
Normal file
180
app/controllers/tickets/TicketPayController.py
Normal 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
|
||||||
82
app/controllers/tickets/TicketPrepController.py
Normal file
82
app/controllers/tickets/TicketPrepController.py
Normal 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
|
||||||
44
app/controllers/tickets/TicketSyncController.py
Normal file
44
app/controllers/tickets/TicketSyncController.py
Normal 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
|
||||||
|
|
@ -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,26 +38,22 @@ 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}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
if on_or_off == "on":
|
if on_or_off == "on":
|
||||||
update_result = update_json(filepath, "use_random", True)
|
update_json(filepath, "use_random", True)
|
||||||
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
|
return {"valid": True}
|
||||||
elif on_or_off == "off":
|
elif on_or_off == "off":
|
||||||
update_result = update_json(filepath, "use_random", False)
|
update_json(filepath, "use_random", False)
|
||||||
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
|
return {"valid": True}
|
||||||
else:
|
else:
|
||||||
ticket_observer.notify("failed_input", None)
|
ticket_observer.notify("failed_input", None)
|
||||||
return {
|
return {
|
||||||
"valid": False,
|
"valid": False,
|
||||||
"message": "Invalid choice for turning on or off",
|
"message": f"Invalid choice for turning on or off",
|
||||||
}
|
}
|
||||||
|
except:
|
||||||
def conclude_and_return(update_result: bool, filepath: str, ticket_observer: TicketObserver) -> dict:
|
notification = f"Error with modifying config file. Check {filepath}"
|
||||||
if update_result:
|
|
||||||
return {"valid": True}
|
|
||||||
else:
|
|
||||||
notification = f"Error with modifying config file. Check the filepath {filepath}"
|
|
||||||
logger.error(notification)
|
|
||||||
ticket_observer.notify("error", subject=notification)
|
ticket_observer.notify("error", subject=notification)
|
||||||
return {"valid": False, "message": notification}
|
return {"valid": False, "message": notification}
|
||||||
|
|
||||||
|
|
@ -82,26 +71,18 @@ 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:
|
|
||||||
which_ticket = "error"
|
|
||||||
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)
|
|
||||||
return which_ticket, error_msg
|
|
||||||
|
|
||||||
random_setting = config_data["value"]
|
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
|
# they want a random ticket
|
||||||
|
if random_setting == True:
|
||||||
data_results = pick_a_random_ticket(ticket_observer)
|
data_results = pick_a_random_ticket(ticket_observer)
|
||||||
|
|
||||||
# invalid format:
|
if "random_ticket" in data_results:
|
||||||
if "random_ticket" not in data_results:
|
which_ticket = data_results["random_ticket"]
|
||||||
|
error_msg = None
|
||||||
|
return which_ticket, error_msg
|
||||||
|
else:
|
||||||
which_ticket = "error"
|
which_ticket = "error"
|
||||||
if "message" in data_results:
|
if "message" in data_results:
|
||||||
error_msg = data_results["message"]
|
error_msg = data_results["message"]
|
||||||
|
|
@ -111,18 +92,26 @@ def do_we_use_a_random_ticket(ticket_observer: TicketObserver) -> tuple:
|
||||||
)
|
)
|
||||||
return which_ticket, error_msg
|
return which_ticket, error_msg
|
||||||
|
|
||||||
# finally get the result:
|
# if it read the config, but the value is false:
|
||||||
which_ticket = data_results["random_ticket"]
|
else:
|
||||||
|
which_ticket = None
|
||||||
error_msg = None
|
error_msg = None
|
||||||
return which_ticket, error_msg
|
return which_ticket, error_msg
|
||||||
|
|
||||||
|
# this is a problem with reading the config itself:
|
||||||
|
else:
|
||||||
|
which_ticket = "error"
|
||||||
|
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)
|
||||||
|
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:
|
||||||
error_msg = "Missing or Invalid Data. Unable to get unused ticket list."
|
if ticket_data["valid"] == True:
|
||||||
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"]
|
list_of_unused_tickets = ticket_data["data"]
|
||||||
|
|
||||||
random_ticket = random.choice(list_of_unused_tickets)
|
random_ticket = random.choice(list_of_unused_tickets)
|
||||||
return {"valid": True, "random_ticket": random_ticket}
|
return {"valid": True, "random_ticket": random_ticket}
|
||||||
|
else:
|
||||||
|
return ticket_data
|
||||||
|
else:
|
||||||
|
error_msg = "Missing or Invalid Data. Unable to get unused ticket list."
|
||||||
|
return {"valid": False, "message": error_msg}
|
||||||
30
app/errors/exceptions.py
Normal file
30
app/errors/exceptions.py
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
84
app/models/Configuration.py
Normal file
84
app/models/Configuration.py
Normal 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
115
app/models/Location.py
Normal 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
64
app/models/Operator.py
Normal 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
|
||||||
19
app/models/OperatorProxySession.py
Normal file
19
app/models/OperatorProxySession.py
Normal 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
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -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
|
||||||
134
app/models/session/ApplicationVersion.py
Normal file
134
app/models/session/ApplicationVersion.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
22
app/models/session/SessionConnection.py
Normal file
22
app/models/session/SessionConnection.py
Normal 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
|
||||||
|
|
@ -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)
|
||||||
15
app/models/system/SystemConnection.py
Normal file
15
app/models/system/SystemConnection.py
Normal 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
|
||||||
|
|
@ -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:
|
|
||||||
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_system_config_path()))
|
|
||||||
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
|
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
|
||||||
if not completed_successfully:
|
if not completed_successfully:
|
||||||
raise ProfileDeletionError('The profile could not be deleted.')
|
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:
|
|
||||||
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_wireguard_configuration_path()), check=True)
|
|
||||||
|
|
||||||
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
|
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.')
|
|
||||||
|
|
||||||
|
|
@ -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):
|
||||||
|
|
||||||
8
app/observers/EncryptedProxyObserver.py
Normal file
8
app/observers/EncryptedProxyObserver.py
Normal 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 = []
|
||||||
|
|
@ -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:
|
||||||
|
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
|
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,36 +145,55 @@ 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
|
||||||
|
|
||||||
|
@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
|
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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
0
app/services/encrypted_proxy/__init__.py
Normal file
0
app/services/encrypted_proxy/__init__.py
Normal file
33
app/services/encrypted_proxy/disable_service.py
Normal file
33
app/services/encrypted_proxy/disable_service.py
Normal 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
|
||||||
102
app/services/encrypted_proxy/hysteria_service.py
Normal file
102
app/services/encrypted_proxy/hysteria_service.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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",
|
if "data" in api_results:
|
||||||
url=url,
|
new_public_key = api_results["data"]
|
||||||
observer=connection_observer,
|
|
||||||
payload=None
|
|
||||||
)
|
|
||||||
|
|
||||||
if api_results.valid:
|
|
||||||
new_public_key = api_results.data
|
|
||||||
return new_public_key
|
return new_public_key
|
||||||
else:
|
|
||||||
logger.error(f"API Results returned were invalid for that key.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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}
|
||||||
19
app/services/helpers/get_plan_data.py
Normal file
19
app/services/helpers/get_plan_data.py
Normal 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
|
||||||
11
app/services/helpers/get_which_billing_key.py
Normal file
11
app/services/helpers/get_which_billing_key.py
Normal 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
|
||||||
76
app/services/networking/evaluate_networking_problem.py
Normal file
76
app/services/networking/evaluate_networking_problem.py
Normal 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 ###############
|
||||||
|
#######################################################
|
||||||
31
app/services/networking/extract_domain.py
Normal file
31
app/services/networking/extract_domain.py
Normal 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]
|
||||||
|
|
@ -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)
|
||||||
56
app/services/networking/is_dns_problem.py
Normal file
56
app/services/networking/is_dns_problem.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
131
app/services/networking/use_tor.py
Normal file
131
app/services/networking/use_tor.py
Normal 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
|
||||||
31
app/services/payment_phase/check_if_paid.py
Normal file
31
app/services/payment_phase/check_if_paid.py
Normal 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
Loading…
Reference in a new issue