Enabled support for editing systemwide profile protocols including vless/hysteria2

This commit is contained in:
SimplifiedPrivacy 2026-10-03 11:39:57 -04:00
parent dba8d79db8
commit f2922b02a0
6 changed files with 157 additions and 70 deletions

View file

@ -5,6 +5,7 @@ from core.services.networking.systemwide.general_tools.manage_sudo_configs impor
# JSON Models
from core.models.BaseProfile import BaseProfile as Profile
from core.models.BaseProfile import ProfileType
from core.models.Subscription import Subscription
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
@ -41,6 +42,8 @@ def update_profile(
key: str,
new_value: str) -> Result:
return Result(valid=False, error_type=ResultError.UNKNOWN, message="This is a test fail")
final_data = None
logger.info(f"[UPDATE PROFILE] Recieved profile_id {profile_id}, key {key}, and new_value {new_value}.")
@ -83,33 +86,12 @@ def update_profile(
profile.application_version = application_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:
error_msg = f"{new_value} is not a systemwide choice"
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# ============= SESSION =============
else:
if new_value in SESSION_CHOICES:
profile.connection.code = new_value
if new_value == 'wireguard':
profile.connection.masked = False
logger.info("Changing the protocol..")
changed_result = change_protocol(profile, new_value)
if not changed_result.valid:
logger.error("Returning a failed change.")
return changed_result
profile = changed_result.data
# ============= LOCATION =============
elif key == "location":
@ -127,10 +109,66 @@ def update_profile(
return Result(valid=False, message="Invalid Value to Edit", error_type=ResultError.INVALID_INPUT)
logger.info("[UPDATE PROFILE] Passing to Profile model to save..")
return _literally_save(profile)
def _literally_save(profile: Profile) -> Result:
try:
profile.save()
logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..")
return Result(valid=True, data=final_data)
except:
error_msg = "Profile save failed"
except Exception as e:
error_msg = f"Profile save failed because {str(e)}"
return Result(valid=False, message=error_msg, error_type=ResultError.UNKNOWN)
def change_protocol(profile: Profile, new_value: str) -> Result:
logger.info(f"changing the protcol to {new_value}")
# ============= SAME VALUE =============
if profile.connection.code == new_value:
error_msg = f"This is NOT changing the protocol, it already was {new_value}"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
# ============= SYSTEMWIDE =============
logger.info(f"Evaluating the profile's connection type of {profile.type}")
if profile.type == ProfileType.SYSTEM:
return change_systemwide_connection(profile, new_value)
else:
return change_session_connection(profile, new_value)
def change_session_connection(profile: Profile, new_value: str) -> Result:
logger.info("This is a session profile")
if new_value not in SESSION_CHOICES:
error_msg = f"{new_value} is not a session choice"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
profile.connection.code = new_value
if new_value == 'wireguard':
profile.connection.masked = False
return Result(valid=True, data=profile)
def change_systemwide_connection(profile: Profile, new_value: str) -> Result:
logger.info("This is a systemwide profile")
if not new_value in SYSTEMWIDE_CHOICES:
error_msg = f"{new_value} is not a systemwide choice"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED)
# REMOVE PAST CONFIG:
logger.info("removing the config from sudo slot..")
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
logger.info("Literally updating the value..")
profile.connection.code = new_value
final_data = "edit_session"
return Result(valid=True, data=profile)

View file

@ -14,20 +14,15 @@ class HysteriaData(BaseModel):
password: str
hysteria2_host: str
server_ip: IPv4Address
location_country_code: str
location_city_code: str
operator_id: int
# operator_name: str = None
operator_domain: str = None
@model_validator(mode='before')
@classmethod
def denormalize(cls, data):
"""This is IF its normalized, which it may not be if the server has the same model."""
if isinstance(data, dict) and 'operator' in data:
return {
**data,
'operator_id': data['operator'].get('id'),
'operator_domain': data['operator'].get('domain'),
'hysteria2_host': data['operator'].get('hysteria2_host'),
}
return data
@ -41,6 +36,16 @@ class HysteriaData(BaseModel):
return v
# 'operator_id': data['operator'].get('id'),
# 'operator_domain': data['operator'].get('domain'),
# location_country_code: str
# location_city_code: str
# operator_id: int
# # operator_name: str = None
# operator_domain: str = None
# likely future addition:
# # @field_validator('operator_id')
# # @classmethod

View file

@ -1,4 +1,3 @@
# generic
from pydantic import BaseModel, field_validator, ValidationError, HttpUrl, ConfigDict, AnyUrl
from sqlalchemy.orm import Session
from pydantic import model_validator, ValidationInfo
@ -10,27 +9,8 @@ import validators
class VlessData(BaseModel):
model_config = ConfigDict(extra="ignore")
username: str = None
links: str # vless link
subscription_url: HttpUrl
links: str
server_ip: IPv4Address
location_country_code: str
location_city_code: str
operator_id: int
operator_domain: str = None # redundant
vless_host: str = None # redundant
@model_validator(mode='before')
@classmethod
def denormalize(cls, data):
if isinstance(data, dict) and 'operator' in data:
return {
**data,
'operator_id': data['operator'].get('id'),
'operator_domain': data['operator'].get('domain'),
'vless_host': data['operator'].get('vless_host'),
}
return data
@field_validator('links', mode='before')
@classmethod
@ -42,3 +22,24 @@ class VlessData(BaseModel):
raise ValueError('links must be a string starting with "vless://"')
return v
# username: str = None
# subscription_url: HttpUrl
# location_country_code: str
# location_city_code: str
# operator_id: int
# operator_domain: str = None # redundant
# vless_host: str = None # redundant
# @model_validator(mode='before')
# @classmethod
# def denormalize(cls, data):
# if isinstance(data, dict) and 'operator' in data:
# return {
# **data,
# # 'operator_id': data['operator'].get('id'),
# # 'operator_domain': data['operator'].get('domain'),
# # 'vless_host': data['operator'].get('vless_host'),
# }
# return data

View file

@ -30,6 +30,8 @@ from core.observers.TicketObserver import TicketObserver
import os
QUANTITY_OF_TRIES = 2
def establish_connection(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
@ -82,8 +84,13 @@ def launch_encrypted_proxy(
"""
# do they have a proxy config already?
config_path = f'{profile.get_system_config_path()}/proxy.json'
tries = 0
exists_already = os.path.isfile(config_path)
while tries < QUANTITY_OF_TRIES:
tries = tries + 1
if exists_already:
server_ip = get_value_from_json_file(filepath=config_path, category="outbounds", key="server")
else:
@ -92,15 +99,26 @@ def launch_encrypted_proxy(
connection_observer=connection_observer
)
logger.info(f"We have a server IP now of {server_ip}")
print(f"We will be aiming to establish a connection to {server_ip}")
return start_singbox(
connection_result = start_singbox(
profile_id=profile.id,
server_ip=server_ip,
protocol_choice=profile.connection.code,
connection_observer=connection_observer
)
if connection_result.valid:
return connection_result
else:
logger.info("The existing config was invalid, trying a new config..")
exists_already = False
# removed = delete_file(config_path)
# if not removed:
# return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Can't delete an invalid config.")
def setup_config_and_return_ip(
profile: Union[SessionProfile, SystemProfile],

View file

@ -38,6 +38,13 @@ def configure_singbox(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver] = None,
) -> Result:
"""
Orchestrates the setup of the config:
1. Networking to get data
2. Validate and save the data
3. Create a config with the server's data & client-side choices
4. Save the config
"""
###################################
# PREP PAYLOADS

View file

@ -0,0 +1,18 @@
from core.errors.logger import logger
from pathlib import Path
def delete_file(file_path: str | Path) -> bool:
"""
Deletes a file at the specified path.
"""
try:
# Path(file_path) handles both string and Path object inputs
# missing_ok=True prevents FileNotFoundError from being raised
Path(file_path).unlink(missing_ok=True)
return True
except (PermissionError, OSError) as e:
# Log the error for debugging purposes
logging.error(f"Failed to delete {file_path}: {e}")
return False