diff --git a/core/controllers/profile_state/update_profile.py b/core/controllers/profile_state/update_profile.py index 7d41975..79bdaaa 100644 --- a/core/controllers/profile_state/update_profile.py +++ b/core/controllers/profile_state/update_profile.py @@ -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) diff --git a/core/models/pydantic_models/HysteriaData.py b/core/models/pydantic_models/HysteriaData.py index e860c18..f4778e2 100644 --- a/core/models/pydantic_models/HysteriaData.py +++ b/core/models/pydantic_models/HysteriaData.py @@ -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 diff --git a/core/models/pydantic_models/VlessData.py b/core/models/pydantic_models/VlessData.py index dd67737..0a2f1dc 100644 --- a/core/models/pydantic_models/VlessData.py +++ b/core/models/pydantic_models/VlessData.py @@ -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 @@ -41,4 +21,25 @@ class VlessData(BaseModel): if not isinstance(v, str) or not v.startswith('vless://'): raise ValueError('links must be a string starting with "vless://"') - return v \ No newline at end of file + 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 diff --git a/core/services/networking/general_connection_tools/connection_enable.py b/core/services/networking/general_connection_tools/connection_enable.py index ff5904d..7a8ff72 100644 --- a/core/services/networking/general_connection_tools/connection_enable.py +++ b/core/services/networking/general_connection_tools/connection_enable.py @@ -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( diff --git a/core/services/networking/systemwide/encrypted_proxy/configure_singbox.py b/core/services/networking/systemwide/encrypted_proxy/configure_singbox.py index 771d051..2922f7e 100644 --- a/core/services/networking/systemwide/encrypted_proxy/configure_singbox.py +++ b/core/services/networking/systemwide/encrypted_proxy/configure_singbox.py @@ -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 diff --git a/core/utils/basic_operations/file_tools.py b/core/utils/basic_operations/file_tools.py new file mode 100644 index 0000000..f769242 --- /dev/null +++ b/core/utils/basic_operations/file_tools.py @@ -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 +