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();
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:

View file

@ -35,6 +35,18 @@ if __name__ == '__main__':
]:
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
distribution = get_distribution()

View file

@ -79,8 +79,21 @@ def handle(arguments, main_parser):
elif arguments.subcommand == 'create':
if arguments.profile_type == 'operator':
connection = SystemConnection('operator', arguments.operator_id, arguments.protocol)
profile = SystemProfile(arguments.id, arguments.name, None, None, connection)
from core.models.manage.session_management import get_session
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)
return
@ -119,8 +132,7 @@ def handle(arguments, main_parser):
monitor = KillswitchMonitor()
timer_state = {'stop': None, 'reconnecting': None, '_retrying': False}
obs._timer_state = timer_state
obs.get_ui_state()['timer_state'] = timer_state
def _stop_timer():
if timer_state['stop'] is not None:
timer_state['stop'].set()
@ -155,7 +167,7 @@ def handle(arguments, main_parser):
_do_disable()
elif choice == '2':
try:
obs._timer_state['_retrying'] = True
obs.get_ui_state()['timer_state']['_retrying'] = True
ProfileController.enable(
profile, ignore=ignore, pristine=False,
asynchronous=True, profile_observer=profile_observer,
@ -189,7 +201,7 @@ def handle(arguments, main_parser):
monitor.set_on_disconnect(_do_disable)
monitor.set_on_leak(_on_leak)
monitor.set_on_displaced(_on_displaced)
obs._monitor = monitor
obs.get_ui_state()['monitor'] = monitor
try:
ProfileController.enable(

View file

@ -2,32 +2,29 @@ import subprocess
import threading
import time
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}
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:
POLL_INTERVAL = 5
LOG_PREFIX = "hydraveil-drop"
def __init__(self):
super().__init__(SingboxRunner())
self._on_disconnect = None
self._on_leak = None
self._on_displaced = None
self._socks5_port = None
self._session_token = None
self._running = False
self._stop_event = threading.Event()
self._cause_event = threading.Event()
self._cause: str | None = None
self._last_poll_ts: str | None = None
# ── Setters ───────────────────────────────────────────────────────────────
def set_on_disconnect(self, fn):
self._on_disconnect = fn
@ -44,6 +41,65 @@ class KillswitchMonitor:
def set_session_token(self, token: str):
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:
since = self._last_poll_ts or "10 seconds ago"
try:
@ -66,46 +122,12 @@ class KillswitchMonitor:
finally:
self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S")
def stop(self):
self._running = False
self._stop_event.set()
# ── Override run_blocking to handle cause_event ───────────────────────────
def _trigger(self, cause: str):
self._cause = cause
self._running = False
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):
def run_blocking(self) -> None:
with self._lock:
self._running = True
self._stop_event.clear()
self._cause_event.clear()
self._cause = None
@ -118,13 +140,4 @@ class KillswitchMonitor:
self._cause_event.wait()
t.join(timeout=2)
cause = self._cause
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()
self.on_monitor_stopped(self._cause or "user_requested")

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.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.InvoiceObserver import InvoiceObserver
from core.observers.ProfileObserver import ProfileObserver
from cli.ui import (
Spinner, label_ok, label_error, label_warn,
label_connected, label_disconnect, label_killswitch,
label_dns, label_ipv6, label_ipv4, progress_bar,
)
from cli.helpers import sanitize_profile
from core.services.encrypted_proxy.observer_helpers import create_ui_state, create_event_handlers
from core.controllers.ApplicationController import ApplicationController
from cli.killswitch_monitor import drop_state, tunnel_state
from cli.ui import progress_bar
from cli.helpers import sanitize_profile
import pprint
# ── Observer instances ────────────────────────────────────────────────────────
application_version_observer = ApplicationVersionObserver()
client_observer = ClientObserver()
connection_observer = ConnectionObserver()
invoice_observer = InvoiceObserver()
profile_observer = ProfileObserver()
_spinner = None
_monitor = None
_timer_state = None
# ── Shared UI state (used by observer_helpers) ────────────────────────────────
_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}...'))
application_version_observer.subscribe('download_progressing',lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
# Public refs for profile.py to access monitor and timer
def get_ui_state():
return _ui_state
# ── Connection event handlers (from core) ─────────────────────────────────────
_handlers = create_event_handlers(_ui_state, drop_state, tunnel_state)
connection_observer.subscribe('connecting', _handlers['on_connecting'])
connection_observer.subscribe('connected', _handlers['on_connected'])
connection_observer.subscribe('connected_token', _handlers['on_connected_token'])
connection_observer.subscribe('disconnected', _handlers['on_disconnected'])
connection_observer.subscribe('error', _handlers['on_error'])
# ── Tor events ────────────────────────────────────────────────────────────────
def _on_tor_bootstrapping(event):
from cli.ui import Spinner
_ui_state['spinner'] = Spinner("Starting Tor...")
_ui_state['spinner'].start()
def _on_tor_bootstrap_progressing(event):
pct = int(event.meta.get("progress", 0))
progress_bar(pct, 100, "Tor bootstrap")
def _on_tor_bootstrapped(event):
from cli.ui import label_ok
if _ui_state['spinner']:
_ui_state['spinner'].stop()
_ui_state['spinner'] = None
print()
label_ok("Tor ready")
connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping)
connection_observer.subscribe('tor_bootstrap_progressing', _on_tor_bootstrap_progressing)
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'))
def _on_connecting(event):
global _spinner, _timer_state
if _timer_state and _timer_state['stop']:
_timer_state['stop'].set()
_timer_state['stop'] = None
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"))
def _on_tor_bootstrapping(event):
global _spinner
_spinner = Spinner("Starting Tor...")
_spinner.start()
def _on_tor_bootstrap_progressing(event):
pct = int(event.meta.get("progress", 0))
progress_bar(pct, 100, "Tor bootstrap")
def _on_tor_bootstrapped(event):
global _spinner
if _spinner:
_spinner.stop()
_spinner = None
print()
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_bootstrap_progressing', _on_tor_bootstrap_progressing)
connection_observer.subscribe('tor_bootstrapped', _on_tor_bootstrapped)
# ── Invoice events ────────────────────────────────────────────────────────────
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('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('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)