Compare commits

..

3 commits

10 changed files with 177 additions and 89 deletions

View file

@ -1,5 +1,9 @@
# 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
### Aug 18, 2026

View file

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

View file

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

View file

@ -1,6 +1,7 @@
# 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
@ -51,8 +52,6 @@ def update_profile(
error_msg = f"Invalid profile id of {profile_id}"
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':
profile.resolution = new_value
@ -83,14 +82,22 @@ def update_profile(
# SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc.
profile.application_version = application_version
# legacy:
# profile.application_version.application_code = browser_type
# profile.application_version.version_number = browser_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:

View file

@ -97,6 +97,7 @@ def launch_encrypted_proxy(
return start_singbox(
profile_id=profile.id,
server_ip=server_ip,
protocol_choice=profile.connection.code,
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.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
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")
backup_path = f'{backup_folder}/backup.conf.bak'
try:
with open(backup_path, "w") as configuration_file:
json.dump(config_data, configuration_file, indent=4)
logger.info("Saved to backup config path!")
except Exception as e:
logger.error(e)
saved_backup = write_json_to_file(data=config_data, filepath=backup_path)
if not saved_backup:
logger.error("Backup Save was not successfull")
return False
configuration_is_attached = False
failed_attempt_count = 0
while not configuration_is_attached and failed_attempt_count < 3:
logger.info(f"Trying attempt {failed_attempt_count} to write to the sudo folder..")
try:
process = subprocess.Popen(('pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744'))
configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
except Exception as e:
logger.error(e)
if not configuration_is_attached:
human_readable_goal = f"Attempt {failed_attempt_count} to write to the sudo folder"
logger.info(human_readable_goal)
command = ['pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744']
result_object = run_generic_command(command, human_readable_goal, timeout=5)
if result_object.valid:
configuration_is_attached = True
else:
failed_attempt_count += 1
if not configuration_is_attached:
raise ProfileModificationError('The configuration could not be attached.')
return False # redundant
@ -230,9 +229,6 @@ def post_operator_proxy(billing_code: str, protocol: str, connection_observer: C
# convert the error message here
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.utils.basic_operations import pid_tools
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.errors.logger import logger
@ -27,6 +30,24 @@ from typing import Optional
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):
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)
logger.info(f"Waiting 6 seconds to see if the process id {process_id} is still alive..")
time.sleep(6)
logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..")
time.sleep(2)
# Evaluate if running by that exact pid:
active = pid_tools.is_running(pid=process_id, process_name="sing-box")
@ -175,6 +196,7 @@ def end_singbox() -> Result:
def start_singbox(
profile_id: int,
server_ip: str,
protocol_choice: str,
connection_observer: Optional[ConnectionObserver] = None
) -> Result:
"""
@ -185,10 +207,13 @@ def start_singbox(
1) Validate inputs
2) Start the binary
3) Evaluate if the process id is still up.
4) If it's up, setup the State JSON
5) Turn on Firewall
6) Turn on DNS
7) Update the State
4) Check if the interface is up (before we wait for timeout period for output). This gives immediate feedback.
5) For a timeout period, we wait to confirm via singbox's output in a text file.
6) If it's up, setup the State JSON
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 =============
requirements = [profile_id, server_ip]
@ -203,6 +228,15 @@ def start_singbox(
return killed_pre_existing
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 =============
launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2)
if not launched.valid:
@ -224,11 +258,17 @@ def start_singbox(
logger.info(f"Is the interface up? {interface_up}")
if interface_up:
logger.info("INTERFACE IS UP")
logger.info("Interface is confirmed to be Up")
else:
logger.error("Interface is DOWN")
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 =============
# 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
if profile.subscription.has_been_activated():
logger.info("sub has already been activated, returnining true")
logger.info("Sub has already been activated")
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)