Compare commits

..

54 commits

Author SHA1 Message Date
9c5636eb8c TunnelMonitor with is_tunnel_alive(), add WireGuardMonitor, arm/disarm killswitch on WireGuard connect/disconnect 2026-07-15 17:05:14 -05:00
a4bbfa4ec5 refactor(core/cli): route vless/hysteria2 through _enable_encrypted_proxy, scope KillswitchMonitor to encrypted proxy only 2026-07-09 18:23:11 -05:00
cae72efee3 feat(core): branch vless/hysteria2 immediately in ProfileController.enable() 2026-07-09 09:44:08 -05:00
1a60511492 refactor: align cli with core architecture 2026-07-08 11:14:41 -05:00
5ed7598315 Extract establish_encrypted_proxy_connection to core — shared logic for CLI and GUI 2026-07-07 16:08:24 -05:00
999ddbf855 Add location_country_code/city_code to operator proxy flow + SingboxProcessMonitor ABC + observer_helpers 2026-07-05 06:17:20 -05:00
68aa315f46 Update CLI to use new ORM and initialize database for sync 2026-06-30 07:20:03 -05:00
59f997ef45 Merge deploy into master: encrypted proxy killswitch + TUN optimization 2026-06-25 15:44:12 -05:00
aca954d364 small fix 20-06-26 2026-06-20 08:43:43 -05:00
7dca2744ce small fix 2026-06-19 06:42:08 -05:00
d6b122b975 June Major Update 2026-06-16 08:11:28 -05:00
c1fb1bc7c6 fix errors kill s. 2026-06-09 09:09:10 -05:00
76d3650979 fix hysteria and vless bug 2026-06-06 09:30:31 -05:00
9372fe4c26 remove .env.example 2026-06-05 18:45:13 -05:00
eff49cf1f7 radical change 2026-06-05 10:31:35 -05:00
399c66e3cb Verify Sing-box before the connection 2026-06-04 07:40:04 -05:00
ed0de4b2d9 Verify Sing-box before the connection 2026-06-04 07:38:55 -05:00
35d94908f4 Core configuration synchronized with the CLI 2026-06-03 17:54:29 -05:00
50bf6503d9 Actualizar lib/steps/singbox.sh 2026-06-02 18:24:02 +00:00
eaa28aada1 Actualizar lib/steps/repos.sh 2026-06-02 17:59:20 +00:00
2d958f9145 Actualizar lib/steps/repos.sh 2026-06-02 17:33:09 +00:00
24dacb2e33 Actualizar lib/steps/repos.sh 2026-06-02 17:02:54 +00:00
bf7bdfa4d7 Actualizar lib/steps/repos.sh 2026-06-02 17:00:21 +00:00
fde1d35f17 Actualizar lib/steps/repos.sh 2026-06-02 16:51:56 +00:00
dd7edfa4ed Actualizar lib/steps/repos.sh 2026-06-02 16:30:48 +00:00
58d7837c7f Actualizar lib/steps/repos.sh 2026-06-02 16:21:59 +00:00
c55706f573 Actualizar lib/steps/repos.sh 2026-06-02 16:11:04 +00:00
594f6a8d4b Actualizar lib/steps/repos.sh 2026-06-02 15:51:37 +00:00
55f2515874 Actualizar lib/steps.sh 2026-06-02 15:51:00 +00:00
5ef45ad6b4 2 july 2026 2026-06-02 07:53:09 -05:00
7387844371 Remove deleted files 2026-06-02 07:26:10 -05:00
bf8eb9d96c all support, core,cli,gui,essentiials 2026-06-02 07:19:50 -05:00
047c5b2ed0 all support, core,cli,gui,essentiials 2026-06-02 07:19:17 -05:00
693c3404ad Actualizar pyproject.toml 2026-05-29 13:05:19 +00:00
f38aa3b6f1 26-05-16-6:29 2026-05-26 18:29:24 -05:00
6849f1a46f Eliminar .env 2026-05-26 21:41:10 +00:00
c5c0e9837a add .env 2026-05-26 21:40:33 +00:00
86621021bc add installer 2026-05-26 16:31:16 -05:00
474a6961b3 fix connection 2026-05-25 01:09:38 +00:00
9aa3518fec fix connection 2026-05-25 01:08:27 +00:00
29bd0e713b fix conection 2026-05-25 01:07:30 +00:00
c93a8572a1 ffi 2026-05-25 01:06:53 +00:00
db8a0f0c74 fix 2xx and subdomain 2026-05-25 00:40:11 +00:00
3077e8d525 fix add subdomain 2026-05-25 00:39:16 +00:00
2e7fce9c1c add subdomain operator 2026-05-25 00:02:57 +00:00
0d5fdf2d66 fix 2xx 2026-05-25 00:01:52 +00:00
866dda72e7 fix system-profile 2026-05-23 15:15:15 +00:00
918c070f73 fix system-profile 2026-05-23 15:14:09 +00:00
a6d412b589 fix subdomain 2026-05-23 04:40:13 +00:00
93fc03c6ca fix subdomain 2026-05-23 04:39:37 +00:00
811b285fe6 add install.sh 2026-05-23 03:12:33 +00:00
8ef8f9e1c9 feat: use SystemProfile path for sing-box config storage 2026-05-23 03:03:23 +00:00
89705bbab7 feat: use SystemProfile path for sing-box config storage 2026-05-23 03:02:36 +00:00
a3cf476af4 core-laravel-proxyTEST 2026-05-16 00:36:11 -05:00
260 changed files with 4568 additions and 20189 deletions

10
.gitignore vendored
View file

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

163
README.md
View file

@ -1,34 +1,163 @@
# sp-hydra-veil-core
# Hydra-Veil — June Major Update
The `sp-hydra-veil-core` library exposes core logic to higher-level components.
## Expanded Killswitch
## Build Instructions
`hydraveil-killswitch.sh` was added to the installer: a wrapper around `nftables` that blocks all traffic not routed through the tunnel. It defaults to `tun0`, but supports a configurable interface IP that the operator will send when creating the profile. The IP is currently hardcoded; it will be removed in the next commit.
### Presumptions
## One Connection at a Time
* Your system is configured to use the Simplified Privacy package registry [1].
First in, first out. When a new connection is established, the previous one receives a signal (`SIGUSR1`) and closes cleanly before the new one takes over.
### Prerequisites
## Unexpected Shutdowns
* `build`
* `twine`
If the terminal is closed or the machine is powered off, the process receives `SIGHUP` and performs a full disconnect: stops `sing-box`, clears state, and releases the network. No orphan processes.
To install them, activate your `venv`, if necessary, and run:
## Fewer Network Requests
```bash
pip install build twine
Unnecessary calls made when establishing and closing connections have been removed.
---
## Repositories
| Repo | Role |
|---|---|
| `core` | Business logic, controllers, models |
| `cli` | Command-line interface |
| `essentials` | Network modules (Tor, WireGuard, proxies) |
| `installer` | Installation scripts and killswitch |
---
---
# Hydra-Veil — Gran Actualización de Junio
## Killswitch ampliado
Se añadió `hydraveil-killswitch.sh` al installer: un wrapper sobre `nftables` que bloquea todo el tráfico que no pase por el túnel. Por defecto usa `tun0`, pero soporta una IP de interfaz configurable que el operador enviará al crear el perfil. Por ahora la IP está hardcodeada; se eliminará en el próximo commit.
## Una conexión a la vez
Primero en entrar, primero en salir. Cuando se establece una nueva conexión, la anterior recibe una señal (`SIGUSR1`) y se cierra limpiamente antes de que la nueva tome el control.
## Cierres inesperados
Si se cierra la terminal o se apaga el equipo, el proceso recibe `SIGHUP` y ejecuta la desconexión completa: para `sing-box`, limpia el estado y libera la red. Sin procesos huérfanos.
## Menos solicitudes de red
Se eliminaron llamadas innecesarias que se hacían al establecer y cerrar conexiones.
---
## Repositorios
| Repo | Rol |
|---|---|
| `core` | Lógica de negocio, controladores, modelos |
| `cli` | Interfaz de línea de comandos |
| `essentials` | Módulos de red (Tor, WireGuard, proxies) |
| `installer` | Scripts de instalación y killswitch |
---
---
## Uso rápido — Desde Core (innecesario, usar CLI)
## 1. Create billing code / Crear billing code
```python
python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
subscription = WebServiceApiService.post_subscription(2, operator_id=<OPERATOR_ID>)
print('billing code:', subscription.billing_code)
"
```
### Build Steps
> Copy the printed `billing_code` — you'll need it in the connect step.
> Copiá el `billing_code` impreso, lo vas a necesitar en el paso de conexión.
* Activate your `venv`, if necessary, and run:
---
consultar estado de invoive
python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
invoice = WebServiceApiService.get_invoice('BILLING_CODE_HERE')
print('status:', invoice.status)
"
## 2. Patch subscription / Parchear suscripción
```bash
rm -rf ./dist/*
python3 -m build
python3 -m twine upload --repository forgejo ./dist/*
sudo docker compose exec laravel 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;
"
```
---
[1] https://forgejo.org/docs/v14.0/user/packages/pypi/#configuring-the-package-registry
## 3. Connect / Conectar
### VLESS
```python
python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.encrypted_proxy.VlessController import VlessController
from core.observers.EncryptedProxyObserver import EncryptedProxyObserver
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))
session = WebServiceApiService.post_operator_proxy('MZKD-QWWI-1TS9-KHMD', 3, 'vless')
print('session:', session)
controller = VlessController(1080)
controller.enable(session.links[0], session.username, observer)
"
```
### Hysteria2
```python
python3 -c "
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
from core.observers.EncryptedProxyObserver import EncryptedProxyObserver
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))
session = WebServiceApiService.post_operator_proxy('MZKD-QWWI-1TS9-KHMD', 3, 'hysteria2')
print('session:', session)
controller = HysteriaController(1080)
controller.enable(session.username, session.password, session.operator_hysteria2_host, observer)
"
```
---
## 4. Disconnect / Desconectar
```python
python3 -c "
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
from core.observers.EncryptedProxyObserver import EncryptedProxyObserver
observer = EncryptedProxyObserver()
observer.subscribe('disconnected', lambda e: print('disconnected'))
controller = HysteriaController(1080)
controller.disable(observer)
"
```
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

View file

@ -1,24 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: application_code
path: ['application', 'code']
required: true
- name: version_number
path: ['version_number']
required: true
- name: format_revision
path: ['format_revision']
- name: download_path
path: ['download_path']
- name: released_at
path: ['released_at']
- name: file_hash
path: ['file_hash']

View file

@ -1,145 +0,0 @@
# Major Change Log:
# Use Ticket Orchestration
### Aug 23, 2026
Large changes to the flow for using tickets. More formalized updating of JSON with explicit tools, instead of generic JSON functions. Orchestration returns Return objects, which are then formatted to dict for the GUI.
<br/>
# Manual WG Enable
### Aug 23, 2026
Manual Explicit Wireguard enable via nmcli for Improved Cross-Linux distro support.
<br/>
# Cython Introduced
### Aug 22, 2026
Cython for Miller's Loop Cryptography first introduced, which dramatically reduces verification times. Includes a setup.py option for manual builds if desired. README for manual compile if desired. And a fallback import using the legacy python-only system.
<br/>
# Vless Introduced
### Aug 20, 2026
Vless is now working. Config Parsing and setup is stable, and increased the wait time for connection testing. But this is a temporary solution. The real answer is not a static time check, but a dynamic result reading for when to test. Which then has it's own timeout time
<br/>
# Assassin Introduced
### Aug 18, 2026
Assassin mode Introduced! The assassin_tools allows for creation/use of a session profile with random attributes, ephemeral wireguard config, and coordinates using a ticket for it. This tools module also allows for wiping the assassin's ticket & wireguard config on disable. Additionally, this git commit sees ProfileController using these assassin_tools functions for a working implementation if the profile is marked "assassin=True". Also the associated eco-system of ApplicationController starting apps, the existing non-ticket legacy subscription handlers, and even GUI needed to be adjusted to allow for this new assassin flow. Finally, the generic JSON utilities have been improved to make this feature more robust and reliable.
<br/>
# Respawn on Delete
### Aug 15, 2026
When a profile is deleted, it now automatically respawns the ticket if it's a ticket profile. Also fixed some bugs with fetching the server's public key with the APIResponse object being used, when it expected a regular dictionary. This is a left-over from the prior transition to API objects.
<br/>
# Codes/Tickets
### Aug 15, 2026
The profile object now has a ticket field. It's now none by default. Also the profile object is passed in, for the use of tickets, both for GUI and core.
<br/>
# Wipe & Respawn Codes/Tickets
### Aug 14, 2026
Introduced Ticket/Billing-Code Respawn, with dual paths on the ticket prep orchestration, it's helper functions, and the main prep controller. This introduced the functions, which were tested with a demo CLI client and local server, and appears to be stable. Server-side endpoints will be pushed as well. Note: This update couples profile slot ids more closely with which ticket slot id is being used for it, but they are not yet officially tied. In the future, they should be.
<br/>
# Reduce Redundancy
### Aug 13, 2026
Isolated download file modules to their own module, then had both the install application versions (browsers), and install dependencies both use that same module.
<br/>
# Singbox Setup
### Aug 12, 2026
Prepared Singbox setup modules, which includes installation, download, move to sudo folder, and sudo setup scripts. Added a `Dependency` model, endpoint, and the ability to sync that model. (Related note: Server-side prepared the endpoint, and stocked with real data.) And also the sync service modules were adjusted to handle new data types more smoothly, before they had errors. As part of that sync flow change, the CachedSync metadata model was transitioned to ints instead of strings, with default 0 values. This should in theory migrate all clients without further changes needed.
### Additional Changes:
ApplicationController transitioned to the HTTPx Client system, instead of using requests library proxy for Tor. Also the GUI's observers now do strings to be more neutral to what object type is being downloaded (application_version objects vs dependency).
<br/>
# Singbox Orchestration
### Aug 10, 2026
Significant progress made on singbox flow, we now have a working:
1) Configure via singbox_configure
2) Enable/disable directly via singbox module
3) Enable/disable with full orchestration via singbox_runner
4) process id utils isolated
5) Singbox takedown uses JSON & process id, but not yet coordinating a mismatch.
Also in this update:
Transition traditional wireguard subscription post/get requests to the new HTTPx system.
# WG Renegotiation
### Aug 7, 2026
Wireguard renegotiation now flows through the new HTTPx modules
<br/>
# EncryptedProxy Configs
### Aug 6, 2026
EncryptedProxy configuration path setup, but not yet functional. Pydantic models for each protocol are being setup.
<br/>
# Transition
### Aug 6, 2026
1) Transition ticket prep from old requests-based GET/POST system to the new httpx one.
2) Adjusted server-side for application_versions, and then the client to match it. It now has a single endpoint, instead of 4 different ones.
3) Have Tor bootstrap use a new config, but same data folder on first attempt.
<br/>
# Sync on No Database Start
### August 5, 2026
Added a forced sync at startup if there's no DB pre-existing, to handle a bug in GUI with no application versions. Isolated application_version database calls to their own module. And fixed a bug in SyncController to catch if application_version updates and split them into the different tables.
</br>
# Sync Now Integrated & ApplicationVersion Model Transition
### August 4, 2026
Sync from the main app streamlines into the Connect module, to do async bulk sync. Additionally ApplicationVersion was transitioned from manual SQL to the ORM, for easier compatibility with the newer sync system. The side effect functions of this have been tested to be stable.
</br>
# Connect Module & Async
### August 3, 2026
Introduced Connect Module, which manages HTTPx Clients, solves network/DNS issues, and coordinates Tor Bootstraps. Introduced async & single endpoint full workflows. This version is stable and tested for DNS & Tor problems. Also modified Configuration to be Pydantic, instead of @dataclass_json, and changed the connection type to enums. Further, there's a new enum function to get the new enum types, but the legacy function exists for backwards compatability.
# HTTPx Client, ThreadPool, & Tor Management
### August 2, 2026
Elaborate HTTPx feature-rich strategy with HTTPx Client Reuse, Tor Management, Tor bootstrap monitor & retry. HTTPx clients can be managed across modules and reused. There's an elaborate HTTPx retry strategy based on status codes. Tor management tries to use the existing Tor default ports, then checks the Tor config, before finally bootstrapping. There is now a ThreadPool futures executor, which even works with Tor, to batch API requests.
# Singbox
### July 29, 2026
Introduced the Singbox Enable Utility & Singbox Sudo Script
<br/>
# 2.4.3
### Systemwide Killswitch Introduced for Enable
### July 23, 2026
Separation of performing subprocess commands, parsing them for error strings, and handling those errors as enum types. Systemwide error handling was introduced via a wrapper to all systemwide functions requiring the sudo scripts.
<br/>
### July 20, 2026
Introduced IP-interface-based killswitch support for systemwide wireguard, on enable only. This version is stable and working for enable only, with a "test" script in the fileslot. And fixed GUI's object save creation. Additionally rolled back Connection data models to previous non-enum version.
<br/>
# 2.4.2
### Surgery on ConnectionController
Huge separation of ConnectionController & ProfileController into naked function modules for enabling a connection, registering wireguard keys, coordinating proxy configs, subscriptions, endpoint verification, and the logistical flow. Additionally enums were used for type checking, instead of polymorphic functions. This version is stable and confirmed working for systemwide and session wireguard, but not yet proxies.
<br/>
# 2.4.1
### Enums for Profile types.
July 18, 2026
Introduce Base profile types on load, which gradually will switch over on save/edits. Transition Connections to using Enum instead of raw strings. End goal here is to transition ConnectionController to use the Enums.
<br/>
# 2.4.0
July 15, 2026
Robust Database Error Handling & Migrations System. For New Client upgrades (Operational Errors) & Old Clients with New JSON keys from an API (TypeErrors). We also added an SQL generic handling wrapper for use across any project or function.
<br/>
# 2.3.9
July 11, 2026
Features: Revamp of GET/POST API requests for both Tor and Clearweb.
Why: Move towards more clear Tor API error feedback. And reduce redundancy.
Status: Stable, works for both clear/tor and GET/POST.
Still Needs: UI feedback, DNS Tor improvements
<br/>

View file

@ -6,91 +6,75 @@ import os
@dataclass(frozen=True)
class Constants:
DB_VERSION_THIS_APP_WANTS = 3
# Fallback for development (running outside AppImage)
fallback_non_appimage = os.path.dirname(os.path.abspath(__file__))
# appimage home
APPDIR_HOME: Final[str] = os.environ.get('APPDIR', fallback_non_appimage)
# ── API ─────────────────────────────────────────────
DB_VERSION_THIS_APP_WANTS = 1
# ticketing group:
TICKET_API_BASE_URL: Final[str] = os.environ.get(
"TICKET_API_BASE_URL", "https://api.hydraveil.net"
# "TICKET_API_BASE_URL", "http://localhost:8000"
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
)
SP_API_BASE_URL: Final[str] = os.environ.get(
'SP_API_BASE_URL', 'https://fake.simplifiedprivacy.org/api/v1'
)
PING_URL: Final[str] = os.environ.get(
'PING_URL', 'https://fake.simplifiedprivacy.org/api/v1//health'
)
# SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'http://localhost:8000/api/v1')
# SP_BASE_URL: Final[str] = os.environ.get('SP_BASE_URL', 'http://localhost:8000/api')
SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')
SP_BASE_URL: Final[str] = os.environ.get('SP_BASE_URL', 'https://api.hydraveil.net/api')
PING_URL: Final[str] = os.environ.get('PING_URL', 'https://api.hydraveil.net/api/v1/health')
CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5'))
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')
# ── Paths base ────────────────────────────────────────────────────────────
HOME: Final[str] = os.path.expanduser('~')
SYSTEM_CONFIG_PATH: Final[str] = '/etc'
# ── XDG estándar ─────────────────────────────────────────────────────────
CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', os.path.join(HOME, '.cache'))
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', os.path.join(HOME, '.local/share'))
STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', os.path.join(HOME, '.local/state'))
# ── hydra-veil dirs ───────────────────────────────────────────────────────
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'
# ── ticketing ─────────────────────────────────────────────
HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data"
TICKET_TRACKER_PATH = f"{HV_TICKETING_CONFIG_HOME}/ticket_tracker.json"
# ── end of ticketing ──────────────────────────────────────
HV_TICKETING_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/ticketing'
HV_TICKETING_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/ticket_data'
HV_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'
# ── killswitch / dns wrappers ─────────────────────────────────────────────
KILLSWITCH_WRAPPER: Final[str] = os.environ.get(
'KILLSWITCH_WRAPPER', '/opt/hydra-veil/killswitch'
)
# ── sing-box ──────────────────────────────────────────────────────────────
SINGBOX_WRAPPER: Final[str] = os.environ.get(
'SINGBOX_WRAPPER', '/usr/local/bin/hydraveil-singbox'
)
SINGBOX_BIN: Final[str] = os.environ.get(
'SINGBOX_BIN', '/usr/bin/sing-box'
)
SINGBOX_CONFIG_DIR: Final[str] = f'{HV_DATA_HOME}/configs'
SINGBOX_PID_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.pid'
SINGBOX_LOG_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.log'
SINGBOX_TUN_IF: Final[str] = os.environ.get('SINGBOX_TUN_IF', 'tun0')
SINGBOX_INTERNAL_SUBNET: Final[str] = os.environ.get('SINGBOX_INTERNAL_SUBNET', '172.19.0.0/30')
SINGBOX_INTERNAL_ADDR: Final[str] = os.environ.get('SINGBOX_INTERNAL_ADDR', '172.19.0.1/30')
SINGBOX_DEFAULT_DNS: Final[str] = "9.9.9.9"
SINGBOX_OUTPUT: Final[str] = f"{HV_DATA_HOME}/sing-box-output.txt"
SUDO_TARGET_FOLDER: Final[str] = "/opt/hydra-veil"
# ── Tor ─────────────────────────────────────────────
DEFAULT_TOR_PORT = 9050
# ── killswitch / dns wrappers ─────────────────────────────────────────────
KILLSWITCH_WRAPPER: Final[str] = os.environ.get(
'KILLSWITCH_WRAPPER', '/usr/local/bin/hydraveil-killswitch'
)
RESOLVECTL_WRAPPER: Final[str] = os.environ.get(
'RESOLVECTL_WRAPPER', '/usr/local/bin/hydraveil-resolvectl'
)
VLESS_DNS_ENABLED: Final[bool] = os.environ.get('VLESS_DNS_ENABLED', 'true').lower() == 'true'
HYSTERIA2_DNS_ENABLED: Final[bool] = os.environ.get('HYSTERIA2_DNS_ENABLED', 'true').lower() == 'true'

View file

@ -1,102 +1,56 @@
class CommandNotFoundError(OSError):
def __init__(self, subject):
self.subject = subject
super().__init__(f"Command '{subject}' could not be found.")
class UnknownClientPathError(Exception):
pass
class UnknownClientVersionError(Exception):
pass
class UnknownConnectionTypeError(Exception):
pass
class UnknownTimeZoneError(Exception):
pass
class ConnectionTerminationError(Exception):
pass
class PolicyAssignmentError(Exception):
pass
class PolicyInstatementError(Exception):
pass
class PolicyRevocationError(Exception):
pass
class ProfileDeletionError(Exception):
pass
class ProfileModificationError(Exception):
pass
class ProfileStateConflictError(Exception):
pass
class ProfileActivationError(Exception):
pass
class ProfileDeactivationError(Exception):
pass
class UnsupportedApplicationVersionError(Exception):
pass
class ApplicationAlreadyInstalledError(Exception):
pass
class MissingLocationError(Exception):
pass
class MissingSubscriptionError(Exception):
pass
class InvalidSubscriptionError(Exception):
pass
class InvoiceNotFoundError(Exception):
pass
class InvoiceExpiredError(Exception):
pass
class InvoicePaymentFailedError(Exception):
pass
class ConnectionUnprotectedError(Exception):
pass
class FileIntegrityError(Exception):
pass
class EndpointVerificationError(Exception):
pass
class SingboxNotInstalledException(Exception):
pass
class PaymentRequiredError(Exception):
pass

View file

@ -1 +0,0 @@

View file

@ -1,57 +0,0 @@
#!/bin/bash
set -euo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo "[killswitch] ERROR: must be run as root" >&2
exit 1
fi
ACTION="${1:-}"
IFACE="${2:-}"
DNS="${3:-}"
_check_prereqs() {
[[ -z "$DNS" ]] && { echo "Error: DNS IP required" >&2; exit 1; }
# ✓ Check it's a valid IPv4 address:
if ! [[ "$DNS" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
echo "Error: Invalid IP: $DNS" >&2
exit 1
fi
# ✓ checks interface exists before configuring
if ! ip link show "$IFACE" &>/dev/null; then
echo "Error: Interface $IFACE not found" >&2
exit 1
fi
}
_set_dns() {
resolvectl dns "$IFACE" "$DNS" || {
echo "ERROR: Failed to set DNS to $DNS" >&2
return 1
}
resolvectl domain "$IFACE" '~.' || {
echo "ERROR: Failed to set domain routing" >&2
return 1
}
resolvectl default-route "$IFACE" true || {
echo "ERROR: Failed to set default route" >&2
return 1
}
}
if [[ "$ACTION" == "set" ]]; then
_check_prereqs
_set_dns
fi
# if [[ "$ACTION" == "revert" ]]; then
# resolvectl revert "$IFACE" 2>/dev/null || true
# fi
if [[ "$ACTION" == "revert" ]]; then
resolvectl revert "$IFACE" || true
fi

View file

@ -1,120 +0,0 @@
#!/bin/bash
set -euo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo "[killswitch] ERROR: must be run as root" >&2
exit 1
fi
ACTION="${1:-}"
SERVER_IP="${2:-}"
TUNNEL_IF="${3:-}"
INTERNAL_SUBNET="${4:-}"
TABLE="hydraveil"
if [[ "$ACTION" != "arm" && "$ACTION" != "disarm" && "$ACTION" != "status" ]]; then
echo "[killswitch] ERROR: invalid action '$ACTION'. Usage: arm <server_ip> <tunnel_if> [internal_subnet] | disarm | status" >&2
exit 1
fi
_default_iface() {
ip route show default 2>/dev/null | awk 'NR==1{print $5}'
}
_log() {
local level="$1"; shift
echo "[killswitch] [$level] $* — $(date '+%Y-%m-%d %H:%M:%S')" >&2
}
if [[ "$ACTION" == "status" ]]; then
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
echo "armed"
else
echo "disarmed"
fi
exit 0
fi
if [[ "$ACTION" == "disarm" ]]; then
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
nft delete table inet "$TABLE"
_log "INFO" "disarmed"
else
_log "INFO" "already disarmed"
fi
exit 0
fi
[[ -z "$SERVER_IP" ]] && { _log "ERROR" "server_ip required for arm"; exit 1; }
[[ -z "$TUNNEL_IF" ]] && { _log "ERROR" "tunnel_if required for arm"; exit 1; }
if ! [[ "$SERVER_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
_log "ERROR" "invalid IPv4: $SERVER_IP"
exit 1
fi
IFS='.' read -r o1 o2 o3 o4 <<< "$SERVER_IP"
for oct in "$o1" "$o2" "$o3" "$o4"; do
if (( oct > 255 )); then
_log "ERROR" "invalid IPv4 octet ($oct) in $SERVER_IP"
exit 1
fi
done
WAN_IFACE=$(_default_iface)
if [[ -z "$WAN_IFACE" ]]; then
_log "ERROR" "could not detect primary network interface"
exit 1
fi
INTERNAL_RULE=""
if [[ -n "$INTERNAL_SUBNET" ]]; then
INTERNAL_RULE=" ip daddr ${INTERNAL_SUBNET} accept"
fi
nft list table inet "$TABLE" &>/dev/null 2>&1 && nft delete table inet "$TABLE" || true
nft -f - << NFTEOF
table inet ${TABLE} {
chain output {
type filter hook output priority filter; policy drop;
oifname "lo" accept
ether type arp drop
ip6 daddr != ::1 drop
ip daddr 224.0.0.0/4 drop
ip daddr 255.255.255.255 drop
ip daddr 192.168.1.0/24 drop
oifname "${WAN_IFACE}" ip daddr ${SERVER_IP} accept
oifname "${TUNNEL_IF}" accept
log prefix "hydraveil-drop " drop
}
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ether type arp drop
ip6 saddr != ::1 drop
ip daddr 224.0.0.0/4 drop
ip daddr 255.255.255.255 drop
ct state established,related accept
iifname "${WAN_IFACE}" ip saddr ${SERVER_IP} accept
iifname "${TUNNEL_IF}" accept
log prefix "hydraveil-drop " drop
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}
NFTEOF
_log "INFO" "armed — wan=${WAN_IFACE} server=${SERVER_IP} tunnel=${TUNNEL_IF} internal=${INTERNAL_SUBNET:-none}"
exit 0

View file

@ -1,103 +0,0 @@
#!/bin/bash
ORIGINAL_FOLDER="${ORIGINAL_FOLDER:-$(pwd)}"
TARGET_FOLDER="${TARGET_FOLDER:-/opt/hydra-veil}"
LINUX_USER="${LINUX_USER:-$SUDO_USER}"
add_sudoers_rule() {
sudo tee /etc/sudoers.d/zzzzzzzzzzzzzzzzzzz > /dev/null <<EOF
${LINUX_USER} ALL=(root) NOPASSWD: /opt/hydra-veil/firewall
${LINUX_USER} ALL=(root) NOPASSWD: /opt/hydra-veil/dns
${LINUX_USER} ALL=(root) NOPASSWD: /opt/hydra-veil/singbox_wrapper
EOF
}
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root (use sudo)" >&2
exit 1
fi
# Validate environment variables
if [[ -z "$ORIGINAL_FOLDER" ]]; then
echo "Error: ORIGINAL_FOLDER environment variable not set" >&2
exit 1
fi
if [[ -z "$TARGET_FOLDER" ]]; then
echo "Error: TARGET_FOLDER environment variable not set" >&2
exit 1
fi
# Validate that source folder exists
if [[ ! -d "$ORIGINAL_FOLDER" ]]; then
echo "Error: ORIGINAL_FOLDER does not exist: $ORIGINAL_FOLDER" >&2
exit 1
fi
# Create target folder if it doesn't exist
if [[ ! -d "$TARGET_FOLDER" ]]; then
echo "Creating target folder: $TARGET_FOLDER"
mkdir -p "$TARGET_FOLDER"
fi
# Get the setup script's own filename, because we'll exclude it from being copied,
SCRIPT_NAME="$(basename "$0")"
echo "Starting file copy from $ORIGINAL_FOLDER to $TARGET_FOLDER"
echo "Script name to exclude: $SCRIPT_NAME"
echo ""
# Counter for tracking progress
COPIED=0
SKIPPED=0
# Loop through all files in source folder
while IFS= read -r -d '' file; do
filename="$(basename "$file")"
# Skip if this is the script itself
if [[ "$filename" == "$SCRIPT_NAME" ]]; then
echo "⊘ Skipping: $filename (this script)"
((SKIPPED++))
continue
fi
# Copy the file
if cp "$file" "$TARGET_FOLDER/"; then
echo "✓ Copied: $filename"
((COPIED++))
# Set permissions to 755
if chmod 755 "$TARGET_FOLDER/$filename"; then
echo " └─ chmod 755 applied"
else
echo " └─ WARNING: chmod 755 failed for $filename" >&2
fi
else
echo "✗ ERROR: Failed to copy $filename" >&2
((SKIPPED++))
fi
done < <(find "$ORIGINAL_FOLDER" -maxdepth 1 -type f -print0)
echo ""
echo "=========================================="
echo "Copy complete!"
echo "Files copied: $COPIED"
echo "Files skipped: $SKIPPED"
echo "Target folder: $TARGET_FOLDER"
echo "=========================================="
# Try to apply the rules, if not emit 1
if add_sudoers_rule; then
echo "Operation successful"
else
echo "Operation failed - sudoers rule not applied"
exit 1
fi
exit 0

View file

@ -1,36 +0,0 @@
#!/bin/bash
set -eo pipefail
SINGBOX_BIN="/opt/hydra-veil/sing-box"
LOG_FILE=$VAR1
ACTION="${1:-}"
PROFILE_OR_PID="${2:-}"
run_binary() {
"$SINGBOX_BIN" run -c /etc/hydra-veil/profiles/"$PROFILE_OR_PID"/proxy.json >> "$LOG_FILE" 2>&1 &
_new_pid=$!
echo "$_new_pid"
}
# get result in python: pid = int(result.stdout.strip())
gracefully_close() {
sudo kill -TERM "$PROFILE_OR_PID" 2>/dev/null || true
}
forcefully_kill() {
sudo kill -TERM "$PROFILE_OR_PID" 2>/dev/null || true
}
if [[ "$ACTION" == "arm" ]]; then
run_binary
fi
if [[ "$ACTION" == "disarm" ]]; then
gracefully_close
fi
if [[ "$ACTION" == "kill" ]]; then
forcefully_kill
fi

View file

@ -1 +0,0 @@

View file

@ -1,24 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: application_code
path: ['application', 'code']
required: true
- name: version_number
path: ['version_number']
required: true
- name: format_revision
path: ['format_revision']
- name: download_path
path: ['download_path']
- name: released_at
path: ['released_at']
- name: file_hash
path: ['file_hash']

View file

@ -1,54 +0,0 @@
fields:
- name: country_code
path: ['country', 'code']
required: true
- name: code # this is city code
path: ['code']
required: true
- name: id
path: ['id']
- name: country_name
path: ['country', 'name']
- name: name # this is CITY name
path: ['name']
- name: time_zone
path: ['time_zone', 'code']
- name: operator_id
path: ['operator_id']
required: true
- name: provider_name
path: ['provider', 'name']
- name: is_proxy_capable
path: ['is_proxy_capable']
- name: is_wireguard_capable
path: ['is_wireguard_capable']
required: true
- name: is_hysteria2_capable
path: ['is_hysteria2_capable']
- name: is_vless_capable
path: ['is_vless_capable']
# original version:
# locations.append((
# 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']))

View file

@ -1,8 +0,0 @@
fields:
- name: group_a_code
path: ['group_a', 'code']
- name: code
path: ['code']
- name: id
path: ['id']
required: true

View file

@ -1,19 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: name
path: ['name']
- name: public_key # this is ed25519
path: ['public_key']
- name: nostr_public_key
path: ['nostr_public_key']
- name: nostr_profile_reference
path: ['nostr_profile_reference']
- name: nostr_attestation_event_reference
path: ['nostr_attestation', 'event_reference']

View file

@ -2,10 +2,7 @@ from core.Constants import Constants
from core.Errors import CommandNotFoundError
from core.controllers.SessionStateController import SessionStateController
from core.models.session.Application import Application
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.session.SessionProfile import SessionProfile
from core.models.session.SessionState import SessionState
from core.observers.ProfileObserver import ProfileObserver
@ -33,9 +30,8 @@ class ApplicationController:
return Application.all()
@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.services.profiles.profile_connections import enable_profile, disable_profile
from core.controllers.ProfileController import ProfileController
persistent_state_path = f'{profile.get_data_path()}/persistent-state'
@ -100,8 +96,7 @@ class ApplicationController:
if not fork_process_id:
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)
disable_profile(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)
sys.exit()
@ -109,8 +104,7 @@ class ApplicationController:
else:
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)
disable_profile(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
ProfileController.disable(profile, False, profile_observer=profile_observer)
@staticmethod
def _sync(proxies: Optional[dict] = None):

View file

@ -1,56 +1,31 @@
from core.services.networking.httpx import httpx_client
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.services.helpers.download_file import download_file_and_verify
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import Configuration, ConnectionChoice
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.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.orm_calls.application_version_calls import get_application_version, execute_get_all
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 core.errors.logger import logger
# import httpx
from io import BytesIO
from typing import Optional
import hashlib
import shutil
import tarfile
from sqlalchemy.orm import Session
from sqlalchemy import select
import os
class ApplicationVersionController:
@staticmethod
def get(application_code: str, version_number: str):
return get_application_version(application_code, version_number)
return ApplicationVersion.find(application_code, version_number)
@staticmethod
def get_all(application: Optional[Application] = None):
database_object = execute_get_all()
if database_object.valid:
return database_object.data
else:
logger.error(f"[Application Version Controller] Got invalid SQL Query which could not be solved by the wrapper, with error message {database_object.message} and type {database_object.error_type}")
return 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:
if not application_version.is_supported():
raise UnsupportedApplicationVersionError('The application version in question is not supported.')
if reinstall:
@ -59,12 +34,8 @@ class ApplicationVersionController:
if application_version.is_installed():
raise ApplicationAlreadyInstalledError('The application in question is already installed.')
# this used to go through "with_preferred_connection", but now re-uses the same HTTPx client as sync,
ApplicationVersionController.__install(application_version, application_version_observer, connection_observer)
# legacy:
# 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)
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):
@ -87,30 +58,45 @@ class ApplicationVersionController:
ApplicationVersion.save_many(application_versions)
@staticmethod
def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None):
target_app_name = application_version.application_code.capitalize()
target_app_version = application_version.version_number
download_path = application_version.download_path
target_file_hash = application_version.file_hash
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', f"Downloading {target_app_name} {target_app_version}. Connecting..")
application_version_observer.notify('downloading', application_version)
download_result = download_file_and_verify(
target_app_name=target_app_name,
download_path=download_path,
target_file_hash=target_file_hash,
application_version_observer=application_version_observer,
target_app_version=target_app_version,
connection_observer=connection_observer
)
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)
################################################
# IT WORKED - SAVE IT
################################################
if download_result.valid:
response_buffer = download_result.data
file_hash = download_result.message
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())
@ -118,93 +104,20 @@ class ApplicationVersionController:
with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file:
hash_file.write(f'{file_hash}\n')
################################################
# FAILED
################################################
else:
if download_result.error_type == ResultError.CONNECTION:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
elif download_result.error_type == ResultError.INVALID_INPUT:
raise FileIntegrityError('Application version file integrity could not be verified.')
else:
return None
raise ConnectionError('The application version could not be downloaded.')
@staticmethod
def __calculate_file_hash(file):
# legacy:
hasher = hashlib.sha3_512()
buffer = file.read(65536)
# @staticmethod
# def __calculate_file_hash(file):
while len(buffer) > 0:
# hasher = hashlib.sha3_512()
# buffer = file.read(65536)
hasher.update(buffer)
buffer = file.read(65536)
# while len(buffer) > 0:
file.seek(0)
# hasher.update(buffer)
# buffer = file.read(65536)
# file.seek(0)
# return hasher.hexdigest()
# def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client:
# connection_type = ConfigurationController.get_connection_enum()
# did_it_work = connect.make_client(connection_type, connection_observer) # always gets boolean
# if not did_it_work:
# raise ConnectionError(f'Could not connect, to download {target_app_name}.')
# else:
# client = httpx_client.get_http_session()
# logger.info(f"client type is {type(client)}")
# return client
# @staticmethod
# def get_all(application: Optional[Application] = None):
# return ApplicationVersion.all(application)
# legacy:
# application_version_observer.notify('downloading', application_version)
################################################
# SETUP HTTP CLIENT
################################################
# client = httpx_client.get_http_session()
# logger.info(f"client type is {type(client)}")
# if client is None:
# client = _get_httpx_client(target_app_name=target_app_name, connection_observer=connection_observer)
# ################################################
# # GET THE DATA
# ################################################
# download_path = application_version.download_path
# logger.info(f"download_path is {download_path}")
# with client.stream('GET', download_path) as response:
# logger.info("doing the stream...")
# 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_bytes(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', f"Downloading {target_app_name} {progress:.2f}% v: {target_app_version}")
# else:
# raise ConnectionError('The application version could not be downloaded.')
# application_version_observer.notify('downloaded', f"Downloaded {target_app_name} {target_app_version}")
# response_buffer.seek(0)
################################################
# VERIFY THE HASH
################################################
# 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.')
return hasher.hexdigest()

View file

@ -2,8 +2,6 @@
from core.models.manage.session_management import init_session, close_session
from core.services.sync.sync_service import coordinate_cache_sync, save_metadata
from core.services.sync.orm_methods.sync_one_orm import sync_one_orm_model
from core.errors.logger import logger
# ORM models that can be sync'ed:
@ -18,6 +16,8 @@ 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
@ -27,13 +27,7 @@ import pathlib
import re
import shutil
import subprocess
import time
def evaluate_errors(final_result: dict, client_observer: Optional[ClientObserver] = None) -> str:
if not final_result["success"] and client_observer:
category = final_result["category"]
client_observer.notify('synchronizing', f'{category} Error! Check error logs')
time.sleep(2) # so they can see it.
class ClientController:
@ -59,86 +53,86 @@ class ClientController:
return not ClientVersionController.is_latest(version)
# @staticmethod
# def legacy_sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
# if client_observer is not None:
# client_observer.notify('synchronizing', "Fetching list of new data ..")
@staticmethod
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
# result = coordinate_cache_sync(client_observer, connection_observer)
if client_observer is not None:
client_observer.notify('synchronizing', "Fetching list of new data ..")
# # Outright Error:
# if not result["success"]:
# error_msg = result["error"]
# if client_observer is not None:
# client_observer.notify('synchronizing', f'Error! {error_msg}')
# return
result = coordinate_cache_sync(ClientObserver, ConnectionObserver)
# # Same:
# changed_tables = result["changed_tables"]
# if not changed_tables:
# if client_observer is not None:
# client_observer.notify('synchronized')
# return
# logger.info(f"We got a Result from the API of {result}")
# # We only make it past this point if there's New Data
# Outright Error:
if not result["success"]:
error_msg = result["error"]
if client_observer is not None:
client_observer.notify('synchronizing', f'Error! {error_msg}')
return
# # flag for after the save,
# data_was_saved = False
# Same:
changed_tables = result["changed_tables"]
if not changed_tables:
if client_observer is not None:
client_observer.notify('synchronized')
return
# # Fetch and update the real data (no longer metadata)...
# We only make it past this point if there's New Data
# # =================== ORM BASED MODELS ==================
# """
# Note: for the new ORM based models,
# it does the Tor/system check in the API call itself.
# """
# flag for after the save,
data_was_saved = False
# if "locations" in changed_tables:
# logger.info("Sync of Locations")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Locations List..')
# Fetch and update the real data (no longer metadata)...
# final_result = sync_one_orm_model(Location, "locations")
# evaluate_errors(final_result)
# =================== ORM BASED MODELS ==================
"""
Note: for the new ORM based models,
it does the Tor/system check in the API call itself.
"""
if "locations" in changed_tables:
logger.info("Sync of Locations")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Locations List..')
# if "operators" in changed_tables:
# logger.info("Sync of Operators")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Operators List..')
final_result = sync_one_orm_model(Location, "locations")
# final_result_two = sync_one_orm_model(Operator, "operators")
# evaluate_errors(final_result_two)
if "operators" in changed_tables:
logger.info("Sync of Operators")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Operators List..')
# # =================== MANUAL-SQL BASED MODELS ==================
# try:
# from core.controllers.ConnectionController import ConnectionController
# ConnectionController.with_preferred_connection(task=ClientController.__sync, changed_tables=changed_tables, client_observer=client_observer, connection_observer=connection_observer)
final_result_two = sync_one_orm_model(Operator, "operators")
# # We set the flag to true,
# # the reason we use a flag, and don't just save it right here,
# # is because we want to isolate the success (or failure) of the real data,
# # from the potential failure of the ORM session metadata.
# data_was_saved = True
# =================== MANUAL-SQL BASED MODELS ==================
try:
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(task=ClientController.__sync, changed_tables=changed_tables, client_observer=client_observer, connection_observer=connection_observer)
# except:
# # sync failed here,
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetch Failed, but you can use old data.')
# finally:
# if data_was_saved:
# filtered_metadata = result["filtered_metadata"] # from the top of the function
# save_successful = save_metadata(filtered_metadata) # the "save_data" function is inside sync_service
# We set the flag to true,
# the reason we use a flag, and don't just save it right here,
# is because we want to isolate the success (or failure) of the real data,
# from the potential failure of the ORM session metadata.
data_was_saved = True
# if client_observer is None:
# logger.error("Error: No client_observer to update the UI, the final part of the sync function skipped")
# return # can't update their UI
except:
# sync failed here,
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetch Failed, but you can use old data.')
finally:
if data_was_saved:
filtered_metadata = result["filtered_metadata"] # from the top of the function
save_successful = save_metadata(filtered_metadata) # the "save_data" function is inside sync_service
# if save_successful:
# logger.info("Metadata Saved Successfully")
# client_observer.notify('synchronized', "Fetch & Save Complete!")
# else:
# client_observer.notify('synchronizing', "Saving List of Metadata Failed.")
if client_observer is None:
logger.error("Error: No client_observer to update the UI, the final part of the sync function skipped")
return # can't update their UI
if save_successful:
logger.info("Metadata Saved Successfully")
client_observer.notify('synchronized', "Fetch & Save Complete!")
else:
client_observer.notify('synchronizing', "Saving List of Metadata Failed.")
@staticmethod
@ -156,40 +150,40 @@ class ClientController:
return path
# @staticmethod
# def __sync(changed_tables: list, client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
@staticmethod
def __sync(changed_tables: list, client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
# if "applications" in changed_tables:
# logger.info("Sync applications..")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Browser List..')
# # noinspection PyProtectedMember
# ApplicationController._sync(proxies=proxies)
if "applications" in changed_tables:
logger.info("Sync applications..")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Browser List..')
# noinspection PyProtectedMember
ApplicationController._sync(proxies=proxies)
# if "application_versions" in changed_tables:
# logger.info("Sync Application Versions..")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Browser Version List..')
# # noinspection PyProtectedMember
# ApplicationVersionController._sync(proxies=proxies)
if "application_versions" in changed_tables:
logger.info("Sync Application Versions..")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Browser Version List..')
# noinspection PyProtectedMember
ApplicationVersionController._sync(proxies=proxies)
# if "client_version" in changed_tables:
# logger.info("Sync of client version")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Client Version List..')
# # noinspection PyProtectedMember
# ClientVersionController._sync(proxies=proxies)
if "client_version" in changed_tables:
logger.info("Sync of client version")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Client Version List..')
# noinspection PyProtectedMember
ClientVersionController._sync(proxies=proxies)
# if "subscriptions" in changed_tables:
# logger.info("Sync of Subscriptions")
# if client_observer is not None:
# client_observer.notify('synchronizing', 'Fetching Subscription List..')
# # noinspection PyProtectedMember
# SubscriptionPlanController._sync(proxies=proxies)
if "subscriptions" in changed_tables:
logger.info("Sync of Subscriptions")
if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Subscription List..')
# noinspection PyProtectedMember
SubscriptionPlanController._sync(proxies=proxies)
# ConfigurationController.update_last_synced_at()
ConfigurationController.update_last_synced_at()
# logger.info("Real Data Fetch Completed Successfully")
logger.info("Real Data Fetch Completed Successfully")
@staticmethod
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):

View file

@ -1,24 +1,14 @@
from core.Errors import UnknownConnectionTypeError
from core.models.Configuration import Configuration, ConnectionChoice
from core.models.Configuration import Configuration
from datetime import datetime, timezone
from typing import Optional
class ConfigurationController:
_config: Optional[Configuration] = None
@staticmethod
def get():
if ConfigurationController._config is None:
ConfigurationController._config = Configuration.get()
return ConfigurationController._config
# return Configuration.get()
@staticmethod
def reload_from_disk():
ConfigurationController._config = None
return Configuration.get()
@staticmethod
def get_or_new():
@ -34,35 +24,19 @@ class ConfigurationController:
def get_connection():
configuration = ConfigurationController.get()
return configuration.connection.value
@staticmethod
def get_connection_enum():
if configuration is None or configuration.connection not in ('system', 'tor', 'vless', 'hysteria2'):
raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
configuration = ConfigurationController.get()
return configuration.connection
@staticmethod
def set_connection(connection_string: Optional[str] = None):
def set_connection(connection: Optional[str] = None):
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.save()
@staticmethod
def set_connection_enum(connection_enum: ConnectionChoice) -> bool:
configuration = ConfigurationController.get_or_new()
configuration.connection = connection_enum
configuration.save()
@staticmethod
def get_auto_sync_enabled():
@ -110,88 +84,10 @@ class ConfigurationController:
@staticmethod
def update_last_synced_at():
configuration = ConfigurationController.get()
configuration = ConfigurationController.get_or_new()
configuration.last_synced_at = datetime.now(timezone.utc)
configuration.save()
@staticmethod
def update_or_create(configuration):
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
@staticmethod
def change_random_ticket(new_value: bool) -> bool:
configuration = ConfigurationController.get()
configuration.use_random = new_value
configuration.save()
return True
@staticmethod
def use_random() -> bool:
configuration = ConfigurationController.get()
return configuration.use_random
@staticmethod
def tickets_setup(new_value: bool) -> bool:
configuration = ConfigurationController.get()
configuration.tickets_setup = new_value
configuration.save()
return True
@staticmethod
def are_tickets_setup() -> bool:
configuration = ConfigurationController.get()
return configuration.tickets_setup
@staticmethod
def do_we_allow_port_prompts() -> bool:
configuration = ConfigurationController.get()
if configuration.allow_port_prompts is None:
print("Setting allow ports to true")
configuration.allow_port_prompts = True
configuration.save()
return configuration.allow_port_prompts
@staticmethod
def change_port_prompts(new_value) -> bool:
configuration = ConfigurationController.get()
configuration.allow_port_prompts = new_value
configuration.save()
return True

View file

@ -1,21 +1,12 @@
from core.errors.exceptions import *
from core.errors.logger import logger
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, await_network_interface
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.observers.TicketObserver import TicketObserver
from core.services.networking.systemwide.general_tools.test_any_protocol import test_session_ip
from core.models.enum_models import ProtocolChoice
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.ProfileController import ProfileController
from core.controllers.SessionStateController import SessionStateController
from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import ProfileType
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.system.SystemState import SystemState
@ -24,17 +15,19 @@ from core.services.WebServiceApiService import WebServiceApiService
from essentials.modules.TorModule import TorModule
from essentials.services.ConnectionService import ConnectionService
from pathlib import Path
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError, CommandNotFoundError, PaymentRequiredError
from subprocess import CalledProcessError
from typing import Union, Optional, Any
import os
import random
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import time
from enum import Enum
# import sys
# import re
from core.models.OperatorProxySession import OperatorProxySession
class ConnectionController:
@ -60,9 +53,114 @@ class ConnectionController:
else:
return None
@staticmethod
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
from core.controllers.ConnectionController import ConnectionController
ConnectionController.verify_singbox_installation()
connection = profile.connection
# needs_proxy_configuration comes from the child connection models
# for the system it returns false blindly. for the session is checks if the connection type is masked.
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()
# The needs_wireguard_configuration comes from the connection model, and can be transitioned to get it directly from the object's data
# The has_wireguard_configuration comes from each polymorph object doing an os check on if the wg config exists
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
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 connection.needs_operator_proxy() and not profile.has_operator_proxy_session():
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 PaymentRequiredError()
profile.attach_operator_proxy_session(operator_proxy_session)
else:
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, ticket_observer: Optional[TicketObserver] = None):
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)
@ -73,14 +171,12 @@ class ConnectionController:
# this is a check from SessionConnection of if there's a systemwide with mask
if profile.connection.is_unprotected():
if not system_uses_wireguard_interface():
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:
from core.services.profiles.profile_connections import disable_profile
disable_profile(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
ProfileController.disable(profile)
if profile.connection.code == 'tor':
@ -92,12 +188,30 @@ class ConnectionController:
elif profile.connection.code == 'wireguard':
if ConfigurationController.get_endpoint_verification_enabled():
verify_wireguard_endpoint(profile, ignore=ignore)
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:
@ -106,31 +220,117 @@ class ConnectionController:
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():
if profile.connection.code == 'wireguard':
test_result = test_session_ip(port=port_number, profile_id=profile.id, protocol=ProtocolChoice.WIREGUARD, connection_observer=connection_observer)
if test_result.valid:
logger.info("Connection established.")
if connection_observer:
connection_observer.notify("connected", "Confirmed")
else:
logger.error(test_result.message)
raise ConnectionError(test_result.message)
elif profile.connection.code == 'tor':
await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
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 _signal_previous_process() -> None:
system_state = SystemStateController.get()
if system_state is None:
return
pid = system_state.pid
if pid is None or pid == os.getpid():
return
try:
os.kill(pid, signal.SIGUSR1)
time.sleep(0.5)
except (ProcessLookupError, PermissionError):
pass
@staticmethod
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
ConnectionController._signal_previous_process()
system_state = SystemStateController.get()
if system_state is not None:
try:
ConnectionController.terminate_system_connection(connection_observer=connection_observer)
except ConnectionTerminationError:
pass
if profile.connection.needs_operator_proxy():
ok = ConnectionController.establish_encrypted_proxy_connection(
profile, socks5_port=1080, observer=connection_observer
)
if not ok:
raise ConnectionError('The connection could not be established.')
token = SystemStateController.create(profile.id)
if connection_observer is not None:
connection_observer.notify('connected_token', {'session_token': token})
return
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_encrypted_proxy_connection(profile, socks5_port: int = 1080, observer=None) -> bool:
"""Shared encrypted proxy connection logic for CLI and GUI."""
import socket
operator_proxy_session = profile.get_operator_proxy_session()
protocol = profile.connection.code
if protocol == 'vless':
from core.controllers.encrypted_proxy.VlessController import VlessController
from core.services.encrypted_proxy.vless_service import parse_vless_link
server_ip = operator_proxy_session.server_ip
if server_ip is None:
vless = parse_vless_link(operator_proxy_session.links[0])
server_ip = socket.gethostbyname(vless['host'])
return VlessController(socks5_port).enable(
operator_proxy_session.links[0],
operator_proxy_session.username,
server_ip,
observer
)
elif protocol == 'hysteria2':
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
server_ip = operator_proxy_session.server_ip
if server_ip is None:
server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host)
return HysteriaController(socks5_port).enable(
operator_proxy_session.username,
operator_proxy_session.password,
operator_proxy_session.operator_hysteria2_host,
server_ip,
observer
)
return False
@staticmethod
def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None):
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.start_service(connection_observer)
@ -138,25 +338,22 @@ class ConnectionController:
for port_number in session_state.network_port_numbers.tor:
tor_module.create_session(port_number)
except:
if connection_observer is not None:
connection_observer.notify('custom_message', "Tor Can't Initialize")
# @staticmethod
# def terminate_tor_connection():
@staticmethod
def terminate_tor_connection():
# tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
# tor_module.stop_service()
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):
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
except TorServiceInitializationError as e:
logger.error(f"TorServiceInitializationError. Tor Can't Start: {e}")
except Exception as e:
logger.error(f"Tor Can't Start: {e}")
if connection_observer is not None:
connection_observer.notify('custom_message', "Tor Can't Initialize")
@staticmethod
def terminate_tor_session_connection(port_number: int):
@ -218,9 +415,43 @@ class ConnectionController:
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(connection_observer: Optional[ConnectionObserver] = None):
system_state = SystemStateController.get()
if system_state is not None:
profile = ProfileController.get(system_state.profile_id)
if profile is not None and profile.connection.needs_operator_proxy():
protocol = profile.connection.get_protocol()
if protocol == 'vless':
from core.controllers.encrypted_proxy.VlessController import VlessController
VlessController(1080).disable(connection_observer)
elif protocol == 'hysteria2':
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
HysteriaController(1080).disable(connection_observer)
SystemState.dissolve()
return
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()
try:
from core.utils.encrypted_proxy import killswitch
killswitch.disarm()
except Exception:
pass
SystemState.dissolve()
if connection_observer is not None:
connection_observer.notify('disconnected', {})
else:
raise ConnectionTerminationError('The connection could not be terminated.')
# Stays here for Tor based only. Will move with Tor later.
@staticmethod
def get_proxies(port_number: int):
@ -229,6 +460,135 @@ class ConnectionController:
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.')
# Arm killswitch for WireGuard
try:
wg_server_ip = ConnectionController.__extract_wireguard_endpoint(profile)
if wg_server_ip:
from core.utils.encrypted_proxy import killswitch
killswitch.arm(wg_server_ip, 'wg')
except Exception:
pass
token = SystemStateController.create(profile.id)
if connection_observer is not None:
connection_observer.notify('connected_token', {'session_token': token})
try:
ConnectionController.await_connection(connection_observer=connection_observer)
except ConnectionError:
raise ConnectionError('The connection could not be established.')
@staticmethod
def __extract_wireguard_endpoint(profile):
import re, socket
try:
config = profile.get_wireguard_configuration()
if config is None:
return None
match = re.search(r'Endpoint\s*=\s*([^\s:]+):\d+', config)
if not match:
return None
host = match.group(1)
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', host):
return host
return socket.gethostbyname(host)
except Exception:
return None
@staticmethod
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
@ -236,9 +596,84 @@ class ConnectionController:
port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer)
await_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
@staticmethod
def verify_singbox_installation():
import subprocess
import os
from core.Errors import SingboxNotInstalledException
singbox_binary = '/usr/bin/sing-box'
singbox_wrapper = '/usr/local/bin/hydraveil-singbox'
if not os.path.exists(singbox_binary):
raise SingboxNotInstalledException(f'sing-box binary not found at {singbox_binary}')
if not os.path.exists(singbox_wrapper):
raise SingboxNotInstalledException(f'sing-box wrapper not found at {singbox_wrapper}')
try:
subprocess.run([singbox_binary, 'version'], capture_output=True, timeout=5, check=True)
except subprocess.CalledProcessError:
raise SingboxNotInstalledException('sing-box binary check failed')
except subprocess.TimeoutExpired:
raise SingboxNotInstalledException('sing-box verification timeout')

View file

@ -1,26 +1,28 @@
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.errors.logger import logger
from core.models.manage.session_management import get_session
from typing import Optional
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.orm_calls.location_calls import execute_location_sql
class LocationController:
@staticmethod
def get(country_code: str, city_code: str):
location_object = execute_location_sql(country_code, city_code)
if location_object.valid:
return location_object.data
else:
critical_error = f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}"
logger.error(critical_error)
print(critical_error)
return None
with get_session() as session:
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
# legacy:
# Location.find(country_code, code)
@staticmethod
def get_all():
@ -30,3 +32,18 @@ class LocationController:
.options(joinedload(Location.operator))
).scalars().all()
return all_records
# legacy:
# return Location.all()
# Deprecated legacy sync,
# from core.services.WebServiceApiService import WebServiceApiService
# @staticmethod
# def _sync(proxies: Optional[dict] = None):
# locations = WebServiceApiService.get_locations(proxies)
# Location.truncate()
# Location.save_many(locations)

View file

@ -1,11 +1,9 @@
from core.services.profiles.ProfileRepository import ProfileRepository
# from core.services.subscriptions import subscriptions
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError, PaymentRequiredError
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.BaseProfile import ProfileType
from core.models.Subscription import Subscription
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
@ -20,105 +18,229 @@ import time
class ProfileController:
# Initialize the singleton repository
_repo = ProfileRepository()
@staticmethod
def get(id: int) -> Union[SessionProfile, SystemProfile, None]:
# Controller no longer talks to the Model; it talks to the Repo
return ProfileController._repo.get(id)
return Profile.find_by_id(id)
@staticmethod
def get_all():
return ProfileController._repo.get_all()
return Profile.all()
@staticmethod
def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
# Call the helper with the 'created' event
ProfileController._save_and_sync(profile, 'created', profile_observer)
profile.save()
if profile_observer is not None:
profile_observer.notify('created', profile)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
# Call the helper with the 'updated' event
ProfileController._save_and_sync(profile, 'updated', profile_observer)
@staticmethod
def _save_and_sync(profile, event_type: str, observer: ProfileObserver = None):
"""Private helper to handle the actual persistence and cache synchronization."""
# 1. Persist to disk
profile.save()
# 2. update cache to prevent drift
ProfileController._repo.update_cache(profile)
# 3. Notify observer
if observer is not None:
observer.notify(event_type, profile)
if profile_observer is not None:
profile_observer.notify('updated', profile)
@staticmethod
def delete(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
"""Remove the profile from both disk and cache"""
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):
# 1. Save to temp variable:
profile_id = profile.id
from core.controllers.ConnectionController import ConnectionController
# 2. Permanently delete from disk
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():
if profile.connection.code in ('vless', 'hysteria2'):
ProfileController._enable_encrypted_proxy(profile, connection_observer=connection_observer)
if profile_observer is not None:
profile_observer.notify('enabled', profile)
return
# WireGuard and other system profiles — legacy flow
from core.controllers.ConnectionController import ConnectionController
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 _enable_encrypted_proxy(profile, connection_observer=None) -> None:
from core.controllers.ConnectionController import ConnectionController
from core.controllers.SystemStateController import SystemStateController
from core.services.WebServiceApiService import WebServiceApiService
if not profile.has_operator_proxy_session():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
operator_proxy_session = ConnectionController.with_preferred_connection(
profile.subscription.billing_code,
profile.subscription.operator_id,
profile.connection.code,
task=WebServiceApiService.post_operator_proxy,
connection_observer=connection_observer
)
if operator_proxy_session is None:
raise ProfileActivationError('Could not obtain operator proxy session.')
profile.attach_operator_proxy_session(operator_proxy_session)
ConnectionController._signal_previous_process()
ok = ConnectionController.establish_encrypted_proxy_connection(
profile, socks5_port=1080, observer=connection_observer
)
if not ok:
raise ProfileActivationError('The profile could not be enabled.')
token = SystemStateController.create(profile.id)
if connection_observer is not None:
connection_observer.notify('connected_token', {'session_token': token})
@staticmethod
def run_monitor(profile: Union[SessionProfile, SystemProfile], monitor) -> None:
if profile.is_system_profile() and profile.connection.needs_operator_proxy():
monitor.run_blocking()
@staticmethod
def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, ignore: tuple[type[Exception]] = (), profile_observer: ProfileObserver = None, connection_observer: ConnectionObserver = 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(connection_observer=connection_observer)
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()
# 3. update cache with the temp variable to prevent drift
ProfileController._repo.invalidate(profile_id=profile_id)
if profile_observer is not None:
profile_observer.notify('destroyed', profile)
@staticmethod
def what_type_is_profile(profile_id: int) -> ProfileType:
all_data = ProfileController._repo.get_all()
given_profile = all_data[profile_id]
return given_profile.type
def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
# print(f"all_data[{profile_id}] is {all_data[profile_id]}")
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():
# @staticmethod
# def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
# Operator profiles (vless/hysteria2): check if already activated locally first.
# If not, poll the server — the payment may have been processed since last attempt.
if profile.subscription.operator_id is not None:
if profile.subscription.has_been_activated():
profile.save()
return
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()
return
else:
raise PaymentRequiredError()
# profile.subscription = subscription
# profile.save()
subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
# @staticmethod
# def activate_subscription(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
if subscription is not None:
profile.subscription = subscription
profile.save()
else:
raise InvalidSubscriptionError()
# from core.controllers.ConnectionController import ConnectionController
else:
raise MissingSubscriptionError()
# if profile.has_subscription():
@staticmethod
def is_enabled(profile: Union[SessionProfile, SystemProfile]):
# subscription = subscriptions.get_subscription(
# billing_code=profile.subscription.billing_code,
# connection_observer=connection_observer
# )
from core.controllers.ConnectionController import ConnectionController
# if subscription is not None:
if profile.is_session_profile():
# profile.subscription = subscription
# profile.save()
session_state = SessionStateController.get_or_new(profile.id)
return len(session_state.network_port_numbers.all) > 0 or len(session_state.process_ids) > 0
# else:
# raise InvalidSubscriptionError()
if profile.is_system_profile():
# else:
# raise MissingSubscriptionError()
system_state = SystemStateController.get()
if system_state is not None and system_state.profile_id is profile.id:
return ConnectionController.system_uses_wireguard_interface()
# @staticmethod
# def get_invoice(profile: Union[SessionProfile, SystemProfile]):
return False
# if profile.has_subscription():
# # return subscriptions.get_invoice(billing_code=profile.subscription.billing_code)
# return WebServiceApiService.get_invoice(profile.subscription.billing_code)
# else:
# return None
@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]):
@ -142,6 +264,29 @@ class ProfileController:
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()
@ -150,20 +295,65 @@ class ProfileController:
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)
# from core.observers.TicketObserver import TicketObserver
# from core.services.assassin.ticket_respawn import respawn_profile
# from core.services.assassin import assassin_tools
# from core.errors.exceptions import FirewallError
# from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
# from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
# from core.services.networking.general_connection_tools.connection_enable import establish_connection
# from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
# from core.services.networking.systemwide.encrypted_proxy.singbox_runner import end_singbox
# from core.services.networking.systemwide import killswitch
#, 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
except EndpointVerificationError as error:
if not EndpointVerificationError in ignore:
profile.address_security_incident()
raise error
@staticmethod
def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
from cryptography.hazmat.primitives.asymmetric import ed25519
import base64
signature = profile.get_wireguard_configuration_metadata('Signature')
wireguard_public_keys = profile.get_wireguard_public_keys()
operator = profile.location.operator
if signature is None:
raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
if not wireguard_public_keys:
raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
if operator is None:
raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
try:
operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
for wireguard_public_key in wireguard_public_keys:
operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
except Exception:
raise EndpointVerificationError('The WireGuard endpoint could not be verified.')
@staticmethod
def __generate_wireguard_keys():
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
raw_private_key = X25519PrivateKey.generate()
public_key = raw_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
)
private_key = raw_private_key.private_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
)
return dict(
private=base64.b64encode(private_key).decode(),
public=base64.b64encode(public_key).decode()
)

View file

@ -1,27 +1,19 @@
from core.models.session.SessionState import SessionState
from core.controllers.ProfileController import ProfileController
from core.infrastructure.global_state import ProfileStateRepository
from core.observers.Notifications import Notifications, EventType
class SessionStateController:
@staticmethod
def get(id: int):
return SessionState.find_by_id(id)
@staticmethod
def get_or_new(id: int):
session_state = SessionStateController.get(id)
if session_state is None:
# Update the Singleton Repo
repo = ProfileStateRepository()
repo.sync_session_added(id)
# Create and save the session
session_state = SessionState(id)
session_state.save()
return session_state
return SessionState(id)
return session_state
@ -35,25 +27,4 @@ class SessionStateController:
@staticmethod
def update_or_create(session_state):
# save to model:
session_state.save()
# get profile id:
profile_id = session_state.id
# Ensure repo knows this session exists
ProfileStateRepository().sync_session_added(profile_id)
# notify UI:
Notifications.emit(EventType.SESSION_PROFILE_ENABLE, profile_id)
@staticmethod
def remove(profile_id: int):
# save to model:
SessionState.dissolve(profile_id)
# cache:
ProfileStateRepository().sync_session_removed(profile_id)
# notify UI:
Notifications.emit(EventType.SESSION_PROFILE_DISABLE, profile_id)

View file

@ -5,7 +5,6 @@ from core.observers.ConnectionObserver import ConnectionObserver
from core.services.WebServiceApiService import WebServiceApiService
from typing import Union
class SubscriptionController:
@staticmethod
@ -18,4 +17,51 @@ class SubscriptionController:
def create(subscription_plan: SubscriptionPlan, profile: Union[SessionProfile, SystemProfile], connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
return ConnectionController.with_preferred_connection(subscription_plan.id, profile.location.id, task=WebServiceApiService.post_subscription, connection_observer=connection_observer)
if profile.location:
return ConnectionController.with_preferred_connection(
subscription_plan.id,
profile.location.id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)
else:
return ConnectionController.with_preferred_connection(
subscription_plan.id,
operator_id=profile.connection.operator_id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)
from core.controllers.ConnectionController import ConnectionController
if profile.location:
return ConnectionController.with_preferred_connection(
subscription_plan.id,
profile.location.id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)
else:
return ConnectionController.with_preferred_connection(
subscription_plan.id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)
from core.controllers.ConnectionController import ConnectionController
if profile.location:
# Para WireGuard (con location)
return ConnectionController.with_preferred_connection(
subscription_plan.id,
profile.location.id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)
else:
return ConnectionController.with_preferred_connection(
subscription_plan.id,
task=WebServiceApiService.post_subscription,
connection_observer=connection_observer
)

View file

@ -1,233 +0,0 @@
from core.observers.Notifications import Notifications, EventType
from core.services.sync import legacy_insert
from core.models.manage.session_management import init_session, close_session
from core.services.sync.sync_service import coordinate_cache_sync, save_metadata
from core.services.sync.insert_for_orm import insert_one_orm_model
from core.services.networking.httpx import connect
# return objects
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# ORM models that can be sync'ed:
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.orm_models.Dependency import Dependency
from core.models.orm_models.Plan import Plan
from core.Constants import Constants
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.SubscriptionPlanController import SubscriptionPlanController
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from core.models.orm_models.ApplicationVersion import ApplicationVersion
# generic
import sys
import json
ORM_TABLES = {
"locations": Location,
"operators": Operator,
"application_versions": ApplicationVersion,
"dependencies": Dependency,
"plans": Plan,
}
LEGACY_SQL_FUNCT_DICT = {
"applications": legacy_insert.for_applications,
"client_version": legacy_insert.for_client_version,
"subscriptions": legacy_insert.for_subscriptions,
}
APP_CODES = {
"firefox": 1,
"chromium": 2,
"brave": 3,
"librewolf": 5
}
def call_legacy_insert_function(key: str, new_data: dict):
func = LEGACY_SQL_FUNCT_DICT.get(key)
if func is None:
return DatabaseOperation(valid=False, error_type=DBErrorType.UNKNOWN_MODEL)
if key in APP_CODES:
code = APP_CODES[key]
return func(new_data, code)
else:
return func(new_data)
def get_orm_model(key) -> bool:
if key not in ORM_TABLES:
return False
return ORM_TABLES[key]
def new_sync(client_observer: ClientObserver, connection_observer: ConnectionObserver) -> Result:
client_observer.notify('synchronizing', "Fetching list of new data ..")
####################################
# METADATA. Should we even sync?
####################################
metadata_result = coordinate_cache_sync(client_observer, connection_observer)
# Outright Error:
if not metadata_result["success"]:
error_msg = metadata_result["error"]
# client_observer.notify('synchronizing', f'Error! {error_msg}')
Notifications.emit(EventType.STATUS_UPDATE, f'Error! {error_msg}')
return Result(valid=False)
# Nothing changed if the 'changed_tables' variable does NOT exist
changed_tables = metadata_result["changed_tables"]
# NEW MODELS:
new_model_types = metadata_result.get("new_model_types", [])
if new_model_types:
for each_new_model in new_model_types:
if each_new_model in ORM_TABLES:
# Then we know how to handle this, let's sync it:
logger.info(f"Adding the brand new {each_new_model} to the changed tables.")
changed_tables.append(each_new_model)
else:
logger.info(f"Skipping {each_new_model} because we don't know how to handle it yet.")
# CHECK ON CHANGED TABLES
if not changed_tables:
client_observer.notify('synchronized')
notification = "Sync Complete. (already up to date)"
Notifications.emit(EventType.STATUS_UPDATE, notification)
return Result(valid=True)
# We only make it past this point if there's New Data
notification = "Fetching Data.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
####################################
# API CALLS: GET NEW DATA IN BULK
####################################
results = connect.bulk_async(
wanted_list=changed_tables,
observer=connection_observer,
client_observer=client_observer
)
if not results.valid:
error_msg = f"Sync failed! {results.error_type}"
logger.error(f"{error_msg} {results.message}")
notification = f'Error! {error_msg}'
# client_observer.notify('synchronizing', notification)
Notifications.emit(EventType.STATUS_UPDATE, notification)
return Result(valid=False)
quantity_of_entries = len(results.data)
logger.info(f"We have valid API call results. There are {quantity_of_entries} entries")
all_results = results.data
####################################
# LOOP INSERT INTO DATABASE
####################################
client_observer.notify('synchronizing', f'Inserting into Database..')
skipped = []
for key, each_api_result in all_results.items():
if not each_api_result.valid:
logger.info(f"Skipping invalid api response for {key}")
skipped.append(key)
continue
logger.info(f"Inserting valid api data for {key} into the Database")
each_insert = insert_data(
key=key,
each_api_calls_data=each_api_result.data,
client_observer=client_observer
)
logger.info(f"Exited the insert data function with a result of {each_insert.valid}")
if each_insert.valid:
logger.info(f"Success with insert of {key}")
notification = f'Inserted {key}'
Notifications.emit(EventType.STATUS_UPDATE, notification)
# client_observer.notify('synchronizing', f'Inserted {key}')
continue
else:
skipped.append(key)
logger.error(f"Database error with inserting {key}, because {each_insert.error_type}")
client_observer.notify('synchronizing', f'Failed inserting {key} because {each_insert.message}')
logger.info("This only gets triggered in errors, but Moving on to the next item..")
####################################
# FINAL EVALUATION
####################################
total_skipped = len(skipped)
logger.info(f"We exited the loop, doing the final evaluation. And a total of {total_skipped} were skipped.")
ConfigurationController.update_last_synced_at()
filtered_metadata = metadata_result["filtered_metadata"] # from the top of the function
if total_skipped == 0:
client_observer.notify('synchronized', "Fetch & Save Complete!")
save_successful = save_metadata(filtered_metadata)
if save_successful:
notification = "Finshed sync."
Notifications.emit(EventType.STATUS_UPDATE, notification)
return Result(valid=True, message=notification)
else:
error_msg = "Finshed sync, but had issues with the saving of metadata for next time."
logger.error(error_msg)
return Result(valid=True, message=error_msg)
elif total_skipped < quantity_of_entries:
error_msg = f"Partial Success. {total_skipped} skipped."
# client_observer.notify('synchronized', error_msg)
Notifications.emit(EventType.STATUS_UPDATE, error_msg)
return Result(valid=True, data=skipped, message=error_msg)
else:
error_msg = f"Sync Failed. All {total_skipped} entries were skipped!"
# client_observer.notify('synchronized', error_msg)
Notifications.emit(EventType.STATUS_UPDATE, error_msg)
return Result(valid=False, data=skipped, message=error_msg)
def insert_data(
key: str,
each_api_calls_data: dict,
client_observer: ClientObserver,
) -> DatabaseOperation:
####################################
# NEW ORM SYSTEM
####################################
new_orm_model = get_orm_model(key)
if new_orm_model:
db_result = insert_one_orm_model(
which_key=key,
which_model=new_orm_model,
new_data=each_api_calls_data,
override=True
)
return db_result
####################################
# LEGACY MANUAL SQL
####################################
return call_legacy_insert_function(key=key, new_data=each_api_calls_data)
####################################
# UNKNOWN MODEL
####################################
return DatabaseOperation(valid=False, error_type=DBErrorType.UNKNOWN_MODEL)

View file

@ -1,11 +1,10 @@
import os
import uuid
from core.models.system.SystemState import SystemState
from core.models.enum_models import ProtocolChoice
# from core.controllers.ProfileController import ProfileController
from core.infrastructure.global_state import ProfileStateRepository
from core.observers.Notifications import Notifications, EventType
from core.errors.logger import logger
class SystemStateController:
@staticmethod
def get():
return SystemState.get()
@ -15,65 +14,16 @@ class SystemStateController:
return SystemState.exists()
@staticmethod
def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState:
# model on disk
current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id)
current_state.save()
# cache (Sync to Repo)
ProfileStateRepository().sync_system_state(current_state)
return current_state
def create(profile_id):
token = str(uuid.uuid4())
pid = os.getpid()
SystemState(profile_id, token, pid).save()
return token
@staticmethod
def update_or_create(system_state):
"""Update the model & cache for a systemwide connection"""
# model
system_state.save()
# cache (Sync to Repo)
ProfileStateRepository().sync_system_state(system_state)
# notify is done separately because this function can be used to modify state in the event of firewall failure.
@staticmethod
def dissolve():
# get singleton:
state_repo = ProfileStateRepository()
# extract current active id:
profile_id = state_repo.global_state.systemwide_profile_id
# model wipes data on disk:
SystemState.dissolve()
# Wipe the Repo's Cache:
state_repo.turn_off_system_connection()
# notify UI with that past id:
Notifications.emit(EventType.SYSTEM_DISABLE, profile_id)
# old version:
# ProfileStateRepository().global_state.systemwide_profile_id = None
# @staticmethod
# def notify_on():
# """Notify the notifications that systemwide state shows on"""
# # get singleton:
# state_repo = ProfileStateRepository()
# # extract current data:
# profile_id = state_repo.global_state.systemwide_profile_id
# systemwide_active = state_repo.global_state.systemwide_active
# # double check, confirm:
# if systemwide_active and systemwide_active:
# # notify UI:
# Notifications.emit(EventType.SYSTEM_ENABLE, profile_id)
# else:
# logger.error("Critical Error, notify_on was called out of place.")
# Notifications.emit(EventType.SYSTEM_DISABLE, profile_id)
return SystemState.dissolve()

View file

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

View file

@ -0,0 +1,31 @@
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, server_ip: 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
if not server_ip:
if observer:
observer.notify("error", "Missing server_ip")
return False
return enable_hysteria(
username=username,
password=password,
server_host=server_host,
server_ip=server_ip,
socks5_port=self.socks5_port,
observer=observer,
)
def disable(self, observer=None) -> bool:
return disable_hysteria(observer=observer)

View file

@ -0,0 +1,29 @@
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, server_ip: 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
if not server_ip:
if observer:
observer.notify("error", "Missing server_ip")
return False
return enable_vless(
vless_link=vless_link,
username=username,
server_ip=server_ip,
socks5_port=self.socks5_port,
observer=observer,
)
def disable(self, observer=None) -> bool:
return disable_vless(observer=observer)

View file

@ -1,107 +0,0 @@
from core.services.payment_phase.payment_state import PaymentData
from core.models.BaseProfile import BaseProfile as Profile
from core.models.orm_models.Plan import Plan
from core.models.orm_calls.model_neutral_tools import get_all_for_model
from core.models.orm_calls.location_calls import get_profile_location_data
from core.models.enum_models import ProtocolChoice, Currency
from core.models.orm_models.Location import Location
from core.errors.logger import logger
from core.models.Result import Result, ResultError
ALL_PROTOCOLS = [e.value for e in ProtocolChoice]
ALL_CURRENCIES = [e.value for e in Currency]
"""
Outputs:
Returns a list of supported protocols for a given location.
Inputs:
location_codes format: 'location_codes': 'us_wa'
"""
def get_supported_protocols_for_location(location_codes: str, profile_type: str = None) -> Result:
"""Get a list of string values for the protocols supported by a given location."""
if "_" not in location_codes:
error_msg = f"This is a developer error. No location data found for {location_codes}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
country, city = location_codes.split("_")
# this IS doing sql:
location = get_profile_location_data(country_code=country, city_code=city)
if not location:
error_msg = f"This is a developer error. No location data found for {location_codes}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# this is NOT doing sql
supported_protocols = _get_supported_protocols_for_location(location=location, as_strings=True)
if profile_type == "system-wide" and ProtocolChoice.SOCK5_PROXY.value in supported_protocols:
supported_protocols.remove(ProtocolChoice.SOCK5_PROXY.value)
return Result(valid=True, data=supported_protocols)
def _get_supported_protocols_for_location(location: Location, as_strings: bool = False) -> list:
"""Doesn't do SQL to get a list of supported locations"""
SUPPORTED_PROTOCOLS = []
for each_protocol in ALL_PROTOCOLS:
supported = location.supports_protocol(enum_value=each_protocol)
if supported:
SUPPORTED_PROTOCOLS.append(each_protocol)
return SUPPORTED_PROTOCOLS
def get_plans_for_profile(profile_id: int) -> Result:
"""
Get a formatted list of all Plans for a given profile.
And save the protocol in the PaymentData singleton.
"""
# Get profile:
profile = Profile.find_by_id(profile_id)
if not profile:
error_msg = f"Invalid profile id of {profile_id}"
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# Get Protocol for that profile:
chosen_protocol = ProtocolChoice(profile.connection.code)
# create or get the singleton:
billing_data = PaymentData(instance_id=profile_id)
# set it's value across modules:
billing_data.protocol_choice = chosen_protocol
# Return plans for that protocol:
return get_all_plans_by_protocol(chosen_protocol)
def get_all_plans_by_protocol(chosen_protocol: ProtocolChoice) -> Result:
"""Get a formatted list of all Plans for a given protocol."""
all_plans = get_all_for_model(model_type=Plan)
if not all_plans:
error_msg = "There are no plans in your local SQL database. Please sync or make sure there's no SQL errors."
return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)
PLANS_TO_RETURN = {}
for each_plan in all_plans:
# filter by protocol:
if chosen_protocol not in each_plan.supported_protocols:
continue
plan_format = str(f"{each_plan.months} months (€{each_plan.cost})")
PLANS_TO_RETURN[plan_format] = each_plan.id
return Result(valid=True, data=PLANS_TO_RETURN)

View file

@ -1,15 +0,0 @@
from core.models.enum_models import ProtocolChoice, valid_enum
from core.controllers.ProfileController import ProfileController
def get_protocol_enum(profile_id: int) -> ProtocolChoice|bool:
"""Return the Enum for a given Profile's Protocol Choice"""
profile = ProfileController.get(profile_id)
protocol_string = profile.connection.code
if not valid_enum(protocol_string, ProtocolChoice):
return False
else:
return ProtocolChoice(protocol_string)

View file

@ -1,186 +0,0 @@
# utils
from core.errors.logger import logger
from core.models.Result import Result, ResultError
from core.services.networking.systemwide.general_tools.manage_sudo_configs import remove_sudo_config
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import BaseProfile as Profile
from core.models.BaseProfile import ProfileType
# ORM Calls
from core.models.orm_calls.location_calls import get_profile_location_data
from core.models.BaseProfile import get_application_version
# previously used:
# from core.models.Subscription import Subscription
# from core.models.session.SessionProfile import SessionProfile
# from core.models.system.SystemProfile import SystemProfile
from core.models.orm_models.Location import Location
# from core.models.orm_models.Operator import Operator
SYSTEMWIDE_CHOICES = ['wireguard', 'hysteria2', 'vless']
SESSION_CHOICES = ['wireguard', 'tor', 'proxy']
"""
Steps:
1) Looks up the profile by id number
2) Filters what values to save, using the ORM for some such as location
3) Saves it with the abstract class' json serialization methods, which use the ORM methods
Returns:
Result Object
Called by:
GUI's editor_page
Raises Errors:
False
"""
def update_profile(
profile_id: int,
key: str,
new_value: str) -> Result:
# final_data = None
logger.info(f"[UPDATE PROFILE] Recieved profile_id {profile_id}, key {key}, and new_value {new_value}.")
if not key or not new_value:
return Result(valid=False, data=f"Invalid Inputs of {key} and {new_value}", error_type=ResultError.INVALID_INPUT)
profile = Profile.find_by_id(profile_id)
if not profile:
error_msg = f"Invalid profile id of {profile_id}"
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
if key == 'dimentions':
profile.resolution = new_value
elif key == 'name':
profile.name = new_value
# ============= CONNECTION TYPE =============
elif key == 'connection':
if new_value == 'tor':
profile.connection.code = new_value
profile.connection.masked = True
elif new_value == 'just proxy':
profile.connection.code = 'system'
profile.connection.masked = True
else:
error_msg = 'System wide profiles not supported atm'
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# ============= BROWSER =============
elif key == 'browser':
browser_type, browser_version = new_value.split(':', 1)
application_version = get_application_version( # SQLAlchemy
application_code=browser_type,
version_number=browser_version
)
# SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc.
profile.application_version = application_version
elif key == 'protocol':
logger.info("Changing the protocol..")
changed_result = change_protocol(profile, new_value)
if not changed_result.valid:
logger.error("Returning a failed change.")
return changed_result
profile = changed_result.data
# ============= LOCATION =============
elif key == "location":
country_code, city_code = new_value.split("_")
new_location = get_profile_location_data( # SQLAlchemy
country_code=country_code,
city_code=city_code
)
pre_existing_protocol = profile.connection.code
if new_location.supports_protocol(pre_existing_protocol):
profile.location = new_location
else:
return Result(valid=False, message=f"The {new_location.name} location doesn't currently support the {str(pre_existing_protocol)} protocol", error_type=ResultError.WRONG_LOCATION)
# Subscription gets wiped on a location change, and is outside ORM
profile.subscription = None
else:
return Result(valid=False, message="Invalid Value to Edit", error_type=ResultError.INVALID_INPUT)
logger.info("[UPDATE PROFILE] Passing to Profile model to save..")
try:
ProfileController.update(profile)
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
return Result(valid=True)
except Exception as e:
error_msg = f"Profile save failed because {str(e)}"
return Result(valid=False, message=error_msg, error_type=ResultError.UNKNOWN)
def change_protocol(profile: Profile, new_value: str) -> Result:
logger.info(f"changing the protcol to {new_value}")
# ============= SAME VALUE =============
if profile.connection.code == new_value:
error_msg = f"This is NOT changing the protocol, it already was {new_value}"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# ============= LOCATION SUPPORTS PROTOCOL =============
logger.info("checking protocol support for this location..")
pre_existing_location = profile.location
if not pre_existing_location.supports_protocol(new_value):
logger.error(f"User is trying to update the protocol to {new_value}, but selected an invalid location to do so.")
error_msg = f"This location does not yet support the {str(new_value)} protocol."
return Result(valid=False, message=error_msg, error_type=ResultError.WRONG_LOCATION)
logger.info("Passed location check")
# ============= SYSTEMWIDE =============
logger.info(f"Evaluating the profile's connection type of {profile.type}")
if profile.type == ProfileType.SYSTEM:
return change_systemwide_connection(profile, new_value)
else:
return change_session_connection(profile, new_value)
def change_session_connection(profile: Profile, new_value: str) -> Result:
logger.info("This is a session profile")
if new_value not in SESSION_CHOICES:
error_msg = f"{new_value} is not a session choice"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
profile.connection.code = new_value
if new_value == 'wireguard':
profile.connection.masked = False
return Result(valid=True, data=profile)
def change_systemwide_connection(profile: Profile, new_value: str) -> Result:
logger.info("This is a systemwide profile")
if not new_value in SYSTEMWIDE_CHOICES:
error_msg = f"{new_value} is not a systemwide choice"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# REMOVE PAST CONFIG:
logger.info("removing the config from sudo slot..")
removed = remove_sudo_config(profile)
if not removed.valid:
error_msg = "You must give permission to delete the previous protocol's config file."
return Result(valid=False, message=error_msg, error_type=ResultError.PERMISSION)
# UPDATE PROFILE
logger.info("Literally updating the value..")
profile.connection.code = new_value
# final_data = "edit_session"
return Result(valid=True, data=profile)
# def save_billing_code(profile: Profile, billing_code: str) -> bool:

View file

@ -4,9 +4,9 @@ if TYPE_CHECKING:
from core.essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
#
# from core.services.prepare_tickets.get_public_key_by_config import (
# get_public_key_from_LOCAL_files_only,
# )
from core.services.prepare_tickets.get_public_key_by_config import (
get_public_key_from_LOCAL_files_only,
)
from core.services.failed_verification.is_the_key_to_blame import is_the_key_to_blame
from core.services.failed_verification.prep_with_previously_saved_blind_sigs import (
prep_with_previously_saved_blind_sigs,

View file

@ -3,31 +3,23 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.essentials.observers.ConnectionObserver import ConnectionObserver
# from core.observers.BaseObserver import BaseObserver
from core.observers.TicketObserver import TicketObserver
from core.utils.basic_operations.write_or_read_from_json import update_json
#
from core.models.invoice.TicketInvoice import TicketInvoice
# from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.observers.BaseObserver import BaseObserver
from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from core.services.payment_phase.check_if_paid import _check_if_paid
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker
from core.services.networking.httpx import connect
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.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.errors.exceptions import *
from core.errors.logger import logger
from core.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.services.payment_phase.ticket_config_tools import do_we_have_billing_id
from core.Constants import Constants
from typing import Optional
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
"""
Inputs: Which plan (key), which crypto, and how many profiles
@ -43,107 +35,128 @@ def initiate_payment(
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
bypass_existing: bool,
) -> Result:
) -> 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:
return Result(valid=False, error_type=ResultError.ALREADY_EXISTS, message="There's already a ticket billing session in progress. Do you want to wipe it?")
else:
made_ticket_tracker = setup_ticket_tracker(how_many_profiles=how_many_profiles)
logger.debug(f"Ticket Tracker doesn't exist, so we made it: {made_ticket_tracker} for {how_many_profiles} profiles")
invoice_data_object.add_error_code("already_exists")
return invoice_data_object
billing_id = do_we_have_billing_id()
if billing_id:
error_msg = "Billing code exists already"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.BILLING_CODE_EXISTS, data=billing_id, message=error_msg)
invoice_data_object.add_error_code("billing_code_exists")
invoice_data_object.temp_billing_code = billing_id
return invoice_data_object
rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=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"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You've picked a currently unsupported profile quantity")
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"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You didn't pick a key plan. It's either blank, none, or invalid.")
invoice_data_object.add_error_code("no_keyplan")
return invoice_data_object
# get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local")
if not isinstance(public_key_results, dict):
notification = "Invalid Key chosen or the server is down for that key."
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
# invoice_data_object.add_error_code("no_pub_key")
# return invoice_data_object
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")
notification = f"Connection Issues or Invalid Key chosen. {error_code}"
logger.error(notification)
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 Result(valid=False, error_type=ResultError.CONNECTION, message=f"There were connection issues with getting the right key. {message}")
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",
"currency": which_cryptocurrency,
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
try:
return save_and_send_intitial_billing(
payload=payload,
connection_observer=connection_observer
# controller sends to the service:
result = save_and_send_intitial_billing(
payload, connection_observer, invoice_data_object
)
except ValueError as e:
if ticket_observer:
ticket_observer.notify("failed_input", subject=str(e))
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e))
if result == False or result == None:
invoice_data_object.add_error_code("failed_save")
except ConnectionError as e:
error_msg = "There were connection issues with getting the right key."
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)
if ticket_observer:
ticket_observer.notify("connection_error", subject=str(e))
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
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,
ticket_observer: Optional[TicketObserver] = None,
) -> dict:
"""Legacy version being kept in only temporarily."""
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)
if ticket_observer:
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "error_code": "rejected"}
else:
@ -156,77 +169,8 @@ def check_if_paid(
url = make_url(which_endpoint)
# literally send:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
reply = send_data_to_server(payload, url, connection_observer)
if not api_reply_object.valid:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return {"valid": False, "message": error_msg}
logger.debug(f"inside ticketpay controller the reply is {reply}")
# return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
return reply_dict
def check_if_ticket_paid(
temp_billing_code: str,
connection_observer: ConnectionObserver,
) -> Result:
"""Coordinate a request with the ticket billing code, to check if an invoice is paid"""
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
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")
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=error_msg)
# prep the JSON payload:
payload = {"temp_billing_code": temp_billing_code}
# prep endpoint:
which_endpoint = "check_paid"
url = make_url(which_endpoint)
# literally send:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
if not api_reply_object.valid:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
print(f"reply_dict is {reply_dict}")
if isinstance(reply_dict, dict):
payment_status = reply_dict.get('payment_status', None)
message = reply_dict.get('message', None)
else:
payment_status = str(reply_dict)
if payment_status == "paid":
saved = update_json(filepath=filepath, key_to_add="paid", value_to_update="paid")
if saved:
return Result(valid=True, data=payment_status)
else:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="You paid, but it was unable to save that to the config")
elif message == "bad_code":
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="This is not a valid subscription code to check.")
return Result(valid=False, error_type=ResultError.UNPAID, message="Not yet paid")
return reply

View file

@ -1,20 +1,16 @@
from core.observers.ConnectionObserver import ConnectionObserver
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.payment_phase.payment_state import TicketPaymentData
from core.services.prepare_tickets.get_pub_key import get_and_save_pub_key
from core.utils.type_checker import type_checker
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.prepare_tickets import ticket_tracker
from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered,
)
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# generic
from typing import Optional
from core.observers.BaseObserver import BaseObserver
"""
Goal:
@ -30,218 +26,60 @@ 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.
"""
# Bulk Prep:
@type_checker
def prepare_tickets(
how_many_profiles: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> Result:
logger.info("prepare_tickets has begun")
# these two are for preparation of a single ticket to a profile.
# can't have one blank without the other. (profile or ticket)
if bool(which_ticket) != bool(billing_code):
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="which ticket must match the billing code")
# return {"valid": False, "error_code": "failed_input"}
) -> dict:
# make sure it's a number:
if not how_many_profiles or not isinstance(how_many_profiles, int):
if ticket_observer is not None:
if not isinstance(how_many_profiles, int):
ticket_observer.notify("failed_input", None)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"The profile quantity must be a number, and not type({how_many_profiles}) with {how_many_profiles}")
# return {"valid": False, "error_code": "failed_input"}
return {"valid": False, "error_code": "failed_input"}
# allow single prep:
# if how_many_profiles != 1:
# # but if it's not a single ticket, then 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:
# if ticket_observer is not None:
# ticket_observer.notify("failed_input", None)
# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"The profile quantity must be a number, and not type({how_many_profiles}) with {how_many_profiles}")
# 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:
logger.info("Checking public key..")
billing_data = TicketPaymentData()
which_key = billing_data.which_key
if not which_key:
error_msg = "You lack knowing the public key for this ticket plan."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
got_key = get_and_save_pub_key(which_key, connection_observer)
if not got_key.valid:
logger.error(got_key.message)
return got_key
public_key = got_key.data
logger.info("Got public key..")
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"}
notification = "Preparing Cryptography Locally"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_orchestrator(
how_many_profiles=how_many_profiles,
public_key=public_key,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket, # only relevant for single ticket prep
billing_code=billing_code # only relevant for single ticket prep
how_many_profiles, ticket_observer, connection_observer
)
if prep_results.valid:
notification = "Done! All Tickets Ready!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
else:
notification = "Ticket Preparation Failed"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# 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 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!"
# if ticket_observer is not None:
# 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!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# notification = f"Error with Ticket Preparation or Verification!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# No profile required
def respawn_billing_code_into_ticket(
billing_code: str,
which_ticket: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
"""
Purpose:
Invalidates a billing code, & produces a valid unused ticket
Requires:
Valid Billing Code.
Any Ticket slot folder, (but not an actual ticket there).
No profile required.
Method:
Converts a valid VPN billing code, into a valid unused ticket.
At the cost of the server expiring the billing code immediately.
And the ticket slot is used (wipes existing used ticket)
This has the effect of "wiping" the billing code, and starting over.
The new ticket can be applied to any other profile now.
Called by:
ticket_respawn's respawn_profile
"""
#################################################
# PREP PRE-REQS
#################################################
if not which_ticket:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing which ticket slot is being respawned")
if not isinstance(which_ticket, int):
try:
which_ticket = int(which_ticket)
except:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.")
#################################################
# SEND TO SERVER
#################################################
ticket_result = prepare_tickets(
how_many_profiles=1,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket,
billing_code=billing_code
)
#################################################
# EVALUATE RESULTS
#################################################
if not isinstance(ticket_result, dict):
return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Error! The prepare_tickets function returned an invalid format of {type(ticket_result)} when it should be a dict. This is being recieved in the respawn_ticket function.")
valid = ticket_result.get('valid', False)
if not valid:
notification = "Error in Respawn Prep!"
if ticket_observer is not None:
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_error_result(ticket_result)
return prep_results
#################################################
# WIPE LOCAL TICKET DATA
#################################################
wiped_sub = ticket_tracker.wipe_one_ticket_sub(which_ticket)
logger.info(f"Did local ticket tracker wipe? {wiped_sub}")
#################################################
# NOTIFY & RETURN
#################################################
notification = f"Respawn Done for Ticket Slot {which_ticket}!"
if ticket_observer is not None:
notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification)
update_msg = f"It worked. Respawn Done! The billing code is no longer valid and you now have a valid ticket at slot {which_ticket}"
logger.info(update_msg)
return Result(valid=True, message=update_msg)
def prep_error_result(ticket_result: dict):
message = ticket_result.get('message', False)
error_code = ticket_result.get('error_code', False)
if message == "missing_data":
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing Input Data. This is an error with the introduction functions.")
elif message == "cant_save":
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Can't save the data. Please check your file permissions and free space.")
elif message == "verification_failed":
how_many_failed = ticket_result.get('how_many_failed', False)
return Result(valid=False, error_type=ResultError.ENCRYPTION, message=f"Verification of the encryption failed for {how_many_failed} profiles.")
elif error_code == "failed":
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="There was an error with setting up ticket tracker or some local filesystem problem.")
else:
if message:
error_msg = message
elif error_code:
error_msg = f"An error occured: {error_code}."
else:
error_msg = "An unknown error occured."
return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg)
return prep_results

View file

@ -6,24 +6,15 @@ if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver
from core.Constants import Constants
# from core.observers.BaseObserver import BaseObserver
from core.models.manage.server_reply_tools import translate_server_failure, invalid_server_format
from core.models.pydantic_models.type_checker import validate_model
from core.models.pydantic_models.ServerReply import ServerReply
from core.services.networking.httpx import connect
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.observers.BaseObserver import BaseObserver
from core.services.networking.get_data_from_server import get_data_from_server
from core.errors.logger import logger
def sync_ticket_prices(
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
notification = "Connecting to get Ticket Pricing..."
ticket_observer: TicketObserver, connection_observer: ConnectionObserver
) -> dict:
notification = f"Connecting to get Ticket Pricing..."
ticket_observer.notify("connecting", subject=notification)
rejected_list = [None, False, ""]
@ -36,50 +27,14 @@ def sync_ticket_prices(
return {"valid": False, "error_code": "invalid_url"}
url = f"{base_url}/sync"
api_response = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
try:
sync_results = get_data_from_server(url, connection_observer)
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
if sync_results in rejected_list:
return {"valid": False, "error_code": "sync_failed"}
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
server_reply = validate_model(ServerReply, raw_reply)
if not server_reply:
logger.error(f"Invalid Structure for ServerReply! The raw data is {raw_reply}")
return invalid_server_format()
if not server_reply.valid:
logger.error(f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}")
# translate by enum error code:
return translate_server_failure(server_reply)
#-----------------------------
# EVALUATE SERVER'S ACTUAL DATA
#-----------------------------
# Layer 3: Payload Data
payload_data = server_reply.data
if not payload_data:
logger.error(f"Failed to create an invoice object with this data: {payload_data}")
return invalid_server_format()
return Result(valid=True, data=payload_data)
# return {"valid": True, "data": payload_data}
logger.debug(f"Inside the sync controller, sync_results is: {sync_results}")
except:
return {"valid": False, "error_code": "sync_failed"}
return sync_results

View file

@ -1,31 +1,23 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver
from core.essentials.observers.ConnectionObserver import ConnectionObserver
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.models.Result import Result, ResultError
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
# from core.services.prepare_tickets import setup_ticket_tracker
from core.services.prepare_tickets.ticket_tracker import (
get_data_for_a_single_ticket,
does_ticket_tracker_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.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.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
from core.models.enum_models import ProtocolChoice
# generic
import random
from typing import Optional
def modify_random_tickets_setting(
@ -36,7 +28,7 @@ def modify_random_tickets_setting(
choices = ["on", "off"]
if on_or_off not in choices:
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
filepath = f"{billing_folder}/billing_choices.json"
@ -46,26 +38,22 @@ def modify_random_tickets_setting(
notification = "First setup the Tickets before picking use"
ticket_observer.notify("failed_input", subject=notification)
return {"valid": False, "message": notification}
else:
try:
if on_or_off == "on":
update_result = update_json(filepath, "use_random", True)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
update_json(filepath, "use_random", True)
return {"valid": True}
elif on_or_off == "off":
update_result = update_json(filepath, "use_random", False)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
update_json(filepath, "use_random", False)
return {"valid": True}
else:
ticket_observer.notify("failed_input", None)
return {
"valid": False,
"message": "Invalid choice for turning on or off",
"message": f"Invalid choice for turning on or off",
}
def conclude_and_return(update_result: bool, filepath: str, ticket_observer: TicketObserver) -> dict:
if update_result:
return {"valid": True}
else:
notification = f"Error with modifying config file. Check the filepath {filepath}"
logger.error(notification)
except:
notification = f"Error with modifying config file. Check {filepath}"
ticket_observer.notify("error", subject=notification)
return {"valid": False, "message": notification}
@ -83,26 +71,18 @@ def do_we_use_a_random_ticket(ticket_observer: TicketObserver) -> tuple:
config_data = get_value_from_config("use_random")
# if the 'value' key is in the config, that means it successfully read the config.
if "value" not 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
if "value" in config_data:
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
if random_setting == True:
data_results = pick_a_random_ticket(ticket_observer)
# invalid format:
if "random_ticket" not in data_results:
if "random_ticket" in data_results:
which_ticket = data_results["random_ticket"]
error_msg = None
return which_ticket, error_msg
else:
which_ticket = "error"
if "message" in data_results:
error_msg = data_results["message"]
@ -112,41 +92,34 @@ def do_we_use_a_random_ticket(ticket_observer: TicketObserver) -> tuple:
)
return which_ticket, error_msg
# finally get the result:
which_ticket = data_results["random_ticket"]
# if it read the config, but the value is false:
else:
which_ticket = None
error_msg = None
return which_ticket, error_msg
# this is a problem with reading the config itself:
else:
which_ticket = "error"
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(format_for_gui: Optional[bool] = False):
"""Get a list of all unused ticket numbers"""
def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
# does the file keeping track of ALL tickets exist:
does_the_file_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False:
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
return Result(valid=False, error_type=ResultError.MISSING_FILE, message=error_msg)
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg}
# Use the Model:
unused_tickets = get_all_unused_tickets()
print(f"unused_tickets is {unused_tickets}")
return unused_tickets
# if not format_for_gui:
# return Result(valid=True, data=unused_tickets)
# # format for gui:
# formatted_data = format_for_gui(unused_tickets)
# return Result(valid=True, data=formatted_data)
# def format_for_gui(raw_tickets) -> list:
# return [(f"Ticket #{t}", t) for t in raw_tickets]
"""
use_ticket function requires:
@ -159,60 +132,56 @@ use_ticket function requires:
def use_ticket(
which_ticket: int,
which_location: int, # Location.id
protocol_choice: ProtocolChoice,
which_location: str,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
profile: Optional[Union[SessionProfile, SystemProfile]] = None, # only for assassin or single profiles.
) -> dict:
if not is_valid_protocol(protocol_choice):
error_msg = f"Invalid Protocol Choice of {protocol_choice}."
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
which_ticket = str(which_ticket) # type: ignore
# does the ticket's file exist:
ticket_exists = does_ticket_file_exist(which_ticket)
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)
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_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False:
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)
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:
ticket_observer.notify("connecting", "Connecting..")
reply_object = use_ticket_orchestrator(which_ticket, which_location, protocol_choice, connection_observer)
reply = use_ticket_orchestrator(which_ticket, which_location, connection_observer)
return reply_object
# 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}
return reply
def pick_a_random_ticket(ticket_observer: TicketObserver) -> dict:
ticket_data = get_unused_tickets(ticket_observer)
if "valid" not in ticket_data:
error_msg = "Missing or Invalid Data. Unable to get unused ticket list."
return {"valid": False, "message": error_msg}
if ticket_data["valid"] != True:
return ticket_data
# it's valid, get the list & pick one:
if "valid" in ticket_data:
if ticket_data["valid"] == True:
list_of_unused_tickets = ticket_data["data"]
random_ticket = random.choice(list_of_unused_tickets)
return {"valid": True, "random_ticket": random_ticket}
else:
return ticket_data
else:
error_msg = "Missing or Invalid Data. Unable to get unused ticket list."
return {"valid": False, "message": error_msg}

View file

@ -1,9 +0,0 @@
from dataclasses import dataclass
@dataclass
class ErrorStack:
file_name: str = False
line_number: int = False
func_name: str = False
message_for_user: str = False
full_stacktrace: str = False

View file

@ -1,10 +0,0 @@
from core.models.Result import Result, ResultError
from core.errors.logger import logger
def not_supported():
NOT_SUPPORTED_PAYMENT = "This is not a supported payment category"
logger.error(NOT_SUPPORTED_PAYMENT)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=NOT_SUPPORTED_PAYMENT)

View file

@ -1,57 +1,3 @@
# MissingData, FailedToSave, InvalidData, CriticalFailure defined here
from core.models.Result import Result, ResultError
from typing import Optional
class DNSError(Exception):
"""There are issues with DNS"""
def __init__(self, result: Optional[Result]):
if result:
self.result = result
if result.message and result.message is not None:
error_msg = result.message
else:
error_msg = result.error_type
else:
error_msg = "Failed to start DNS"
super().__init__(error_msg)
class FirewallError(Exception):
"""There are issues with the Firewall"""
def __init__(self, result: Optional[Result]):
if result:
self.result = result
if result.message and result.message is not None:
error_msg = result.message
else:
error_msg = result.error_type
else:
error_msg = "Failed to start the firewall"
super().__init__(error_msg)
class SudoScript(Exception):
"""There are issues with the sudo scripts"""
def __init__(self, result: Result):
self.result = result
if result.message and result.message is not None:
error_msg = result.message
else:
error_msg = result.error_type
super().__init__(error_msg)
class MissingPreReqs(Exception):
"""User can't take an action because they lack the prereqs"""
def __init__(self, result: Result):
self.result = result
if result.message and result.message is not None:
error_msg = result.message
else:
error_msg = result.error_type
super().__init__(error_msg)
class TorServiceInitializationError(Exception):
pass

View file

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

View file

@ -1,120 +0,0 @@
from core.controllers.ConfigurationController import ConfigurationController
from core.errors.ErrorStack import ErrorStack
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from core.Errors import (
CommandNotFoundError, EndpointVerificationError, FileIntegrityError,
InvalidSubscriptionError, MissingSubscriptionError, ProfileActivationError,
ProfileModificationError, ProfileStateConflictError, UnsupportedApplicationVersionError,
)
from core.utils.type_checker import type_checker
from typing import Type, Optional
import traceback
import os
ERROR_TO_UI_MESSAGE = {
ProfileActivationError: "The profile could not be enabled",
UnsupportedApplicationVersionError: "The application version in question is not supported",
FileIntegrityError: "Application version file integrity could not be verified.",
ProfileModificationError: "WireGuard configuration could not be attached.",
}
def translate_error(error: Type[Exception], profile_id: int) -> Result:
"""Translate legacy core errors into actions and/or messages via Result Objects."""
logger.error(str(error))
#----------------------
# PURE MESSAGES (NO SOLUTION FUNCTION)
#----------------------
if error in ERROR_TO_UI_MESSAGE:
error_msg = ERROR_TO_UI_MESSAGE[error]
return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg)
#----------------------
# ENDPOINT VERIFICATION
#----------------------
if isinstance(error, EndpointVerificationError):
return Result(valid=False, error_type=ResultError.ENDPOINT_VERIFICATION, message="Operator verification failed. Do you want to continue anyway?")
#----------------------
# STATE CONFLICT
#----------------------
if isinstance(error, ProfileStateConflictError):
# function_choice = literally_connect(
# profile_id=profile_id,
# ignore_state_conflict=True
# )
error_msg = "The profile is already enabled. Do you want to force enable it anyway?"
return Result(valid=False, error_type=ResultError.STATE_CONFLICT, message=error_msg)
#----------------------
# SUBSCRIPTION
#----------------------
if isinstance(error, (InvalidSubscriptionError, MissingSubscriptionError)):
error_msg = f"Subscription missing or invalid for profile {profile_id}. Do you want to get a new sub?"
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=error_msg)
#----------------------
# COMMAND NOT FOUND
#----------------------
if isinstance(error, CommandNotFoundError):
# add install later!!
name_of_package = str(error.subject)
error_msg = f"You're missing the {name_of_package} package. Please install it."
return Result(valid=False, error_type=ResultError.MISSING_SOFTWARE, message=error_msg)
else:
error_stack = get_traceback_line(error)
return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_stack.message_for_user)
def strip_filename(path: str) -> str:
"""Returns only the filename from a full system path."""
return os.path.basename(path)
def get_traceback_line(error) -> ErrorStack:
"""Extract the traceback from the exception object"""
tb = error.__traceback__
summary = traceback.extract_tb(tb)
if summary:
last_frame = summary[-1]
func_name = last_frame.name # The function name
line_no = last_frame.lineno # The line number
file_name = strip_filename(last_frame.filename) # The file path
else:
func_name, line_no, file_name = "Unknown", "Unknown", "Unknown"
# Generate the user-facing message and the full formatted stacktrace
error_msg = f"Error in function {func_name} in file {file_name} on line {line_no}. That error is: {str(error)}"
full_trace = "".join(traceback.format_exception(type(error), error, tb))
logger.error(error_msg)
error_stack = ErrorStack(
file_name=file_name,
line_number=line_no,
func_name=func_name,
message_for_user=error_msg,
full_stacktrace=full_trace
)
return error_stack
@type_checker
def get_ignored_exceptions(ignore_state_conflict: Optional[bool] = False):
ignored_as_list = []
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
if endpoint_verification_enabled:
ignored_as_list.append(EndpointVerificationError)
if ignore_state_conflict:
ignored_as_list.append(ProfileStateConflictError)
ignored = tuple(ignored_as_list)
return ignored

View file

@ -1,15 +0,0 @@
from core.models.Result import Result, ResultError
from core.controllers.SyncController import new_sync
ENUM_MAP = {
ResultError.UNKNOWN: None,
ResultError.NEED_SYNC: (new_sync),
ResultError.INVALID_API_REPLY: None,
ResultError.ENCRYPTION: None,
# ResultError.NO_TICKET: (start_billing),
# ResultError.NO_PROFILE: (go_to_new_profile_page),
# ResultError.ALREADY_EXISTS: (get_invoice), # bypass_existing: True
# ResultError.BILLING_CODE_EXISTS: (get_invoice) # bypass_existing: True
}

View file

@ -1,30 +0,0 @@
from core.errors.ErrorStack import ErrorStack
from typing import Protocol, Optional, Callable, Any, Type
class PopupProtocol(Protocol):
def __init__(
self,
choice: bool,
title: str,
message: str,
end_app: Optional[bool] = False,
on_answer: Optional[Callable] = None,
passed_result: Optional[Any] = None,
**kwargs
) -> None: ...
def show(self) -> None:
"""Framework-neutral method to display the popup"""
...
class StackErrorProtocol(Protocol):
def __init__(
self,
error_data: ErrorStack,
**kwargs
) -> None: ...
def show_window(self) -> None:
"""Framework-neutral method to display the popup"""
...

View file

@ -1,75 +0,0 @@
from core.models.system.SystemState import SystemState
from core.models.session.SessionState import SessionState
from core.models.enum_models import ProtocolChoice
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GlobalState:
systemwide_active: bool = False
protocol: ProtocolChoice = None
firewalled: bool = False
dns_set: bool = False
systemwide_profile_id: Optional[int] = None
session_profile_ids: list[int] = field(default_factory=list)
verified: bool = False
changed_port: bool = False
class ProfileStateRepository:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ProfileStateRepository, cls).__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
self.global_state = GlobalState()
# Initial load: Sync memory with whatever is currently on disk
sys = SystemState.get()
if sys:
self.global_state.systemwide_profile_id = sys.profile_id
self.global_state.firewalled = sys.firewalled
self.global_state.dns_set = sys.dns_set
# Load existing session IDs from disk into the list
self.global_state.session_profile_ids = [s.id for s in SessionState.all()]
def turn_off_system_connection(self):
"""Clear the system-wide memory cache."""
self.global_state.systemwide_profile_id = None
self.global_state.protocol = None
self.global_state.firewalled = False
self.global_state.dns_set = False
self.global_state.systemwide_active = False
def sync_system_state(self, state: SystemState):
"""Called by SystemStateController to update memory cache."""
self.global_state.systemwide_profile_id = state.profile_id
self.global_state.protocol = state.protocol
self.global_state.firewalled = state.firewalled
self.global_state.dns_set = state.dns_set
if state.profile_id:
self.global_state.systemwide_active = True
def sync_session_added(self, profile_id: int):
"""Called by SessionStateController to update memory cache."""
if profile_id not in self.global_state.session_profile_ids:
self.global_state.session_profile_ids.append(profile_id)
def sync_session_removed(self, profile_id: int):
"""Called by SessionStateController to update memory cache."""
if profile_id in self.global_state.session_profile_ids:
self.global_state.session_profile_ids.remove(profile_id)
def is_profile_active(self, profile_id: int) -> bool:
# Check system profile
if self.global_state.systemwide_active and self.global_state.systemwide_profile_id == profile_id:
return True
# Check session profiles
return profile_id in self.global_state.session_profile_ids

View file

@ -1,208 +0,0 @@
from core.services.networking.general_connection_tools.connection_enable import establish_connection
from core.controllers.ConfigurationController import ConfigurationController
from core.infrastructure.global_state import ProfileStateRepository
from core.controllers.ProfileController import ProfileController
from core.controllers.SystemStateController import SystemStateController
from core.controllers.SessionStateController import SessionStateController
from core.models.system.SystemState import SystemState
from core.models.enum_models import ProtocolChoice
from core.models.Result import Result, ResultError
from core.services.networking.systemwide import killswitch
from core.services.networking.systemwide.general_tools import interface_tools
from core.services.networking.systemwide.general_tools.test_any_protocol import test_ip_systemwide_for_any_protocol
from core.services.networking.general_connection_tools.cross_protocol import kill_any_protocol
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
from core.services.networking.systemwide.dns_tools.process_dns_result import quick_domain_dns_check
from core.observers.Notifications import Notifications, EventType
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants
from core.errors.logger import logger
# generic
from functools import partial
SINGBOX_PROTOCOLS = (ProtocolChoice.HYSTERIA2, ProtocolChoice.VLESS)
def verify_systemwide_state(current_state: SystemState) -> bool:
"""Confirm a valid connection, to update GlobalState (for UI)"""
state_repo = ProfileStateRepository()
state_repo.sync_system_state(current_state)
state_repo.verified = True
Notifications.emit(EventType.SYSTEM_ENABLE, current_state.profile_id)
def is_systemwide_on() -> bool:
"""Returns a boolean if systemwide is connected. True is Yes, False is No"""
state_repo = ProfileStateRepository()
return state_repo.global_state.systemwide_active
def connected_system_profile_id() -> int|None:
"""Returns a single number of the connected system profile"""
state_repo = ProfileStateRepository()
print(f"state_repo is {state_repo.global_state}")
return state_repo.global_state.systemwide_profile_id
def connected_session_profile_ids() -> list|None:
"""Returns a list of the currently connected session profile IDs"""
state_repo = ProfileStateRepository()
return state_repo.global_state.session_profile_ids
def is_connected(profile_id: int) -> bool:
"""Returns a boolean if a given profile is connected"""
# system-wide:
systemwide_profile_id = connected_system_profile_id()
if profile_id == systemwide_profile_id:
return True
# session:
state_repo = ProfileStateRepository()
is_session_connected = state_repo.is_profile_active(profile_id)
print(f"Is this session profile connected? {profile_id}")
return is_session_connected
def coordinate_evaluation(connection_observer: ConnectionObserver) -> Result:
"""See if a connection can be salvaged. If not, rip it down & start over."""
# Claimed JSON state:
if not SystemStateController.exists():
error_msg = "State vanished inbetween the previous error prompt and now. That state is an external file, so that is technically possible, but a critical error."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)
json_state = SystemStateController.get()
protocol = json_state.protocol
profile_id = json_state.profile_id
firewalled = json_state.firewalled
logger.info("Extracted JSON state data")
# full profile:
profile = ProfileController.get(profile_id)
if not profile:
error_msg = "Critical Failure where the profile to evaluate the connection can't be found. Resetting state."
logger.error(error_msg)
SystemStateController.dissolve()
killed_firewall = killswitch.disarm()
logger.info(f"killed_firewall: {killed_firewall.valid}")
reverted_dns = revert_dns()
logger.info(f"reverted_dns is {reverted_dns}")
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
logger.info("Got the relevant profile. Doing a pre-flight check")
#-----------------------
# INTERFACE
#-----------------------
if protocol == ProtocolChoice.WIREGUARD:
interface_up = interface_tools.exists("wg")
elif protocol in SINGBOX_PROTOCOLS:
interface_up = interface_tools.exists(Constants.SINGBOX_TUN_IF)
else:
interface_up = False
logger.error("Unsupported protocol in systemwide state.")
# RULE OUT:
if not interface_up:
logger.info("Doing a new connection due to the interface being down")
return establish_connection(profile, ignore={}, connection_observer=connection_observer)
#-----------------------
# FIREWALL
#-----------------------
# Previously User requested Configuration requests
user_wants_firewall = ConfigurationController.get_firewall_setting()
# Wireguard is voluntary opt-in, so they might not have the script installed,
if not user_wants_firewall and protocol == ProtocolChoice.WIREGUARD:
firewall_on = False
# unless they explicitly opted out with WG, then check:
else:
if firewalled:
firewall_on = killswitch.status()
else:
firewall_on = False
#-----------------------
# # DNS
#-----------------------
if protocol == ProtocolChoice.WIREGUARD:
target_interface = "wg"
elif protocol in SINGBOX_PROTOCOLS:
target_interface = Constants.SINGBOX_TUN_IF
else:
target_interface = "GARBAGE_FALSE"
logger.error("Unsupported protocol in systemwide state.")
# this doesn't need sudo to run, and its just regular commands (no script setup):
dns_on = quick_domain_dns_check(target_interface=target_interface)
logger.info("Evaluation Data Gathered")
# ------------------
# DO API CALL & EVALUATE
# ------------------
test_result = test_ip_systemwide_for_any_protocol(profile_id=profile_id, protocol=protocol, connection_observer=connection_observer)
if not test_result.valid:
logger.info("IP address check was not valid, so we're doing a new connection.")
return establish_connection(profile, ignore={}, connection_observer=connection_observer)
if firewall_dns_in_line_with_expectations(firewalled=firewall_on, dns_set=dns_on, protocol=protocol):
logger.info("IP address is connected and firewall & DNS expectations match reality. Update/Verify Global State that it's on")
verify_systemwide_state(current_state=json_state) # this notifies the UI
return Result(valid=True)
logger.error("Current connection did not meet the criteria to continue to use it as is.")
return establish_connection(profile, ignore={}, connection_observer=connection_observer)
def firewall_dns_in_line_with_expectations(firewalled: bool, dns_set: bool, protocol: ProtocolChoice) -> bool:
"""We assume the IP address is okay. Is that enough?"""
if protocol != ProtocolChoice.WIREGUARD:
if firewalled and dns_set:
return True
else:
logger.info("Only wireguard is optional firewall/dns.")
return False
# Previously User requested Configuration requests
user_wants_firewall = ConfigurationController.get_firewall_setting()
user_wants_dns = ConfigurationController.get_dns_setting()
if user_wants_firewall and not firewalled:
return False
if user_wants_dns and not dns_set:
return False
return True
def evaluate_state_on_startup() -> Result:
"""When the app first starts, evaluate pre-existing state"""
#-----------------------
# SYSTEM-WIDE
#-----------------------
systemwide_active = SystemStateController.get()
if systemwide_active:
error_msg = "The previous system-wide connection was not correctly turned off before the app was killed. Do you want to evaluate it? (no blindly kills it)"
no_option = partial(kill_any_protocol, current_state=systemwide_active, active_protocol=systemwide_active.protocol)
return Result(valid=False, no_option=no_option, error_type=ResultError.ZOMBIE_CONNECTION, message=error_msg)
#-----------------------
# SESSION
#-----------------------
active_session_profiles = SessionStateController.all()
if active_session_profiles:
for each_profile in active_session_profiles:
logger.info(f"Removing this profile {active_session_profiles} from session since it's a fresh restart.")
SessionStateController.remove(each_profile.id)
return Result(valid=True)

View file

@ -1,360 +0,0 @@
from core.infrastructure.thread_dispatch import MainThreadDispatcher
from core.infrastructure.error_popup import PopupProtocol, StackErrorProtocol
from core.infrastructure.service_registry import ServiceLocator
from core.errors.translate_error import get_traceback_line
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# generic:
import threading
from typing import Callable, Any, Optional, Type
import inspect
from functools import partial
import sys
MAX_ATTEMPTS = 2
class ThreadPlumbing:
"""
Manage Execution of functions on threads, and orchestrate their retries,
then pass the data through to popup prompts for the user to decide how to solve any error, by enum type.
"""
def __init__(self, action: Callable, ui_dispatcher: MainThreadDispatcher, error_implementation: Type[PopupProtocol], stack_error_implementation: Type[StackErrorProtocol], ENUM_MAP: dict, auto_navigate_dict: dict, ui_caller: Optional[Callable] = None, *args, **kwargs):
self.action = action
self.ui_dispatcher = ui_dispatcher
self.ui_caller = ui_caller
self.error_implementation = error_implementation
self.stack_error_implementation = stack_error_implementation
self.ENUM_MAP = ENUM_MAP
self.auto_navigate_dict = auto_navigate_dict
self.navigator = ServiceLocator.get("navigator")
self.args = args
self.kwargs = kwargs
self.attempt_count = 0
def execute(self, task = None):
"""Kicks off the action in a background thread"""
# 0. If there's no task passed in, get it from the object initalization,
if not task:
# 1. Create the partial immediately in the main thread
task = partial(self.action, *self.args, **self.kwargs)
# 2. Define a simple wrapper to run the task and then handle the result
def wrapper():
try:
result = task() # Run the partial
logger.info(f"post complete result here is {result}")
except Exception as e:
error_msg = f"There was an error: {str(e)}"
logger.error(error_msg)
error_stack = get_traceback_line(e)
error_msg = error_stack.message_for_user
logger.error(error_msg)
result = Result(valid=False, error_stack=error_stack, error_type=ResultError.UNKNOWN, message=error_msg)
finally:
self._on_complete(result) # Process the result
# 3. Spin up a single thread and start it
thread = threading.Thread(target=wrapper, daemon=True)
thread.start()
def _on_complete(self, result: Result):
"""Get a potential function prescription and return it to the main thread."""
#----------------------------
# Function Prescription:
#----------------------------
if not result.valid:
if not result.function_choice:
result.function_choice = resolve_prescription(result, self.ENUM_MAP, *self.args, **self.kwargs)
logger.info(f"result.function_choice is {result.function_choice}")
else:
logger.info("Using the pre-existing function choice for this error.")
#----------------------------
# Back to main thread
#----------------------------
# logger.info(f"result in plumbing background thread is {result}")
self.ui_dispatcher.run_on_main_thread(self._back_on_main_thread, result)
# print("ran on main thread")
def _back_on_main_thread(self, result: Result):
"""Decide: retry, show popup, or final callback"""
logger.info(f"We are on the main thread with result {result}")
#----------------------------
# OUTRIGHT ERROR STACK
#----------------------------
if result.error_stack:
error_window = self.stack_error_implementation(error_data=result.error_stack)
user_choice = error_window.show_window()
print(f"user_choice is {user_choice}")
if user_choice == 2:
sys.exit()
return
#----------------------------
# NAVIGATE AWAY
#----------------------------
# If the result sender explicitly asked us to navigate to a page with a ui callback, then bypass the popup:
if result.error_type == ResultError.NAVIGATE_TO and self.ui_caller:
logger.info("Plumbing was asked to NAVIGATE_TO so skipping to the ui_caller..")
self.ui_caller(result)
#----------------------------
# MAX TRIES
#----------------------------
if result.valid or self.attempt_count >= MAX_ATTEMPTS:
if self.ui_caller:
print("calling back with result")
if not result:
result = Result(valid=False, error_type=ResultError.UNKNOWN, message="Plumbing is missing a result.")
self.ui_caller(result)
return
#----------------------------
# SHOW CHOICE & POPUP
#----------------------------
# do we need to decide on an action?
choice = bool(result.function_choice)
logger.info(f"Do we have to make a choice? {choice}")
# if there's a message or a choice to make:
if result.message or choice:
# Then Show popup and wait for user choice
popup = self.error_implementation(
choice=bool(result.function_choice),
title=f"Error: {result.error_type.value}",
message=result.message,
on_answer=self._on_popup_answer,
passed_result=result,
end_app=False
)
# framework neutral show method
logger.info("Showing a popup..")
popup.show()
logger.info("Showed the error popup.")
#----------------------------
# COMPLETE
#----------------------------
# otherwise return it as is:
if self.ui_caller:
logger.info("Returning from plumbing..")
self.ui_caller(result)
logger.error("No ui caller!!!")
def _on_popup_answer(self, user_choice: bool, passed_result: Result):
"""User responded to popup"""
#---------------------
# RUN THE CHOICE:
#---------------------
if user_choice and passed_result.function_choice:
self.attempt_count += 1
self.execute(passed_result.function_choice)
else:
#---------------------
# OR RUN THE "NO" CHOICE:
#---------------------
# if the result originally prescribed it's own "no option":
no_option = passed_result.no_option
if no_option:
no_option()
return
#---------------------
# OR Just return back,
#---------------------
# otherwise do the original UI call:
if self.ui_caller:
self.ui_caller(passed_result)
# STANDALONE HELPERS:
def normalize_seq(s):
"""Ensures input is a tuple regardless of if it was a list or tuple."""
return tuple(s) if s is not None else ()
def resolve_prescription(result: Result, ENUM_MAP: dict, *args, **kwargs):
"""
Find the correct solution to the type of enum problem,
And then add the args from either the original caller's args,
or the defaults for that error type
"""
logger.info(f"resolve_prescription is being called for {result}.")
# Priority 1: If the function explicitly provided a full partial, use it.
if result.function_choice:
logger.info("The result object itself has a function choice!")
return result.function_choice
# Priority 2: Look up the Enum map
logger.info("checking enum map")
if result.error_type not in ENUM_MAP:
# logger.info(f"No solution in the ENUM_MAP of {ENUM_MAP}")
return None # No solution
enum_value = ENUM_MAP[result.error_type]
logger.info(f"got enum_value {enum_value}")
# Handle the None case (no solution)
if enum_value is None:
return None
# Unpack the enum value - could be just a function or (function, default_args)
if isinstance(enum_value, tuple):
func, default_args = enum_value
else:
func = enum_value
default_args = None
# What does this function require?
sig = inspect.signature(func)
function_requirements = sig.parameters
# requires nothing, we're done, return it.
if not function_requirements:
return func
# Priority 3: Does it want the result itself?
if 'result' in function_requirements:
logger.info("result required; baking result into partial.")
# 1. Prepare the arguments we have available right now
# Start with default_args from the ENUM_MAP if they exist
current_args = {}
if isinstance(default_args, dict):
current_args.update(default_args)
# Inject the result object
current_args['result'] = result
# 2. Use bind_partial so it doesn't fail if other args are still missing
bound = sig.bind_partial(**current_args)
# 3. Fill in any function-defined defaults
bound.apply_defaults()
# 4. Return the partial with the bound arguments
return partial(func, **bound.arguments)
# Priority 4: Use original args/kwargs
try:
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
return partial(func, **bound.arguments)
except TypeError:
pass # Not enough, move to priority 4
# Priority 5: Use original args/kwargs PLUS defaults
if default_args is None:
return False
try:
if isinstance(default_args, dict):
# original kwargs take precedence over default_args
merged_kwargs = {**default_args, **kwargs}
bound = sig.bind(**merged_kwargs)
else:
# 1. Handle Type Mismatch: Normalize both to tuples
d_tuple = normalize_seq(default_args)
o_tuple = normalize_seq(args)
# 2. Positional Override: Replace defaults with original args by index
merged_args = list(d_tuple)
for i, val in enumerate(o_tuple):
if i < len(merged_args):
merged_args[i] = val # Override default at this position
else:
merged_args.append(val) # Append if original args exceed defaults
# 3. Collision Fix: Convert positional merge to a name-map first.
# This prevents "multiple values for argument" if the same arg
# is present in both merged_args and kwargs.
pos_bound = sig.bind(*merged_args)
# Now merge the positional map with kwargs (kwargs win)
final_kwargs = {**pos_bound.arguments, **kwargs}
bound = sig.bind(**final_kwargs)
bound.apply_defaults()
return partial(func, **bound.arguments)
except TypeError:
pass
return False # Still args or kwargs lacking
def convert_to_result(result: Any) -> Result:
"""Turn a non-Result object into a Result object"""
error_msg = "Internal error where the result was returned in the wrong format."
logger.error(error_msg)
if isinstance(result, bool):
return Result(valid=result, error_type=ResultError.INVALID_INPUT, message=error_msg)
elif isinstance(result, dict):
valid = result.get('valid', False)
message = result.get('message', False)
if not message:
message = error_msg
return Result(valid=valid, error_type=ResultError.INVALID_INPUT, message=error_msg)
else:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# Optional Standalone Public API
def take_plumbing_action(action: Callable, ui_dispatcher: MainThreadDispatcher, error_implementation: Type[PopupProtocol], ui_caller: Callable, *args, **kwargs):
controller = ThreadPlumbing(action, ui_dispatcher, ui_caller, *args, **kwargs)
controller.execute()
# def execute(self):
# """Entry point: kick off the first attempt"""
# self.attempt_count += 1
# self._spawn_action(action=self.action, first_time=True)
# def _spawn_action(self, action: Callable, first_time: bool = None):
# """Spawn action on executor"""
# if first_time:
# future = executor.submit(action, *self.args, **self.kwargs) # original args/kwargs
# else:
# future = executor.submit(action) # this is a pre-loaded partial
# future.add_done_callback(self._on_complete)
# result = future.result()
# if not isinstance(result, Result):
# result = convert_to_result(result)
# Auto navigate:
# does this have an auto-navigate page?
# if self.navigator and self.auto_navigate_dict:
# logger.info("checking auto-navigate..")
# self.check_auto_navigation(result)
# def check_auto_navigation(self, result: Result):
# auto_navigate_page = self.auto_navigate_dict.get(result.error_type)
# if auto_navigate_page is None:
# return
# self.navigator.request_navigation.emit(auto_navigate_page)

View file

@ -1,10 +0,0 @@
class ServiceLocator:
_services = {}
@classmethod
def register(cls, name, service):
cls._services[name] = service
@classmethod
def get(cls, name):
return cls._services.get(name)

View file

@ -1,18 +0,0 @@
from typing import Protocol
class MainThreadDispatcher(Protocol):
def run_on_main_thread(self) -> None:
...
# previous ABC implementation was cut due to PyQt conflict,
# from abc import ABC, abstractmethod
# class MainThreadDispatcher(ABC):
# @abstractmethod
# def run_on_main_thread(self, func, *args, **kwargs):
# """Schedules the given function to run on the main UI thread."""
# pass

View file

@ -1,21 +1,15 @@
from dataclasses import dataclass
from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class BaseConnection:
code: str
# Called by: ConnectionController
# it uses it for basic type checks on wireguard code
# called by: connection controller for basic type checks on wireguard code
def needs_wireguard_configuration(self):
return self.code == 'wireguard'
# Called By SubscriptionPlan
def is_session_connection(self):
return type(self).__name__ == 'SessionConnection'
# Not called. Dead code
def is_system_connection(self):
return type(self).__name__ == 'SystemConnection'
def needs_operator_proxy(self):
return False

View file

@ -1,6 +1,4 @@
from core.models.manage.session_management import get_session
from core.errors.logger import logger
from sqlalchemy import select
from sqlalchemy.orm import joinedload
@ -8,22 +6,13 @@ from abc import ABC, abstractmethod
from core.Constants import Constants
from core.Helpers import write_atomically
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.models.Location import Location
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.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 core.models.session.ApplicationVersion import ApplicationVersion
from dataclasses import dataclass, field, asdict
from dataclasses_json import config, Exclude, dataclass_json
@ -34,39 +23,7 @@ import os
import re
import shutil
import tempfile
from sqlalchemy.orm import Session
from enum import Enum
import inspect
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
@ -76,7 +33,6 @@ class BaseProfile(ABC):
)
name: str
subscription: Optional[Subscription]
type: ProfileType
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.
@ -108,52 +64,12 @@ class BaseProfile(ABC):
def is_system_profile(self):
return type(self).__name__ == 'SystemProfile'
def save(self: Self, app_version_dict: dict = None):
"""
First warn via the logs, if the caller is bypassing the Cache and accessing it directly.
Then persist changes in the profile/model to disk.
"""
#--------------------------
# Cache Drift Protection
#--------------------------
# 1. Get the call stack
# stack[0] is this current 'save' method
# stack[1] is the immediate caller
stack = inspect.stack()
caller_frame = stack[1]
caller_function_name = caller_frame.function
caller_module_name = caller_frame.filename # Full path to the file
# 2. Only the Repo or Controller is allowed to save
allowed_callers = ['create', 'update_cache']
is_allowed_file = "core/controllers/ProfileController.py" in caller_module_name or "core/services/profiles/ProfileRepository.py" in caller_module_name
if caller_function_name not in allowed_callers and not is_allowed_file:
logger.warning(
f"ARCHITECTURE VIOLATION: .save() called directly from "
f"function '{caller_function_name}' in {caller_module_name}. "
f"Please route this through ProfileController to ensure cache integrity."
)
#--------------------------
# Actual Save
#--------------------------
# === SERIALIZATION ===
def save(self: Self):
config_dict = self.to_dict()
location_dict = self.location.to_dict() if self.location else None
# === 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:
@ -269,59 +185,54 @@ class BaseProfile(ABC):
profiles_location = profile['location']
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")
print("ERROR! CANT FIND country code or city")
return
else:
# potentially coming from SQLAlchemy ALREADY:
# potentially coming from SQLAlchemy:
country_code = profile.location.country_code
city_code = profile.location.code
# =========== GET DATA USING THAT COUNTRY & LOCATION ===========
location_dict = get_profile_location_data(country_code, city_code)
with get_session() as session:
location_object = session.execute(
select(Location)
.where((Location.country_code == country_code) & (Location.code == city_code))
.options(joinedload(Location.operator))
).scalar_one_or_none()
if location_dict:
profile['location'] = location_dict
if location_object:
profile['location'] = location_object
# this needs error handling if there's no location or malconformed config.
# =========== SESSION ===========
# 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
if 'application_version' in profile:
profile['type'] = ProfileType.SESSION
if profile['application_version'] is not None:
application_version = get_application_version(
profile['application_version']['application_code'] or None,
profile['application_version']['version_number'] or None
)
application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None)
if application_version is not None:
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
# noinspection PyUnresolvedReferences
profile = SessionProfile.from_dict(profile)
# =========== SYSTEM ===========
else:
profile['type'] = ProfileType.SYSTEM
from core.models.system.SystemProfile import SystemProfile
# noinspection PyUnresolvedReferences
@ -329,7 +240,6 @@ class BaseProfile(ABC):
return profile
@staticmethod
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')
@ -362,15 +272,3 @@ class BaseProfile(ABC):
@staticmethod
def __get_data_path(id: int):
return f'{Constants.HV_PROFILE_DATA_HOME}/{str(id)}'
# legacy phased out since SQLAlchemy handles the time_zone.
# if location is not None:
# if profile['location'].get('time_zone') is not None:
# location.time_zone = profile['location']['time_zone']
# profile['location'] = location

View file

@ -1,109 +1,63 @@
from core.errors.logger import logger
from core.Constants import Constants
from core.Helpers import write_atomically
#######################
from enum import Enum
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
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
from typing import Optional, Self, ClassVar
import dataclasses_json
import json
import os
import sys
class ConnectionChoice(str, Enum):
TOR = "tor"
SYSTEM = "system"
class Configuration(BaseModel):
model_config = ConfigDict(
extra='ignore',
exclude_none=True
@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
)
)
_instance: ClassVar[Optional[Self]] = None
connection: Optional[ConnectionChoice] = None
auto_sync_enabled: Optional[bool] = None
endpoint_verification_enabled: Optional[bool] = False
last_synced_at: Optional[datetime] = None
firewall: Optional[bool] = False
dns: Optional[bool] = False
did_sudo_setup: Optional[bool] = False
singbox: Optional[str] = None
tickets_setup: Optional[bool] = False
use_random: Optional[bool] = False
allow_port_prompts: Optional[bool] = True
def save(self: Self):
@field_validator('last_synced_at', mode='before')
@classmethod
def parse_datetime(cls, v):
if isinstance(v, str):
v = v.replace('Z', '+00:00') # Z → +00:00 for parsing
return v
@field_serializer('last_synced_at')
def serialize_datetime(self, value: datetime) -> str:
if value:
value = value.replace(tzinfo=ZoneInfo('UTC'))
return value.isoformat().replace('+00:00', 'Z') # +00:00 → Z for JSON
return None
@classmethod
def get(cls) -> Optional[Self]:
# Return the cached instance if it exists
if cls._instance is not None:
return cls._instance
try:
with open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r') as f:
config_file_contents = f.read()
except FileNotFoundError:
return None
try:
configuration_dict = json.loads(config_file_contents)
except ValueError:
sys.exit(1)
# Create the instance and cache it before returning
cls._instance = cls(**configuration_dict)
return cls._instance
def save(self) -> None:
# model_dump_json reflects the current state of this specific instance
config_file_contents = f'{self.model_dump_json(indent=4)}\n'
os.makedirs(Constants.HOME, exist_ok=True)
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():
def get_setting(looking_for: str):
# Use the singleton getter
config = Configuration.get()
if not config:
logger.error(f"[CONFIG] Can't load the entire config")
return None
# getattr because Pydantic models are objects, not dicts
result = getattr(config, looking_for, None)
if result is None:
logger.error(f"[CONFIG] What you want isn't in the config: {looking_for}")
return None
return result
def read_config():
"""
Returns it as a dict, not as a model.
"""
try:
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
except FileNotFoundError:
@ -112,6 +66,19 @@ def read_config():
try:
configuration = json.loads(config_file_contents)
except ValueError:
logger.error(f"[CONFIG] Can't load JSON config")
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)

View file

@ -1,55 +0,0 @@
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Any
class DBErrorType(Enum):
"""Classified error categories."""
SUCCESS = "success"
WRONG_DATA_FORMAT = "wrong_data_format"
NEED_MIGRATION = "need_migration"
MIGRATION_FAILED = "migration_failed"
OLD_CLIENT_NEW_API = "old_client_new_api"
MALFORMED_SQL = "malformed_sql_file"
MISSING_SQL = "missing_sql_file"
MISSING_DEPENDENCY = "missing_dependency"
INTEGRITY_ERROR = "integrity_error"
PERMISSION_ERROR = "permission_error"
FILESYSTEM_FULL = "filesystem_full"
DATABASE_LOCKED = "database_locked"
CORRUPTED_DATABASE = "corrupted_database"
PYTHON_MODEL_STRUCTURE = "python_model_structure"
SQLALCHEMY_ERROR = "sqlalchemy_error"
UNKNOWN_MODEL = "unknown_model"
UNKNOWN = "unknown"
@dataclass
class DatabaseOperation:
valid: bool
error_type: DBErrorType = DBErrorType.SUCCESS
data: Optional[Any] = None
message: Optional[str] = None
tried_migration: bool = False
tried_filtered: bool = False
which_table: str = None
def is_recoverable(self) -> bool:
"""Can the caller attempt a retry or manual fix?"""
return self.db_error_type in {
DBErrorType.OLD_CLIENT_NEW_API,
DBErrorType.NEED_MIGRATION
}
def user_message(self) -> str:
"""Human-readable error for the UI."""
messages = {
DBErrorType.SUCCESS: "Operation completed successfully.",
DBErrorType.NEED_MIGRATION: "Database schema needs update. Contact admin.",
DBErrorType.OLD_CLIENT_NEW_API: "Client version incompatible. Please upgrade.",
DBErrorType.MALFORMED_SQL: "Migration file is corrupted. Contact admin.",
DBErrorType.MISSING_SQL: "Migration file missing. Contact admin.",
DBErrorType.MISSING_DEPENDENCY: "Database dependency missing. Contact admin.",
DBErrorType.UNKNOWN: f"Unexpected error: {self.message}",
}
return messages.get(self.error_type, "Unknown error")

View file

@ -0,0 +1,22 @@
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
server_ip: Optional[str] = None # pre-resolved IP — avoids DNS leak at connect time
location_country_code: Optional[str] = None
location_city_code: Optional[str] = None

View file

@ -1,83 +0,0 @@
from core.services.networking.api_requests.ApiResponseModel import BackoffStrategy
from core.errors.ErrorStack import ErrorStack
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Any
from functools import partial
class ResultError(Enum):
"""Classified error categories."""
SUCCESS = "success"
NOT_SUPPORTED = "NOT_SUPPORTED"
INVALID_INPUT = "invalid_input"
MISSING_FILE = "missing_file"
MISSING_DEPENDENCY = "missing_dependency"
MISSING_DATA = "missing_data"
MISSING_SOFTWARE = "missing_software"
FILE_SYSTEM = "filesystem"
CONNECTION = "connection"
DATABASE = "database"
PERMISSION = "permission"
SUBSCRIPTION = "subscription"
EXPIRED_INVOICE = "expired_invoice" # meant for invoice
UNPAID = "unpaid"
PAID = "paid" # this is in-case it was expected to be unpaid
EXPIRED_SUB = "expired_sub"
ACTIVE_BILLING = "active_billing"
BILLING_CONFLICT = "billing_conflict"
STATE_CONFLICT = "state_conflict"
ZOMBIE_CONNECTION = "zombie_connection"
TICKET = "ticket"
NO_TICKET = "no_ticket"
RANDOM_TICKETS_OFF = "random_tickets_off"
TICKET_SETUP = "ticket_setup"
TICKET_EXPIRED = "ticket_expired"
ALREADY_EXISTS = "already_exists"
BILLING_CODE_EXISTS = "billing_code_exists"
PROCESS_GOT_KILLED = "process_got_killed"
PROCESS_WONT_START = "process_wont_start"
PROCESS_MISMATCH = "process_mismatch"
NMCLI = "nmcli_issues"
FIREWALL = "firewall"
CLIENT_DNS = "client_dns"
EXTERNAL_DNS = "external_dns"
INTERFACE = "interface"
TIMEOUT = "timeout"
INVALID_API_REPLY = "invalid_api_reply"
ENCRYPTION = "encryption"
ENDPOINT_VERIFICATION = "endpoint_verification"
LEAK_ISSUE = "leak_issue"
NEED_SYNC = "need_sync"
WRONG_LOCATION = "wrong_location"
SERVER_SIDE = "server_side"
MTU = "mtu"
TOR_NEEDED = "tor_needed"
API_DOWN = "api_down"
NODE_ERROR = "node_error"
NO_INTERNET = "no_internet"
PORT = "port"
NAVIGATE_TO = "navigate_to"
UNKNOWN = "unknown"
@dataclass
class Result():
valid: bool
goal_result: Optional[bool] = None
error_type: ResultError = ResultError.SUCCESS
error_stack: Optional[ErrorStack] = None
data: Optional[Any] = None
message: Optional[str] = None
goal: Optional[str] = None
backoff_strategy: Optional[BackoffStrategy] = None
function_choice: Optional[partial] = None
no_option: Optional[partial] = None
navigate_to: Optional[str] = None
# def user_message(self) -> str:
# """Human-readable error for the UI."""
# messages = {
# ResultError.SUCCESS: "Operation completed successfully.",
# ResultError.UNKNOWN: f"Error: {self.message}",
# }
# return messages.get(self.error_type, "Unknown error")

View file

@ -10,6 +10,13 @@ import dataclasses_json
@dataclass
class Subscription:
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(
default=None,
metadata=config(
@ -20,7 +27,6 @@ class Subscription:
exclude=lambda value: value is None
)
)
billing_pending: bool = False
def get_sanitized_billing_code(self):
return '-'.join(['****'] * 3 + [self.billing_code[-4:]])

View file

@ -53,6 +53,9 @@ class SubscriptionPlan(Model):
if connection.code == 'wireguard':
features_wireguard = True
if connection.code in ('operator', 'vless', 'hysteria2'):
features_proxy = True
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
return Model._query_one('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ? AND duration = ? LIMIT 1', SubscriptionPlan.factory, [features_proxy, features_wireguard, duration])
@ -76,6 +79,9 @@ class SubscriptionPlan(Model):
if connection.code == 'wireguard':
features_wireguard = True
if connection.code in ('operator', 'vless', 'hysteria2'):
features_proxy = True
return Model._query_all('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ?', SubscriptionPlan.factory, [features_proxy, features_wireguard])
@staticmethod

View file

@ -1,41 +0,0 @@
from enum import Enum
from typing import Type
class ProtocolChoice(Enum):
WIREGUARD = "wireguard"
HYSTERIA2 = "hysteria2"
VLESS = "vless"
SOCK5_PROXY = "proxy"
# MASKED_WIREGUARD = "masked_wireguard"
SINGBOX_PROTOCOLS = [ProtocolChoice.HYSTERIA2, ProtocolChoice.VLESS]
class Currency(Enum):
MONERO = "monero"
BITCOIN = "bitcoin"
LITECOIN = "litecoin"
LIGHTNING = "lightning"
class PaymentType(Enum):
CRYPTO = "crypto"
TICKET = "ticket"
FREE = "free"
class PaymentCategory(Enum):
INDIVIDUAL = "individual"
TICKET = "ticket"
def is_valid_protocol(potential_candidate: str) -> bool:
return potential_candidate in [choice.value for choice in ProtocolChoice]
def get_choice_list(model: Type[Enum]) -> list:
"""Gets a list of tuple/name for any Enum model"""
return [(e.value, e.name) for e in model]
def valid_enum(potential_candidate: str, enum_model: Type[Enum]) -> bool:
return potential_candidate in [choice.value for choice in enum_model]

View file

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

View file

@ -1,97 +0,0 @@
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.wrapper import safe_db_operation
from core.errors.logger import logger
from typing import Type
from sqlalchemy.orm import Session
@safe_db_operation
def _drop_sql_model_wrapped(model_class: Type, session=None) -> DatabaseOperation:
"""
Purpose:
Drop the entire table
Confusion:
It gets the session from the wrapper
The wrapper converts the returning dictionary into DatabaseOperation.data
"""
model_class.__table__.drop(bind=session.bind)
session.commit()
return {
'dropped': model_class.__name__,
'action': 'drop_fallback'
}
@safe_db_operation
def _clear_sql_model_wrapped(model_class: Type, session=None, **filters) -> DatabaseOperation:
"""
Delete all records from a SQLAlchemy model table, optionally filtered.
Preserves the table structure.
Args:
model_class: The SQLAlchemy model class to clear
session: Database session (injected by @safe_db_operation decorator)
**filters: Optional column=value filters for selective deletion
e.g., clear_sql_model(User, status='inactive')
Returns:
DatabaseOperation with deletion count and model name in the .data
Raises:
AttributeError: If a filter column doesn't exist on the model
"""
query = session.query(model_class)
# Validate and apply filters
for column_name, value in filters.items():
if not hasattr(model_class, column_name):
raise AttributeError(
f"Model {model_class.__name__} has no column '{column_name}'"
)
column = getattr(model_class, column_name)
query = query.filter(column == value)
# Delete and commit
deleted_count = query.delete(synchronize_session=False)
session.commit()
return {
'deleted_rows': deleted_count,
'model': model_class.__name__,
'filters_applied': filters if filters else None
}
def clear_sql_model(model_class: Type, **filters) -> dict:
"""
Purpose:
Clear table; fallback to DROP if clear fails
Role:
Public handler that unwraps DatabaseOperation
"""
result = _clear_sql_model_wrapped(model_class, **filters)
if result.valid:
return result.data
else:
logger.warning(
f"Failed to clear {model_class.__name__} "
f"({result.error_type}), attempting DROP as fallback..."
)
drop_result = _drop_sql_model_wrapped(model_class)
if drop_result.valid:
return drop_result.data
else:
logger.error(
f"[CLEAR SQL MODEL] Both clear and drop failed for {model_class.__name__}: {drop_result.message}"
)
return False

View file

@ -53,10 +53,6 @@ def denormalize(data, mapping_file):
"""
value = extract_from_nested(each_item, field['path'])
# If nested path didn't work, try using the field name as a flat key
if value is None:
value = each_item.get(field['name'])
# Store the extracted value. If it wasn't there, then it's None
each_denormalized_piece[field['name']] = value

View file

@ -1,29 +1,19 @@
from core.errors.logger import logger
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from typing import Type, Dict, Any
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session
from core.errors.logger import logger
from core.models.manage.session_management import get_session
from core.models.orm_models.Base import BaseModel
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
# all models it knows how to do:
from core.models.orm_models.CachedSync import CachedSync
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.orm_models.Dependency import Dependency
from core.models.orm_models.EncryptedProxy import EncryptedProxy
# This is the public interface,
def insert_into_model(model_class: Type, all_data: dict | list, override=False) -> DatabaseOperation:
def insert_into_model(model_class: Type, all_data: dict | list, override=False) -> bool:
"""
Purpose:
Generic ORM insert for any model. Keeping it generic for reuse.
Args:
@ -32,78 +22,45 @@ def insert_into_model(model_class: Type, all_data: dict | list, override=False)
override: If True, wipe the table before inserting
Returns:
DatabaseOperation object with true/false
True if successful
"""
# Normalize to list for uniform handling
data_list = all_data if isinstance(all_data, list) else [all_data]
# Call the wrapped function with normalized data
return _wrapped_insert(model_class=model_class, data_list=data_list, override=override)
@safe_db_operation
def _wrapped_insert(model_class: Type, data_list: dict | list, session: Session, override=False) -> DatabaseOperation:
"""Insert items. Doesn't manage session. It's managed by the wrapper safe_db_operation"""
logger.info(f"All the public data preparing to be inserted is {all_data}")
with get_session() as session:
# Step 1: Wipe the table if override=True
if override:
logger.info(f"First, WIPING the pre-existing data for {model_class.__name__}. Are you sure you intended to completely delete the old data?")
session.query(model_class).delete()
session.commit()
logger.info(f"Starting insert for {model_class.__name__}")
# Get valid field names upfront
valid_fields = {col.name for col in model_class.__table__.columns}
# Filter data proactively
filtered_data_list = []
all_dropped_fields = set()
for each_json in data_list:
filtered_json = {k: v for k, v in each_json.items() if k in valid_fields}
filtered_data_list.append(filtered_json)
# Track dropped fields
dropped = set(each_json.keys()) - valid_fields
all_dropped_fields.update(dropped)
# Log if we dropped anything
if all_dropped_fields:
logger.info(f"Filtered unknown fields before insert: {all_dropped_fields}")
# Normalize to list for uniform handling
data_list = all_data if isinstance(all_data, list) else [all_data]
try:
for filtered_json in filtered_data_list:
instance = model_class(**filtered_json)
for each_json in data_list:
instance = model_class(**each_json)
session.add(instance)
session.commit()
logger.info(f"Completed! SQL Insertion Successful following the {model_class.__name__} Model’s Type Rules")
return True
logger.info("Finished wrapped insert!")
return DatabaseOperation(
valid=True,
tried_filtered=bool(all_dropped_fields),
message=f"Inserted successfully" + (f" after filtering {all_dropped_fields}" if all_dropped_fields else "")
)
except Exception as e:
# Unexpected error during insert
except TypeError as e:
print(f"TypeError caught: {e}")
session.rollback()
raise WrapperRollback(DatabaseOperation(
valid=False,
error_type=DBErrorType.UNKNOWN,
message=f"Wrapped Insert Failed: {str(e)}"
))
# =================== Isolation Test ======================
"""
Spoof Failure of an operational error to trigger/test a migration
"""
# from sqlalchemy import text
# # 1. Drop a column the model expects
# if model_class == Location:
# with get_session() as session:
# print("dropping provider_name from location..")
# session.execute(text("ALTER TABLE locations DROP COLUMN provider_name"))
# session.commit()
# print("dropped provider_name from location!!!")
# # 2. Now insert will trigger OperationalError
raise ValueError(f"Invalid fields for {model_class.__name__}: {e}")
except IntegrityError as e:
print(f"IntegrityError caught: {e.orig}")
session.rollback()
raise ValueError(f"Constraint violation: {e.orig}")
except SQLAlchemyError as e:
print(f"SQLAlchemyError caught: {e}")
session.rollback()
raise
except Exception as e:
print(f"Generic exception caught: {e}")
session.rollback()
raise e

View file

@ -1,150 +0,0 @@
# custom
from core.errors.logger import logger
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.Constants import Constants
from core.models.manage.session_management import create_ALL_tables
# from core.models.orm_models.Location import Location
# from core.models.orm_models.Operator import Operator
# from core.models.orm_models.CachedSync import CachedSync
# from core.models.orm_models.EncryptedProxy import EncryptedProxy
# generic
from pathlib import Path
from sqlalchemy import Column, Integer, String, create_engine, inspect, text
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.exc import OperationalError, DBAPIError
from sqlalchemy import inspect
from sqlalchemy import text
"""
Purpose:
Sync SQL database schema to current Python model definitions.
Situation Used:
Python Models have new values missing from the same SQL database
Metaphor:
This replaces something like alembic, by doing it directly
Confusion:
This may be confusing because we're using an ORM in general,
but doing migrations manually.
Why:
The reason is because alembic is good for servers, but not clients,
due to a large amount of boilerplate for migrations.
"""
def get_model_columns(model_class):
"""
Extract all columns directly from a model class.
Bypasses metadata, caching, and all SQLAlchemy indirection.
"""
try:
mapper = inspect(model_class)
columns = {}
for column in mapper.columns:
columns[column.name] = {
'type': str(column.type),
'nullable': column.nullable,
'default': column.default,
'primary_key': column.primary_key,
}
return columns
except Exception as e:
raise ValueError(f"Failed to inspect {model_class}: {e}")
def migrate_sql() -> DatabaseOperation:
from core.models.manage.session_management import engine
from core.models.orm_models.Base import BaseModel, Base
try:
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.orm_models.CachedSync import CachedSync
from core.models.orm_models.EncryptedProxy import EncryptedProxy
from core.models.orm_models.Dependency import Dependency
from core.models.orm_models.Plan import Plan
MODELS = [Location, Operator, CachedSync, EncryptedProxy, Dependency, Plan]
logger.info(f"[MIGRATION] Tables loaded")
except:
logger.error(f"[MIGRATION] Could not load models. Critical failure.")
changes_made = []
# Step 1: Extract column definitions directly from each model
model_schema = {}
for model_class in MODELS:
table_name = model_class.__tablename__
try:
model_schema[table_name] = get_model_columns(model_class)
except ValueError as e:
return DatabaseOperation(
valid=False,
error_type=DBErrorType.PYTHON_MODEL_STRUCTURE,
message=e
)
logger.info(f"[MIGRATION] Model '{table_name}': {list(model_schema[table_name].keys())}")
# Step 2: Compare to database schema
inspector = inspect(engine)
db_tables = set(inspector.get_table_names())
for table_name, expected_columns in model_schema.items():
if table_name not in db_tables:
changes_made.append(table_name)
logger.info(f"[MIGRATION] Table '{table_name}' is MISSING from database")
else:
db_columns = {col['name']: col for col in inspector.get_columns(table_name)}
for col_name, col_info in expected_columns.items():
if col_name not in db_columns:
changes_made.append(col_name)
logger.info(f"[MIGRATION] Column '{col_name}' is MISSING from table '{table_name}'")
# Step 3: Perform the actual migration
if changes_made:
logger.info("[MIGRATION] Changes detected. Running schema update...")
Base.metadata.create_all(engine)
try:
with engine.begin() as conn:
for table_name, expected_columns in model_schema.items():
inspector = inspect(engine)
if table_name in inspector.get_table_names():
db_columns = {col['name'] for col in inspector.get_columns(table_name)}
for col_name, col_info in expected_columns.items():
if col_name not in db_columns:
col_type = col_info['type']
nullable = "NULL" if col_info['nullable'] else "NOT NULL"
print(f"[MIGRATION] Adding column '{col_name}' to '{table_name}'")
conn.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {col_type} {nullable}"))
except (OperationalError, DBAPIError) as e:
logger.error(f"[MIGRATION] Failed: {str(e)}")
return DatabaseOperation(
valid=False,
error_type=DBErrorType.UNKNOWN,
message=f"Schema sync failed: {str(e)}"
)
logger.info(f"[MIGRATION] Completed! Schema synced successfully with these changes: {changes_made}")
return DatabaseOperation(
valid=True,
data=changes_made,
message=f"Synced these tables and columns: {'; '.join(changes_made)}"
)
else:
no_migration_needed = "Schema already matches models. No migration needed."
logger.info(f"[MIGRATION] {no_migration_needed}")
return DatabaseOperation(
valid=True,
message=no_migration_needed
)

View file

@ -1,97 +0,0 @@
from core.models.pydantic_models.HysteriaData import HysteriaData
from core.errors.logger import logger
# generic
import os
import json
from pathlib import Path
from pydantic import BaseModel
import subprocess
def save_to_sudo_folder(model: BaseModel, filepath: str) -> bool:
"""
Save model to a sudo-protected file using pkexec.
"""
try:
json_data = model.model_dump_json(indent=2)
parent_dir = str(Path(filepath).parent)
# Single pkexec call: mkdir + write
process = subprocess.Popen(
('pkexec', 'bash', '-c', f'mkdir -p "{parent_dir}" && tee "{filepath}"'),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate(input=json_data)
if process.returncode != 0:
logger.error(f"Error: {stderr}")
return False
logger.info(f"Saved to {filepath} (elevated)")
return True
except FileNotFoundError as e:
logger.error(f"Error: Parent directory doesn't exist: {filepath}")
return False
except PermissionError as e:
logger.error(f"Error: Permission denied writing to {filepath}")
return False
except OSError as e:
logger.error(f"Error: OS error (disk full?): {e}")
return False
except TypeError as e:
logger.error(f"Error: Model serialization failed (invalid type): {e}")
return False
except Exception as e:
logger.error(f"Error: Unexpected error saving {filepath}: {e}")
return False
def save(model: BaseModel, filepath: str) -> bool:
"""
Purpose:
Serialize a Pydantic model to JSON file WITHOUT sudo.
Returns:
Returns True on success,
False on failure.
"""
try:
logger.info(f"Saving to {filepath}")
# Ensure parent directory exists
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
with open(filepath, 'w') as f:
f.write(model.model_dump_json(indent=2))
return True
except FileNotFoundError as e:
logger.error(f"Error: Parent directory doesn't exist: {filepath}")
return False
except PermissionError as e:
logger.error(f"Error: Permission denied writing to {filepath}")
return False
except OSError as e:
logger.error(f"Error: OS error (disk full?): {e}")
return False
except TypeError as e:
logger.error(f"Error: Model serialization failed (invalid type): {e}")
return False
except Exception as e:
logger.error(f"Error: Unexpected error saving {filepath}: {e}")
return False

View file

@ -1,176 +0,0 @@
from core.Errors import InvalidSubscriptionError
from core.models.manage.validate_dataclass import validate_model
from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Result import Result, ResultError
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.observers.ConnectionObserver import ConnectionObserver
from core.services.networking.httpx import connect
from core.errors.logger import logger
from enum import Enum
from typing import Optional
class ApiCall(Enum):
GET = "get"
POST = "post"
# Translate Server-side to client-side errors in THIS CONTEXT,
TRANSLATION_DICT = {
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
ServerError.NODE_ERROR: ResultError.WRONG_LOCATION,
ServerError.CRYPTO_NODE: ResultError.SERVER_SIDE,
ServerError.INVALID: ResultError.INVALID_INPUT,
ServerError.MISSING_DATA: ResultError.INVALID_INPUT,
ServerError.UNPAID: ResultError.SUBSCRIPTION,
ServerError.SERVER_SIDE: ResultError.SERVER_SIDE,
ServerError.EXPIRED: ResultError.EXPIRED_INVOICE
}
def format_server_response(api_response: ApiResponse|dict, desired_type: str) -> Result:
print("Starting format_server_response conversion")
"""Turn an API response into a Result Object"""
# if it's in object format, then convert it,
if isinstance(api_response, ApiResponse):
raw_reply = api_response.data
elif isinstance(api_response, dict):
# otherwise take it as is:
raw_reply = api_response
elif isinstance(api_response, str):
# invalid format:
return invalid_server_format(error_msg=api_response)
else:
logger.error("Can't convert server response.")
return invalid_server_format()
validated_model, server_reply = validate_model(ServerReply, raw_reply)
if not validated_model:
logger.error(f"Invalid structure for ServerReply. The original raw reply was {raw_reply}")
return invalid_server_format(error_msg=server_reply)
# Failure:
if not server_reply.valid:
logger.error(f"invalid server reply: {server_reply.error_type}")
logger.error(f"invalid server reply: {server_reply.message}")
final_return = translate_server_failure(server_reply)
print(f"final_return is {final_return}")
return final_return # result object
print("Server claims the result was valid, let's check if it has the data type")
if not hasattr(server_reply, desired_type):
error_msg = "Developer Error: You're asking for a data type that isn't in the object!"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
data = getattr(server_reply, desired_type)
if data:
return Result(valid=True, data=data)
else:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Server Replied it was valid, but failed to provide the data.")
def translate_server_failure(server_reply: ServerReply) -> Result:
"""Translate the server's failure to the UI"""
logger.error(f"Server-side failure occured. The server claims the reason is of type {server_reply.error_type} with {server_reply.message}")
client_error = TRANSLATION_DICT[server_reply.error_type]
return Result(valid=False, error_type=client_error, message=server_reply.message)
def invalid_server_format(error_msg: Optional[str] = None) -> Result:
if error_msg:
error_msg = f"The server gave an invalid response format {error_msg}."
else:
error_msg = "The server gave an invalid response format."
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg)
def api_call_and_extraction(
desired_type: Optional[str],
method: ApiCall,
url: str,
connection_observer: ConnectionObserver,
payload: dict,
billing_code: Optional[str] = None,
return_server_reply: bool = False,
) -> Result:
"""Do an API call, extract the data, and if there's an error, then translate the server's reply to a Result Object"""
#-----------------------------
# SEND TO API
#-----------------------------
api_response = connect.single_endpoint(
method=method.value,
url=url,
observer=connection_observer,
payload=payload,
billing_code=billing_code
)
if not api_response:
error_msg = "Complete failure of the function to produce a valid api response"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
print(f"api_response is {api_response}")
print(f"api_response is {api_response.data}")
if not api_response.valid and not api_response.error_type in (ErrorType.AUTHENTICATION_ERROR, ErrorType.INVALID_REQUEST):
# legacy system
if api_response.error_type == ErrorType.AUTHENTICATION_ERROR:
logger.error(f"api_response is {api_response}")
logger.error(f"Server is giving an Authentication error for billing ID: {billing_code}")
raise InvalidSubscriptionError(f"Invalid Subscription for {billing_code}")
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
pre_validated_reply = api_response.data
logger.info(f"Validating the Validity of the Server's Reply {pre_validated_reply}")
worked, server_reply = validate_model(ServerReply, pre_validated_reply)
error_msg = f"Validation of the Server's Reply had issues! Pre-validate: {pre_validated_reply}. Post validate: {server_reply}"
logger.info("Checking if validation worked..")
if not worked:
logger.error(error_msg)
return invalid_server_format()
if not server_reply:
logger.error(error_msg)
return invalid_server_format()
# Opt into getting the ServerReply object (instead of Result object):
if return_server_reply and desired_type is None:
logger.info("Validation successfull. Returning a raw ServerReply object")
return server_reply
logger.info("Validation successfull. Checking if the server's reply says the operation worked..")
if not server_reply.valid:
error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {server_reply}"
logger.error(error_message)
# translate by enum error code:
return translate_server_failure(server_reply)
logger.info(f"Checking for the desired data type of {desired_type}")
if not hasattr(server_reply, desired_type):
error_msg = "Developer Error: You're asking for a data type that isn't in the object!"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
print(f"right before check, server_reply is {server_reply} and desired_type is {desired_type}")
data = getattr(server_reply, desired_type)
logger.info(f"Successfully returning the data of {data}")
return Result(valid=True, data=data)

View file

@ -1,16 +1,10 @@
from core.errors.logger import logger
from core.models.orm_models.Base import BaseModel
from core.models.manage.version_check import insert_new_version
from core.Constants import Constants
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker
import os
from pathlib import Path
import sys
"""
Note:
At the bottom it initializes the Session and sets it up outside the function loose.
@ -59,8 +53,6 @@ def _reinitialize_engine_and_session():
Session = sessionmaker(bind=engine)
def init_session():
"""Initialize the global _session from the global Session factory."""
global _session
@ -94,38 +86,13 @@ def create_ONLY_db_version_table():
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
from core.models.orm_models.DatabaseVersion import database_version
try:
database_version.create(engine, checkfirst=True)
logger.info("[DB MANAGEMENT] database_version table created.")
database_version.create(engine)
except:
logger.info("[DB MANAGEMENT] database_version already exists, but was attempted to be made again by calling create_ONLY_db_version_table.")
# CRITICAL FIX: Populate the table immediately
session = get_session()
existing_row = session.query(database_version).first()
if not existing_row:
logger.info("[DB MANAGEMENT] Table was empty. Initializing with current app version.")
insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
else:
logger.info("[DB MANAGEMENT] Table already has a version row.")
except Exception as e:
logger.error(f"[DB MANAGEMENT] Failed to create/initialize database_version table: {e}")
# def create_ONLY_db_version_table():
# """Create only the database_version table using the global engine."""
# if engine is None:
# raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
# from core.models.orm_models.DatabaseVersion import database_version
# try:
# database_version.create(engine)
# except:
# logger.info("[DB MANAGEMENT] database_version already exists, but was attempted to be made again by calling create_ONLY_db_version_table.")
# # if using without checking or try except blocks:
# # database_version.create(engine, checkfirst=True)
# if using without checking or try except blocks:
# database_version.create(engine, checkfirst=True)
def does_db_version_table_exist():
@ -146,16 +113,12 @@ def create_ALL_tables():
from core.models.orm_models.Operator import Operator
from core.models.orm_models.CachedSync import CachedSync
from core.models.orm_models.EncryptedProxy import EncryptedProxy
from core.models.orm_models.Plan import Plan
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.orm_models.Dependency import Dependency
# legacy:
from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.session.ApplicationVersion import ApplicationVersion
try:
BaseModel.metadata.create_all(engine, checkfirst=True)
logger.info("[DB MANAGEMENT] All Tables have been successfully created.")
print("created all tables")
return True
except:
logger.error("[DB MANAGEMENT] Fatal Error with creating all tables in the create_ALL_tables function of session management.")
@ -167,6 +130,6 @@ def create_ALL_tables():
# ============================================================================
_reinitialize_engine_and_session()
# logger.info("[DB MANAGEMENT] Started Engine in Session Management Module..")
logger.info("[DB MANAGEMENT] Started Engine in Session Management Module..")
init_session()
# logger.info("[DB MANAGEMENT] Initialized Session")
logger.info("[DB MANAGEMENT] Initialized Session")

View file

@ -1,70 +0,0 @@
from core.errors.logger import logger
from dataclasses import dataclass, fields
from dataclasses_json import dataclass_json
from typing import Any, Type, TypeVar
from datetime import datetime, date, time
import json
def validate_field_type(field_name: str, value: Any, expected_type: Type):
"""Reusable validator to check if a value matches the expected type."""
if not isinstance(value, expected_type):
raise TypeError(
f"Field '{field_name}' expected type {expected_type.__name__}, "
f"but got {type(value).__name__}."
)
def validate_types(cls):
"""Wrapper / Class decorator that injects type validation into __post_init__."""
original_post_init = getattr(cls, "__post_init__", None)
def __post_init__(self):
for field in fields(self):
value = getattr(self, field.name)
validate_field_type(field.name, value, field.type)
if original_post_init:
original_post_init(self)
cls.__post_init__ = __post_init__
return cls
# TypeVar so that the editor knows the function returns an instance of whatever model_type is passed in.
T = TypeVar('T')
def validate_model(model_type: Type[T], data: dict|str) -> tuple:
"""Utility Conversion tool for the wrapper, to be called to convert."""
try:
if isinstance(data, dict):
final_result = model_type.from_dict(data)
elif isinstance(data, str):
final_result = model_type.from_json(data)
else:
error_msg = f"Invalid data format of {type(data)}"
logger.error(error_msg)
final_result = None
return False, error_msg
return True, final_result
except (TypeError, ValueError) as e:
# TypeError comes from the @validate_types decorator
# ValueError can come from dataclasses-json if the JSON is malformed
error_msg = f"Validation Error: {e}"
logger.error(error_msg)
return False, error_msg
class ValidatedDataclass:
"""Mixin that provides validate_payload validation for dataclasses."""
def validate_payload(self) -> bool:
for field in fields(self):
value = getattr(self, field.name)
try:
validate_field_type(field.name, value, field.type)
except TypeError as e:
logger.error(f"Failed to validate type {field.name} with value {value}")
return False
logger.info("Entire payload looks fine.")
return True

View file

@ -2,6 +2,7 @@ from core.models.orm_models.DatabaseVersion import database_version
from core.Constants import Constants
from core.errors.logger import logger
# generic
from sqlalchemy import exc, text
from sqlalchemy.orm import Session
@ -71,7 +72,7 @@ def get_database_version(session: Session) -> Optional[int]:
"This may indicate corrupted data."
)
# logger.info(f"{func_name}: Successfully retrieved version: {version}")
logger.info(f"{func_name}: Successfully retrieved version: {version}")
return version
except exc.NoSuchTableError:
@ -270,10 +271,9 @@ def get_custom_message(reason: str, compatability_dict: dict) -> str:
GUI's startup script in __main__
"""
if reason == "upgrade":
custom_error = "You just upgraded HydraVeil, so let's upgrade your database to handle the new data types. This will NOT harm your existing profiles or browser sessions."
custom_error = "You just upgraded HydraVeil, so let's upgrade your database to handle the new data types."
elif reason == "old_app":
custom_error = "Upgrade Time! You're using an old version of HydraVeil, with a newer version of the database. This means the older app would crash trying to handle the newer data. This will NOT harm your existing profiles or browser sessions."
custom_error = "You're using an old version of HydraVeil, with a newer version of the database. This means the older app would crash trying to handle the newer data."
else:
error_msg = compatability_dict.get("error", "Unknown reason.")
custom_error = f"There was an error with upgrading your database version. Please tell customer support: {error_msg}."
@ -346,110 +346,3 @@ def check_database_compatibility(session: Session) -> dict:
"old_db_version": db_version_you_have,
"error": "unknown result"
}
# DEAD CODE. Not used. But COULD be in the future.
def delete_sync_metadata(session: Session) -> bool:
"""
Called By:
Nobody, dead code. I originally was going to use this but cut it's use.
Purpose:
Clears existing rows of metadata
Rank:
Helper
Args:
session: SQLAlchemy session
Returns:
bool: True if successful, False otherwise.
does NOT raise errors.
"""
func_name = "delete_sync_metadata" # for logging purposes
# Validate session
if session is None:
logger.error(f"{func_name}: Session is None")
return False
if not isinstance(session, Session):
logger.error(f"{func_name}: Invalid session type: {type(session)}")
return False
try:
logger.debug(f"[{func_name}]: Starting transaction for metatable entry delete")
# Check if table exists before proceeding
try:
session.query(database_version).limit(1).all()
except exc.NoSuchTableError:
logger.info(
f"{func_name}: Table 'database_version' does not exist, this is good because we wanted to clear it."
)
return True # TRUE! this is great that it's gone already.
# Delete existing rows
logger.debug(f"{func_name}: Deleting existing version rows..")
deleted_count = session.query(database_version).delete()
logger.debug(f"{func_name}: Deleted {deleted_count} existing row(s)")
if deleted_count > 1:
logger.warning(
f"{func_name}: Deleted {deleted_count} rows. "
"Table should contain only a single row at any time."
)
# Commit transaction
session.commit()
return True
except exc.IntegrityError as e:
logger.error(
f"{func_name}: Integrity constraint violation (e.g., primary key conflict). "
f"Details: {str(e)}"
)
session.rollback()
return False
except exc.OperationalError as e:
logger.info(
f"{func_name}: Sync metadata never existed. "
f"Details: {str(e)}"
)
session.rollback()
return True # this is good
except exc.DatabaseError as e:
logger.error(
f"{func_name}: General database error. "
f"Details: {str(e)}"
)
session.rollback()
return False
except exc.StatementError as e:
logger.error(
f"{func_name}: SQL statement error. "
f"Details: {str(e)}"
)
session.rollback()
return False
except ValueError as e:
logger.error(
f"{func_name}: Value error during metadata deletion."
f"Details: {str(e)}"
)
session.rollback()
return False
except Exception as e:
logger.critical(
f"{func_name}: Unexpected exception type {type(e).__name__}. "
f"Details: {str(e)}",
exc_info=True
)
session.rollback()
return False

View file

@ -1,123 +0,0 @@
# custom
from core.errors.logger import logger
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.migrations import migrate_sql
from core.models.manage.session_management import get_session
from core.Constants import Constants
# generic
from sqlalchemy.orm import Session
from sqlalchemy.exc import (
OperationalError,
IntegrityError,
SQLAlchemyError
)
from typing import Type, Dict, Any, Callable, TypeVar
from functools import wraps
from enum import Enum
import traceback
"""
Purpose:
This is a wrapper for other SQL functions to catch errors and do solutions/fixes.
Used/Called by:
insert_into_model
Returns:
DatabaseOperation Objects
"""
def safe_db_operation(func):
def wrapper(*args, **kwargs):
# get the Session for the function it's wrapping,
session = get_session()
try:
# passes the session to the function,
result = func(*args, session=session, **kwargs)
# If func already returns DatabaseOperation, pass it through
if isinstance(result, DatabaseOperation):
return result
# Otherwise, wrap success
return DatabaseOperation(valid=True, data=result)
except OperationalError as e:
logger.error(f"Schema mismatch in {func.__name__}: {e}")
session.rollback()
try:
logger.info("Attempting migration...")
migrate_sql()
# try again:
result = func(*args, session=session, **kwargs)
return DatabaseOperation(
valid=True,
data=result,
tried_migration=True,
message="Recovered via migration"
)
except Exception as retry_error: # migration failed:
logger.error(f"Migration failed with error: {str(retry_error)}", exc_info=True)
session.rollback()
return DatabaseOperation(
valid=False,
error_type=DBErrorType.MIGRATION_FAILED,
message=str(retry_error)
)
# This is really TypeErrors, passed from the insert function.
except WrapperRollback as e:
session.rollback()
return e.database_operation
except IntegrityError as e:
error_msg = f"IntegrityError caught: {e.orig}"
logger.error(error_msg)
session.rollback()
return DatabaseOperation(
valid=False,
error_type=DBErrorType.INTEGRITY_ERROR,
message=error_msg
)
except SQLAlchemyError as e:
error_msg = f"SQLAlchemyError caught: {e}"
logger.error(error_msg)
session.rollback()
return DatabaseOperation(
valid=False,
error_type=DBErrorType.SQLALCHEMY_ERROR,
message=str(e)
)
except Exception as e:
logger.info("=" * 60)
logger.info(traceback.print_exc())
logger.info("=" * 60)
session.rollback()
return DatabaseOperation(
valid=False,
error_type=DBErrorType.UNKNOWN,
message=str(e)
)
return wrapper
"""
If a wrapped function needs to pass up an error to this wrapper,
so that the wrapper can roll back sessions,
Then it uses this error exception and passes the DatabaseOperation object.
"""
class WrapperRollback(Exception):
def __init__(self, database_operation: DatabaseOperation):
self.database_operation = database_operation

View file

@ -1,71 +0,0 @@
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.session.Application import Application
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.errors.logger import logger
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Optional
import random
@safe_db_operation
def execute_application_sql(application_code: str, version_number: str, session: Session) -> DatabaseOperation:
"""Query for a specific application version"""
data = session.query(ApplicationVersion).filter(
ApplicationVersion.application_code == application_code,
ApplicationVersion.version_number == version_number
).first()
return DatabaseOperation(valid=True, data=data)
def get_application_version(application_code: str, version_number: str) -> Optional[ApplicationVersion]:
database_object = execute_application_sql(application_code, version_number)
if database_object.valid:
return database_object.data
else:
logger.error(f"Critical Database error with fetching data for {application_code} and version {version_number}: {database_object.error_type} with {database_object.message}")
return None
@safe_db_operation
def execute_get_all(application: Optional[Application] = None, session: Session = None) -> DatabaseOperation:
query = select(ApplicationVersion)
if application is not None:
query = query.where(ApplicationVersion.application_code == application.code)
result = session.execute(query).scalars().all()
return DatabaseOperation(valid=True, data=result)
@safe_db_operation
def get_all_supported_apps(session: Session) -> DatabaseOperation:
"""Get all supported apps"""
data = session.query(ApplicationVersion).filter(
ApplicationVersion.format_revision == 2
).all()
return DatabaseOperation(valid=True, data=data)
def get_random_installed_app() -> Optional[ApplicationVersion]:
database_object = get_all_supported_apps()
if not database_object.valid:
logger.error(f"Critical Database error with fetching all supported apps. {database_object.error_type} with {database_object.message}")
return None
list_of_data = list(database_object.data)
while len(list_of_data) >= 1:
random_item = random.choice(list_of_data)
print(f"random_item is {random_item}")
if random_item.installed:
print("it is installed")
return random_item
else:
id_of_random_item = random_item.id
list_of_data.remove(random_item)
return None

View file

@ -1,24 +0,0 @@
from core.models.orm_models.Dependency import Dependency
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.errors.logger import logger
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Optional
@safe_db_operation
def execute_dependency_sql(name: str, session: Session) -> DatabaseOperation:
data = session.query(Dependency).filter(
Dependency.name == name
).first()
return DatabaseOperation(valid=True, data=data)
def get_dependency_version(name: str) -> Optional[Dependency]:
database_object = execute_dependency_sql(name)
if database_object.valid:
return database_object.data
else:
logger.error(f"Critical Database error with fetching data for {name} with {database_object.message}")
return None

View file

@ -1,61 +0,0 @@
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.errors.logger import logger
# generic
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session
from typing import Optional
import random
@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:
location_object = execute_location_sql(country_code, city_code)
if location_object.valid:
return location_object.data
else:
critical_error = f"Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}"
logger.error(critical_error)
print(critical_error)
return None
@safe_db_operation
def execute_all_locations(session: Session) -> DatabaseOperation:
return session.query(Location).all()
def get_random_location(filter_out_list: Optional[list] = None) -> Location:
location_object = execute_all_locations()
if not location_object.valid:
critical_error = f"Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}"
logger.error(critical_error)
return None
list_of_choices = location_object.data
while True:
random_pick = random.choice(list_of_choices)
# SPOOF: to force assassin to be a certain location, this can be implemented as a feature later:
# if random_pick.id == 4:
# return random_pick
if not filter_out_list:
return random_pick
if random_pick not in filter_out_list:
return random_pick

View file

@ -1,28 +0,0 @@
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.errors.logger import logger
# generic
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session
from typing import Optional, Type, TypeVar
T = TypeVar('T')
@safe_db_operation
def execute_get_all(model_type: Type[T], session: Session) -> DatabaseOperation:
return session.query(model_type).all()
def get_all_for_model(model_type: Type[T], filter_out_list: Optional[list] = None) -> Type[T]:
"""Return all entries for a given model, with potential filter."""
sql_object = execute_get_all(model_type)
if not sql_object.valid:
critical_error = f"Got invalid SQL Query which could not be solved by the wrapper, with error message {sql_object.message} and type {sql_object.error_type}"
logger.error(critical_error)
return None
return sql_object.data

View file

@ -1 +0,0 @@

View file

@ -1,79 +0,0 @@
from sqlalchemy.orm import Mapped
from core.models.orm_models.Base import BaseModel
from core.Constants import Constants
from functools import cached_property
from sqlalchemy import Column, Integer, String, Boolean, DateTime, UniqueConstraint, TypeDecorator
from sqlalchemy.orm import declarative_base
from datetime import datetime
from typing import Optional
import os
class ISODateTime(TypeDecorator):
impl = String
cache_ok = True
def process_bind_param(self, value, dialect):
if value is not None:
return value.isoformat() if isinstance(value, datetime) else value
return value
def process_result_value(self, value, dialect):
if value is not None:
return datetime.fromisoformat(value.replace('Z', '+00:00'))
return value
class ApplicationVersion(BaseModel):
__tablename__ = 'application_versions'
id = Column(Integer, primary_key=True)
application_code = Column(String, unique=False, nullable=False)
version_number = Column(String, unique=False, nullable=False)
format_revision = Column(Integer, nullable=True)
download_path = Column(String, unique=True, nullable=True)
# released_at = Column(DateTime, nullable=True)
released_at = Column(ISODateTime, nullable=True)
file_hash = Column(String, nullable=True)
__table_args__ = (
UniqueConstraint('application_code', 'version_number', name='uq_app_version'),
)
@property
def supported(self) -> bool:
"""Computed property: supported if format_revision is 2"""
return self.format_revision == 2
@property
def is_supported(self) -> bool:
return self.format_revision == 2
@cached_property
def installed(self) -> bool:
return self.is_installed()
def convert_to_dict(self) -> dict:
return {
"application_code": self.application_code,
"version_number": self.version_number
}
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 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())

View file

@ -16,18 +16,9 @@ class CachedSync(Base):
# version of the cached sync itself is the primary key
version: Mapped[int] = mapped_column(Integer, primary_key=True)
applications: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
application_versions: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
client_version: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
operators: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
locations: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
dependencies: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
# legacy:
subscriptions: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
# v2 refactor:
plans: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
applications: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)
application_versions: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)
client_version: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)
operators: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)
locations: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)
subscriptions: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None)

View file

@ -1,15 +0,0 @@
from sqlalchemy.orm import Mapped
from core.models.orm_models.Base import BaseModel
from core.Constants import Constants
from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint, TypeDecorator
from sqlalchemy.orm import declarative_base
class Dependency(BaseModel):
__tablename__ = 'dependencies'
id = Column(Integer, primary_key=True)
name = Column(String, unique=True, nullable=False)
version_number = Column(String, unique=False, nullable=False)
download_path = Column(String, unique=True, nullable=False)
file_hash = Column(String, nullable=False)

View file

@ -1,11 +1,11 @@
from sqlalchemy import Integer, String, ForeignKey, Boolean
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, mapped_column, Mapped, relationship
from typing import Optional
from sqlalchemy.orm import Mapped
from core.models.orm_models.Base import BaseModel
from core.models.orm_models.Operator import Operator
from core.models.enum_models import ProtocolChoice
class Location(BaseModel):
__tablename__ = 'locations'
@ -40,41 +40,20 @@ class Location(BaseModel):
)
provider_name: Mapped[Optional[str]] = mapped_column(String, nullable=True, default=None)
available: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None)
is_proxy_capable: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None)
is_wireguard_capable: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None)
is_hysteria2_capable: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None)
is_vless_capable: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None)
available: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
is_proxy_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
is_wireguard_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
is_hysteria2_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
is_vless_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
def convert_to_dict(self):
def to_dict(self):
return {
"country_code": self.country_code,
"code": self.code,
"time_zone": self.time_zone,
"location_id": self.id
"time_zone": self.time_zone
}
def supports_protocol(self, enum_value: ProtocolChoice|str) -> bool:
"""Check if a given location supports a given protocol"""
if enum_value is None:
return False
# socks5 proxy name doesn't match 'proxy'
if enum_value == ProtocolChoice.SOCK5_PROXY:
return self.is_proxy_capable
if isinstance(enum_value, ProtocolChoice):
string_version = enum_value.value
else:
string_version = enum_value
field_name = f'is_{string_version}_capable'
return bool(getattr(self, field_name))
# to use:
# Lookup by location/city codes
# record = session.get(Location, (country_code, code))

View file

@ -1,50 +0,0 @@
from core.models.enum_models import ProtocolChoice
from core.models.orm_models.Base import BaseModel
from core.Constants import Constants
from core.errors.logger import logger
import enum
from sqlalchemy.orm import Mapped
from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint, JSON, TypeDecorator, Numeric
from sqlalchemy.orm import declarative_base
class EnumList(TypeDecorator):
"""Stores a list of ProtocolChoice enums as a JSON list of strings."""
impl = JSON
def process_bind_param(self, value, dialect):
if value is not None:
# Create a set of all valid values
valid_values = {item.value for item in ProtocolChoice}
filtered = []
for item in value:
# 1. Get the raw value regardless of if it's an Enum or String
val = item.value if isinstance(item, enum.Enum) else item
# 2. Only keep it if it exists in our valid_values set
if val in valid_values:
filtered.append(val)
else:
logger.error(f"Dropped an invalid item: {val}")
pass
return filtered
return None
def process_result_value(self, value, dialect):
# Convert strings from DB back into enum members
if value is not None:
return [ProtocolChoice(item) for item in value]
return None
class Plan(BaseModel):
__tablename__ = 'plans'
id = Column(Integer, primary_key=True)
months = Column(Integer, unique=False, nullable=False)
supported_protocols = Column(EnumList, nullable=False)
cost = Column(Numeric(precision=10, scale=2), nullable=False)

View file

@ -1,62 +0,0 @@
# generic
from pydantic import BaseModel, field_validator, ValidationError, HttpUrl, ConfigDict, AnyUrl
from sqlalchemy.orm import Session
from pydantic import model_validator, ValidationInfo
from typing_extensions import Self
from ipaddress import IPv4Address
from pydantic_core import PydanticUndefinedType
import validators
class HysteriaData(BaseModel):
model_config = ConfigDict(extra="ignore")
username: str
password: str
hysteria2_host: str
server_ip: IPv4Address
@model_validator(mode='before')
@classmethod
def denormalize(cls, data):
"""This is IF its normalized, which it may not be if the server has the same model."""
if isinstance(data, dict) and 'operator' in data:
return {
**data,
'hysteria2_host': data['operator'].get('hysteria2_host'),
}
return data
@field_validator('hysteria2_host', mode='before')
@classmethod
def validate_domain(cls, v):
if isinstance(v, str):
if not validators.domain(v):
raise ValueError('Invalid domain')
return v
# 'operator_id': data['operator'].get('id'),
# 'operator_domain': data['operator'].get('domain'),
# location_country_code: str
# location_city_code: str
# operator_id: int
# # operator_name: str = None
# operator_domain: str = None
# likely future addition:
# # @field_validator('operator_id')
# # @classmethod
# # def validate_operator_exists(cls, v, info):
# # db = info.context.get('db')
# # if not db:
# # raise ValueError("Database session not provided")
# # operator = db.query(Operator).filter(Operator.id == v).first()
# # if not operator:
# # raise ValueError(f"Operator ID ID {v} does not exist")
# # return v

View file

@ -1,61 +0,0 @@
from core.models.pydantic_models.type_checker import validate_types
from core.models.enum_models import ProtocolChoice, Currency
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config
from datetime import datetime
from enum import Enum
from typing import Any, Optional
class ServerError(str, Enum):
"""Why the Server's Operation was False."""
NONE = "none"
DATABASE = "database"
EXPIRED = "expired"
MATH = "math"
QUANTITY = "quantity"
UNPAID = "unpaid"
INVALID = "invalid"
ALREADY_USED = "already_used"
MISSING_DATA = "missing_data"
WRONG_LOCATION = "wrong_location"
NODE_ERROR = "node_error"
CRYPTO_NODE = "crypto_node"
SERVER_SIDE = "server_side"
# @validate_types
@dataclass_json
@dataclass
class ServerReply():
"""Server-Client Communication Format."""
valid: bool
signed_data: Optional[list] = None
error_type: Optional[ServerError] = ServerError.NONE
message: Optional[str] = None
model_version: int = 1
data: Optional[dict] = None
boolean_result: Optional[bool] = None
conf: Optional[str] = None
payment_status: Optional[bool] = None
expires_at: Optional[datetime] = field(
default=None,
metadata=config(
decoder=lambda x: datetime.fromisoformat(x.replace('Z', '+00:00')) if x else None
)
)
@dataclass_json
@validate_types
@dataclass
class SubscriptionInvoice:
billing_code: str
expires_at: datetime = field(metadata=config(decoder=datetime.fromisoformat))
selected_currency: Currency
due_amount: float
address: str
payment_type: str

View file

@ -1,45 +0,0 @@
from pydantic import BaseModel, field_validator, ValidationError, HttpUrl, ConfigDict, AnyUrl
from sqlalchemy.orm import Session
from pydantic import model_validator, ValidationInfo
from typing_extensions import Self
from ipaddress import IPv4Address
from pydantic_core import PydanticUndefinedType
import validators
class VlessData(BaseModel):
model_config = ConfigDict(extra="ignore")
links: str
server_ip: IPv4Address
@field_validator('links', mode='before')
@classmethod
def validate_link(cls, v):
if isinstance(v, list):
v = v[0]
if not isinstance(v, str) or not v.startswith('vless://'):
raise ValueError('links must be a string starting with "vless://"')
return v
# username: str = None
# subscription_url: HttpUrl
# location_country_code: str
# location_city_code: str
# operator_id: int
# operator_domain: str = None # redundant
# vless_host: str = None # redundant
# @model_validator(mode='before')
# @classmethod
# def denormalize(cls, data):
# if isinstance(data, dict) and 'operator' in data:
# return {
# **data,
# # 'operator_id': data['operator'].get('id'),
# # 'operator_domain': data['operator'].get('domain'),
# # 'vless_host': data['operator'].get('vless_host'),
# }
# return data

View file

@ -1,52 +0,0 @@
from core.errors.logger import logger
from dataclasses import dataclass, fields
from dataclasses_json import dataclass_json
from typing import Any, Type, TypeVar
from datetime import datetime, date, time
import json
def validate_field_type(field_name: str, value: Any, expected_type: Type):
"""Reusable validator to check if a value matches the expected type."""
if not isinstance(value, expected_type):
raise TypeError(
f"Field '{field_name}' expected type {expected_type.__name__}, "
f"but got {type(value).__name__}."
)
def validate_types(cls):
"""Wrapper / Class decorator that injects type validation into __post_init__."""
original_post_init = getattr(cls, "__post_init__", None)
def __post_init__(self):
for field in fields(self):
value = getattr(self, field.name)
validate_field_type(field.name, value, field.type)
if original_post_init:
original_post_init(self)
cls.__post_init__ = __post_init__
return cls
# TypeVar so that the editor knows the function returns an instance of whatever model_type is passed in.
T = TypeVar('T')
def validate_model(model_type: Type[T], data: dict|str) -> T | None:
"""Utility Conversion tool for the wrapper, to be called to convert."""
try:
if isinstance(data, dict):
final_result = model_type.from_dict(data)
elif isinstance(data, str):
final_result = model_type.from_json(data)
else:
logger.error(f"Invalid data format of {type(data)}")
final_result = None
return final_result
except (TypeError, ValueError) as e:
# TypeError comes from the @validate_types decorator
# ValueError can come from dataclasses-json if the JSON is malformed
logger.error(f"Validation Error: {e}")
return None

View file

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

View file

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

View file

@ -6,47 +6,19 @@ class SessionConnection(BaseConnection):
masked: bool = False
def __post_init__(self):
if self.code not in ('system', 'tor', 'wireguard'):
if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'):
raise ValueError('Invalid connection code.')
# Called by: ConnectionController
# called by connection controller
# doesn't even make sense, it's checking if the Session is code system. this will always be false.
def is_unprotected(self):
return self.code == 'system' and self.masked is False
# Called by: SessionProfile.determine_timezone
def needs_proxy_configuration(self):
return self.masked is True
def needs_operator_proxy(self):
return self.code in ('vless', 'hysteria2')
# Potential refactor
# from dataclasses import dataclass, field
# from dataclasses_json import dataclass_json, config
# from enum import Enum
# class SessionConnectionTypes(str, Enum):
# WIREGUARD = "wireguard"
# TOR = "tor"
# @dataclass_json
# @dataclass
# class SessionConnection:
# code: SessionConnectionTypes = field(
# metadata=config(
# encoder=lambda x: x.value, # Convert enum to string on save
# decoder=lambda x: SessionConnectionTypes(x) # Convert string to enum on load
# )
# )
# masked: bool = False
# def __post_init__(self):
# # Convert string to enum for deserialization
# if isinstance(self.code, str):
# self.code = SessionConnectionTypes(self.code)
# # Validation
# if self.code not in SessionConnectionTypes:
# raise ValueError(f'Invalid code: {self.code}')
def get_protocol(self):
return self.code if self.needs_operator_proxy() else None

View file

@ -1,11 +1,7 @@
from core.Constants import Constants
from core.Errors import UnknownTimeZoneError
from core.models.BaseProfile import BaseProfile
# from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.session.ProxyConfiguration import ProxyConfiguration
from core.models.session.SessionConnection import SessionConnection
from dataclasses import dataclass
@ -20,48 +16,30 @@ import shutil
class SessionProfile(BaseProfile):
resolution: str
application_version: Optional[ApplicationVersion]
connection: Optional[SessionConnection] = None
ticket: Optional[int] = None
assassin: Optional[bool] = False
connection: Optional[SessionConnection]
def has_connection(self):
return self.connection is not None
def save(self):
print("We are able to trigger save on the child session")
if 'application_version' in self._get_dirty_keys():
persistent_state_path = f'{self.get_data_path()}/persistent-state'
if os.path.isdir(persistent_state_path):
shutil.rmtree(persistent_state_path, ignore_errors=True)
if 'location' in self._get_dirty_keys():
self.__delete_proxy_configuration()
self.delete_wireguard_configuration()
# === 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)
self.__delete_wireguard_configuration()
super().save()
def attach_proxy_configuration(self, proxy_configuration):
proxy_configuration_file_contents = f'{proxy_configuration.to_json(indent=4)}\n'
os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
proxy_configuration_file_path = self.get_proxy_configuration_path()
with open(proxy_configuration_file_path, 'w') as proxy_configuration_file:
proxy_configuration_file.write(proxy_configuration_file_contents)
def attach_wireguard_configuration(self, wireguard_configuration):
wireguard_configuration_file_path = self.get_wireguard_configuration_path()
with open(wireguard_configuration_file_path, 'w') as wireguard_configuration_file:
wireguard_configuration_file.write(wireguard_configuration)
@ -72,19 +50,15 @@ class SessionProfile(BaseProfile):
return f'{self.get_config_path()}/wg.conf'
def get_proxy_configuration(self):
try:
config_file_contents = open(self.get_proxy_configuration_path(), 'r').read()
except FileNotFoundError:
return None
try:
proxy_configuration = json.loads(config_file_contents)
except ValueError:
return None
proxy_configuration = ProxyConfiguration.from_dict(proxy_configuration)
return proxy_configuration
def has_proxy_configuration(self):
@ -94,36 +68,26 @@ class SessionProfile(BaseProfile):
return os.path.isfile(f'{self.get_config_path()}/wg.conf')
def address_security_incident(self):
super().address_security_incident()
self.delete_wireguard_configuration()
self.__delete_wireguard_configuration()
def determine_timezone(self):
time_zone = None
if self.has_connection():
if self.connection.needs_proxy_configuration():
if self.has_proxy_configuration():
time_zone = self.get_proxy_configuration().time_zone
elif self.connection.needs_wireguard_configuration():
if self.has_wireguard_configuration():
time_zone = self.get_wireguard_configuration_metadata('TZ')
if time_zone is None and self.has_location():
time_zone = self.location.time_zone
if time_zone is None:
raise UnknownTimeZoneError('The preferred time zone could not be determined.')
return time_zone
def __delete_proxy_configuration(self):
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)

View file

@ -1,29 +1,22 @@
from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Optional
# legacy:
@dataclass
class SystemConnection(BaseConnection):
operator_id: Optional[int] = field(default=None)
protocol: Optional[str] = field(default=None)
def __post_init__(self):
if self.code not in ('vless', 'hysteria2', 'wireguard'):
if self.code not in ('wireguard', 'operator', 'vless', 'hysteria2'):
raise ValueError('Invalid connection code.')
@staticmethod
def needs_proxy_configuration():
return False
# Potential refactor:
# from enum import Enum
# from typing import Literal
def needs_operator_proxy(self):
return self.code in ('operator', 'vless', 'hysteria2')
# class SystemConnectionTypes(str, Enum):
# WIREGUARD = "wireguard"
# HYSTERIA2 = "hysteria2"
# VLESS = "vless"
# @dataclass
# class SystemConnection(BaseConnection):
# code: SystemConnectionTypes
# masked: Literal[False] = False
def get_protocol(self):
return self.protocol if self.protocol else self.code

View file

@ -4,16 +4,14 @@ from core.models.BaseProfile import BaseProfile
from core.models.system.SystemConnection import SystemConnection
from dataclasses import dataclass
from typing import Optional
import json
import os
import shutil
import subprocess
@dataclass
class SystemProfile(BaseProfile):
connection: Optional[SystemConnection]
ticket: Optional[int] = None
assassin: Optional[bool] = False
def get_system_config_path(self):
filepath = self.__get_system_config_path(self.id)
@ -21,33 +19,23 @@ class SystemProfile(BaseProfile):
return filepath
def save(self):
if 'location' in self._get_dirty_keys():
self.__delete_wireguard_configuration()
super().save()
def attach_wireguard_configuration(self, wireguard_configuration):
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
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:
wireguard_configuration_file.write(wireguard_configuration)
wireguard_configuration_is_attached = False
failed_attempt_count = 0
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'))
wireguard_configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not wireguard_configuration_is_attached:
failed_attempt_count += 1
if not wireguard_configuration_is_attached:
raise ProfileModificationError('The WireGuard configuration could not be attached.')
@ -63,7 +51,6 @@ class SystemProfile(BaseProfile):
return False
def address_security_incident(self):
super().address_security_incident()
self.__delete_wireguard_configuration()
@ -72,7 +59,6 @@ class SystemProfile(BaseProfile):
self.__delete_wireguard_configuration()
except ProfileModificationError:
raise ProfileDeletionError('The WireGuard configuration could not be deleted.')
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
@ -86,16 +72,56 @@ class SystemProfile(BaseProfile):
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)
if operator_proxy_session.location_country_code and operator_proxy_session.location_city_code:
try:
from core.models.orm_models.Location import Location
from core.models.manage.session_management import get_session
from sqlalchemy import select
session = get_session()
loc = session.execute(
select(Location).where(
(Location.country_code == operator_proxy_session.location_country_code) &
(Location.code == operator_proxy_session.location_city_code)
)
).scalar_one_or_none()
if loc:
self.location = loc
self.save()
except Exception:
pass
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):
if self.has_wireguard_configuration():
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
try:
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_wireguard_configuration_path()), check=True)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
except subprocess.CalledProcessError as e:
completed_successfully = True
@ -109,29 +135,3 @@ class SystemProfile(BaseProfile):
def __get_system_config_path(id: int):
config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}'
return config_path
def attach_encrypted_proxy_config(self, config_data: dict):
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
backup_path = f'{self.get_config_path()}/proxy.conf.bak'
with open(backup_path, 'w') as configuration_file:
configuration_file.write(config_data)
configuration_is_attached = False
failed_attempt_count = 0
while not configuration_is_attached and failed_attempt_count < 3:
process = subprocess.Popen(('pkexec', 'install', '-D', backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744'))
configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not configuration_is_attached:
failed_attempt_count += 1
if not configuration_is_attached:
raise ProfileModificationError('The WireGuard configuration could not be attached.')

View file

@ -1,34 +1,22 @@
from core.models.enum_models import ProtocolChoice
from core.Constants import Constants
from core.utils.basic_operations.enum_tools import serialize_enums
# generic
from dataclasses import dataclass
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json
from pathlib import Path
from typing import Self
from typing import Optional, Self
import json
import os
import pathlib
from typing import Optional
@dataclass_json
@dataclass
class SystemState:
profile_id: int
firewalled: bool
dns_set: bool
protocol: ProtocolChoice
process_id: Optional[int] = None
session_token: Optional[str] = field(default=None)
pid: Optional[int] = field(default=None)
def save(self: Self):
data_as_dict = self.to_json(indent=4)
print(f"data_as_dict is {data_as_dict}")
data_without_enums = serialize_enums(data_as_dict)
print(f"data_without_enums is {data_without_enums}")
system_state_file_contents = f'{data_without_enums}\n'
system_state_file_contents = f'{self.to_json(indent=4)}\n'
os.makedirs(SystemState.__get_state_path(), exist_ok=True, mode=0o700)
system_state_file_path = f'{SystemState.__get_state_path()}/system.json'
@ -37,8 +25,6 @@ class SystemState:
with open(system_state_file_path, 'w') as system_state_file:
system_state_file.write(system_state_file_contents)
print("saved system state")
@staticmethod
def get():
@ -47,7 +33,6 @@ class SystemState:
system_state_file_contents = open(f'{SystemState.__get_state_path()}/system.json', 'r').read()
system_state_dict = json.loads(system_state_file_contents)
# noinspection PyUnresolvedReferences
return SystemState.from_dict(system_state_dict)
except (FileNotFoundError, ValueError, KeyError):
@ -70,16 +55,3 @@ class SystemState:
@staticmethod
def __get_state_path():
return Constants.HV_STATE_HOME
# @dataclass
# class StateEvaluation:
# interface: bool
# firewalled: bool
# dns_set: bool
# ip_address_correct: Optional[bool] = None
# firewall_killed_internet: Optional[bool] = None
# dns_killed_internet: Optional[bool] = None
# has_internet: Optional[bool] = None
# killed_vpn: Optional[bool] = None

View file

@ -4,9 +4,9 @@ from essentials.observers.BaseObserver import BaseObserver
class ClientObserver(BaseObserver):
def __init__(self):
pass
# self.on_synchronizing = []
# self.on_synchronized = []
# self.on_updating = []
# self.on_update_progressing = []
# self.on_updated = []
self.on_synchronizing = []
self.on_synchronized = []
self.on_updating = []
self.on_update_progressing = []
self.on_updated = []

View file

@ -1,5 +1,9 @@
from essentials.observers.ConnectionObserver import ConnectionObserver as BaseConnectionObserver
class ConnectionObserver(BaseConnectionObserver):
pass
def __init__(self):
super().__init__()
self.on_connected = []
self.on_disconnected = []
self.on_error = []

View file

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

View file

@ -1,19 +0,0 @@
class EventBus:
_subscribers = {}
@classmethod
def subscribe(cls, event_type, callback):
if event_type not in cls._subscribers:
cls._subscribers[event_type] = []
cls._subscribers[event_type].append(callback)
@classmethod
def emit(cls, event_type, data=None):
for callback in cls._subscribers.get(event_type, []):
callback(data)
@classmethod
def unsubscribe(cls, event_type, callback):
if event_type in cls._subscribers:
cls._subscribers[event_type].remove(callback)

View file

@ -1,31 +0,0 @@
from enum import Enum, auto
class EventType(Enum):
SESSION_PROFILE_ENABLE = auto()
SESSION_PROFILE_DISABLE = auto()
SYSTEM_ENABLE = auto()
SYSTEM_DISABLE = auto()
PROFILE_CREATED = auto()
PROFILE_DELETED = auto()
STATUS_UPDATE = auto()
SHOW_POPUP = auto()
RESUME_ACTIONS = auto()
class Notifications:
_subscribers = {}
@classmethod
def subscribe(cls, event_type: EventType, callback):
if event_type not in cls._subscribers:
cls._subscribers[event_type] = []
cls._subscribers[event_type].append(callback)
@classmethod
def emit(cls, event_type: EventType, data=None):
for callback in cls._subscribers.get(event_type, []):
callback(data)

View file

@ -4,9 +4,9 @@ from essentials.observers.BaseObserver import BaseObserver
class ProfileObserver(BaseObserver):
def __init__(self):
pass
# self.on_created = []
# self.on_updated = []
# self.on_destroyed = []
# self.on_disabled = []
# self.on_enabled = []
self.on_created = []
self.on_updated = []
self.on_destroyed = []
self.on_disabled = []
self.on_enabled = []

View file

@ -1,15 +1,14 @@
from core.Constants import Constants
from core.models.ClientVersion import ClientVersion
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
# from core.models.Location import Location # migrated to ORM
# from core.models.Operator import Operator # migrated to ORM
from core.models.OperatorProxySession import OperatorProxySession
from core.models.Subscription import Subscription
from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.invoice.Invoice import Invoice
from core.models.invoice.PaymentMethod import PaymentMethod
from core.models.session.Application import Application
# from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.session.ProxyConfiguration import ProxyConfiguration
from typing import Optional
import re
@ -20,12 +19,10 @@ class WebServiceApiService:
@staticmethod
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)
applications = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
for application in response.json()['data']:
applications.append(Application(application['code'], application['name'], application['id']))
@ -34,12 +31,10 @@ class WebServiceApiService:
@staticmethod
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)
application_versions = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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']))
@ -48,12 +43,10 @@ class WebServiceApiService:
@staticmethod
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)
client_versions = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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']))
@ -62,26 +55,22 @@ class WebServiceApiService:
@staticmethod
def get_operators(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/operators', None, proxies)
operators = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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
@staticmethod
def get_locations(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/locations', None, proxies)
locations = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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']))
@ -90,60 +79,57 @@ class WebServiceApiService:
@staticmethod
def get_subscription_plans(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/subscription-plans', None, proxies)
subscription_plans = []
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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']))
return subscription_plans
@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
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'])
body = {'subscription_plan_id': subscription_plan_id}
if operator_id is not None:
body['operator_id'] = operator_id
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
@staticmethod
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_fragments = re.findall('....?', billing_code)
billing_code = '-'.join(billing_code_fragments)
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']
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
@staticmethod
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)
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
response_data = response.json()['data']
@ -159,36 +145,58 @@ class WebServiceApiService:
return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods))
else:
return None
@staticmethod
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)
if response.status_code == status_codes.OK:
if 200 <= response.status_code < 300:
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'])
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'),
data.get('server_ip'),
data.get('location_country_code'),
data.get('location_city_code'),
)
return None
@staticmethod
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, {
'public_key': public_key,
}, proxies)
if response.status_code == status_codes.CREATED:
if 200 <= response.status_code < 300:
return response.text
else:
return None
@staticmethod
@ -201,7 +209,7 @@ class WebServiceApiService:
else:
headers = None
return requests.get(Constants.SP_API_BASE_URL + path, headers=headers, proxies=proxies)
return requests.get(Constants.SP_API_BASE_URL + path, headers=headers, proxies=proxies, timeout=30)
@staticmethod
def __post(path, billing_code: Optional[str] = None, body: Optional[dict] = None, proxies: Optional[dict] = None):
@ -213,16 +221,16 @@ class WebServiceApiService:
else:
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, timeout=30)
# @staticmethod
# def get_cached_sync(proxies: Optional[dict] = None):
@staticmethod
def get_cached_sync(proxies: Optional[dict] = None):
# from requests.status_codes import codes as status_codes
from requests.status_codes import codes as status_codes
# response = WebServiceApiService.__get('/cachedsync', None, proxies)
response = WebServiceApiService.__get('/cachedsync', None, proxies)
# if response.status_code == status_codes.OK:
# return response.json()
# else:
# return None
if response.status_code == status_codes.OK:
return response.json()
else:
return None

View file

@ -1,167 +0,0 @@
from core.models.orm_calls.application_version_calls import get_random_installed_app
from core.models.orm_calls.location_calls import get_random_location
from core.services.assassin.screen_size import pick_random_resolution
from core.services.assassin import ticket_respawn
from core.services.assassin.location_tools import get_systemwide_location
from core.controllers.tickets.UseTicketController import use_ticket
from core.models.Result import Result, ResultError
from core.models.session.SessionProfile import SessionProfile
from core.models.Subscription import Subscription
from core.models.BaseProfile import ProfileType
from core.models.orm_models.Location import Location
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.Errors import MissingSubscriptionError
from core.errors.logger import logger
def create(
profile: SessionProfile,
max_resolution: tuple,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
"""
Purpose:
Gets the requirements for an assassin profile, which is returned as an object.
Rank:
Feature's King Orchestrator
Called by:
ProfileController
"""
if not profile:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Requires a profile")
if not profile.assassin:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Requires an assassin profile")
# We print for the Assassin to avoid logs.
print(f"Begun creating Assassin for profile id {profile.id}")
# APPLICATION
random_app = get_random_installed_app()
print(f"random_app is {random_app}")
if not random_app:
return Result(valid=False, error_type=ResultError.MISSING_SOFTWARE, message="You need to install more browsers to have available choices for the Assassin. Please sync & download..")
# LOCATION
systemwide_location = get_systemwide_location()
filter_out_list = [systemwide_location]
random_location = get_random_location(filter_out_list=filter_out_list)
print(f"random_location is {random_location}")
if not random_app:
return Result(valid=False, error_type=ResultError.NEED_SYNC, message="You need to sync to get location choices for the Assassin.")
# SCREEN SIZE:
if not max_resolution:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Could not create a screen size without a max_resolution")
random_screen_size = pick_random_resolution(max_resolution)
print(f"random_screen_size is {random_screen_size}")
if not random_screen_size:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Could not create a screen size with the given inputs of {str(max_resolution)}")
# use underlying profile's connection:
assassins_connection = profile.connection
print(f"assassins_connection is {assassins_connection}")
# SETUP ASSASSIN:
assassin = SessionProfile(
id=profile.id,
name="assassin_ON",
type=ProfileType.SESSION,
location=random_location,
resolution=random_screen_size,
application_version=random_app,
connection=assassins_connection,
subscription=None,
assassin=True
)
# DELETE the pre-existing WG config. Even though this is a redundant duplicate of the turn off function,
# we don't know if assassin mode was added to a profile that already had a wg config for non-assassin use.
profile.delete_wireguard_configuration()
print("deleted the assassin's pre-existing wg config.")
# returns Result after using the sub:
return use_ticket_get_sub(
profile=assassin,
random_location=random_location,
connection_observer=connection_observer,
ticket_observer=ticket_observer
)
def use_ticket_get_sub(
profile: SessionProfile,
random_location: Location,
connection_observer: ConnectionObserver,
ticket_observer: TicketObserver
) -> Result:
print("using a ticket for the assassin")
# GET TICKET FROM LOCAL FILES
which_ticket = ticket_respawn.get_by_any_means(profile)
print(f"assassin's which_ticket is {which_ticket}")
if not which_ticket:
raise MissingSubscriptionError
# USE TICKET
print("using ticket..")
ticket_result = use_ticket(
which_ticket=which_ticket,
which_location=random_location.id,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
print(f"ticket_result is {ticket_result}")
valid = ticket_result.get("valid", False)
if not valid:
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY)
billing_code = ticket_result.get("billing_code", False)
if not billing_code:
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY)
assassins_subscription = Subscription(billing_code=billing_code)
profile.subscription = assassins_subscription
profile.ticket = which_ticket
from core.controllers.ProfileController import ProfileController
ProfileController.update(profile)
return Result(valid=True, data=profile)
def wipe(
profile: SessionProfile,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
####################################
# RESPAWN TICKET
####################################
print("respawning assassin")
respawned = ticket_respawn.respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not respawned:
logger.error("Serious issue with respawning the ticket!")
return Result(valid=False, error_type=ResultError.TICKET, message=f"Could NOT respawn the ticket for profile {profile.id}")
print("respawn worked, now saving..")
profile.name = "assassin"
profile.subscription = None
profile.ticket = None
ProfileController.update(profile)
# DELETE the pre-existing WG config:
profile.delete_wireguard_configuration()
print("deleted the pre-existing wg config")
return Result(valid=True)

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