Compare commits
No commits in common. "07f0cf800757354591cc88044b3a108f2c02f58c" and "5923cc8a7f98d345e24682e4ce7ae0bd63bbebc1" have entirely different histories.
07f0cf8007
...
5923cc8a7f
10 changed files with 89 additions and 177 deletions
|
|
@ -1,9 +1,5 @@
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,21 @@ 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:
|
||||||
|
|
@ -30,3 +45,18 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# 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
|
||||||
|
|
@ -52,6 +51,8 @@ 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
|
||||||
|
|
||||||
|
|
@ -82,22 +83,14 @@ 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
|
||||||
|
|
||||||
elif key == 'protocol':
|
# legacy:
|
||||||
# ============= SAME VALUE =============
|
# profile.application_version.application_code = browser_type
|
||||||
if profile.connection.code == new_value:
|
# profile.application_version.version_number = browser_version
|
||||||
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)
|
|
||||||
|
|
||||||
|
elif key == 'protocol':
|
||||||
# ============= 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:
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ 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
|
||||||
|
|
@ -179,24 +177,27 @@ 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'
|
||||||
|
|
||||||
saved_backup = write_json_to_file(data=config_data, filepath=backup_path)
|
try:
|
||||||
if not saved_backup:
|
with open(backup_path, "w") as configuration_file:
|
||||||
logger.error("Backup Save was not successfull")
|
json.dump(config_data, configuration_file, indent=4)
|
||||||
|
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:
|
||||||
human_readable_goal = f"Attempt {failed_attempt_count} to write to the sudo folder"
|
logger.info(f"Trying attempt {failed_attempt_count} to write to the sudo folder..")
|
||||||
logger.info(human_readable_goal)
|
try:
|
||||||
command = ['pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744']
|
process = subprocess.Popen(('pkexec', 'install', '-D', backup_path, sudo_filepath, '-o', 'root', '-m', '744'))
|
||||||
result_object = run_generic_command(command, human_readable_goal, timeout=5)
|
configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
|
||||||
if result_object.valid:
|
except Exception as e:
|
||||||
configuration_is_attached = True
|
logger.error(e)
|
||||||
else:
|
|
||||||
failed_attempt_count += 1
|
|
||||||
|
|
||||||
|
if not configuration_is_attached:
|
||||||
|
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.')
|
||||||
|
|
@ -229,6 +230,9 @@ 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,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ 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
|
||||||
|
|
@ -30,24 +27,6 @@ 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()
|
||||||
|
|
||||||
|
|
@ -82,8 +61,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 2 seconds to see if the process id {process_id} is still alive..")
|
logger.info(f"Waiting 6 seconds to see if the process id {process_id} is still alive..")
|
||||||
time.sleep(2)
|
time.sleep(6)
|
||||||
|
|
||||||
# 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")
|
||||||
|
|
@ -196,7 +175,6 @@ 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:
|
||||||
"""
|
"""
|
||||||
|
|
@ -207,13 +185,10 @@ 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) Check if the interface is up (before we wait for timeout period for output). This gives immediate feedback.
|
4) If it's up, setup the State JSON
|
||||||
5) For a timeout period, we wait to confirm via singbox's output in a text file.
|
5) Turn on Firewall
|
||||||
6) If it's up, setup the State JSON
|
6) Turn on DNS
|
||||||
7) Test the IP address to match the server via an external API
|
7) Update the State
|
||||||
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]
|
||||||
|
|
@ -228,15 +203,6 @@ 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:
|
||||||
|
|
@ -258,17 +224,11 @@ 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 confirmed to be Up")
|
logger.info("INTERFACE IS 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
# Already activated—nothing to do
|
||||||
if profile.subscription.has_been_activated():
|
if profile.subscription.has_been_activated():
|
||||||
logger.info("Sub has already been activated")
|
logger.info("sub has already been activated, returnining true")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ==================================================
|
# ==================================================
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
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