diff --git a/core/.metadata.md b/core/.metadata.md deleted file mode 100644 index 86fc4bb..0000000 --- a/core/.metadata.md +++ /dev/null @@ -1 +0,0 @@ -this is june 13 version diff --git a/core/controllers/ConnectionController.py b/core/controllers/ConnectionController.py index 8a85ec0..d122470 100644 --- a/core/controllers/ConnectionController.py +++ b/core/controllers/ConnectionController.py @@ -1,3 +1,6 @@ +from core.errors.exceptions import * +from core.errors.logger import logger + from collections.abc import Callable from core.Constants import Constants from core.controllers.ConfigurationController import ConfigurationController @@ -58,6 +61,8 @@ class ConnectionController: connection = profile.connection + # needs_proxy_configuration comes from the child connection models + # for the system it returns false blindly. for the session is checks if the connection type is masked. if connection.needs_proxy_configuration() and not profile.has_proxy_configuration(): if profile.has_subscription(): @@ -75,6 +80,8 @@ class ConnectionController: else: raise MissingSubscriptionError() + # The needs_wireguard_configuration comes from the connection model, and can be transitioned to get it directly from the object's data + # The has_wireguard_configuration comes from each polymorph object doing an os check on if the wg config exists if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): if profile.has_subscription(): @@ -161,6 +168,7 @@ class ConnectionController: port_number = None proxy_port_number = None + # this is a check from SessionConnection of if there's a systemwide with mask if profile.connection.is_unprotected(): if not ConnectionController.system_uses_wireguard_interface(): @@ -246,42 +254,11 @@ class ConnectionController: pass if profile.connection.needs_operator_proxy(): - operator_proxy_session = profile.get_operator_proxy_session() - protocol = profile.connection.get_protocol() - - if protocol == 'vless': - from core.controllers.encrypted_proxy.VlessController import VlessController - from core.services.encrypted_proxy.vless_service import parse_vless_link - import socket - server_ip = operator_proxy_session.server_ip - if server_ip is None: - vless = parse_vless_link(operator_proxy_session.links[0]) - server_ip = socket.gethostbyname(vless['host']) - ok = VlessController(1080).enable( - operator_proxy_session.links[0], - operator_proxy_session.username, - server_ip, - connection_observer - ) - elif protocol == 'hysteria2': - from core.controllers.encrypted_proxy.HysteriaController import HysteriaController - import socket - server_ip = operator_proxy_session.server_ip - if server_ip is None: - server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host) - ok = HysteriaController(1080).enable( - operator_proxy_session.username, - operator_proxy_session.password, - operator_proxy_session.operator_hysteria2_host, - server_ip, - connection_observer - ) - else: - ok = False - + ok = ConnectionController.establish_encrypted_proxy_connection( + profile, socks5_port=1080, observer=connection_observer + ) if not ok: raise ConnectionError('The connection could not be established.') - token = SystemStateController.create(profile.id) if connection_observer is not None: connection_observer.notify('connected_token', {'session_token': token}) @@ -317,6 +294,40 @@ class ConnectionController: ConnectionController.terminate_tor_connection() time.sleep(1.0) + @staticmethod + def establish_encrypted_proxy_connection(profile, socks5_port: int = 1080, observer=None) -> bool: + """Shared encrypted proxy connection logic for CLI and GUI.""" + import socket + operator_proxy_session = profile.get_operator_proxy_session() + protocol = profile.connection.code + + if protocol == 'vless': + from core.controllers.encrypted_proxy.VlessController import VlessController + from core.services.encrypted_proxy.vless_service import parse_vless_link + server_ip = operator_proxy_session.server_ip + if server_ip is None: + vless = parse_vless_link(operator_proxy_session.links[0]) + server_ip = socket.gethostbyname(vless['host']) + return VlessController(socks5_port).enable( + operator_proxy_session.links[0], + operator_proxy_session.username, + server_ip, + observer + ) + elif protocol == 'hysteria2': + from core.controllers.encrypted_proxy.HysteriaController import HysteriaController + server_ip = operator_proxy_session.server_ip + if server_ip is None: + server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host) + return HysteriaController(socks5_port).enable( + operator_proxy_session.username, + operator_proxy_session.password, + operator_proxy_session.operator_hysteria2_host, + server_ip, + observer + ) + return False + @staticmethod def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None): @@ -336,9 +347,13 @@ class ConnectionController: @staticmethod def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None): - - tor_module = TorModule(Constants.HV_TOR_STATE_HOME) - tor_module.create_session(port_number, connection_observer) + try: + tor_module = TorModule(Constants.HV_TOR_STATE_HOME) + tor_module.create_session(port_number, connection_observer) + except TorServiceInitializationError as e: + logger.error(f"TorServiceInitializationError. Tor Can't Start: {e}") + except Exception as e: + logger.error(f"Tor Can't Start: {e}") @staticmethod def terminate_tor_session_connection(port_number: int): diff --git a/core/controllers/SubscriptionPlanController.py b/core/controllers/SubscriptionPlanController.py index f7d9b93..2180662 100644 --- a/core/controllers/SubscriptionPlanController.py +++ b/core/controllers/SubscriptionPlanController.py @@ -9,10 +9,23 @@ class SubscriptionPlanController: @staticmethod def get(connection: Union[SessionConnection, SystemConnection], duration: int): + """ + Called by: + GUI in worker.py + + Purpose: + confirm the subscription's length is valid + """ return SubscriptionPlan.find(connection, duration) @staticmethod def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None): + """ + Not used. Good candidate to be cut. + + GUI's create_interface_elements + inside duration selection page actually has hardcoded amounts + """ return SubscriptionPlan.all(connection) @staticmethod diff --git a/core/errors/exceptions.py b/core/errors/exceptions.py index 021d33f..cd81195 100644 --- a/core/errors/exceptions.py +++ b/core/errors/exceptions.py @@ -1,3 +1,7 @@ + +class TorServiceInitializationError(Exception): + pass + class ApplicationError(Exception): pass diff --git a/core/models/BaseConnection.py b/core/models/BaseConnection.py index d5f3995..da588e5 100644 --- a/core/models/BaseConnection.py +++ b/core/models/BaseConnection.py @@ -1,19 +1,15 @@ from dataclasses import dataclass from dataclasses_json import dataclass_json - - @dataclass_json @dataclass class BaseConnection: code: str - + # called by: connection controller for basic type checks on wireguard code def needs_wireguard_configuration(self): return self.code == 'wireguard' - def is_session_connection(self): return type(self).__name__ == 'SessionConnection' - def is_system_connection(self): return type(self).__name__ == 'SystemConnection' def needs_operator_proxy(self): - return False \ No newline at end of file + return False diff --git a/core/models/SubscriptionPlan.py b/core/models/SubscriptionPlan.py index 540e7fa..51eaa7b 100644 --- a/core/models/SubscriptionPlan.py +++ b/core/models/SubscriptionPlan.py @@ -53,7 +53,7 @@ class SubscriptionPlan(Model): if connection.code == 'wireguard': features_wireguard = True - if connection.code == 'operator': + if connection.code in ('operator', 'vless', 'hysteria2'): features_proxy = True Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition) @@ -79,7 +79,7 @@ class SubscriptionPlan(Model): if connection.code == 'wireguard': features_wireguard = True - if connection.code == 'operator': + if connection.code in ('operator', 'vless', 'hysteria2'): features_proxy = True return Model._query_all('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ?', SubscriptionPlan.factory, [features_proxy, features_wireguard]) diff --git a/core/models/manage/denormalize.py b/core/models/manage/denormalize.py index 795ac9c..c6e3f4c 100644 --- a/core/models/manage/denormalize.py +++ b/core/models/manage/denormalize.py @@ -27,11 +27,10 @@ def extract_from_nested(obj, yaml_keys, default=None): This function removes unnecessary nesting, by comparing it to a pre-made YAML mapping """ -def denormalize(data, which_yaml_file): +def denormalize(data, mapping_file): final_results = [] # prep the mapping config: - mapping_file = f"assets/yaml_mappings/{which_yaml_file}" with open(mapping_file) as f: mapping = yaml.safe_load(f) diff --git a/core/models/manage/session_management.py b/core/models/manage/session_management.py index 90571de..8170644 100644 --- a/core/models/manage/session_management.py +++ b/core/models/manage/session_management.py @@ -1,6 +1,6 @@ from core.errors.logger import logger from core.models.orm_models.Base import BaseModel -from sqlalchemy import create_engine +from sqlalchemy import create_engine, inspect from sqlalchemy.orm import sessionmaker import os from pathlib import Path @@ -18,9 +18,15 @@ def get_path(): """Returns XDG Base Directory path or ~/.local/share/hydra-veil""" xdg_data = os.getenv("XDG_DATA_HOME") if xdg_data: - return Path(xdg_data) / "my-app" + return Path(xdg_data) / "hydra-veil" return Path.home() / ".local" / "share" / "hydra-veil" +def does_it_exist(filepath): + if filepath.exists(): + return True + else: + return False + # ============================================================================ # GLOBAL STATE # ============================================================================ @@ -80,33 +86,42 @@ def create_ONLY_db_version_table(): raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.") from core.models.orm_models.DatabaseVersion import database_version - database_version.create(engine, checkfirst=True) - # DatabaseVersion.__table__.create(engine, checkfirst=True) + try: + database_version.create(engine) + except: + logger.info("[DB MANAGEMENT] database_version already exists, but was attempted to be made again by calling create_ONLY_db_version_table.") + + # if using without checking or try except blocks: + # database_version.create(engine, checkfirst=True) + + +def does_db_version_table_exist(): + if engine is None: + raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.") + + inspector = inspect(engine) + table_exists = inspector.has_table("database_version") + return table_exists def create_ALL_tables(): """Create all tables from BaseModel.metadata using the global engine.""" - print("starting create_ALL_tables") if engine is None: raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.") - print("passed engine test") from core.models.orm_models.Location import Location from core.models.orm_models.Operator import Operator from core.models.orm_models.CachedSync import CachedSync from core.models.orm_models.EncryptedProxy import EncryptedProxy - from core.models.SubscriptionPlan import SubscriptionPlan from core.models.session.ApplicationVersion import ApplicationVersion - try: - print("trying this") BaseModel.metadata.create_all(engine, checkfirst=True) - print("Returning True for Creating all Tables..") + logger.info("[DB MANAGEMENT] All Tables have been successfully created.") return True except: - print("couldn't make all tables") + logger.error("[DB MANAGEMENT] Fatal Error with creating all tables in the create_ALL_tables function of session management.") return False diff --git a/core/models/session/SessionConnection.py b/core/models/session/SessionConnection.py index 4e99b68..8b33c21 100644 --- a/core/models/session/SessionConnection.py +++ b/core/models/session/SessionConnection.py @@ -9,6 +9,8 @@ class SessionConnection(BaseConnection): if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'): raise ValueError('Invalid connection code.') + # called by connection controller + # doesn't even make sense, it's checking if the Session is code system. this will always be false. def is_unprotected(self): return self.code == 'system' and self.masked is False diff --git a/core/models/system/SystemConnection.py b/core/models/system/SystemConnection.py index d146002..2688e25 100644 --- a/core/models/system/SystemConnection.py +++ b/core/models/system/SystemConnection.py @@ -8,7 +8,7 @@ class SystemConnection(BaseConnection): protocol: Optional[str] = field(default=None) def __post_init__(self): - if self.code not in ('wireguard', 'operator'): + if self.code not in ('wireguard', 'operator', 'vless', 'hysteria2'): raise ValueError('Invalid connection code.') @staticmethod @@ -16,7 +16,7 @@ class SystemConnection(BaseConnection): return False def needs_operator_proxy(self): - return self.code == 'operator' + return self.code in ('operator', 'vless', 'hysteria2') def get_protocol(self): - return self.protocol \ No newline at end of file + return self.protocol if self.protocol else self.code diff --git a/core/services/networking/use_tor.py b/core/services/networking/use_tor.py index 7479a7c..18b8ddd 100644 --- a/core/services/networking/use_tor.py +++ b/core/services/networking/use_tor.py @@ -16,54 +16,98 @@ from core.errors.logger import logger import json, os from typing import Any from core.Constants import Constants - +import time def tor_get_request(custom_url: str, connection_observer: ConnectionObserver) -> dict: import requests + # ============================================================================ + # INITIALIZE TOR + # ============================================================================ port_number = ConnectionService.get_random_available_port_number() - logger.debug(f"Using Tor on port number {port_number}") + logger.debug(f"[USE TOR SERVICE] Using Tor on port number {port_number}") try: tor_module = TorModule(Constants.HV_TOR_STATE_HOME) - tor_module.create_session(port_number, connection_observer) + logger.debug(f"[USE TOR SERVICE] Tor Started a Session") - proxies = { - "http": f"socks5h://127.0.0.1:{port_number}", - "https": f"socks5h://127.0.0.1:{port_number}", - } + except TorServiceInitializationError as e: + error_msg = f"[USE TOR SERVICE] Tor failed to start: {e}" + tor_module.destroy_session(port_number) + logger.error(error_msg, exc_info=True) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results + except Exception as e: + logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}") + tor_module.destroy_session(port_number) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results - logger.debug(f"Doing Request through Tor via port {port_number}") + proxies = { + "http": f"socks5h://127.0.0.1:{port_number}", + "https": f"socks5h://127.0.0.1:{port_number}", + } + + # ============================================================================ + # USE TOR AS PROXY + # ============================================================================ + try: + logger.debug(f"[TOR GET REQUEST] Using Tor on port number {port_number}") response = requests.get(custom_url, proxies=proxies, timeout=5) - logger.debug("Request through Tor successful.") + logger.debug("[TOR GET REQUEST] Request through Tor Successful!") - # if it crashes from the above error check, then it won't destroy the proxy, + # if it crashes from an error in trying to get a JSON out of it, then we'd skip destroying the Tor session, + # so we'll destroy it now first, + logger.debug("[TOR GET REQUEST] DESTROY SESSION!") tor_module.destroy_session(port_number) - # return response - + # Now that it's safe, get a JSON out of it, and return the response dictonary_of_response = response.json() + logger.debug(f"[TOR GET REQUEST] Got Data out of the reply. {dictonary_of_response}") final_reply = {"valid": True, "data": dictonary_of_response} return final_reply except requests.exceptions.ConnectionError: + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( - custom_url, connection_observer, port_number, proxies + custom_url, connection_observer ) return problem_results except requests.exceptions.Timeout: logger.debug("Connection timed out") + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( - custom_url, connection_observer, port_number + custom_url, connection_observer ) return problem_results except requests.exceptions.HTTPError: logger.debug(f"HTTP error: {response.status_code}") + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( - custom_url, connection_observer, port_number + custom_url, connection_observer + ) + return problem_results + except requests.exceptions.ProxyError: + error_msg = "[USE TOR SERVICE] Requests Proxy Error" + tor_module.destroy_session(port_number) + logger.error(error_msg, exc_info=True) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results + + except Exception as e: + logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}") + tor_module.destroy_session(port_number) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer ) return problem_results @@ -80,41 +124,63 @@ def tor_post_request( "status": "unable_to_get" } # incase this goes to the except block, it's "defined" + # ============================================================================ + # INITIALIZE TOR + # ============================================================================ port_number = ConnectionService.get_random_available_port_number() logger.debug(f"Using Tor on port number {port_number}") try: tor_module = TorModule(Constants.HV_TOR_STATE_HOME) - tor_module.create_session(port_number, connection_observer) + except TorServiceInitializationError as e: + error_msg = f"[USE TOR SERVICE] Tor failed to start: {e}" + tor_module.destroy_session(port_number) + logger.error(error_msg, exc_info=True) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results + except Exception as e: + logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}") + tor_module.destroy_session(port_number) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results - proxies = { - "http": f"socks5h://127.0.0.1:{port_number}", - "https": f"socks5h://127.0.0.1:{port_number}", - } - - logger.debug(f"Sending data through Tor via port {port_number}") + proxies = { + "http": f"socks5h://127.0.0.1:{port_number}", + "https": f"socks5h://127.0.0.1:{port_number}", + } + logger.debug(f"Sending data through Tor via port {port_number}") + # ============================================================================ + # USE TOR AS PROXY + # ============================================================================ + try: response = requests.post(custom_url, json=payload, proxies=proxies) - logger.debug("Sending data through Tor successful.") + logger.debug("[USE TOR SERVICE] Sent data through Tor successfully. Now deleting the session..") tor_module.destroy_session(port_number) return response except requests.exceptions.ConnectionError: - error_msg = "There was a Connection Error with the Tor Module." + error_msg = "[USE TOR SERVICE] There was a Connection Error with the Use Tor Module." logger.error(error_msg, exc_info=True) logger.debug(error_msg) + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( custom_url, connection_observer ) return problem_results except requests.exceptions.Timeout: - error_msg = "Connection timed out" + error_msg = "[USE TOR SERVICE] Connection timed out" logger.error(error_msg, exc_info=True) logger.debug(error_msg) + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( custom_url, connection_observer ) @@ -122,10 +188,28 @@ def tor_post_request( except requests.exceptions.HTTPError: if isinstance(response, requests.Response): - error_msg = f"HTTP error: {response.status_code}" + error_msg = f"[USE TOR SERVICE] HTTP error: {response.status_code}" logger.error(error_msg, exc_info=True) logger.debug(error_msg) + tor_module.destroy_session(port_number) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results + + except requests.exceptions.ProxyError: + error_msg = "[USE TOR SERVICE] Requests Proxy Error" + logger.error(error_msg, exc_info=True) + tor_module.destroy_session(port_number) + problem_results = evaluate_tor_networking_problem( + custom_url, connection_observer + ) + return problem_results + + except Exception as e: + logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}") + tor_module.destroy_session(port_number) problem_results = evaluate_tor_networking_problem( custom_url, connection_observer ) diff --git a/core/services/sync/orm_methods/sync_one_orm.py b/core/services/sync/orm_methods/sync_one_orm.py index 5e87f5c..274351e 100644 --- a/core/services/sync/orm_methods/sync_one_orm.py +++ b/core/services/sync/orm_methods/sync_one_orm.py @@ -5,6 +5,7 @@ from core.models.orm_models.Base import Base from core.observers.ClientObserver import ClientObserver from core.observers.ConnectionObserver import ConnectionObserver from core.errors.logger import logger +from core.utils.basic_operations.get_parent_directory import get_parent_directory from typing import Optional import json @@ -29,8 +30,11 @@ def sync_one_orm_model( new_data = api_result.get("data") # prep data (denormalize) + parent_directory = get_parent_directory() yaml_filename = f"{which_endpoint}.yaml" - denormalized_data = denormalize(new_data, yaml_filename) + full_yaml_path = f"{parent_directory}/assets/yaml_mappings/{yaml_filename}" + + denormalized_data = denormalize(new_data, str(full_yaml_path)) if denormalized_data: # Debug Pretty print with indentation diff --git a/core/utils/basic_operations/get_parent_directory.py b/core/utils/basic_operations/get_parent_directory.py new file mode 100644 index 0000000..e3e5a81 --- /dev/null +++ b/core/utils/basic_operations/get_parent_directory.py @@ -0,0 +1,8 @@ +from pathlib import Path +import core + +def get_parent_directory(): + # Get core's parent directory + parent_directory = Path(core.__file__).parent.parent + + return str(parent_directory) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d76cfc2..023c436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sp-hydra-veil-core" -version = "2.3.6" +version = "2.3.8" authors = [ { name = "Simplified Privacy" }, ] @@ -19,7 +19,7 @@ dependencies = [ "pysocks ~= 1.7.1", "python-dateutil ~= 2.9.0.post0", "requests ~= 2.32.5", - "sp-essentials ~= 1.1.0", + "sp-essentials ~= 1.2.0", "annotated-types==0.7.0", "certifi==2026.4.22", "charset-normalizer==3.4.7", @@ -38,6 +38,7 @@ dependencies = [ "pydantic_core==2.46.3", "pydeps==3.0.6", "pytokens==0.4.1", + "PyYAML==6.0.3", "stdlib-list==0.12.0", "SQLAlchemy==2.0.51", "toolz==1.1.0",