Update operator profile creation to use vless/hysteria2 code with location from SQLite

This commit is contained in:
Zenaku 2026-07-07 16:08:54 -05:00
parent c458d4da95
commit 4b0e6c1a3f
5 changed files with 177 additions and 191 deletions

View file

@ -102,6 +102,19 @@ sudo docker compose exec laravel php artisan tinker --execute="
\$sub->save(); \$sub->save();
echo 'OK' . PHP_EOL; echo 'OK' . PHP_EOL;
" "
docker compose exec laravel php artisan tinker --execute="
\$sub = new \App\Models\Subscription();
\$sub->subscription_plan_id = 3;
\$sub->operator_id = 3;
\$sub->billing_code = 'TEST-' . strtoupper(substr(uniqid(), -4)) . '-' . strtoupper(substr(uniqid(), -4)) . '-' . strtoupper(substr(uniqid(), -4));
\$sub->payment_reference = 'bypass_' . uniqid();
\$sub->duration = 720;
\$sub->expires_at = '2027-12-31 23:59:59';
\$sub->save();
echo \$sub->billing_code . PHP_EOL;
"
``` ```
Then enable again: Then enable again:

View file

@ -35,6 +35,18 @@ if __name__ == '__main__':
]: ]:
Path(path).mkdir(parents=True, exist_ok=True, mode=0o700) Path(path).mkdir(parents=True, exist_ok=True, mode=0o700)
# Bootstrap DB if needed
try:
from core.models.manage.session_management import create_ALL_tables, create_ONLY_db_version_table
from core.models.manage.version_check import insert_new_version, get_database_version
from core.models.manage.session_management import get_session
create_ONLY_db_version_table()
if get_database_version(get_session()) is None:
create_ALL_tables()
insert_new_version(get_session(), Constants.DB_VERSION_THIS_APP_WANTS)
except Exception:
pass
sys.excepthook = _handle_exception sys.excepthook = _handle_exception
distribution = get_distribution() distribution = get_distribution()

View file

@ -79,8 +79,21 @@ def handle(arguments, main_parser):
elif arguments.subcommand == 'create': elif arguments.subcommand == 'create':
if arguments.profile_type == 'operator': if arguments.profile_type == 'operator':
connection = SystemConnection('operator', arguments.operator_id, arguments.protocol) from core.models.manage.session_management import get_session
profile = SystemProfile(arguments.id, arguments.name, None, None, connection) from core.models.orm_models.Location import Location
from sqlalchemy import select
session = get_session()
capable_field = Location.is_vless_capable if arguments.protocol == 'vless' else Location.is_hysteria2_capable
loc = session.execute(
select(Location).where(
(Location.operator_id == arguments.operator_id) &
(capable_field == True)
)
).scalar_one_or_none()
if loc is None:
main_parser.error(f'No location found for operator {arguments.operator_id} with protocol {arguments.protocol}')
connection = SystemConnection(arguments.protocol, arguments.operator_id)
profile = SystemProfile(arguments.id, arguments.name, None, loc, connection)
ProfileController.create(profile, profile_observer=profile_observer) ProfileController.create(profile, profile_observer=profile_observer)
return return
@ -119,8 +132,7 @@ def handle(arguments, main_parser):
monitor = KillswitchMonitor() monitor = KillswitchMonitor()
timer_state = {'stop': None, 'reconnecting': None, '_retrying': False} timer_state = {'stop': None, 'reconnecting': None, '_retrying': False}
obs._timer_state = timer_state obs.get_ui_state()['timer_state'] = timer_state
def _stop_timer(): def _stop_timer():
if timer_state['stop'] is not None: if timer_state['stop'] is not None:
timer_state['stop'].set() timer_state['stop'].set()
@ -155,7 +167,7 @@ def handle(arguments, main_parser):
_do_disable() _do_disable()
elif choice == '2': elif choice == '2':
try: try:
obs._timer_state['_retrying'] = True obs.get_ui_state()['timer_state']['_retrying'] = True
ProfileController.enable( ProfileController.enable(
profile, ignore=ignore, pristine=False, profile, ignore=ignore, pristine=False,
asynchronous=True, profile_observer=profile_observer, asynchronous=True, profile_observer=profile_observer,
@ -189,7 +201,7 @@ def handle(arguments, main_parser):
monitor.set_on_disconnect(_do_disable) monitor.set_on_disconnect(_do_disable)
monitor.set_on_leak(_on_leak) monitor.set_on_leak(_on_leak)
monitor.set_on_displaced(_on_displaced) monitor.set_on_displaced(_on_displaced)
obs._monitor = monitor obs.get_ui_state()['monitor'] = monitor
try: try:
ProfileController.enable( ProfileController.enable(

View file

@ -2,32 +2,29 @@ import subprocess
import threading import threading
import time import time
from core.utils.encrypted_proxy import killswitch from core.utils.encrypted_proxy import killswitch
from core.utils.encrypted_proxy.singbox import SingboxRunner from core.utils.encrypted_proxy.singbox import SingboxRunner, SingboxProcessMonitor
from core.utils.encrypted_proxy.singbox import CAUSE_DISCONNECT, CAUSE_LEAK, CAUSE_DISPLACED
drop_state: dict = {"count": 0} drop_state: dict = {"count": 0}
tunnel_state: dict = {"alive": True} tunnel_state: dict = {"alive": True}
CAUSE_DISCONNECT = 'disconnect'
CAUSE_LEAK = 'leak'
CAUSE_DISPLACED = 'displaced'
class KillswitchMonitor(SingboxProcessMonitor):
"""CLI-specific monitor: adds killswitch + drops detection."""
class KillswitchMonitor: LOG_PREFIX = "hydraveil-drop"
POLL_INTERVAL = 5
LOG_PREFIX = "hydraveil-drop"
def __init__(self): def __init__(self):
self._on_disconnect = None super().__init__(SingboxRunner())
self._on_leak = None self._on_disconnect = None
self._on_displaced = None self._on_leak = None
self._socks5_port = None self._on_displaced = None
self._session_token = None self._socks5_port = None
self._running = False self._session_token = None
self._stop_event = threading.Event() self._cause_event = threading.Event()
self._cause_event = threading.Event()
self._cause: str | None = None self._cause: str | None = None
self._last_poll_ts: str | None = None
# ── Setters ───────────────────────────────────────────────────────────────
def set_on_disconnect(self, fn): def set_on_disconnect(self, fn):
self._on_disconnect = fn self._on_disconnect = fn
@ -44,6 +41,65 @@ class KillswitchMonitor:
def set_session_token(self, token: str): def set_session_token(self, token: str):
self._session_token = token self._session_token = token
# ── Abstract impl ─────────────────────────────────────────────────────────
def on_process_alive(self) -> None:
tunnel_state["alive"] = True
def on_process_dead(self) -> None:
tunnel_state["alive"] = False
def on_monitor_stopped(self, cause: str) -> None:
if cause == CAUSE_DISCONNECT and self._on_disconnect:
self._on_disconnect()
elif cause == CAUSE_LEAK and self._on_leak:
self._on_leak()
elif cause == CAUSE_DISPLACED and self._on_displaced:
self._on_displaced()
# ── Override monitor loop to add CLI-specific checks ──────────────────────
def _monitor_loop(self) -> None:
while True:
with self._lock:
if not self._running:
break
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
if interrupted:
break
# Killswitch check
if not killswitch.status():
tunnel_state["alive"] = False
self._trigger(CAUSE_DISCONNECT)
return
# Sing-box check
alive = self._runner.is_running()
tunnel_state["alive"] = alive
if not alive:
self._trigger(CAUSE_LEAK)
return
# Session token check
if self._session_token is not None:
from core.models.system.SystemState import SystemState
current = SystemState.get()
if current is None or current.session_token != self._session_token:
tunnel_state["alive"] = False
self._trigger(CAUSE_DISPLACED)
return
self._poll_drops()
def _trigger(self, cause: str):
self._cause = cause
with self._lock:
self._running = False
self._cause_event.set()
self._stop_event.set()
def _poll_drops(self) -> None: def _poll_drops(self) -> None:
since = self._last_poll_ts or "10 seconds ago" since = self._last_poll_ts or "10 seconds ago"
try: try:
@ -66,65 +122,22 @@ class KillswitchMonitor:
finally: finally:
self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S") self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S")
def stop(self): # ── Override run_blocking to handle cause_event ───────────────────────────
self._running = False
self._stop_event.set()
def _trigger(self, cause: str): def run_blocking(self) -> None:
self._cause = cause with self._lock:
self._running = False self._running = True
self._cause_event.set()
self._stop_event.set()
def _monitor_loop(self):
while self._running:
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
if interrupted or not self._running:
break
if not killswitch.status():
tunnel_state["alive"] = False
self._trigger(CAUSE_DISCONNECT)
return
singbox_alive = SingboxRunner().is_running()
tunnel_state["alive"] = singbox_alive
if not singbox_alive:
self._trigger(CAUSE_LEAK)
return
if self._session_token is not None:
from core.models.system.SystemState import SystemState
current = SystemState.get()
if current is None or current.session_token != self._session_token:
tunnel_state["alive"] = False
self._trigger(CAUSE_DISPLACED)
return
self._poll_drops()
def run_blocking(self):
self._running = True
self._stop_event.clear() self._stop_event.clear()
self._cause_event.clear() self._cause_event.clear()
self._cause = None self._cause = None
drop_state["count"] = 0 drop_state["count"] = 0
tunnel_state["alive"] = True tunnel_state["alive"] = True
self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S") self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S")
t = threading.Thread(target=self._monitor_loop, daemon=True) t = threading.Thread(target=self._monitor_loop, daemon=True)
t.start() t.start()
self._cause_event.wait() self._cause_event.wait()
t.join(timeout=2) t.join(timeout=2)
cause = self._cause self.on_monitor_stopped(self._cause or "user_requested")
if cause == CAUSE_DISCONNECT:
if self._on_disconnect:
self._on_disconnect()
elif cause == CAUSE_LEAK:
if self._on_leak:
self._on_leak()
elif cause == CAUSE_DISPLACED:
if self._on_displaced:
self._on_displaced()

View file

@ -1,158 +1,94 @@
from core.utils.encrypted_proxy import killswitch
from core.controllers.ApplicationController import ApplicationController
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
from core.observers.ClientObserver import ClientObserver from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.InvoiceObserver import InvoiceObserver from core.observers.InvoiceObserver import InvoiceObserver
from core.observers.ProfileObserver import ProfileObserver from core.observers.ProfileObserver import ProfileObserver
from cli.ui import ( from core.services.encrypted_proxy.observer_helpers import create_ui_state, create_event_handlers
Spinner, label_ok, label_error, label_warn, from core.controllers.ApplicationController import ApplicationController
label_connected, label_disconnect, label_killswitch,
label_dns, label_ipv6, label_ipv4, progress_bar,
)
from cli.helpers import sanitize_profile
from cli.killswitch_monitor import drop_state, tunnel_state from cli.killswitch_monitor import drop_state, tunnel_state
from cli.ui import progress_bar
from cli.helpers import sanitize_profile
import pprint import pprint
# ── Observer instances ────────────────────────────────────────────────────────
application_version_observer = ApplicationVersionObserver() application_version_observer = ApplicationVersionObserver()
client_observer = ClientObserver() client_observer = ClientObserver()
connection_observer = ConnectionObserver() connection_observer = ConnectionObserver()
invoice_observer = InvoiceObserver() invoice_observer = InvoiceObserver()
profile_observer = ProfileObserver() profile_observer = ProfileObserver()
_spinner = None # ── Shared UI state (used by observer_helpers) ────────────────────────────────
_monitor = None
_timer_state = None
_ui_state = create_ui_state()
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...')) # Public refs for profile.py to access monitor and timer
application_version_observer.subscribe('download_progressing',lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r')) def get_ui_state():
application_version_observer.subscribe('downloaded', lambda event: print('\n')) return _ui_state
client_observer.subscribe('synchronizing', lambda event: print('Synchronizing...\n')) # ── Connection event handlers (from core) ─────────────────────────────────────
client_observer.subscribe('updating', lambda event: print('Updating client...'))
client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
client_observer.subscribe('updated', lambda event: print('\n'))
_handlers = create_event_handlers(_ui_state, drop_state, tunnel_state)
def _on_connecting(event): connection_observer.subscribe('connecting', _handlers['on_connecting'])
global _spinner, _timer_state connection_observer.subscribe('connected', _handlers['on_connected'])
if _timer_state and _timer_state['stop']: connection_observer.subscribe('connected_token', _handlers['on_connected_token'])
_timer_state['stop'].set() connection_observer.subscribe('disconnected', _handlers['on_disconnected'])
_timer_state['stop'] = None connection_observer.subscribe('error', _handlers['on_error'])
if _timer_state and _timer_state['reconnecting'] is None and _timer_state.get('_retrying'):
from cli.ui import connecting_spinner
_timer_state['reconnecting'] = connecting_spinner()
attempt = event.subject.get("attempt_count", "?")
total = event.subject.get("maximum_number_of_attempts", "?")
_spinner = Spinner(f"Connecting... attempt {attempt}/{total}")
_spinner.start()
def _on_connected(event):
global _spinner, _monitor, _timer_state
if _spinner:
_spinner.stop()
_spinner = None
if _timer_state and _timer_state.get('reconnecting'):
_timer_state['reconnecting'].set()
_timer_state['reconnecting'] = None
if _timer_state:
_timer_state['_retrying'] = False
d = event.subject or {}
tunnel_if = d.get('tunnel_if', '?')
server_ip = d.get('server_ip')
socks5_port = d.get('socks5_port', '?')
label_connected(f"tunnel={tunnel_if} port={socks5_port}")
label_killswitch(killswitch.status())
label_dns(
enabled=d.get('dns_enabled', True),
active=d.get('dns_active', False),
)
label_ipv6(blocked=True)
if server_ip:
label_ipv4(server_ip=server_ip, reachable=True)
if _monitor is not None:
_monitor.set_connection_data(socks5_port=d.get('socks5_port'))
if _timer_state is not None:
from cli.ui import session_timer
_timer_state['stop'] = session_timer(drop_state=drop_state, tunnel_state=tunnel_state)
def _on_connected_token(event):
global _monitor
if _monitor is not None:
token = (event.subject or {}).get('session_token')
if token:
_monitor.set_session_token(token)
def _on_disconnected(event):
global _spinner, _monitor, _timer_state
if _spinner:
_spinner.stop()
_spinner = None
if _timer_state and _timer_state['stop']:
_timer_state['stop'].set()
_timer_state['stop'] = None
if _monitor is not None:
_monitor.stop()
_monitor = None
d = event.subject or {}
label_disconnect(f"tunnel={d.get('tunnel_if','?')}")
def _on_error(event):
global _spinner, _monitor, _timer_state
if _spinner:
_spinner.stop()
_spinner = None
if _timer_state and _timer_state['stop']:
_timer_state['stop'].set()
_timer_state['stop'] = None
if _monitor is not None:
_monitor.stop()
_monitor = None
label_error(str(event.subject or "Unknown error"))
# ── Tor events ────────────────────────────────────────────────────────────────
def _on_tor_bootstrapping(event): def _on_tor_bootstrapping(event):
global _spinner from cli.ui import Spinner
_spinner = Spinner("Starting Tor...") _ui_state['spinner'] = Spinner("Starting Tor...")
_spinner.start() _ui_state['spinner'].start()
def _on_tor_bootstrap_progressing(event): def _on_tor_bootstrap_progressing(event):
pct = int(event.meta.get("progress", 0)) pct = int(event.meta.get("progress", 0))
progress_bar(pct, 100, "Tor bootstrap") progress_bar(pct, 100, "Tor bootstrap")
def _on_tor_bootstrapped(event): def _on_tor_bootstrapped(event):
global _spinner from cli.ui import label_ok
if _spinner: if _ui_state['spinner']:
_spinner.stop() _ui_state['spinner'].stop()
_spinner = None _ui_state['spinner'] = None
print() print()
label_ok("Tor ready") label_ok("Tor ready")
connection_observer.subscribe('connecting', _on_connecting)
connection_observer.subscribe('connected', _on_connected)
connection_observer.subscribe('connected_token', _on_connected_token)
connection_observer.subscribe('disconnected', _on_disconnected)
connection_observer.subscribe('error', _on_error)
connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping) connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping)
connection_observer.subscribe('tor_bootstrap_progressing', _on_tor_bootstrap_progressing) connection_observer.subscribe('tor_bootstrap_progressing', _on_tor_bootstrap_progressing)
connection_observer.subscribe('tor_bootstrapped', _on_tor_bootstrapped) connection_observer.subscribe('tor_bootstrapped', _on_tor_bootstrapped)
# ── Application version events ────────────────────────────────────────────────
application_version_observer.subscribe(
'downloading',
lambda event: print(
f'Downloading {ApplicationController.get(event.subject.application_code).name}, '
f'version {event.subject.version_number}...'
)
)
application_version_observer.subscribe(
'download_progressing',
lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r')
)
application_version_observer.subscribe('downloaded', lambda event: print('\n'))
# ── Client events ─────────────────────────────────────────────────────────────
client_observer.subscribe('synchronizing', lambda event: print('Synchronizing...\n'))
client_observer.subscribe('updating', lambda event: print('Updating client...'))
client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
client_observer.subscribe('updated', lambda event: print('\n'))
# ── Invoice events ────────────────────────────────────────────────────────────
invoice_observer.subscribe('retrieved', lambda event: print(f'\n{pprint.pp(event.subject)}\n')) invoice_observer.subscribe('retrieved', lambda event: print(f'\n{pprint.pp(event.subject)}\n'))
invoice_observer.subscribe('processing', lambda event: print('A payment has been detected and is being verified...\n')) invoice_observer.subscribe('processing', lambda event: print('A payment has been detected and is being verified...\n'))
invoice_observer.subscribe('settled', lambda event: print('The payment has been successfully verified.\n')) invoice_observer.subscribe('settled', lambda event: print('The payment has been successfully verified.\n'))
# ── Profile events ────────────────────────────────────────────────────────────
profile_observer.subscribe('created', lambda event: pprint.pp((sanitize_profile(event.subject), 'Created'))) profile_observer.subscribe('created', lambda event: pprint.pp((sanitize_profile(event.subject), 'Created')))
profile_observer.subscribe('destroyed', lambda event: pprint.pp((sanitize_profile(event.subject), 'Destroyed'))) profile_observer.subscribe('destroyed', lambda event: pprint.pp((sanitize_profile(event.subject), 'Destroyed')))
profile_observer.subscribe('disabled', lambda event: pprint.pp((sanitize_profile(event.subject), 'Disabled')) if event.meta.get('explicitly') else None) profile_observer.subscribe('disabled', lambda event: pprint.pp((sanitize_profile(event.subject), 'Disabled')) if event.meta.get('explicitly') else None)