Compare commits

...

3 commits

10 changed files with 177 additions and 89 deletions

View file

@ -1,5 +1,9 @@
# Major Change Log: # Major Change Log:
# 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 # Assassin Introduced
### Aug 18, 2026 ### Aug 18, 2026

View file

@ -156,40 +156,40 @@ class ClientController:
return path return path
@staticmethod # @staticmethod
def __sync(changed_tables: list, client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None): # def __sync(changed_tables: list, client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
if "applications" in changed_tables: # if "applications" in changed_tables:
logger.info("Sync applications..") # logger.info("Sync applications..")
if client_observer is not None: # if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Browser List..') # client_observer.notify('synchronizing', 'Fetching Browser List..')
# noinspection PyProtectedMember # # noinspection PyProtectedMember
ApplicationController._sync(proxies=proxies) # ApplicationController._sync(proxies=proxies)
if "application_versions" in changed_tables: # if "application_versions" in changed_tables:
logger.info("Sync Application Versions..") # logger.info("Sync Application Versions..")
if client_observer is not None: # if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Browser Version List..') # client_observer.notify('synchronizing', 'Fetching Browser Version List..')
# noinspection PyProtectedMember # # noinspection PyProtectedMember
ApplicationVersionController._sync(proxies=proxies) # ApplicationVersionController._sync(proxies=proxies)
if "client_version" in changed_tables: # if "client_version" in changed_tables:
logger.info("Sync of client version") # logger.info("Sync of client version")
if client_observer is not None: # if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Client Version List..') # client_observer.notify('synchronizing', 'Fetching Client Version List..')
# noinspection PyProtectedMember # # noinspection PyProtectedMember
ClientVersionController._sync(proxies=proxies) # ClientVersionController._sync(proxies=proxies)
if "subscriptions" in changed_tables: # if "subscriptions" in changed_tables:
logger.info("Sync of Subscriptions") # logger.info("Sync of Subscriptions")
if client_observer is not None: # if client_observer is not None:
client_observer.notify('synchronizing', 'Fetching Subscription List..') # client_observer.notify('synchronizing', 'Fetching Subscription List..')
# noinspection PyProtectedMember # # noinspection PyProtectedMember
SubscriptionPlanController._sync(proxies=proxies) # SubscriptionPlanController._sync(proxies=proxies)
ConfigurationController.update_last_synced_at() # ConfigurationController.update_last_synced_at()
logger.info("Real Data Fetch Completed Successfully") # logger.info("Real Data Fetch Completed Successfully")
@staticmethod @staticmethod
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None): def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):

View file

@ -22,21 +22,6 @@ class LocationController:
print(critical_error) print(critical_error)
return None return None
# with get_session() as session:
# location_object = session.execute(
# select(Location)
# .where((Location.country_code == country_code) & (Location.code == city_code))
# .options(joinedload(Location.operator))
# ).scalar_one_or_none()
# return location_object
# legacy:
# Location.find(country_code, code)
@staticmethod @staticmethod
def get_all(): def get_all():
with get_session() as session: with get_session() as session:
@ -45,18 +30,3 @@ class LocationController:
.options(joinedload(Location.operator)) .options(joinedload(Location.operator))
).scalars().all() ).scalars().all()
return all_records return all_records
# legacy:
# return Location.all()
# Deprecated legacy sync,
# from core.services.WebServiceApiService import WebServiceApiService
# @staticmethod
# def _sync(proxies: Optional[dict] = None):
# locations = WebServiceApiService.get_locations(proxies)
# Location.truncate()
# Location.save_many(locations)

View file

@ -1,6 +1,7 @@
# utils # utils
from core.errors.logger import logger from core.errors.logger import logger
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.services.networking.systemwide.general_tools.manage_sudo_configs import remove_sudo_config
# JSON Models # JSON Models
from core.models.BaseProfile import BaseProfile as Profile from core.models.BaseProfile import BaseProfile as Profile
@ -51,8 +52,6 @@ def update_profile(
error_msg = f"Invalid profile id of {profile_id}" error_msg = f"Invalid profile id of {profile_id}"
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT) return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# logger.info(f"[UPDATE PROFILE] Found a relevant profile {profile_id}.")
if key == 'dimentions': if key == 'dimentions':
profile.resolution = new_value profile.resolution = new_value
@ -83,14 +82,22 @@ def update_profile(
# SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc. # SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc.
profile.application_version = application_version profile.application_version = application_version
# legacy:
# profile.application_version.application_code = browser_type
# profile.application_version.version_number = browser_version
elif key == 'protocol': 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 ============= # ============= SYSTEMWIDE =============
if profile.connection == 'system-wide': if profile.connection == 'system-wide':
if new_value in SYSTEMWIDE_CHOICES: 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 profile.connection.code = new_value
final_data = "edit_session" final_data = "edit_session"
else: else:

View file

@ -97,6 +97,7 @@ def launch_encrypted_proxy(
return start_singbox( return start_singbox(
profile_id=profile.id, profile_id=profile.id,
server_ip=server_ip, server_ip=server_ip,
protocol_choice=profile.connection.code,
connection_observer=connection_observer connection_observer=connection_observer
) )

View file

@ -4,6 +4,8 @@ from core.services.networking.httpx import connect
from core.services.networking.systemwide.encrypted_proxy.hysteria_config import build_hysteria_config from core.services.networking.systemwide.encrypted_proxy.hysteria_config import build_hysteria_config
from core.services.networking.systemwide.encrypted_proxy.vless_config import build_vless_config, parse_vless_link from core.services.networking.systemwide.encrypted_proxy.vless_config import build_vless_config, parse_vless_link
from core.utils.basic_operations.write_or_read_from_json import write_json_to_file
from core.utils.run_commands import run_generic_command
# Models # Models
from core.models.pydantic_models.HysteriaData import HysteriaData from core.models.pydantic_models.HysteriaData import HysteriaData
@ -177,28 +179,25 @@ def attach_config_to_sudo_folder(
logger.info("About to save to backup config path") logger.info("About to save to backup config path")
backup_path = f'{backup_folder}/backup.conf.bak' backup_path = f'{backup_folder}/backup.conf.bak'
try: saved_backup = write_json_to_file(data=config_data, filepath=backup_path)
with open(backup_path, "w") as configuration_file: if not saved_backup:
json.dump(config_data, configuration_file, indent=4) logger.error("Backup Save was not successfull")
logger.info("Saved to backup config path!")
except Exception as e:
logger.error(e)
return False return False
configuration_is_attached = False configuration_is_attached = False
failed_attempt_count = 0 failed_attempt_count = 0
while not configuration_is_attached and failed_attempt_count < 3: while not configuration_is_attached and failed_attempt_count < 3:
logger.info(f"Trying attempt {failed_attempt_count} to write to the sudo folder..") human_readable_goal = f"Attempt {failed_attempt_count} to write to the sudo folder"
try: logger.info(human_readable_goal)
process = subprocess.Popen(('pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744')) command = ['pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744']
configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8) result_object = run_generic_command(command, human_readable_goal, timeout=5)
except Exception as e: if result_object.valid:
logger.error(e) configuration_is_attached = True
else:
if not configuration_is_attached:
failed_attempt_count += 1 failed_attempt_count += 1
if not configuration_is_attached: if not configuration_is_attached:
raise ProfileModificationError('The configuration could not be attached.') raise ProfileModificationError('The configuration could not be attached.')
return False # redundant return False # redundant
@ -230,9 +229,6 @@ def post_operator_proxy(billing_code: str, protocol: str, connection_observer: C
# convert the error message here # convert the error message here
return False return False
# no longer needed:
# 'location_id': location_id,
# 'subscription_plan_id': subscription_plan_id,

View file

@ -5,6 +5,9 @@ from core.services.networking.systemwide import killswitch
from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
from core.utils.basic_operations import pid_tools from core.utils.basic_operations import pid_tools
from core.services.networking.general_connection_tools import ip from core.services.networking.general_connection_tools import ip
from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
from core.utils.search_tools import search_for_phrase
from core.utils.basic_operations.does_file_exist import does_file_exist
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
@ -27,6 +30,24 @@ from typing import Optional
QUANTITY_OF_ATTEMPTS = 2 QUANTITY_OF_ATTEMPTS = 2
def confirm_proxy_started(protocol_choice: str) -> bool:
SINGBOX_OUTPUT = Constants.SINGBOX_OUTPUT
if not does_file_exist(SINGBOX_OUTPUT):
return False
if protocol_choice == "vless":
timeout = 13
else:
timeout = 7
return search_for_phrase(
phrase="sing-box started",
textfile_path=SINGBOX_OUTPUT,
timeout=timeout
)
def set_dns_for_singbox(current_state: SystemState): def set_dns_for_singbox(current_state: SystemState):
do_they_even_use_systemd = dns.is_systemd_enabled() do_they_even_use_systemd = dns.is_systemd_enabled()
@ -61,8 +82,8 @@ def launch_singbox_binary(profile_id: int) -> Result:
process_id = int(activation_result.data) process_id = int(activation_result.data)
logger.info(f"Waiting 6 seconds to see if the process id {process_id} is still alive..") logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..")
time.sleep(6) time.sleep(2)
# Evaluate if running by that exact pid: # Evaluate if running by that exact pid:
active = pid_tools.is_running(pid=process_id, process_name="sing-box") active = pid_tools.is_running(pid=process_id, process_name="sing-box")
@ -175,6 +196,7 @@ def end_singbox() -> Result:
def start_singbox( def start_singbox(
profile_id: int, profile_id: int,
server_ip: str, server_ip: str,
protocol_choice: str,
connection_observer: Optional[ConnectionObserver] = None connection_observer: Optional[ConnectionObserver] = None
) -> Result: ) -> Result:
""" """
@ -185,10 +207,13 @@ def start_singbox(
1) Validate inputs 1) Validate inputs
2) Start the binary 2) Start the binary
3) Evaluate if the process id is still up. 3) Evaluate if the process id is still up.
4) If it's up, setup the State JSON 4) Check if the interface is up (before we wait for timeout period for output). This gives immediate feedback.
5) Turn on Firewall 5) For a timeout period, we wait to confirm via singbox's output in a text file.
6) Turn on DNS 6) If it's up, setup the State JSON
7) Update the State 7) Test the IP address to match the server via an external API
8) Turn on Firewall
9) Turn on DNS
10) Update the State
""" """
# ============= INPUT VALIDATION ============= # ============= INPUT VALIDATION =============
requirements = [profile_id, server_ip] requirements = [profile_id, server_ip]
@ -203,6 +228,15 @@ def start_singbox(
return killed_pre_existing return killed_pre_existing
logger.info("Pre-existing process ended.") logger.info("Pre-existing process ended.")
# ========= WIPE PRE-EXISTING LOGS =======
wiped = write_string_to_text_file(content_to_write="", file_path=Constants.SINGBOX_OUTPUT)
if not wiped:
error_msg = f"Critical Error with wiping the log at {Constants.SINGBOX_OUTPUT}."
if protocol_choice == "vless":
logger.error(f"{error_msg} which vless relies upon to see when the connection started.")
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)
logger.error(f"{error_msg} but that's okay, because {protocol_choice} doesn't use it.")
# ============= START BINARY ============= # ============= START BINARY =============
launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2) launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2)
if not launched.valid: if not launched.valid:
@ -224,11 +258,17 @@ def start_singbox(
logger.info(f"Is the interface up? {interface_up}") logger.info(f"Is the interface up? {interface_up}")
if interface_up: if interface_up:
logger.info("INTERFACE IS UP") logger.info("Interface is confirmed to be Up")
else: else:
logger.error("Interface is DOWN") logger.error("Interface is DOWN")
return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id) return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id)
# ======= CONFIRM VIA SINGBOX OUTPUT ==========
its_up = confirm_proxy_started(protocol_choice=protocol_choice)
if not its_up:
return Result(valid=False, error_type=ResultError.CONNECTION, message="After a timeout period, the singbox output is still not showing the connection having started.")
logger.info("Singbox output confirming it's up.")
# ============= SETUP STATE ============= # ============= SETUP STATE =============
# Even if the connection is dead, firewall is off, we want to save the fact we turned Singbox on, before we raise errors. # Even if the connection is dead, firewall is off, we want to save the fact we turned Singbox on, before we raise errors.

View file

@ -0,0 +1,21 @@
from core.utils.run_commands import run_generic_command
from core.models.Result import Result
from core.models.system.SystemProfile import SystemProfile
def get_target_filename(profile: SystemProfile) -> str:
if profile.connection.code == "wireguard":
return "wg.conf"
else:
return "proxy.json"
def remove_sudo_config(profile: SystemProfile) -> Result:
# FILE PATH:
base_folder = profile.get_system_config_path()
file_name = get_target_filename(profile)
sudo_filepath = f"{base_folder}/{file_name}"
# REMOVE:
command = ['pkexec', 'rm', '-f', sudo_filepath]
human_readable_goal = "Removing file from sudo folder"
return run_generic_command(command, human_readable_goal, timeout=35)

View file

@ -52,7 +52,7 @@ def activate_subscription(
# Already activated—nothing to do # Already activated—nothing to do
if profile.subscription.has_been_activated(): if profile.subscription.has_been_activated():
logger.info("sub has already been activated, returnining true") logger.info("Sub has already been activated")
return True return True
# ================================================== # ==================================================

View file

@ -0,0 +1,49 @@
import time
def search_for_phrase(
phrase: str,
textfile_path: str,
timeout: int) -> bool:
"""
Monitor a text file for a phrase.
Args:
phrase: The phrase to search for
textfile_path: Path to the text file to monitor
timeout: Maximum time in seconds to search
Returns:
True if phrase is found, False if timeout exceeded
"""
start_time = time.monotonic()
last_position = 0
while True:
# Check if timeout has been exceeded
elapsed = time.monotonic() - start_time
if elapsed >= timeout:
return False
try:
# Open file and read from last known position
with open(textfile_path, 'r') as f:
f.seek(last_position)
new_content = f.read()
# Check if phrase is in new content
if phrase in new_content:
return True
# Update position for next iteration
last_position = f.tell()
except FileNotFoundError:
# File doesn't exist yet, continue trying
pass
except Exception as e:
# Handle other file errors gracefully
pass
# Wait before checking again
time.sleep(0.2)