Compare commits

..

No commits in common. "master" and "encrypted_proxy" have entirely different histories.

196 changed files with 1791 additions and 15168 deletions

9
.gitignore vendored
View file

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

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

@ -1,3 +0,0 @@
from core.utils.run_commands import test_broken_pipe_on_process_death
test_broken_pipe_on_process_death()

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

@ -5,17 +5,6 @@ import os
@dataclass(frozen=True)
class Constants:
DB_VERSION_THIS_APP_WANTS = 2
# 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 ─────────────────────────────────────────────
# ticketing group:
TICKET_API_BASE_URL: Final[str] = os.environ.get(
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
@ -51,12 +40,9 @@ class Constants:
HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles'
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles'
# ── ticketing ─────────────────────────────────────────────
# ticketing group:
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_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'
@ -69,23 +55,3 @@ class Constants:
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_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

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,7 +30,7 @@ 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.controllers.ProfileController import ProfileController
@ -99,7 +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)
ProfileController.disable(profile, False, profile_observer=profile_observer)
time.sleep(1.0)
sys.exit()
@ -107,7 +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)
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

@ -1,23 +1,11 @@
# new sync refactor:
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:
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
# prior versions:
from core.Constants import Constants
from core.Errors import UnknownClientPathError, UnknownClientVersionError, CommandNotFoundError
from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientVersionController import ClientVersionController
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.LocationController import LocationController
from core.controllers.OperatorController import OperatorController
from core.controllers.SubscriptionPlanController import SubscriptionPlanController
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
@ -27,13 +15,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,87 +41,11 @@ 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 ..")
# result = coordinate_cache_sync(client_observer, connection_observer)
# # 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
# # Same:
# changed_tables = result["changed_tables"]
# if not changed_tables:
# if client_observer is not None:
# client_observer.notify('synchronized')
# return
# # We only make it past this point if there's New Data
# # flag for after the save,
# data_was_saved = False
# # Fetch and update the real data (no longer metadata)...
# # =================== 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..')
# final_result = sync_one_orm_model(Location, "locations")
# evaluate_errors(final_result)
# 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_two = sync_one_orm_model(Operator, "operators")
# evaluate_errors(final_result_two)
# # =================== 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)
# # 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
# 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 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
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(task=ClientController.__sync, client_observer=client_observer, connection_observer=connection_observer)
@staticmethod
def update(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
@ -155,43 +61,32 @@ class ClientController:
return path
@staticmethod
def __sync(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 client_observer is not None:
client_observer.notify('synchronizing')
# 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)
# noinspection PyProtectedMember
ApplicationController._sync(proxies=proxies)
# noinspection PyProtectedMember
ApplicationVersionController._sync(proxies=proxies)
# noinspection PyProtectedMember
ClientVersionController._sync(proxies=proxies)
# noinspection PyProtectedMember
OperatorController._sync(proxies=proxies)
# noinspection PyProtectedMember
LocationController._sync(proxies=proxies)
# noinspection PyProtectedMember
SubscriptionPlanController._sync(proxies=proxies)
# 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)
ConfigurationController.update_last_synced_at()
# 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)
# ConfigurationController.update_last_synced_at()
# logger.info("Real Data Fetch Completed Successfully")
if client_observer is not None:
client_observer.notify('synchronized')
@staticmethod
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
if ClientController.can_be_updated():

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'):
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,45 +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

View file

@ -1,20 +1,10 @@
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 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
@ -27,26 +17,18 @@ from subprocess import CalledProcessError
from typing import Union, Optional, Any
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
import time
from enum import Enum
# import sys
# import re
class ConnectionController:
@staticmethod
def with_preferred_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
"""
This function does a task with a preferred connection type.
It is currently being CALLED UPON with keyword based kwargs only, because any args would execute immediately.
However, the *args keeps it open to future uses.
"""
connection = ConfigurationController.get_connection()
@ -59,9 +41,84 @@ class ConnectionController:
else:
return None
@staticmethod
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
connection = profile.connection
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
if profile.has_subscription():
if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
proxy_configuration = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_proxy_configuration, connection_observer=connection_observer)
if proxy_configuration is None:
raise InvalidSubscriptionError()
profile.attach_proxy_configuration(proxy_configuration)
else:
raise MissingSubscriptionError()
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
if profile.has_subscription():
if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
else:
if profile.is_system_profile():
if ConnectionController.system_uses_wireguard_interface() and SystemStateController.exists():
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise MissingSubscriptionError()
if profile.is_session_profile():
try:
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
else:
raise ConnectionError('The connection could not be established.')
if profile.is_system_profile():
try:
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
else:
raise ConnectionError('The connection could not be established.')
return None
@staticmethod
def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None, 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)
@ -69,28 +126,25 @@ class ConnectionController:
port_number = None
proxy_port_number = None
# 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.controllers.ProfileController import ProfileController
ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
ProfileController.disable(profile)
if profile.connection.code == 'tor':
port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer)
session_state.network_port_numbers.tor.append(port_number)
elif profile.connection.code == 'wireguard':
if ConfigurationController.get_endpoint_verification_enabled():
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)
@ -105,42 +159,74 @@ class ConnectionController:
session_state.network_port_numbers.proxy.append(proxy_port_number)
if not profile.connection.is_unprotected():
await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
ConnectionController.await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
SessionStateController.update_or_create(session_state)
return proxy_port_number or port_number
@staticmethod
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
if ConfigurationController.get_endpoint_verification_enabled():
ProfileController.verify_wireguard_endpoint(profile, ignore=ignore)
try:
ConnectionController.__establish_system_connection(profile, connection_observer)
except ConnectionError:
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
except CalledProcessError:
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
try:
ConnectionController.__establish_system_connection(profile, connection_observer)
except (ConnectionError, CalledProcessError):
try:
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
ConnectionController.terminate_tor_connection()
time.sleep(1.0)
@staticmethod
def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None):
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.start_service(connection_observer)
for session_state in SessionStateController.all():
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.start_service(connection_observer)
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")
for session_state in SessionStateController.all():
# @staticmethod
# def terminate_tor_connection():
for port_number in session_state.network_port_numbers.tor:
tor_module.create_session(port_number)
# tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
# tor_module.stop_service()
@staticmethod
def terminate_tor_connection():
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.stop_service()
@staticmethod
def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None):
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
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")
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
@staticmethod
def terminate_tor_session_connection(port_number: int):
@ -202,9 +288,26 @@ 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():
if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli')
if SystemStateController.exists():
process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if completed_successfully or not ConnectionController.system_uses_wireguard_interface():
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ConnectionController.terminate_tor_connection()
SystemState.dissolve()
else:
raise ConnectionTerminationError('The connection could not be terminated.')
# Stays here for Tor based only. Will move with Tor later.
@staticmethod
def get_proxies(port_number: int):
@ -213,6 +316,107 @@ 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.')
SystemStateController.create(profile.id)
try:
ConnectionController.await_connection(connection_observer=connection_observer)
except ConnectionError:
raise ConnectionError('The connection could not be established.')
@staticmethod
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
@ -220,9 +424,62 @@ 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

View file

@ -1,32 +1,22 @@
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 core.models.Location import Location
from core.services.WebServiceApiService import WebServiceApiService
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
def get(country_code: str, code: str):
return Location.find(country_code, code)
@staticmethod
def get_all():
with get_session() as session:
all_records = session.execute(
select(Location)
.options(joinedload(Location.operator))
).scalars().all()
return all_records
return Location.all()
@staticmethod
def _sync(proxies: Optional[dict] = None):
locations = WebServiceApiService.get_locations(proxies)
Location.truncate()
Location.save_many(locations)

View file

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

View file

@ -1,17 +1,3 @@
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.general_connection_tools.connection_enable import establish_connection
from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
from core.services.networking.systemwide.encrypted_proxy.singbox_runner import end_singbox
from core.services.networking.systemwide import killswitch
from core.services.subscriptions import subscriptions
from core.errors.exceptions import FirewallError
from core.models.Result import Result, ResultError
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.logger import logger
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController
@ -49,121 +35,63 @@ class ProfileController:
if profile_observer is not None:
profile_observer.notify('created', profile)
@staticmethod
def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
profile.save()
if profile_observer is not None:
profile_observer.notify('updated', profile)
@staticmethod
def enable(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
pristine: bool = False,
asynchronous: bool = False,
profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = None
):
def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController
# =========== ALREADY ENABLED ============
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)
# =========== PRISTINE ============
if pristine:
profile.delete_data()
# ============================================================================
# SESSION
# ============================================================================
if profile.is_session_profile():
# ASSASSIN MODE
if profile.assassin:
assassin_result = assassin_tools.create(
profile=profile,
max_resolution=max_resolution,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with setting up Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileActivationError(f'Assassin NOT enabled: {assassin_result.message}')
# random assassin values become the main profile:
profile = assassin_result.data
# CONCLUSION OF ASSASSIN CODE
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 = establish_connection(profile, ignore=ignore, connection_observer=connection_observer, ticket_observer=ticket_observer)
port_number = ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.')
except ValueError:
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, ticket_observer=ticket_observer, connection_observer=connection_observer)
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer)
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile():
try:
connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
if connection_result.valid:
if profile_observer is not None:
profile_observer.notify('enabled', profile)
else:
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
error_msg = connection_result.message
raise ProfileActivationError(error_msg)
except FirewallError:
raise
ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.')
except ValueError:
raise ProfileActivationError('The profile could not be enabled.')
if profile_observer is not None:
profile_observer.notify('enabled', profile)
@staticmethod
def disable(
profile: Union[SessionProfile, SystemProfile],
explicitly: bool = True,
ignore: tuple[type[Exception]] = (),
profile_observer: ProfileObserver = None,
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
wipe_assassin: bool = False
):
def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, ignore: tuple[type[Exception]] = (), profile_observer: ProfileObserver = None):
from core.controllers.ConnectionController import ConnectionController
if profile.is_session_profile():
# START ASSASSIN SECTION
if profile.assassin and wipe_assassin:
print("triggering to WIPE assassin")
assassin_result = assassin_tools.wipe(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with wiping respawn of Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileDeactivationError(f'Assassin NOT disabled: {assassin_result.message}')
# END ASSASSIN SECTION
if SessionStateController.exists(profile.id):
session_state = SessionStateController.get(profile.id)
@ -175,15 +103,6 @@ class ProfileController:
session_state.dissolve(session_state.id)
if profile_observer is not None:
profile_observer.notify('disabled', profile, dict(
explicitly=explicitly,
))
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile():
subjects = ProfileController.get_all().values()
@ -203,52 +122,21 @@ class ProfileController:
raise ProfileDeactivationError('The profile could not be disabled.')
try:
# ================= SETTINGS =================
firewall_setting = get_firewall_setting()
dns_setting = get_dns_setting()
# ======= KILL SYSTEMWIDE =============
if profile.connection.code == "wireguard":
terminate_system_connection(
firewall_setting=firewall_setting,
dns_setting=dns_setting
)
elif profile.connection.code in ("hysteria2", "vless"):
end_singbox()
else:
raise ProfileDeactivationError('Unsupported protocol.')
# ================= UPDATE UI ================
# if it made it this far, it worked in theory.
if profile_observer is not None:
profile_observer.notify('disabled', profile, dict(
explicitly=explicitly,
))
ConnectionController.terminate_system_connection()
except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.')
except ValueError:
raise ProfileDeactivationError('The profile could not be disabled.')
except FirewallError:
raise
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, ticket_observer: TicketObserver = None, connection_observer: ConnectionObserver = None):
def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
####################################
# DESTROY TICKET
####################################
which_ticket = profile.ticket
respawned = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawned:
logger.error("Serious issue with respawning the ticket. We might raise an error here.")
####################################
# DESTROY PROFILE
####################################
ProfileController.disable(profile)
profile.delete()
@ -268,13 +156,7 @@ class ProfileController:
if profile.has_subscription():
subscription = subscriptions.get_subscription(
billing_code=profile.subscription.billing_code,
connection_observer=connection_observer
)
# legacy:
# subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
if subscription is not None:
@ -302,7 +184,7 @@ class ProfileController:
system_state = SystemStateController.get()
if system_state is not None and system_state.profile_id is profile.id:
return system_uses_wireguard_interface()
return ConnectionController.system_uses_wireguard_interface()
return False
@ -310,7 +192,6 @@ class ProfileController:
def get_invoice(profile: Union[SessionProfile, SystemProfile]):
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
@ -337,6 +218,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()
@ -346,11 +250,64 @@ class ProfileController:
return profile.has_wireguard_configuration()
@staticmethod
def turn_on_assassin(profile: SessionProfile):
profile.assassin = True
profile.save()
def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
try:
ProfileController.__verify_wireguard_endpoint(profile)
except EndpointVerificationError as error:
if not EndpointVerificationError in ignore:
profile.address_security_incident()
raise error
@staticmethod
def turn_off_assassin(profile: SessionProfile):
profile.assassin = False
profile.save()
def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
from cryptography.hazmat.primitives.asymmetric import ed25519
import base64
signature = profile.get_wireguard_configuration_metadata('Signature')
wireguard_public_keys = profile.get_wireguard_public_keys()
operator = profile.location.operator
if signature is None:
raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
if not wireguard_public_keys:
raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
if operator is None:
raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
try:
operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
for wireguard_public_key in wireguard_public_keys:
operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
except Exception:
raise EndpointVerificationError('The WireGuard endpoint could not be verified.')
@staticmethod
def __generate_wireguard_keys():
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
raw_private_key = X25519PrivateKey.generate()
public_key = raw_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
)
private_key = raw_private_key.private_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
)
return dict(
private=base64.b64encode(private_key).decode(),
public=base64.b64encode(public_key).decode()
)

View file

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

View file

@ -1,217 +0,0 @@
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.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
}
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}')
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')
return Result(valid=True)
# We only make it past this point if there's New Data
####################################
# 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}")
client_observer.notify('synchronizing', f'Error! {error_msg}')
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}")
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:
return Result(valid=True, message="Finshed sync.")
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)
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)
return Result(valid=False, data=skipped, message="Complete Failure, all data failed to insert.")
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

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

View file

@ -1,136 +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
# JSON Models
from core.models.BaseProfile import BaseProfile as Profile
from core.models.Subscription import Subscription
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
# ORM Models
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
# ORM Calls
from core.models.orm_calls.location_calls import get_profile_location_data
from core.models.BaseProfile import get_application_version
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':
# ============= SAME VALUE =============
if profile.connection.code == new_value:
error_msg = f"This is NOT changing the protocol, it already was {new_value}"
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# ============= SYSTEMWIDE =============
if profile.connection == 'system-wide':
if new_value in SYSTEMWIDE_CHOICES:
# REMOVE PAST CONFIG:
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
profile.connection.code = new_value
final_data = "edit_session"
else:
error_msg = f"{new_value} is not a systemwide choice"
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# ============= SESSION =============
else:
if new_value in SESSION_CHOICES:
profile.connection.code = new_value
if new_value == 'wireguard':
profile.connection.masked = False
# ============= LOCATION =============
elif key == "location":
country_code, city_code = new_value.split("_")
location = get_profile_location_data( # SQLAlchemy
country_code=country_code,
city_code=city_code
)
# SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc.
profile.location = 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:
profile.save()
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
return Result(valid=True, data=final_data)
except:
error_msg = "Profile save failed"
return Result(valid=False, message=error_msg, error_type=ResultError.UNKNOWN)

View file

@ -9,22 +9,16 @@ from core.models.invoice.TicketInvoice import TicketInvoice
from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.observers.BaseObserver import BaseObserver
from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing
from core.services.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.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
"""
Inputs: Which plan (key), which crypto, and how many profiles
@ -42,90 +36,119 @@ def initiate_payment(
) -> TicketInvoice:
###############
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")
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)
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)
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")
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.")
# get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local")
if not isinstance(public_key_results, dict):
notification = f"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
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)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.CONNECTION, message="There were connection issues with getting the right key.")
# In this case, the controller is going to send the whole JSON payload to the service,
# instead of passing 4 values seperately.
payload = {
"which_key": which_key,
"payment_type": "crypto",
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
invoice_data_object = TicketInvoice()
try:
return save_and_send_intitial_billing(
payload=payload,
connection_observer=connection_observer
if bypass_existing == False:
tickets_exist_already, path = does_ticket_tracker_exist()
logger.debug(f"tickets_exist_already is {tickets_exist_already}")
if tickets_exist_already:
invoice_data_object.add_error_code("already_exists")
return invoice_data_object
rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if not valid_profile_quantity(how_many_profiles):
notification = "Invalid profile quantity"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if which_key in rejected_choices:
notification = "Missing key plan, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_keyplan")
return invoice_data_object
# confirm the key choice is among the choices from their sync file,
# and if not, then sync again, and try the results from that new file,
is_valid_key = confirm_its_a_valid_key_choice(which_key, ticket_observer)
if not is_valid_key:
sync_results = sync_ticket_prices(ticket_observer, connection_observer)
second_try_to_match = confirm_its_a_valid_key_choice(
which_key, ticket_observer
)
if not second_try_to_match:
invoice_data_object.add_error_code("invalid_key")
return invoice_data_object
# get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local")
if isinstance(public_key_results, dict):
status = public_key_results.get("valid", False)
if status == False:
message = public_key_results.get(
"message", "Please fix before you continue"
)
error_code = public_key_results.get("error_code", "No error_code")
logger.debug(f"error_code: {error_code}")
invoice_data_object.add_error_code(error_code)
notification = f"Connection Issues or Invalid Key chosen. {message}"
ticket_observer.notify("failed_input", subject=notification)
return invoice_data_object
else:
notification = f"Invalid Key chosen or the server is down for that key."
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_pub_key")
return invoice_data_object
# In this case, the controller is going to send the whole JSON payload to the service,
# instead of passing 4 values seperately.
payload = {
"which_key": which_key,
"payment_type": "crypto",
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
# controller sends to the service:
result = save_and_send_intitial_billing(
payload, connection_observer, invoice_data_object
)
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(
@ -150,20 +173,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}
# 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
logger.debug(f"inside ticketpay controller the reply is {reply}")
return reply

View file

@ -7,16 +7,10 @@ if TYPE_CHECKING:
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.observers.BaseObserver import BaseObserver
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# generic
from typing import Optional
"""
Goal:
@ -32,54 +26,34 @@ 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:
def prepare_tickets(
how_many_profiles: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None,
billing_code: Optional[int] = None
) -> dict:
# 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 {"valid": False, "error_code": "failed_input"}
# make sure it's a number:
if not isinstance(how_many_profiles, int):
if ticket_observer is not None:
ticket_observer.notify("failed_input", None)
ticket_observer.notify("failed_input", None)
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 {"valid": False, "error_code": "failed_input"}
# make sure that "how_many_profiles" is actually the number of profiles ordered
# (which is based on locally saved data from the previous step):
how_many_ordered = get_how_many_profiles_were_ordered()
if how_many_profiles != how_many_ordered:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# make sure this guy has a public key to verify against:
does_he_have_public_key = make_sure_pub_key_exists(connection_observer)
if does_he_have_public_key == False:
if ticket_observer is not None:
ticket_observer.notify("failed_input", None)
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,
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
)
# rest of this function is evaluating the results:
@ -92,8 +66,7 @@ def prepare_tickets(
if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
ticket_observer.notify("preparing", subject=notification)
return prep_results
if "how_many_failed" in prep_results:
@ -101,118 +74,9 @@ def prepare_tickets(
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)
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)
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 = f"Error in Respawn Prep!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_error_result(ticket_result)
#################################################
# 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:
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)

View file

@ -7,18 +7,13 @@ if TYPE_CHECKING:
from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver
# from core.services.networking.api_requests.step1_get_or_post import get_data_from_api
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.services.networking.get_data_from_server import get_data_from_server
from core.services.helpers.save_sync_results import save_sync_results
from core.errors.logger import logger
def sync_ticket_prices(
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
ticket_observer: TicketObserver, connection_observer: ConnectionObserver
) -> dict:
notification = f"Connecting to get Ticket Pricing..."
ticket_observer.notify("connecting", subject=notification)
@ -34,35 +29,16 @@ def sync_ticket_prices(
url = f"{base_url}/sync"
try:
# api_result = get_data_from_api(url, None, connection_observer)
api_result = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
sync_results = get_data_from_server(url, connection_observer)
# 2nd try:
if not api_result.valid:
logger.error(f"Invalid API call result: {api_result.error_type}")
if sync_results in rejected_list:
return {"valid": False, "error_code": "sync_failed"}
# if not api_result.valid:
# error_msg = api_result.message
# logger.error(f"[TICKET SYNC Controller] There's an issue with the sync of endpoint {url} the API Reply: {error_msg}")
# api_result = solve_api_problems(
# api_reply_object=api_result,
# get_or_post="get",
# url=url,
# payload=None,
# connection_observer=connection_observer,
# client_observer=None
# )
# # 2nd try:
# if not api_result.valid:
# return {"valid": False, "error_code": "sync_failed"}
return {"valid": True, "data": api_result.data}
logger.debug(f"Inside the sync controller, sync_results is: {sync_results}")
except:
return {"valid": False, "error_code": "sync_failed"}
did_it_save = save_sync_results(sync_results)
logger.debug(f"Inside the sync controller, did_it_save is {did_it_save}")
return sync_results

View file

@ -1,30 +1,23 @@
from __future__ import annotations
from 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.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
# generic
import random
from typing import Optional
def modify_random_tickets_setting(
@ -35,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"
@ -45,28 +38,24 @@ 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}
if on_or_off == "on":
update_result = update_json(filepath, "use_random", True)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
elif on_or_off == "off":
update_result = update_json(filepath, "use_random", False)
return conclude_and_return(update_result=update_result, filepath=filepath, ticket_observer=ticket_observer)
else:
ticket_observer.notify("failed_input", None)
return {
"valid": False,
"message": "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)
ticket_observer.notify("error", subject=notification)
return {"valid": False, "message": notification}
try:
if on_or_off == "on":
update_json(filepath, "use_random", True)
return {"valid": True}
elif on_or_off == "off":
update_json(filepath, "use_random", False)
return {"valid": True}
else:
ticket_observer.notify("failed_input", None)
return {
"valid": False,
"message": f"Invalid choice for turning on or off",
}
except:
notification = f"Error with modifying config file. Check {filepath}"
ticket_observer.notify("error", subject=notification)
return {"valid": False, "message": notification}
"""
@ -82,47 +71,47 @@ def do_we_use_a_random_ticket(ticket_observer: TicketObserver) -> tuple:
config_data = get_value_from_config("use_random")
# 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:
if "value" in config_data:
random_setting = config_data["value"]
# they want a random ticket
if random_setting == True:
data_results = pick_a_random_ticket(ticket_observer)
if "random_ticket" in data_results:
which_ticket = data_results["random_ticket"]
error_msg = None
return which_ticket, error_msg
else:
which_ticket = "error"
if "message" in data_results:
error_msg = data_results["message"]
else:
error_msg = (
"Missing or Invalid Data. Unable to get unused ticket list."
)
return which_ticket, error_msg
# if it read the config, but the value is false:
else:
which_ticket = None
error_msg = None
return which_ticket, error_msg
# this is a problem with reading the config itself:
else:
which_ticket = "error"
error_msg = "There is an error with the config file, or no config. Are you sure you have tickets?"
ticket_observer.notify("failed_input", subject=error_msg)
return which_ticket, error_msg
random_setting = config_data["value"]
# if it read the config, but the value is false:
if not random_setting:
which_ticket = None
error_msg = None
return which_ticket, error_msg
# they want a random ticket
data_results = pick_a_random_ticket(ticket_observer)
# invalid format:
if "random_ticket" not in data_results:
which_ticket = "error"
if "message" in data_results:
error_msg = data_results["message"]
else:
error_msg = (
"Missing or Invalid Data. Unable to get unused ticket list."
)
return which_ticket, error_msg
# finally get the result:
which_ticket = data_results["random_ticket"]
error_msg = None
return which_ticket, error_msg
def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
# 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)
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg}
# Use the Model:
@ -143,10 +132,9 @@ use_ticket function requires:
def use_ticket(
which_ticket: int,
which_location: int, # Location.id
which_location: str,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver,
profile: Optional[Union[SessionProfile, SystemProfile]] = None, # only for assassin or single profiles.
) -> dict:
which_ticket = str(which_ticket) # type: ignore
@ -154,44 +142,46 @@ def use_ticket(
# 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, 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:
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}
if ticket_data["valid"] != True:
return ticket_data
# it's valid, get the list & pick one:
list_of_unused_tickets = ticket_data["data"]
random_ticket = random.choice(list_of_unused_tickets)
return {"valid": True, "random_ticket": random_ticket}

View file

@ -1,61 +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
class ApplicationError(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,21 +1,17 @@
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
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'

View file

@ -1,32 +1,11 @@
from core.models.manage.session_management import get_session
from core.errors.logger import logger
from sqlalchemy import select
from sqlalchemy.orm import joinedload
from abc import ABC, abstractmethod
from 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.controllers.LocationController import LocationController
from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator
from core.models.Location import Location
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
from pathlib import Path
from typing import Optional, Self
import json
@ -34,40 +13,8 @@ import os
import re
import shutil
import tempfile
from sqlalchemy.orm import Session
from enum import Enum
from core.models.manage.session_management import init_session, get_session
@safe_db_operation
def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation:
location_object = session.execute(
select(Location)
.where((Location.country_code == country_code) & (Location.code == city_code))
.options(joinedload(Location.operator))
).scalar_one_or_none()
return location_object
def get_profile_location_data(country_code: str, city_code: str) -> Location:
# with get_session() as session:
api_reply_object = execute_location_sql(country_code, city_code)
if api_reply_object.valid:
data = api_reply_object.data
location_obj = api_reply_object.data
return location_obj
else:
logger.error(f"[get_profile_location_data] Got invalid SQL Query")
return None
class ProfileType(str, Enum):
SESSION = "session"
SYSTEM = "system"
@dataclass_json
@dataclass
class BaseProfile(ABC):
@ -76,11 +23,7 @@ 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.
# location: Optional[Location]
location: Optional[Location]
@abstractmethod
def get_wireguard_configuration_path(self):
@ -108,25 +51,9 @@ class BaseProfile(ABC):
def is_system_profile(self):
return type(self).__name__ == 'SystemProfile'
def save(self: Self):
def save(self: Self, app_version_dict: dict = None):
# === SERIALIZATION ===
config_dict = self.to_dict()
# === LOCATION ===
location_dict = self.location.convert_to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
if self.location:
config_dict["location"] = location_dict
# === APPLICATION ===
if app_version_dict:
config_dict["application_version"] = app_version_dict
# === FILE I/O ===
config_file_contents = json.dumps(config_dict, indent=4) + '\n'
# legacy version:
# config_file_contents = f'{self.to_json(indent=4)}\n'
config_file_contents = f'{self.to_json(indent=4)}\n'
os.makedirs(self.get_config_path(), exist_ok=True)
os.makedirs(self.get_data_path(), exist_ok=True)
@ -219,7 +146,6 @@ class BaseProfile(ABC):
return list([key for key, value in asdict(self).items() if value != asdict(reference).get(key)])
@staticmethod
def find_by_id(id: int):
@ -235,62 +161,30 @@ class BaseProfile(ABC):
profile['id'] = id
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")
return
else:
# potentially coming from SQLAlchemy ALREADY:
country_code = profile.location.country_code
city_code = profile.location.code
# =========== GET DATA USING THAT COUNTRY & LOCATION ===========
location_dict = get_profile_location_data(country_code, city_code)
location = Location.find(profile['location']['country_code'] or None, profile['location']['code'] or None)
if location_dict:
profile['location'] = location_dict
if location is not None:
# this needs error handling if there's no location or malconformed config.
if profile['location'].get('time_zone') is not None:
location.time_zone = profile['location']['time_zone']
profile['location'] = location
# =========== SESSION ===========
if 'application_version' in profile:
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
@ -298,7 +192,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')
@ -331,10 +224,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,100 +1,84 @@
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 zoneinfo import ZoneInfo
from marshmallow import fields
from typing import Optional, Self
from zoneinfo import ZoneInfo
import dataclasses_json
import json
import os
import sys
class ConnectionChoice(str, Enum):
TOR = "tor"
SYSTEM = "system"
class Configuration(BaseModel):
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
model_config = ConfigDict(
extra='ignore', # Ignore unknown fields in JSON
exclude_none=True # Don't serialize None values
@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
)
)
@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
@staticmethod
def get():
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)
return Configuration(**configuration_dict) # Pydantic validates on init
def save(self: Self):
config_file_contents = f'{self.model_dump_json(indent=4)}\n'
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 read_config():
try:
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
except FileNotFoundError:
return None
try:
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
except FileNotFoundError:
return None
try:
configuration = json.loads(config_file_contents)
except ValueError:
logger.error(f"[CONFIG] Can't load JSON config")
return configuration
try:
configuration = json.loads(config_file_contents)
except ValueError:
sys.exit(1)
def get_setting(looking_for):
config = read_config()
if not config:
logger.error(f"[CONFIG] Can't load the entire config")
return None
# noinspection PyUnresolvedReferences
configuration = Configuration.from_dict(configuration)
if looking_for not in config:
logger.error(f"[CONFIG] What you want isn't in the config")
return None
result = config[looking_for]
return configuration
return result
@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")

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

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

56
core/models/Operator.py Normal file
View file

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

View file

@ -1,54 +0,0 @@
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Any
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"
TICKET = "ticket"
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"
LEAK_ISSUE = "leak_issue"
NEED_SYNC = "need_sync"
UNKNOWN = "unknown"
@dataclass
class Result():
valid: bool
goal_result: Optional[bool] = None
error_type: ResultError = ResultError.SUCCESS
data: Optional[Any] = None
message: Optional[str] = None
goal: 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

@ -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

@ -1,74 +0,0 @@
import yaml
"""
This function extracts the value from a nested dictionary,
By looping through each layer. And comparing the current dictionary's key to pre-made YAML key mappings.
"""
def extract_from_nested(obj, yaml_keys, default=None):
# loop through each key from the YAML mapping:
for each_yaml_key in yaml_keys:
# Check if each layer down of the 'object' is a dictionary with that key,
if isinstance(obj, dict) and each_yaml_key in obj:
# then extract the value for that key, and replace the object placeholder itself with it.
obj = obj[each_yaml_key]
# this has the effect of going deeper into the nesting on the next round of the loop,
# But if it's not a dictionary or doesn't have the key,
else:
# then we hit a dead end,
return default
# We finished looping through all the keys of valid dictionaries, and extracted the values,
return obj
"""
This function removes unnecessary nesting,
by comparing it to a pre-made YAML mapping
"""
def denormalize(data, mapping_file):
final_results = []
# prep the mapping config:
with open(mapping_file) as f:
mapping = yaml.safe_load(f)
# Extract out the 'data' variable if it exists, otherwise just use the data directly
extracted_data = data.get('data', data)
# go through the data
for each_item in extracted_data:
# setup temp flags/containers
each_denormalized_piece = {}
valid = True
# go through mapping:
for field in mapping['fields']:
"""
Extract the values based on the YAML mapping
field['path'] - Gets a Python list of the keys from the YAML mapping
extract_from_nested - This function checks it's actually a dictionary and extracts it
"""
value = extract_from_nested(each_item, field['path'])
# Store the extracted value. If it wasn't there, then it's None
each_denormalized_piece[field['name']] = value
# if it's required & not there, ditch it,
if field.get('required') and value is None:
print(f"It's required and value is none, skipping {value}")
valid = False
break
# Only add items where all required fields were present
if valid:
final_results.append(each_denormalized_piece)
# Return all successfully denormalized items
return final_results
# Isolated testing:
# items = denormalize(response.json(), 'example.yaml')

View file

@ -1,35 +0,0 @@
from core.models.manage.session_management import get_session
from typing import Type, List, Dict, Any
from sqlalchemy.exc import SQLAlchemyError
def get_from_model(model_class: Type, **filters) -> List[Dict[str, Any]]:
"""
Retrieve data from a model. Keeping this generic for reuse
Args:
model_class: The ORM model class
**filters: Optional column=value pairs to filter by (e.g., id=1, name='John')
Returns:
List of dictionaries representing matching rows
"""
with get_session() as session:
try:
query = session.query(model_class)
# Apply filters dynamically
if filters:
for column, value in filters.items():
if hasattr(model_class, column):
query = query.filter(getattr(model_class, column) == value)
# Convert to list of dicts
results = [
{c.name: getattr(row, c.name) for c in row.__table__.columns}
for row in query.all()
]
return results
except SQLAlchemyError as e:
raise ValueError(f"Query failed: {e}")

View file

@ -1,109 +0,0 @@
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:
"""
Purpose:
Generic ORM insert for any model. Keeping it generic for reuse.
Args:
model_class: The ORM model class
all_data: Dictionary or list of dictionaries with data matching the model
override: If True, wipe the table before inserting
Returns:
DatabaseOperation object with true/false
"""
# 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"""
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()
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}")
try:
for filtered_json in filtered_data_list:
instance = model_class(**filtered_json)
session.add(instance)
session.commit()
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
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

View file

@ -1,149 +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
MODELS = [Location, Operator, CachedSync, EncryptedProxy, Dependency]
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,170 +0,0 @@
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.
"""
# ============================================================================
# PATH & INITIALIZATION
# ============================================================================
def get_path():
"""Returns XDG Base Directory path or ~/.local/share/hydra-veil"""
xdg_data = os.getenv("XDG_DATA_HOME")
if xdg_data:
return Path(xdg_data) / "hydra-veil"
return Path.home() / ".local" / "share" / "hydra-veil"
def does_it_exist(filepath):
if filepath.exists():
return True
else:
return False
# ============================================================================
# GLOBAL STATE
# ============================================================================
system_path = get_path()
database_path = system_path / "storage.db"
engine = None
Session = None
_session = None
# ============================================================================
# ENGINE & SESSION MANAGEMENT
# ============================================================================
def _reinitialize_engine_and_session():
"""
Recreate the global engine and Session factory.
Call this after deleting the database to sync globals with the new DB file.
"""
global engine, Session
system_path.mkdir(parents=True, exist_ok=True)
engine = create_engine(f"sqlite:///{database_path}")
Session = sessionmaker(bind=engine)
def init_session():
"""Initialize the global _session from the global Session factory."""
global _session
if Session is None:
raise RuntimeError("Session factory not initialized. Call _reinitialize_engine_and_session() first.")
_session = Session()
def get_session():
"""Return the global _session or raise RuntimeError."""
if _session is None:
raise RuntimeError("Session not initialized. Call init_session() first.")
return _session
def close_session():
"""Close and reset the global _session."""
global _session
if _session is not None:
_session.close()
_session = None
# ============================================================================
# DATABASE OPERATIONS
# ============================================================================
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, checkfirst=True)
logger.info("[DB MANAGEMENT] database_version table created.")
# 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)
def does_db_version_table_exist():
if engine is None:
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
inspector = inspect(engine)
table_exists = inspector.has_table("database_version")
return table_exists
def create_ALL_tables():
"""Create all tables from BaseModel.metadata using the global engine."""
if engine is None:
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
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.SubscriptionPlan import SubscriptionPlan
from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.models.orm_models.Dependency import Dependency
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.")
return False
# ============================================================================
# STARTUP: Initialize engine and session on import
# ============================================================================
_reinitialize_engine_and_session()
logger.info("[DB MANAGEMENT] Started Engine in Session Management Module..")
init_session()
logger.info("[DB MANAGEMENT] Initialized Session")

View file

@ -1,455 +0,0 @@
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
from typing import Optional
def get_database_version(session: Session) -> Optional[int]:
"""
Purpose:
Retrieve the current database version from the single-row version table.
Rank:
Helper
Called By:
check_database_compatibility
Features:
Handles errors with multiple items, failed loads, and other conditions.
Returns:
int: The database version, or None if retrieval fails.
"""
func_name = "get_database_version"
# Validate session
if session is None:
logger.error(f"{func_name}: Session is None")
return None
if not isinstance(session, Session):
logger.error(f"{func_name}: Invalid session type: {type(session)}")
return None
try:
# Attempt to query the table
logger.debug(f"{func_name}: Attempting to query database_version table")
result = session.query(database_version).all()
# Handle empty table
if not result:
logger.warning(f"{func_name}: Table is empty, no version row found")
return None
# Handle multiple rows (shouldn't happen, but log it)
if len(result) > 1:
logger.warning(
f"{func_name}: Expected 1 row, found {len(result)}. "
"Table should contain only a single row. Returning first version."
)
# Extract version from first row
version_row = result[0]
version = version_row.version
# Type validation
if not isinstance(version, int):
logger.error(
f"{func_name}: Version column type mismatch. "
f"Expected int, got {type(version).__name__}: {version}"
)
return None
if version < 0:
logger.warning(
f"{func_name}: Version is negative ({version}). "
"This may indicate corrupted data."
)
logger.info(f"{func_name}: Successfully retrieved version: {version}")
return version
except exc.NoSuchTableError:
logger.error(
f"{func_name}: Table 'database_version' does not exist. "
"Ensure the table is created with `database_version.create(engine, checkfirst=True)`"
)
return None
except exc.OperationalError as e:
logger.error(
f"{func_name}: Database operational error (connection, locked, or permissions). "
f"Details: {str(e)}"
)
return None
except exc.DatabaseError as e:
logger.error(
f"{func_name}: General database error. "
f"Details: {str(e)}"
)
return None
except exc.StatementError as e:
logger.error(
f"{func_name}: SQL statement error. "
f"Details: {str(e)}"
)
return None
except AttributeError as e:
logger.error(
f"{func_name}: Row does not have 'version' attribute. "
f"Verify table schema matches definition. Details: {str(e)}"
)
return None
except TypeError as e:
logger.error(
f"{func_name}: Type error when accessing version. "
f"Details: {str(e)}"
)
return None
except Exception as e:
logger.critical(
f"{func_name}: Unexpected exception type {type(e).__name__}. "
f"Details: {str(e)}",
exc_info=True
)
return None
def insert_new_version(session: Session, new_version: int) -> bool:
"""
Purpose:
Insert (or replace) the database version. Clears existing rows and inserts new version.
Rank:
Helper
Called By:
GUI's startup script in __main__
Args:
session: SQLAlchemy session
new_version: The version number to insert (must be a positive integer)
Returns:
bool: True if successful, False otherwise.
does NOT raise errors.
"""
func_name = "insert_new_version" # 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
# Validate new_version parameter
if new_version is None:
logger.error(f"{func_name}: new_version is None")
return False
if not isinstance(new_version, int):
logger.error(
f"{func_name}: new_version must be an integer, got {type(new_version).__name__}: {new_version}"
)
return False
if new_version < 0:
logger.error(
f"{func_name}: new_version must be non-negative, got {new_version}"
)
return False
try:
logger.debug(f"{func_name}: Starting transaction for version insert/replace")
# Check if table exists before proceeding
try:
session.query(database_version).limit(1).all()
except exc.NoSuchTableError:
logger.error(
f"{func_name}: Table 'database_version' does not exist. "
"Ensure the table is created with `database_version.create(engine, checkfirst=True)`"
)
return False
# 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."
)
# Actually Insert the new version
logger.debug(f"{func_name}: Inserting new version {new_version}")
insert_stmt = database_version.insert().values(version=new_version)
session.execute(insert_stmt)
# Commit transaction
session.commit()
logger.info(f"{func_name}: Successfully inserted version {new_version}")
return True
except exc.NoSuchTableError:
logger.error(
f"{func_name}: Table 'database_version' does not exist. "
"Ensure the table is created with `database_version.create(engine, checkfirst=True)`"
)
session.rollback()
return False
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.error(
f"{func_name}: Database operational error (connection, locked, or permissions). "
f"Details: {str(e)}"
)
session.rollback()
return False
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 insert. "
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
"""Spits back a string based on the reason and error."""
def get_custom_message(reason: str, compatability_dict: dict) -> str:
"""
Called By:
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."
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."
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}."
return custom_error
# ============================================================================
# ORCHESTRATOR
# ============================================================================
def check_database_compatibility(session: Session) -> dict:
"""
Purpose:
Evaluates if the user's existing database is incompatible with the app's version.
Rank:
Orchestrator
Called By:
GUI's startup script in __main__
Returns:
Dictionary of True/False, with reason
NO error raises.
"""
db_version_you_have = get_database_version(session)
if db_version_you_have is None:
print("Nothing there")
# First run, corrupted, or empty
return {
"result": True,
"reason": "no_database" # Allow initialization still, since there's nothing to delete.
}
logger.info(f"[DB MANAGEMENT] The Database version this APP WANTS is {Constants.DB_VERSION_THIS_APP_WANTS}")
logger.info(f"[DB MANAGEMENT] The Database version this device currently has is {db_version_you_have}")
if Constants.DB_VERSION_THIS_APP_WANTS == db_version_you_have:
logger.info(f"[DB MANAGEMENT] Great, the database versions match exactly.")
return {
"result": True,
"reason": None,
}
# BAD!! Old version,
elif Constants.DB_VERSION_THIS_APP_WANTS < db_version_you_have:
logger.info(f"[DB MANAGEMENT] The App version is older, this is NOT compatabile")
# make them wipe the tables, or switch versions of the app
return {
"result": False,
"reason": "old_app",
"old_db_version": db_version_you_have
}
# Upgrade User:
elif Constants.DB_VERSION_THIS_APP_WANTS > db_version_you_have:
logger.info(f"[DB MANAGEMENT] The user has an upgraded App, and an older database")
return {
"result": False,
"reason": "upgrade",
"old_db_version": db_version_you_have
}
# this is an error
else:
logger.error(f"[DB MANAGEMENT] Unknown Error inside version check module with the King Orchestration")
return {
"result": False,
"reason": "error",
"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,60 +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,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

@ -1,19 +0,0 @@
# base.py
from sqlalchemy.orm import declarative_base
from sqlalchemy import inspect
Base = declarative_base()
"""
This mapper exists so the children classes
have the ability to print human-readable strings
as dictionaries for the UI.
"""
class BaseModel(Base):
__abstract__ = True
def __repr__(self):
mapper = inspect(self.__class__)
fields = ', '.join(f'{col.name}={getattr(self, col.name)}' for col in mapper.columns)
return f'{self.__class__.__name__}({fields})'

View file

@ -1,25 +0,0 @@
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 Base
"""
This is one of the SQLAlchemy Models from the refactor.
Thats why it lacks functions to get or insert data.
"""
class CachedSync(Base):
__tablename__ = 'cached_sync'
# 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)
subscriptions: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
dependencies: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)

View file

@ -1,11 +0,0 @@
from sqlalchemy import Column, Integer, MetaData, Table
# Separate metadata for the compatibility table only
compat_metadata = MetaData()
database_version = Table(
'database_version',
compat_metadata,
Column('version', Integer, primary_key=True)
)

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,40 +0,0 @@
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, mapped_column
from typing import Optional
from sqlalchemy.orm import Mapped
from core.models.orm_models.Base import Base
from core.models.orm_models.Operator import Operator
from core.models.orm_models.Location import Location
class EncryptedProxy(Base):
__tablename__ = 'encryptedproxies'
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
protocol_type: Mapped[str] = mapped_column(String, nullable=False) # changed from name "type"
username: Mapped[str] = mapped_column(String, nullable=True, default=None)
password: Mapped[str] = mapped_column(String, nullable=False)
links: Mapped[str] = mapped_column(String, nullable=False)
subscription_url: Mapped[str] = mapped_column(String, nullable=False)
# Foreign key column
operator_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("operators.id"),
nullable=True,
default=None
)
# Foreign key column
location_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("locations.id"),
nullable=True,
default=None
)
operator_domain: Mapped[str] = mapped_column(String, nullable=True, default=None)
operator_hysteria2_host: Mapped[str] = mapped_column(String, nullable=True, default=None)
operator_vless_host: Mapped[str] = mapped_column(String, nullable=True, default=None)
server_ip: Mapped[str] = mapped_column(String, nullable=True, default=None) # pre-resolved IP — avoids DNS leak at connect time

View file

@ -1,74 +0,0 @@
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
class Location(BaseModel):
__tablename__ = 'locations'
# model primary key, but only used for business lookups
id: Mapped[Optional[int]] = mapped_column(Integer, primary_key=True)
# BUSINESS logic primary keys:
country_code: Mapped[str] = mapped_column(String, nullable=False)
code: Mapped[str] = mapped_column(String, unique=True, nullable=False)
# country name:
country_name: Mapped[Optional[str]] = mapped_column(String, nullable=True, default=None)
# this is CITY name:
name: Mapped[Optional[str]] = mapped_column(String, nullable=True, default=None)
time_zone: Mapped[Optional[str]] = mapped_column(String, nullable=True, default=None)
# Foreign key column
operator_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("operators.id"),
nullable=True,
default=None
)
# Relationship with selectin eager loading
operator: Mapped[Optional["Operator"]] = relationship(
"Operator",
lazy="selectin"
)
provider_name: Mapped[Optional[str]] = mapped_column(String, 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):
return {
"country_code": self.country_code,
"code": self.code,
"time_zone": self.time_zone,
"location_id": self.id
}
# to use:
# Lookup by location/city codes
# record = session.get(Location, (country_code, code))
# example data:
"""
id: 7
country_code: is
country_name: Iceland
code: 1
name: Capital Region
time_zone: Atlantic/Reykjavik
operator_id: 6
provider_name: FlokiNET
is_proxy_capable: 0
is_wireguard_capable: 1
"""

View file

@ -1,18 +0,0 @@
from sqlalchemy import Integer, String
from sqlalchemy.orm import declarative_base, mapped_column
from typing import Optional
from sqlalchemy.orm import Mapped
from core.models.orm_models.Base import BaseModel
class Operator(BaseModel):
__tablename__ = 'operators'
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
public_key: Mapped[str] = mapped_column(String, nullable=True, default=None)
nostr_public_key: Mapped[str] = mapped_column(String, nullable=False)
nostr_profile_reference: Mapped[str] = mapped_column(String, nullable=False)
nostr_attestation_event_reference: Mapped[str] = mapped_column(String, nullable=False)
# legacy:
# operator: Mapped[str] = mapped_column(String, nullable=False)

View file

@ -1,57 +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
location_country_code: str
location_city_code: int
operator_id: int
# operator_name: str = None
operator_domain: str = None
@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'),
'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
# 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,44 +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 VlessData(BaseModel):
model_config = ConfigDict(extra="ignore")
username: str = None
links: str # vless link
subscription_url: HttpUrl
server_ip: IPv4Address
location_country_code: str
location_city_code: int
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
@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

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

@ -1,6 +1,7 @@
from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass
@dataclass
class SessionConnection(BaseConnection):
masked: bool = False
@ -10,43 +11,8 @@ class SessionConnection(BaseConnection):
if self.code not in ('system', 'tor', 'wireguard'):
raise ValueError('Invalid connection code.')
# Called by: ConnectionController
# 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
# 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}')

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,15 +16,12 @@ 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():
@ -40,13 +33,9 @@ class SessionProfile(BaseProfile):
if 'location' in self._get_dirty_keys():
self.__delete_proxy_configuration()
self.delete_wireguard_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)
super().save()
def attach_proxy_configuration(self, proxy_configuration):
@ -96,7 +85,7 @@ class SessionProfile(BaseProfile):
def address_security_incident(self):
super().address_security_incident()
self.delete_wireguard_configuration()
self.__delete_wireguard_configuration()
def determine_timezone(self):
@ -125,5 +114,5 @@ class SessionProfile(BaseProfile):
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,15 @@
from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass
# legacy:
@dataclass
class SystemConnection (BaseConnection):
def __post_init__(self):
if self.code not in ('vless', 'hysteria2', 'wireguard'):
if self.code != 'wireguard':
raise ValueError('Invalid connection code.')
@staticmethod
def needs_proxy_configuration():
return False
# Potential refactor:
# from enum import Enum
# from typing import Literal
# class SystemConnectionTypes(str, Enum):
# WIREGUARD = "wireguard"
# HYSTERIA2 = "hysteria2"
# VLESS = "vless"
# @dataclass
# class SystemConnection(BaseConnection):
# code: SystemConnectionTypes
# masked: Literal[False] = False

View file

@ -12,13 +12,9 @@ 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)
the_id = self.id
return filepath
return self.__get_system_config_path(self.id)
def save(self):
@ -52,15 +48,10 @@ class SystemProfile(BaseProfile):
raise ProfileModificationError('The WireGuard configuration could not be attached.')
def get_wireguard_configuration_path(self):
filepath = f'{self.get_system_config_path()}/wg.conf'
return filepath
return f'{self.get_system_config_path()}/wg.conf'
def has_wireguard_configuration(self):
filepath = f'{self.get_system_config_path()}/wg.conf'
if os.path.isdir(os.path.dirname(filepath)):
return os.path.isfile(filepath)
else:
return False
return os.path.isfile(f'{self.get_system_config_path()}/wg.conf')
def address_security_incident(self):
@ -68,6 +59,7 @@ class SystemProfile(BaseProfile):
self.__delete_wireguard_configuration()
def delete(self):
try:
self.__delete_wireguard_configuration()
except ProfileModificationError:
@ -76,13 +68,11 @@ class SystemProfile(BaseProfile):
if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec')
try:
process = subprocess.run(('pkexec', 'rm', '-rf', self.get_system_config_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not completed_successfully:
raise ProfileDeletionError('The profile could not be deleted.')
except:
print("skipping the delete of the WG folder.")
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_system_config_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not completed_successfully:
raise ProfileDeletionError('The profile could not be deleted.')
super().delete()
@ -93,45 +83,12 @@ class SystemProfile(BaseProfile):
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
except:
completed_successfully = True
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_wireguard_configuration_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not completed_successfully:
raise ProfileModificationError('The WireGuard configuration could not be deleted.')
@staticmethod
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.')
return f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}'

View file

@ -6,15 +6,11 @@ from typing import Self
import json
import os
import pathlib
from typing import Optional
@dataclass_json
@dataclass
class SystemState:
profile_id: int
firewalled: bool
dns_set: bool
process_id: Optional[int] = None
def save(self: Self):

View file

@ -1,15 +1,13 @@
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
from core.models.Operator import Operator
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
@ -214,15 +212,3 @@ class WebServiceApiService:
headers = None
return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies)
# @staticmethod
# def get_cached_sync(proxies: Optional[dict] = None):
# from requests.status_codes import codes as status_codes
# response = WebServiceApiService.__get('/cachedsync', None, proxies)
# if response.status_code == status_codes.OK:
# return response.json()
# else:
# return None

View file

@ -1,166 +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
profile.save()
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
profile.save()
# DELETE the pre-existing WG config:
profile.delete_wireguard_configuration()
print("deleted the pre-existing wg config")
return Result(valid=True)

View file

@ -1,20 +0,0 @@
from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import BaseProfile as Profile
def get_systemwide_location() -> int:
current_state = SystemStateController.get()
if not current_state:
return None
systemwide_id = current_state.profile_id
if not systemwide_id:
return None
systemwide_profile = Profile.find_by_id(systemwide_id)
if not systemwide_profile:
return None
return systemwide_profile

View file

@ -1,21 +0,0 @@
from core.errors.logger import logger
import random
def pick_random_resolution(max_resolution: str) -> str:
if "x" in max_resolution:
max_width, max_height = max_resolution.split("x")
else:
logger.error(f"Developer Error: Invalid format of max_resolution being {max_resolution}, going with the defaults")
max_height = 800
max_width = 800
min_resolution = 650
width = random.randint(min_resolution, int(max_width))
height = random.randint(min_resolution, int(max_height))
final_resolution = f"{width}x{height}"
return final_resolution

View file

@ -1,132 +0,0 @@
from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket
from core.services.prepare_tickets.ticket_tracker import find_ticket_by_sub
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.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# generic
from typing import Union
import random
# This is in the Assassin folder, but it can be used for anything (such as respawn on a systemwide non-assassin profile).
# Why: We keep it in Assassin, to remind us to do print statements instead of log. Assassin is ephemeral.
def respawn_profile(
profile: Union[SessionProfile, SystemProfile],
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
) -> Result:
"""
Purpose:
Voids a subscription for a profile and respawns the ticket for it.
Requires:
A profile. This is NOT just a ticket.
Rank:
Controller
Called by:
ProfileController.destroy
"""
which_ticket = profile.ticket # which slot is to be used for this.
try:
billing_code = profile.subscription.billing_code
except Exception as e:
logger.error(f"FAILED to get the billing code: {str(e)}")
billing_code = None
# try via lookup:
if not which_ticket:
logger.info(f"Unable to find the ticket # in the profile's native config for {profile.id}. Checking the ticket tracker JSON")
if not billing_code:
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message="You lack a valid subscription on the profile slot is being respawned")
which_ticket = find_ticket_by_sub(billing_code)
# Regardless of how it was acquired,
if not which_ticket:
return Result(valid=False, error_type=ResultError.TICKET, message="This profile is not associated with a ticket. And/or you lack a ticket slot to respawn.")
notification = f"Respawn Started for Ticket {which_ticket}"
logger.info(notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
print(f"Sending to respawn_billing_code_into_ticket with billing_code: {billing_code} and ticket: {which_ticket}")
respawn_result = respawn_billing_code_into_ticket(
billing_code=billing_code,
which_ticket=which_ticket,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not respawn_result.valid:
notification = "Error with Ticket Respawn!"
logger.error(f"{notification} {respawn_result.error_type} with message: {respawn_result.message}")
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return respawn_result
logger.info(f"We have a valid ticket respawn, but not yet dealt with profile {profile.id}'s billing id..")
notification = "Ticket Respawned!"
logger.info(notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# Now that the billing code has been wiped by the server, wipe it locally:
print("We are now wiping the billing code for this profile.")
profile.subscription = None
# save the ticket,
profile.ticket = which_ticket
profile.save()
return Result(valid=True, message="This profile fully respawned and had it's billing code wiped.")
def get_by_any_means(profile: SessionProfile) -> int | None:
"""
Purpose:
Get the Ticket for a Profile by any means.
1) Try the profile itself
2) Search the ticket tracker
3) See if ANY unused tickets are available
Rank:
Mini-Orchestrator
Called by:
respawn_profile
"""
which_ticket = None
# Ticket tied to profile?
if profile:
which_ticket = profile.ticket
if which_ticket:
print(f"we found a ticket for this profile of {which_ticket}")
return which_ticket
# Ticket used with it?
if profile.subscription:
target_subscription = profile.subscription.billing_code
which_ticket = find_ticket_by_sub(target_subscription)
if which_ticket:
return which_ticket
else:
print("skipping there is no sub yet.")
# Any available ticket?
print("checking ANY available tickets.")
availablity_dict = get_all_unused_tickets()
valid = availablity_dict.get("valid", False)
if not valid:
return False
unused_tickets = availablity_dict.get("data", False)
print(f"all unused_tickets is {unused_tickets}")
if not unused_tickets or len(unused_tickets) == 0:
return False
print("picking a random ticket")
which_ticket = random.choice(unused_tickets)
return which_ticket

View file

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

View file

@ -1,22 +0,0 @@
# Overview
We use Cython to compile certain cryptographic operations to C, to make it faster. This is the situation with miller's loop from py_ecc.
# Pre-reqs:
In the same venv as the project itself:
```bash
pip install cython
pip install setuptools
```
<br/>
# Setup Compile
```bash
python3 setup.py build_ext --inplace
```
<br/>
# If any issues
Try the exact path to your venv.
```bash
path/to/your/venv/python3 setup.py build_ext --inplace
```

View file

@ -1,125 +0,0 @@
from py_ecc.fields import (
bls12_381_FQ as FQ,
bls12_381_FQ2 as FQ2,
bls12_381_FQ12 as FQ12
)
from py_ecc.fields.field_properties import (
field_properties,
)
from py_ecc.typing import (
Field,
Point2D,
)
from py_ecc.bls12_381 import (
G1,
add,
b,
b2,
curve_order,
double,
is_on_curve,
multiply,
twist,
)
field_modulus = field_properties["bls12_381"]["field_modulus"]
ate_loop_count = 15132376222941642752
log_ate_loop_count = 62
from typing import Optional, Tuple, Union
from py_ecc.fields import bls12_381_FQ as FQ_py_ecc
from py_ecc.utils import prime_field_inv
def linefunc(
P1: Tuple[Field, Field],
P2: Tuple[Field, Field],
T: Tuple[Field, Field]
) -> Field:
"""
Create a function representing the line between P1 and P2,
and evaluate it at T
"""
if P1 is None or P2 is None or T is None:
raise ValueError("Invalid input - no points-at-infinity allowed")
x1, y1 = P1
x2, y2 = P2
xt, yt = T
if x1 != x2:
m = (y2 - y1) / (x2 - x1)
return m * (xt - x1) - (yt - y1)
elif y1 == y2:
m = 3 * x1**2 / (2 * y1)
return m * (xt - x1) - (yt - y1)
else:
return xt - x1
def cast_point_to_fq12(
pt: Optional[Tuple[FQ, FQ]]
) -> Optional[Tuple[FQ12, FQ12]]:
if pt is None:
return None
x, y = pt
return (FQ12([x.n] + [0] * 11), FQ12([y.n] + [0] * 11))
# Check consistency of the "line function"
one = G1
two = double(G1)
three = multiply(G1, 3)
negone = multiply(G1, curve_order - 1)
negtwo = multiply(G1, curve_order - 2)
negthree = multiply(G1, curve_order - 3)
conditions = [
linefunc(one, two, one) == FQ(0),
linefunc(one, two, two) == FQ(0),
linefunc(one, two, three) != FQ(0),
linefunc(one, two, negthree) == FQ(0),
linefunc(one, negone, one) == FQ(0),
linefunc(one, negone, negone) == FQ(0),
linefunc(one, negone, two) != FQ(0),
linefunc(one, one, one) == FQ(0),
linefunc(one, one, two) != FQ(0),
linefunc(one, one, negtwo) == FQ(0),
]
if not all(conditions):
raise ValueError("Line function is inconsistent")
def miller_loop(Q: Point2D[FQ12], P: Point2D[FQ12]) -> FQ12:
cdef int i
if Q is None or P is None:
return FQ12.one()
R = Q
f = FQ12.one()
for i in range(log_ate_loop_count, -1, -1):
f = f * f * linefunc(R, R, P)
R = double(R)
if ate_loop_count & (2**i):
f = f * linefunc(R, Q, P)
R = add(R, Q)
return f ** ((field_modulus**12 - 1) // curve_order)
def pairing(Q: Point2D[FQ2], P: Point2D[FQ]) -> FQ12:
if not is_on_curve(Q, b2):
raise ValueError("Invalid input - point Q is not on the correct curve")
if not is_on_curve(P, b):
raise ValueError("Invalid input - point P is not on the correct curves")
return miller_loop(twist(Q), cast_point_to_fq12(P))
def final_exponentiate(p: FQ12) -> FQ12:
return p ** ((field_modulus**12 - 1) // curve_order)

View file

@ -1,19 +0,0 @@
"""
This is the Manual Setup for Cython extensions.
Compile with:
pip install cython setuptools
python3 setup.py build_ext --inplace
& Put the path to venv prior to the python3 if any issues.
path/to/venv/python3 setup.py build_ext --inplace
"""
from Cython.Build import cythonize
from setuptools import setup
setup(
ext_modules=cythonize("bls12_381_pairing.pyx", language_level=3)
)

View file

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

View file

@ -11,10 +11,7 @@ from core.services.prepare_tickets.get_public_key_by_config import get_public_ke
from core.services.prepare_tickets.get_pub_key import key_is_in_valid_format, get_pub_key
from core.services.failed_verification.test_if_new_key_works import test_if_new_key_works
from core.services.networking.make_url import make_url
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.get_data_from_server import get_data_from_server
# utils
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
@ -73,21 +70,12 @@ def get_new_pubkey_from_api(connection_observer: ConnectionObserver) -> dict | N
url = make_url(which_key_plan)
# the result of this is a python dictionary with single '
# api_results = get_data_from_server(url, None, connection_observer)
api_results = connect.single_endpoint(
method="get",
url=url,
observer=connection_observer,
payload=None
)
api_results = get_data_from_server(url, connection_observer)
if api_results.valid:
new_public_key = api_results.data
return new_public_key
else:
logger.error(f"API Results returned were invalid for that key.")
return None
if "data" in api_results:
new_public_key = api_results["data"]
return new_public_key
def are_keys_different(old_public_key, new_public_key) -> bool:
@ -108,7 +96,7 @@ def is_the_key_to_blame(
# invalid key
notification = f"Invalid Numbers on the public_key"
ticket_observer.notify("preparing", subject=notification)
return {"valid": False, "message": "invalid_key"}
return {"valid": True, "message": "invalid_key"}
new_public_key = get_new_pubkey_from_api(connection_observer)
@ -133,7 +121,7 @@ def is_the_key_to_blame(
if not result_of_comparison:
error_msg = "New key is the SAME as the old one."
ticket_observer.notify("preparing", subject=error_msg)
return {"valid": False, "comparison": "same"}
return {"valid": False, "message": "same"}
status_update = "New key is DIFFERENT from the old one!"
ticket_observer.notify("preparing", subject=status_update)
@ -154,9 +142,9 @@ def is_the_key_to_blame(
if quantity_results > 0:
logger.debug("Therefore, the new key works.")
return {"valid": True, "comparison": "different"}
return {"valid": True, "comparison": "different", "matters": False}
else:
logger.debug(
"Therefore, the new key doesn't help. It is different, but also produces invalid blind signatures."
)
return {"valid": False, "comparison": "different"}
return {"valid": False, "comparison": "different", "matters": False}

View file

@ -1,58 +0,0 @@
dns_script = """#!/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,117 +0,0 @@
firewall_script = """#!/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,101 +0,0 @@
setup_script = """
#!/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
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,107 +0,0 @@
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.errors.logger import logger
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import Configuration, ConnectionChoice
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
from core.observers.ConnectionObserver import ConnectionObserver
import httpx
from io import BytesIO
from typing import Optional
import hashlib
def download_file_and_verify(
target_app_name: str,
download_path: str,
target_file_hash: str,
application_version_observer: Optional[ApplicationVersionObserver] = None,
target_app_version: Optional[str] = None,
connection_observer: Optional[ConnectionObserver] = None
) -> Result:
"""
Download/stream a file, and return the BytesIO buffer
"""
################################################
# SETUP HTTP CLIENT
################################################
client = httpx_client.get_http_session()
if client is None:
client = _get_httpx_client(target_app_name=target_app_name, connection_observer=connection_observer)
################################################
# GET THE DATA
################################################
with client.stream('GET', download_path) as response:
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:
if target_app_version:
application_version_observer.notify('download_progressing', f"Downloading {target_app_name} {progress:.2f}% v: {target_app_version}")
else:
application_version_observer.notify('download_progressing', f"Downloading {target_app_name} {progress:.2f}%")
else:
error_msg = f"Could not download {target_app_name} because of a Connection Error."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
if application_version_observer is not None:
application_version_observer.notify('downloaded', f"Downloaded {target_app_name}")
response_buffer.seek(0)
################################################
# VERIFY THE HASH
################################################
real_file_hash = __calculate_file_hash(response_buffer)
if real_file_hash != target_file_hash:
error_msg = f'Application version file integrity could not be verified. We are targeting {target_file_hash}, but got {real_file_hash}'
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
else:
return Result(valid=True, data=response_buffer, message=real_file_hash)
def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client:
connection_type = ConfigurationController.get_connection_enum()
made_client = connect.make_client(connection_type, connection_observer) # always gets boolean
if not made_client:
if connection_type == ConnectionChoice.SYSTEM:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
else: # Tor:
if connection_observer:
connection_observer.notify('message', "Tor Bootstrap..")
bootstrap_results = connect.coordinate_bootstrap(connection_observer)
if not bootstrap_results.valid:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
client = httpx_client.get_http_session()
return client
def __calculate_file_hash(file):
hasher = hashlib.sha3_512()
buffer = file.read(65536)
while len(buffer) > 0:
hasher.update(buffer)
buffer = file.read(65536)
file.seek(0)
return hasher.hexdigest()

View file

@ -5,28 +5,15 @@ if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver
# services
from core.services.networking.make_url import make_url
# from core.services.networking.api_requests.step1_get_or_post import send_data_to_server
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.send_data_to_server import send_data_to_server
# use temp billing to get the plan details
def get_plan_data(
temp_billing_code: str, connection_observer: ConnectionObserver
) -> dict | ApiResponse:
# prep:
) -> dict:
which_endpoint = "/plan"
url = make_url(which_endpoint)
payload = {"temp_billing_code": temp_billing_code}
# legacy:
# reply = send_data_to_server(payload, url, connection_observer)
# send:
return connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
reply = send_data_to_server(payload, url, connection_observer)
return reply

View file

@ -1,15 +1,11 @@
# from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
# from core.Constants import Constants
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.Constants import Constants
# def get_which_billing_key() -> str:
# billing_folder = Constants.HV_TICKETING_CONFIG_HOME
# filepath = f"{billing_folder}/billing_choices.json"
def get_which_billing_key() -> str:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
# json_key = "which_key"
# which_key = get_value_from_json_file(filepath, json_key)
# return which_key
# Function moved to 'ticket_config_tools' module. This is commented out until confirmed stable later.
json_key = "which_key"
which_key = get_value_from_json_file(filepath, json_key)
return which_key

View file

@ -1,380 +0,0 @@
from core.services.networking.httpx.httpx_client import get_http_session, init_session
from core.services.helpers.download_file import download_file_and_verify
from core.utils.basic_operations.folder_tools import validate_folder_structure
from core.utils.basic_operations.does_file_exist import does_file_exist
from core.models.Result import Result, ResultError
from core.Constants import Constants
from core.errors.logger import logger
from core.utils.run_commands import run_generic_command
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
from core.models.orm_models.Dependency import Dependency
from core.models.orm_calls.dependency_calls import get_dependency_version
from core.controllers.ConfigurationController import ConfigurationController
from core.utils.basic_operations.compare_versions import version_update_required
from core.observers.ConnectionObserver import ConnectionObserver
import httpx
from io import BytesIO
from typing import Optional
import hashlib
import tarfile
import os
SUDO_SINGBOX_LOCATION = f"{Constants.SUDO_TARGET_FOLDER}/sing-box"
def setup_singbox_binary(
application_version_observer: Optional[ApplicationVersionObserver],
connection_observer: Optional[ConnectionObserver]
) -> Result:
"""
Rank:
Module's Main Orchestrator
Purpose:
Other modules can call upon this before approving the use of singbox,
it checks if it's ready, updated, and if required downloads and sets it up.
Method:
1) Checks if it's already installed & updated
If not:
2) Gets the new version if required
3) Moves the binary to the sudo protected folder
4) Updates the config JSON with the new version.
Returns On Success:
Return Object with valid=True
Returns On Failure:
No error raises, but caller needs to deal with:
ResultError.NEED_SYNC = needs sync
ResultError.MISSING_FILE = incomplete downloads, operations, or filesystem errors
ResultError.CONNECTION = failed to fetch from the API the new version
ResultError.INVALID_INPUT = rare bugs
"""
already_in_sudo_folder = does_file_exist(SUDO_SINGBOX_LOCATION)
update_result = update_needed()
# new version sql could not be done, so we can't even update without the new version info:
if not update_result.valid:
logger.info("Sync is required. We could not access the information on the most current version locally.")
return update_result # (user MUST sync)
if already_in_sudo_folder and update_result.goal_result:
# no update required:
no_update = "No update for singbox is required."
logger.info(no_update)
return Result(valid=True, message=no_update)
required_update = update_result.data
if required_update is None or not isinstance(required_update, Dependency):
error_msg = "You need to Sync, because you lack the dependency data for singbox."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.NEED_SYNC, message=error_msg)
# UPDATE FROM HERE ON
logger.info("Updating Singbox..")
# PREP VARIABLES
target_version = str(required_update.version_number)
download_path = required_update.download_path
target_file_hash = required_update.file_hash
target_folder = f"{Constants.HV_APPLICATION_DATA_HOME}/singbox/"
original_file = f"{target_folder}/{target_version}/sing-box"
# CHECK IF WE ALREADY HAVE IT LOCALLY:
if already_downloaded(which_version=target_version):
logger.info("Using already downloaded version from local files..")
return move_and_update_config(
original_file=original_file,
target_version=target_version
)
# DOWNLOAD FROM API:
file_result = download_and_verify(
download_path=download_path,
target_folder=target_folder,
target_file_hash=target_file_hash,
target_app_name="sing-box",
target_version=target_version,
application_version_observer=application_version_observer,
connection_observer=connection_observer
)
if not file_result.valid:
logger.error(f"Download or verification failed: {file_result.error_type}")
return file_result
# FINALLY MOVE FILE & UPDATE STATE:
return move_and_update_config(
original_file=original_file,
target_version=target_version
)
def move_and_update_config(original_file: str, target_version: str) -> Result:
"""
Rank:
Coordinator
Purpose:
Move the binary to the sudo protected folder.
Update the configuration that it's installed the "target_version"
Returns:
Result Object.
Wrapper should in theory handle external file errors.
"""
# SETUP/MOVE:
logger.info("Requesting permission to move to a sudo folder...")
moved_results = _move_singbox_to_sudo_folder(
original_file=original_file
)
if not moved_results.valid:
return moved_results
# TEST IT'S THERE:
setup_in_sudo_folder = does_file_exist(SUDO_SINGBOX_LOCATION)
if setup_in_sudo_folder:
# UPDATE CONFIG TO REFLECT IT:
logger.info("The move to sudo folder was successfull. We're now updating the Configuration with the newest version installed.")
updated = ConfigurationController.update_singbox_version(target_version)
if updated:
logger.info("Configuration is updated. Singbox is ready. Complete.")
return Result(valid=True, message="Ready to proceed with singbox installed properly")
else:
# ConfigurationController doesn't even return False ever, so this is essentially a full-blown breakdown
error_msg = "RARE FILESYSTEM CRISIS CAUSES INFINITE LOOP! User actually updated the singbox binary into the sudo folder, but it's not reflecting that. Please evaluate the permissions on the config, and check if you have enough space. Check the config is even there."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)
else:
error_msg = "If you denied the sudo request, it should have already given a permission error before reaching this point. So now it's possibly a corrupt filesystem or file issue."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)
def _move_singbox_to_sudo_folder(original_file: str) -> Result:
if not does_file_exist(original_file):
return Result(valid=False, error_type=ResultError.MISSING_FILE)
target_file = SUDO_SINGBOX_LOCATION
command = ['pkexec', 'install', '-D', original_file, target_file, '-o', 'root', '-m', '755']
human_readable_goal = "Move Singbox to an elevated sudo folder."
return run_generic_command(command, human_readable_goal, timeout=40)
def update_needed() -> Result:
"""
Rank:
Coordinator
Purpose:
Evaluates if an update is needed.
Returns:
Always a Result Object.
No update needed = goal_result=True
Update needed = goal_result=False
Can't complete the checks, valid=False
"""
app_name = "singbox"
rejected_values = [None, False, ""]
################################################
# WHAT YOU NEED
################################################
version_sql_query = get_dependency_version(app_name)
if not isinstance(version_sql_query, Dependency):
return Result(valid=False, error_type=ResultError.NEED_SYNC, message="You need to Sync, you lack the dependency data for singbox.")
new_version = version_sql_query.version_number
if not new_version or new_version in rejected_values:
return Result(valid=False, error_type=ResultError.NEED_SYNC, message="You need to Sync, you lack the dependency data for singbox.")
logger.info(f"The new version for {app_name} from public sync data that we need is: {new_version}")
################################################
# WHAT YOU GOT
################################################
version_installed = ConfigurationController.get_singbox_version()
logger.info(f"While the version of {app_name} we installed in our configuration is {version_installed}")
if version_installed in rejected_values:
logger.info(f"We need to update to the new {new_version}")
return Result(valid=True, goal_result=False, data=version_sql_query)
################################################
# EVALUATE
################################################
try:
need_update = version_update_required(
new_version=new_version,
version_installed=version_installed
)
except ValueError as e:
if "new_version" in str(e):
logger.error(f"The new_version is in the wrong format: {str(e)}")
return Result(valid=False, error_type=ResultError.NEED_SYNC, message="You need to Sync, the data is corrupt. Also wipe the version in the config.")
elif "version_installed" in str(e):
logger.error(f"The version_installed is in the wrong format: {str(e)}. But we can still update to the new version..")
# wipe config:
changed_config = ConfigurationController.update_singbox_version(None) # can make this dynamic if more apps are added.
if not changed_config:
logger.error("Critical Issue with wiping the config version")
need_update = True # the version_sql_query data is still valid.
else:
error_msg = f"Corrupt data, corrupt filesystem, or outright developer bug. Please contact customer support with new_version: {new_version} and version_installed {version_installed} tried to see if it should update but {str(e)}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
if need_update:
return Result(valid=True, goal_result=True, data=version_sql_query, message="update")
else:
return Result(valid=True, goal_result=False, data=version_sql_query, message="Not needed.")
def already_downloaded(which_version: str) -> bool:
"""
Prior Context:
Downloads first go to a non-sudo folder, then are moved to the sudo folder.
Purpose:
This function evaluates if we need to download it again to that non-sudo folder
"""
non_sudo_file = f"{Constants.HV_APPLICATION_DATA_HOME}/singbox/{which_version}/sing-box"
return does_file_exist(non_sudo_file)
def download_and_verify(
download_path: str,
target_folder: str,
target_file_hash: str,
target_app_name: str,
target_version: str,
application_version_observer: Optional[ApplicationVersionObserver] = None,
connection_observer: Optional[ConnectionObserver] = None
) -> Result:
"""
Purpose:
Stream download file reusing pre-existing HTTPx client, then verify the hash.
Result object error types:
ResultError.CONNECTION = couldn't connect
ResultError.INVALID_INPUT = verification hash doesn't match
"""
if application_version_observer is not None:
application_version_observer.notify('downloading', "singbox")
################################################
# GET & VERIFY
################################################
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_version,
connection_observer=connection_observer
)
if not download_result.valid:
return download_result
################################################
# SAVE IT IN CORRECT STRUCTURE
################################################
response_buffer = download_result.data
temp_dir = f"{target_folder}/temp_dir"
final_target_folder = f"{target_folder}/{target_version}"
# Create the temp folder (if it doesn't exist)
os.makedirs(temp_dir, exist_ok=True)
# Save buffer to a temp directory:
with tarfile.open(fileobj=response_buffer, mode = 'r:gz') as tar_file:
tar_file.extractall(temp_dir)
# make sure it has the file in the temp, and move it to the correct structure,
target_file_is_in_payload = validate_folder_structure(
temp_dir=temp_dir,
target_folder=final_target_folder,
target_file=target_app_name
)
if target_file_is_in_payload:
return Result(valid=True)
else:
return Result(valid=False, error_type=ResultError.MISSING_FILE)
# Legacy:
# def __calculate_file_hash(file):
# hasher = hashlib.sha3_512()
# buffer = file.read(65536)
# while len(buffer) > 0:
# hasher.update(buffer)
# buffer = file.read(65536)
# file.seek(0)
# return hasher.hexdigest()
# ################################################
# # SETUP HTTP CLIENT
# ################################################
# init_session()
# client = get_http_session()
# if client is None:
# init_session
# client = get_http_session()
################################################
# GET THE DATA
################################################
# with client.stream('GET', download_path) as response:
# 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}%")
# else:
# error_msg = f"Could not download {target_app_name} because of a Connection Error."
# logger.error(error_msg)
# return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
# if application_version_observer is not None:
# application_version_observer.notify('downloaded', f"Downloaded {target_app_name}")
# response_buffer.seek(0)
# ################################################
# # VERIFY THE HASH
# ################################################
# real_file_hash = __calculate_file_hash(response_buffer)
# if real_file_hash != target_file_hash:
# error_msg = f'Application version file integrity could not be verified. We are targeting {target_file_hash}, but got {real_file_hash}'
# logger.error(error_msg)
# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)

View file

@ -1,96 +0,0 @@
from core.Constants import Constants
from core.errors.logger import logger
from core.services.helpers.assets_as_strings.sudo_scripts.dns import dns_script
from core.services.helpers.assets_as_strings.sudo_scripts.firewall import firewall_script
# from core.services.helpers.assets_as_strings.sudo_scripts.setup_script import setup_script
# generic
from pathlib import Path
import shutil
import os
from importlib import resources
from string import Template
initial_appimage_assets = f"{Constants.APPDIR_HOME}/assets"
current_assets_folder = f"{Constants.HV_DATA_HOME}/assets"
def prep_singbox_wrapper():
script_content = resources.files('core.assets.sudo_scripts').joinpath('singbox_wrapper').read_text()
template = Template(script_content)
return template.safe_substitute(VAR1=Constants.SINGBOX_OUTPUT)
def setup_script():
return resources.files('core.assets.sudo_scripts').joinpath('setup.sh').read_text()
def assets_folder_setup():
if os.path.exists(current_assets_folder):
return True
return copy_folders(initial_appimage_assets, current_assets_folder)
def updated_assets_folder_changes():
return copy_folders(initial_appimage_assets, current_assets_folder)
def sudo_assets_folder_setup() -> bool:
current_assets_folder = f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts"
singbox_wrapper = prep_singbox_wrapper()
data = {
"dns": dns_script,
"firewall": firewall_script,
"singbox_wrapper": singbox_wrapper,
"setup.sh": setup_script()
}
# Create the folder (if it doesn't exist)
os.makedirs(current_assets_folder, exist_ok=True)
logger.info(f"[MANAGE ASSETS] Folder in Downloads is setup at {current_assets_folder}.")
# Iterate through the dictionary and write files
for filename, content in data.items():
file_path = os.path.join(current_assets_folder, filename)
logger.info(f"[MANAGE ASSETS] We are preparing {filename}.")
# Skip if file already exists
if os.path.exists(file_path):
continue
try:
with open(file_path, 'w') as f:
f.write(content.lstrip('\n'))
logger.info(f"[MANAGE ASSETS] File written for {filename}.")
except Exception as e:
logger.error(f"[MANAGE ASSETS] Failed to write to file for {filename} at {file_path} because {e}.")
return False
return True
def copy_folders(initial_appimage_assets: str, current_assets_folder: str) -> bool:
try:
os.makedirs(current_assets_folder, exist_ok=True)
for item in os.listdir(initial_appimage_assets):
src = os.path.join(initial_appimage_assets, item)
dst = os.path.join(current_assets_folder, item)
if os.path.isdir(src):
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
logger.info(f"[MANAGE ASSETS] Successfully copied over the folder.")
return True
except Exception as e:
logger.error(f"[MANAGE ASSETS] initial_appimage_assets is {initial_appimage_assets}")
logger.error(f"[MANAGE ASSETS] current_assets_folder is {current_assets_folder}")
logger.error(f"[MANAGE ASSETS] ERROR: {e}")
return False
def cleanup_setup_dir():
"""After install completes"""
delete_this_folder = f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts"
try:
shutil.rmtree(delete_this_folder)
logger.info(f"[MANAGE ASSETS] Cleaned up {delete_this_folder}")
return True
except Exception as e:
logger.error(f"[MANAGE ASSETS] We could not delete {delete_this_folder} because {e}")
return False

View file

@ -1,75 +0,0 @@
from core.services.helpers.manage_assets import sudo_assets_folder_setup
from core.utils.basic_operations.confirm_files_exist import confirm_files_and_folders_exist
from core.utils.basic_operations.does_file_exist import does_file_exist
from core.models.Result import Result, ResultError
from core.Constants import Constants
from core.errors.logger import logger
import subprocess
from pathlib import Path
import shutil
import os
from subprocess import CalledProcessError
import copy
FILES_TO_CHECK = ["setup.sh", "firewall", "dns", "singbox_wrapper"]
def auto_install_sudo_script() -> Result:
setup_dir = Path(f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts")
setup_script = Path(setup_dir, 'setup.sh')
original_linux_user = os.getlogin()
# Make sure the folder exists
files_exist = confirm_files_and_folders_exist(
folder_path=setup_dir, files_to_check=FILES_TO_CHECK
)
if not files_exist:
copied = sudo_assets_folder_setup()
if not copied:
return Result(valid=False, message="Could not copy scripts to begin.")
else:
logger.info(f"Folder setup in download folder at {setup_dir}")
else:
logger.info(f"Folder already existed in the download folder at {setup_dir}")
try:
result = subprocess.run(
['pkexec', 'env', f'ORIGINAL_FOLDER={setup_dir}', f'TARGET_FOLDER={Constants.SUDO_TARGET_FOLDER}', f'LINUX_USER={original_linux_user}', 'bash', setup_script],
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.info("Sudo Setup Ran. Now testing..")
return Result(valid=True)
else:
logger.error(f"Sudo setup setup failed: {result.stderr.strip()}")
return Result(valid=False, message=result.stderr.strip())
except CalledProcessError as e:
return Result(valid=False, message=e)
except Exception as e:
return Result(valid=False, message=e)
def test_if_in_sudo_folder() -> Result:
final_sudo_folder = Path("/opt/hydra-veil/")
_files_to_check = FILES_TO_CHECK.copy() # to not affect the other functions.
# setup script itself doesn't get copied over:
_files_to_check.remove("setup.sh")
# Now verify it copied them:
sudo_files_copied = confirm_files_and_folders_exist(
folder_path=final_sudo_folder, files_to_check=_files_to_check
)
if sudo_files_copied:
return Result(valid=True, message="Sudo Script Setup Completed Successfully!")
else:
error_msg = "We failed to find the files in the sudo folder."
logger.error(error_msg)
return Result(valid=False, message=error_msg)
def is_singbox_wrapper_ready() -> bool:
wrapper_location = f"{Constants.SUDO_TARGET_FOLDER}/singbox_wrapper"
return does_file_exist(wrapper_location)

View file

@ -1,48 +0,0 @@
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.BaseProfile import BaseProfile as Profile
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from typing import Union, Optional
import base64
def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
try:
__verify_wireguard_endpoint(profile)
except EndpointVerificationError as error:
if not EndpointVerificationError in ignore:
profile.address_security_incident()
raise error
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.')

View file

@ -1,107 +0,0 @@
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants
from core.errors.logger import logger
from typing import Union, Optional
import base64
import re
def register_wireguard_session(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver] = None
):
"""Register a WireGuard session for the given profile."""
if not profile.has_subscription():
raise MissingSubscriptionError()
if not profile.has_location():
raise MissingLocationError()
wireguard_keys = _generate_wireguard_keys()
public_key = wireguard_keys.get('public', None)
if not public_key:
logger.error("No Public Key Generated. Raising ValueError")
raise ValueError("No Public Key Generated.")
country_code = profile.location.country_code
location_code = profile.location.code
billing_code = profile.subscription.billing_code
url = f'{Constants.SP_API_BASE_URL}/countries/{country_code}/locations/{location_code}/wireguard-sessions'
payload = {
'public_key': public_key
}
api_result = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload,
billing_code=billing_code
)
if not api_result.valid:
if api_result.error_type == ErrorType.AUTHENTICATION_ERROR:
logger.error(f"Server is giving an Authentication error for billing ID: {billing_code}")
raise InvalidSubscriptionError(f"Invalid Subscription for {billing_code}")
else:
error_msg = f"Could Not Connect to API: {api_result.error_type}"
logger.error(error_msg)
raise ConnectionError(error_msg)
wireguard_configuration = api_result.data
if wireguard_configuration is None:
raise InvalidSubscriptionError()
wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private'))
profile.attach_wireguard_configuration(wireguard_configuration)
def _generate_wireguard_keys() -> dict:
"""Generate WireGuard public/private key pair."""
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()
)
def _inject_private_key(config: str, private_key: str) -> str:
"""Inject private key into WireGuard config."""
expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE)
return re.sub(expression, r'\1 ' + private_key, config)
# legacy:
# 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
# )

View file

@ -1,81 +0,0 @@
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional, Any
class BackoffStrategy(Enum):
"""How to handle retries."""
NO_RETRY = "no_retry" # Permanent error
RETRY_IMMEDIATE = "retry_immediate" # Try again in <1s (502, 503, 504)
RETRY_EXPONENTIAL = "retry_exponential" # Exponential backoff (500, 429)
RETRY_WITH_AUTH_REFRESH = "retry_with_auth_refresh" # 401 after refreshing creds
FIX_CLIENT_SIDE_INFO = "fix_client_side_info" # change payload or 405 get/post
class ErrorType(Enum):
"""Classified error categories."""
SUCCESS = "success"
INVALID_ENDPOINT = "invalid_endpoint" # 404 - could be wrong URL or middleware rejecting
INVALID_REQUEST = "invalid_request" # 400, 405 - bad method/body
AUTHENTICATION_ERROR = "authentication_error" # 401 - need credentials
AUTHORIZATION_ERROR = "authorization_error" # 403 - no permission
RATE_LIMITED = "rate_limited" # 429
SERVER_ERROR = "server_error" # 5xx transient
NETWORK_ERROR = "network_error" # Connection issues
TOR_COMMENTED_PORT = "tor_commented_port"
TOR_FILE_MISSING = "tor_file_missing"
DEFAULT_TOR_PORT_DEAD = "default_tor_port_dead"
TOR_WORKS_BUT_UNRELIABLE = "tor_works_but_unreliable"
TOR_NOT_INSTALLED = "tor_not_installed"
REFUSAL_TO_INSTALL_TOR = "refusal_to_install_tor"
PORT_OPEN = "port_open"
PORT_USED = "port_used"
TOR_ON_DIFFERENT_PORT = "tor_on_different_port"
TOR_INSTALLED_BUT_DEAD = "tor_installed_but_dead"
CANT_BOOTSTRAP = "cant_bootstrap"
TOR_NOT_WORKING = "tor_not_working"
TOR_DNS_BLOCKED = "tor_dns_blocked"
DNS_RESOLUTION = "dns_resolution"
DNS_TEMPORARY = "dns_temporary"
DNS_PERMANENT = "dns_permanent"
QUAD9_DNS_RESOLUTION = "quad9_dns_resolution"
NO_INTERNET = "no_internet"
CONNECTION_ERROR = "connection_error"
INVALID_INPUT = "invalid_input"
PERMISSION_ERROR = "permission_error"
DEVELOPER_ERROR = "developer_error"
PORT_NOT_LISTENING = "port_not_listening"
UNKNOWN = "unknown"
@dataclass
class ApiResponse:
"""Unified response from all API operations."""
valid: bool
error_type: Optional[ErrorType] = None
data: Optional[Any] = None
failures: dict = field(default_factory=dict)
message: Optional[str] = None
backoff_strategy: BackoffStrategy = BackoffStrategy.NO_RETRY
retry_now: bool = False # legacy
retry_later: bool = False # legacy
tor: bool = False
ip_address: str = None
ask_clearweb: bool = None
port: int = None
tor_needs_install: bool = False
def to_dict(self) -> dict:
"""Convert to dict for backwards compatibility."""
return {
"valid": self.valid,
"error_code": self.error_type.value if self.error_type else None,
"data": self.data,
"message": self.message,
"retry_now": self.retry_now,
"retry_later": self.retry_later,
"tor": self.tor,
"ask_clearweb": self.ask_clearweb
}
def get(self, key: str, default=None):
"""Dict-like access for backwards compatibility."""
return getattr(self, key, default)

View file

@ -1,102 +0,0 @@
from core.services.networking.api_requests.step2_execute import _execute_tor_request, _execute_regular_request
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.subtools.get_connection_type import get_connection_type
from core.services.networking.api_requests.subtools.replace_http_with_https import replace_http_with_https
from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from typing import Optional
from core.errors.exceptions import *
from core.errors.logger import logger
def get_data_from_api(
full_request_url: str,
client_observer: Optional[ClientObserver] = None,
connection_observer: Optional[ConnectionObserver] = None,
clearweb_resolved_ip: str = None,
) -> ApiResponse:
"""
GET wrapper. Desktop passes observer for updates; Android passes None.
"""
if not full_request_url:
return ApiResponse(
valid=False,
error_type=ErrorType.INVALID_INPUT,
message="Input URL is not properly configured"
)
connection_type = get_connection_type()
if connection_type == "tor":
# Tor: can push intermediate updates (desktop-only)
result_object = _execute_tor_request(
"get",
full_request_url,
None, # get request has no payload
connection_observer,
client_observer,
timeout=30,
clearweb_resolved_ip=clearweb_resolved_ip
)
else:
# Regular, no tor:
result_object = _execute_regular_request(
"get",
full_request_url,
None, # get request has no payload
connection_observer,
client_observer,
timeout=15
)
if not result_object.valid and client_observer:
error_msg = result_object.message
client_observer.notify('synchronizing', f"Connection Issue: {error_msg}")
return result_object
def send_data_to_server(
payload: dict,
original_url: str,
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
clearweb_resolved_ip: str = None,
) -> ApiResponse:
"""
POST wrapper. Same pattern as GET.
"""
url = replace_http_with_https(original_url)
connection_type = get_connection_type()
if connection_type == "tor":
result_object = _execute_tor_request(
"post",
url,
payload,
connection_observer,
client_observer,
timeout=10,
clearweb_resolved_ip=clearweb_resolved_ip
)
else:
result_object = _execute_regular_request(
"post",
url,
payload,
connection_observer,
client_observer,
timeout=5
)
# Push final error to UI if provided
if not result_object.valid and client_observer:
error_msg = result_object.message
client_observer.notify('synchronizing', f"Connection Issue: {error_msg}")
return result_object

View file

@ -1,131 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import requests
from core.Constants import Constants
# Networking Group
from core.services.networking.api_requests.step3_classify_http import classify_http_response
from core.services.networking.api_requests.step4_error_classifier import classify_request_error
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.subtools.custom_dns import prep_url_with_known_ip, load_custom_dns_resolver
# tor
from essentials.modules.TorModule import TorModule
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
from core.observers.ClientObserver import ClientObserver
# errors
from core.errors.exceptions import *
from core.errors.logger import logger
# generic
import json, os
from typing import Any, Optional
import time
def _execute_tor_request(
method: str,
url: str,
payload: Optional[dict],
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
timeout: int = 30,
clearweb_resolved_ip: str = None # if DNS via Tor is blocked
) -> ApiResponse:
"""
Tor request with optional intermediate UI feedback (desktop-only).
"""
import requests
port_number = None
tor_module = None
try:
if client_observer:
status_update = "Initializing Tor connection..."
client_observer.notify('synchronizing', status_update)
port_number = ConnectionService.get_random_available_port_number()
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
if client_observer:
status_update = f"Executing {method.upper()} via Tor..."
client_observer.notify('synchronizing', status_update)
proxies = {"http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}"}
# ================= OPTION: DNS OUTSIDE TOR. (IF DNS VIA TOR IS BLOCKED) ===============
if clearweb_resolved_ip:
CustomHostnameAdapter = load_custom_dns_resolver()
url_with_ip, original_hostname = prep_url_with_known_ip(clearweb_resolved_ip, url)
# Set up session with custom adapter for DNS resolution:
session = requests.Session()
session.mount('https://', CustomHostnameAdapter(original_hostname))
session.headers.update({"Host": original_hostname})
# Make the request to the raw IP, but tell the server it's the original domain
if method.lower() == "get":
response = session.get(url_with_ip, proxies=proxies, timeout=timeout, verify=True)
else:
response = requests.post(url_with_ip, json=payload, proxies=proxies, timeout=timeout, verify=True)
else:
# ================= USING DNS VIA TOR (normal-use) ===============
if method.lower() == "get":
response = requests.get(url, proxies=proxies, timeout=timeout)
else:
response = requests.post(url, json=payload, proxies=proxies, timeout=timeout)
# ================= Working Response ===============
result = classify_http_response(response)
except Exception as e:
logger.debug(f"Tor {method.upper()} failed inside _execute_tor_request: {type(e).__name__}: {e}")
result = classify_request_error(url, "tor", connection_observer)
finally:
if tor_module and port_number:
try:
tor_module.destroy_session(port_number)
except Exception as e:
logger.warning(f"Error destroying Tor session: {e}")
return result
def _execute_regular_request(
method: str,
url: str,
payload: Optional[dict],
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[client_observer] = None,
timeout: int = 20,
) -> ApiResponse:
"""
Regular (non-Tor) request. NO observer coupling.
Cross-platform reusable: desktop UI and Android/Kivy both consume ApiResponse.
"""
import requests
try:
logger.debug(f"Executing {method.upper()} to {url}")
if method.lower() == "get":
response = requests.get(url, timeout=timeout)
else:
response = requests.post(url, json=payload, timeout=timeout)
return classify_http_response(response)
except Exception as e:
logger.debug(f"{method.upper()} request failed: {type(e).__name__}: {e}")
return classify_request_error(url, "regular", connection_observer)

View file

@ -1,84 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import requests
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
import json
def classify_http_response(response) -> ApiResponse:
"""
Classify HTTP response status and extract error info.
"""
import requests
if not isinstance(response, requests.Response):
return ApiResponse(
valid=False,
error_type=ErrorType.NETWORK_ERROR,
message="Invalid response object"
)
print(f"We're inside classify http testing the status code of the response: {response.status_code}")
# Success case
if 200 <= response.status_code < 300:
try:
data = response.json()
return ApiResponse(valid=True, data=data)
except ValueError:
return ApiResponse(
valid=True,
data=response.text
)
# Rate limiting
if response.status_code == 429:
return ApiResponse(
valid=False,
error_type=ErrorType.RATE_LIMITED,
message="Rate limit exceeded",
retry_now=True
)
# Client errors (4xx)
if 400 <= response.status_code < 500:
try:
resp_json = response.json()
error_msg = resp_json.get("message") or resp_json.get("error") or resp_json.get("error_code")
except ValueError:
error_msg = response.text
error_map = {
404: ErrorType.INVALID_INPUT,
400: ErrorType.INVALID_INPUT,
401: ErrorType.INVALID_INPUT,
403: ErrorType.INVALID_INPUT,
405: ErrorType.INVALID_INPUT,
}
error_type = error_map.get(response.status_code, ErrorType.INVALID_INPUT)
return ApiResponse(
valid=False,
error_type=error_type,
message=error_msg or f"Client error {response.status_code}"
)
# Server errors (5xx)
if response.status_code >= 500:
retry_now = response.status_code in (502, 503, 504)
retry_later = response.status_code in (500, 501, 505)
return ApiResponse(
valid=False,
error_type=ErrorType.SERVER_ERROR,
message=f"Server error {response.status_code}",
retry_now=retry_now,
retry_later=retry_later
)
return ApiResponse(
valid=False,
error_type=ErrorType.UNKNOWN,
message=f"Unexpected status {response.status_code}"
)

View file

@ -1,91 +0,0 @@
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# diagnosis services
from core.services.networking.api_requests.subtools.internet_test import do_we_have_internet
from core.services.networking.api_requests.subtools.dns_evaluation import is_dns_problem, dns_works_via_tor
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.services.networking.api_requests.subtools.is_tor_working import is_tor_working
# errors
from core.errors.exceptions import *
from core.errors.logger import logger
from typing import Optional
def classify_request_error(
url: str,
connection_type: str,
connection_observer: ConnectionObserver,
client_observer: Optional[ClientObserver] = None,
) -> ApiResponse:
"""
Classify all error requests,
Tor Connection.
DNS
Internet Connection
"""
domain_only = extract_domain(url)
if connection_type == "tor":
# Tor-specific diagnostics
if not is_tor_working(connection_observer):
logger.debug("Tor is not working")
return ApiResponse(
valid=False,
error_type=ErrorType.TOR_NOT_WORKING,
tor=True,
message="Tor connection failed"
)
ip_via_tor = dns_works_via_tor(domain_only, connection_observer)
if not ip_via_tor:
logger.debug("DNS resolution via Tor failed")
return ApiResponse(
valid=False,
error_type=ErrorType.DNS_RESOLUTION,
tor=True,
message=f"Cannot resolve {domain_only} via Tor"
)
else:
return ApiResponse(
valid=False,
error_type=ErrorType.DNS_RESOLUTION,
tor=True,
ip_address=ip_via_tor,
message=f"For Unknown reasons, we can't connect, but we can resolve {domain_only} to {ip_via_tor} via Tor"
)
else:
# Regular (non-Tor) diagnostics
if not do_we_have_internet():
logger.error("No internet connection")
return ApiResponse(
valid=False,
tor=False,
error_type=ErrorType.NO_INTERNET,
message="No internet connectivity"
)
if is_dns_problem(domain_only, connection_observer, client_observer):
logger.error("Local DNS resolution failed")
return ApiResponse(
valid=False,
tor=False,
error_type=ErrorType.DNS_RESOLUTION,
message=f"Cannot resolve {domain_only}"
)
# Fallback: unknown error
logger.error(f"Unknown error for {connection_type} request to {domain_only}")
return ApiResponse(
valid=False,
error_type=ErrorType.UNKNOWN,
message="Request failed for unknown reason"
)

View file

@ -1,60 +0,0 @@
from core.errors.logger import logger
from core.Constants import Constants
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
from core.observers.ClientObserver import ClientObserver
# tools to solve:
from core.services.networking.api_requests.subtools.direct_dns_tools import get_DNS_then_use_it
# generic
from typing import Optional
def solve_api_problems(
api_reply_object: ApiResponse,
get_or_post: str,
url: str,
payload: str = None,
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
) -> ApiResponse:
if api_reply_object.valid:
logger.debug(f"[API SOLVER] The API call worked, so there's no reason to try to solve it.")
return api_reply_object
reason_for_error = api_reply_object.error_type
cant_be_solved = [ErrorType.QUAD9_DNS_RESOLUTION, ErrorType.UNKNOWN, ErrorType.NO_INTERNET]
if reason_for_error in cant_be_solved:
logger.debug(f"[API SOLVER] This can't be solved if the reason is {reason_for_error}")
return api_reply_object
elif reason_for_error == ErrorType.DEVELOPER_ERROR:
logger.error(f"[API SOLVER] The developer made an error {api_reply_object.message}")
return api_reply_object
elif reason_for_error == ErrorType.DNS_RESOLUTION:
logger.debug(f"[API SOLVER] We're solving a DNS resolution error {api_reply_object.message}")
if api_reply_object.tor:
dns_fix = get_DNS_then_use_it(get_or_post, url, payload, api_reply_object.ip_address, connection_observer, client_observer)
return dns_fix
else:
logger.error(f"[API SOLVER] We have NOT yet setup clearweb DNS solutions")
return api_reply_object
elif reason_for_error == ErrorType.TOR_NOT_WORKING:
api_reply_object.ask_clearweb = True
return api_reply_object
elif reason_for_error == ErrorType.NETWORK_ERROR:
if api_reply_object.tor:
api_reply_object.ask_clearweb = True
return api_reply_object
else:
logger.error(f"[API SOLVER] Unknown reason {reason_for_error} with message: {api_reply_object.message}")
return api_reply_object

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