Compare commits
3 commits
5923cc8a7f
...
07f0cf8007
| Author | SHA1 | Date | |
|---|---|---|---|
| 07f0cf8007 | |||
| fe19044928 | |||
| bdfb933548 |
10 changed files with 177 additions and 89 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
# ==================================================
|
||||
|
|
|
|||
49
core/utils/search_tools.py
Normal file
49
core/utils/search_tools.py
Normal 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)
|
||||
|
||||
Loading…
Reference in a new issue