Enabled support for editing systemwide profile protocols including vless/hysteria2
This commit is contained in:
parent
dba8d79db8
commit
f2922b02a0
6 changed files with 157 additions and 70 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,24 +84,40 @@ 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)
|
||||
|
||||
if exists_already:
|
||||
server_ip = get_value_from_json_file(filepath=config_path, category="outbounds", key="server")
|
||||
else:
|
||||
server_ip = setup_config_and_return_ip(
|
||||
profile=profile,
|
||||
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:
|
||||
server_ip = setup_config_and_return_ip(
|
||||
profile=profile,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
|
||||
print(f"We will be aiming to establish a connection to {server_ip}")
|
||||
|
||||
connection_result = start_singbox(
|
||||
profile_id=profile.id,
|
||||
server_ip=server_ip,
|
||||
protocol_choice=profile.connection.code,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
|
||||
logger.info(f"We have a server IP now of {server_ip}")
|
||||
if connection_result.valid:
|
||||
return connection_result
|
||||
else:
|
||||
logger.info("The existing config was invalid, trying a new config..")
|
||||
exists_already = False
|
||||
|
||||
return start_singbox(
|
||||
profile_id=profile.id,
|
||||
server_ip=server_ip,
|
||||
protocol_choice=profile.connection.code,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
|
||||
# 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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
18
core/utils/basic_operations/file_tools.py
Normal file
18
core/utils/basic_operations/file_tools.py
Normal 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
|
||||
|
||||
Loading…
Reference in a new issue