from core.Constants import Constants from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ProfileController import ProfileController from core.controllers.SessionStateController import SessionStateController from core.controllers.SystemStateController import SystemStateController from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemState import SystemState from core.observers import ConnectionObserver from core.services.WebServiceApiService import WebServiceApiService from pathlib import Path from typing import Union, Optional import os import re import requests import socket import subprocess import tempfile import time class ConnectionController: @staticmethod def with_preferred_connection(*args, task: callable, connection_observer: Optional[ConnectionObserver] = None, **kwargs): connection = ConfigurationController.get_connection() if connection == 'system': return task(*args, **kwargs) elif connection == 'tor': return ConnectionController.__with_tor_connection(*args, task=task, connection_observer=connection_observer, **kwargs) @staticmethod def establish_connection(profile: Union[SessionProfile, SystemProfile], force: bool = False, connection_observer: Optional[ConnectionObserver] = None): connection = profile.connection if connection.needs_proxy_configuration() and not profile.has_proxy_configuration(): if profile.has_subscription(): if not profile.subscription.has_been_activated(): ProfileController.activate_subscription(profile, connection_observer=connection_observer) proxy_configuration = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_proxy_configuration, connection_observer=connection_observer) if proxy_configuration is None: raise InvalidSubscriptionError() profile.attach_proxy_configuration(proxy_configuration) else: raise MissingSubscriptionError() if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): if profile.has_subscription(): if not profile.subscription.has_been_activated(): ProfileController.activate_subscription(profile, connection_observer=connection_observer) wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.code, profile.subscription.billing_code, task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer) if wireguard_configuration is None: raise InvalidSubscriptionError() profile.attach_wireguard_configuration(wireguard_configuration) else: raise MissingSubscriptionError() if profile.is_session_profile(): return ConnectionController.establish_session_connection(profile, force=force, connection_observer=connection_observer) if profile.is_system_profile(): return ConnectionController.establish_system_connection(profile) @staticmethod def establish_session_connection(profile: SessionProfile, force: Optional[bool] = False, connection_observer: Optional[ConnectionObserver] = None): session_directory = tempfile.mkdtemp(prefix='sp-') session_state = SessionStateController.get_or_new(profile.id) port_number = None proxy_port_number = None if profile.connection.is_unprotected(): if not ConnectionController.system_uses_wireguard_interface(): if not force: raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.') else: ProfileController.disable(profile) if profile.connection.code == 'tor': port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_tor_session_connection(session_directory, port_number) session_state.network_port_numbers.append(port_number) elif profile.connection.code == 'wireguard': port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number) session_state.network_port_numbers.append(port_number) if profile.connection.masked: while proxy_port_number is None or proxy_port_number == port_number: proxy_port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_proxy_session_connection(profile, session_directory, port_number, proxy_port_number) session_state.network_port_numbers.append(proxy_port_number) if not profile.connection.is_unprotected(): ConnectionController.await_connection(proxy_port_number or port_number, connection_observer=connection_observer) SessionStateController.update_or_create(session_state) return proxy_port_number or port_number @staticmethod def establish_system_connection(profile: SystemProfile): if subprocess.getstatusoutput('pkexec --help')[0] == 127: raise OSError('The polkit toolkit does not appear to be installed.') if subprocess.getstatusoutput('wg-quick --help')[0] == 127: raise OSError('WireGuard tools does not appear to be installed.') process = subprocess.Popen(('pkexec', 'wg-quick', 'up', profile.get_wireguard_configuration_path()), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) if completed_successfully: SystemStateController.update_or_create(SystemState(profile.id)) else: ProfileController.disable(profile) process_1 = subprocess.Popen(('pkexec', 'wg-quick', 'down', profile.get_wireguard_configuration_path()), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) process_2 = subprocess.Popen(('pkexec', 'wg-quick', 'up', profile.get_wireguard_configuration_path()), stdin=process_1.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) completed_successfully = not bool(os.waitpid(process_2.pid, 0)[1] >> 8) if completed_successfully: SystemStateController.update_or_create(SystemState(profile.id)) else: raise ConnectionError('The connection could not be established.') time.sleep(1.0) @staticmethod def establish_tor_session_connection(session_directory: str, port_number: int): if subprocess.getstatusoutput('tor --help')[0] == 127: raise OSError('Tor does not appear to be installed.') tor_session_directory = f'{session_directory}/tor' Path(tor_session_directory).mkdir(exist_ok=True, mode=0o700) process = subprocess.Popen(('echo', f'DataDirectory {tor_session_directory}/tor\nSocksPort {port_number}'), stdout=subprocess.PIPE) return subprocess.Popen(('tor', '-f', '-'), stdin=process.stdout, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @staticmethod def establish_wireguard_session_connection(profile: SessionProfile, session_directory: str, port_number: int): if not profile.has_wireguard_configuration(): raise FileNotFoundError('No valid WireGuard configuration file detected.') wireguard_session_directory = f'{session_directory}/wireguard' Path(wireguard_session_directory).mkdir(exist_ok=True, mode=0o700) wireproxy_configuration_file_path = f'{wireguard_session_directory}/wireproxy.conf' Path(wireproxy_configuration_file_path).touch(exist_ok=True, mode=0o600) with open(wireproxy_configuration_file_path, 'w') as wireproxy_configuration_file: wireproxy_configuration_file.write(f'WGConfig = {profile.get_wireguard_configuration_path()}\n\n[Socks5]\nBindAddress = 127.0.0.1:{str(port_number)}\n') wireproxy_configuration_file.close() return subprocess.Popen((f'{Constants.SP_DATA_HOME}/wireproxy/wireproxy', '-c', wireproxy_configuration_file_path), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @staticmethod def establish_proxy_session_connection(profile: SessionProfile, session_directory: str, port_number: int, proxy_port_number: int): if subprocess.getstatusoutput('proxychains4 --help')[0] == 127: raise OSError('ProxyChains does not appear to be installed.') if subprocess.getstatusoutput('microsocks --help')[0] == 127: raise OSError('MicroSocks does not appear to be installed.') if profile.has_proxy_configuration(): proxy_configuration = profile.get_proxy_configuration() else: raise FileNotFoundError('No valid proxy configuration file detected.') proxy_session_directory = f'{session_directory}/proxy' Path(proxy_session_directory).mkdir(parents=True, exist_ok=True, mode=0o700) proxychains_proxy_list = '' if port_number is not None: proxychains_proxy_list = f'socks5 127.0.0.1 {port_number}\n' proxychains_proxy_list = proxychains_proxy_list + f'socks5 {proxy_configuration.ip_address} {proxy_configuration.port_number} {proxy_configuration.username} {proxy_configuration.password}' proxychains_template_file_path = f'{Constants.SP_DATA_HOME}/proxychains.ptpl' with open(proxychains_template_file_path, 'r') as proxychains_template_file: proxychains_configuration_file_path = f'{proxy_session_directory}/proxychains.conf' Path(proxychains_configuration_file_path).touch(exist_ok=True, mode=0o600) proxychains_configuration_file_contents = proxychains_template_file.read().format(proxy_list=proxychains_proxy_list) with open(proxychains_configuration_file_path, 'w') as proxychains_configuration_file: proxychains_configuration_file.write(proxychains_configuration_file_contents) proxychains_configuration_file.close() return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @staticmethod def terminate_system_connection(profile: SystemProfile): if subprocess.getstatusoutput('pkexec --help')[0] == 127: raise OSError('The polkit toolkit does not appear to be installed.') if subprocess.getstatusoutput('wg-quick --help')[0] == 127: raise OSError('WireGuard tools does not appear to be installed.') process = subprocess.Popen(('pkexec', 'wg-quick', 'down', profile.get_wireguard_configuration_path()), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) if completed_successfully or not ConnectionController.system_uses_wireguard_interface(): system_state = SystemStateController.get() system_state.dissolve() else: raise ConnectionTerminationError('The connection could not be terminated.') @staticmethod def get_proxies(port_number: int): return dict( http=f'socks5h://127.0.0.1:{port_number}', https=f'socks5h://127.0.0.1:{port_number}' ) @staticmethod def get_random_available_port_number(): socket_instance = socket.socket() socket_instance.bind(('', 0)) port_number = socket_instance.getsockname()[1] socket_instance.close() return port_number @staticmethod def await_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None): maximum_number_of_attempts = 5 retry_interval = 5.0 for retry_count in range(maximum_number_of_attempts): if connection_observer is not None: connection_observer.notify('connecting', dict( retry_interval=retry_interval, maximum_number_of_attempts=maximum_number_of_attempts, attempt_count=retry_count + 1 )) try: ConnectionController.__test_proxy_connection(port_number) return except ConnectionError: time.sleep(retry_interval) retry_count += 1 raise ConnectionError('The connection could not be established.') @staticmethod def system_uses_wireguard_interface(): if subprocess.getstatusoutput('ip --help')[0] == 127: raise OSError('The iproute2 utility package does not appear to be installed.') process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE) process_output = str(process.stdout.read()) return bool(re.search('dev wg', str(process_output))) @staticmethod def __with_tor_connection(*args, task: callable, connection_observer: Optional[ConnectionObserver] = None, **kwargs): session_directory = tempfile.mkdtemp(prefix='sp-') port_number = ConnectionController.get_random_available_port_number() process = ConnectionController.establish_tor_session_connection(session_directory, port_number) ConnectionController.await_connection(port_number, connection_observer=connection_observer) task_output = task(*args, proxies=ConnectionController.get_proxies(port_number), **kwargs) process.terminate() return task_output @staticmethod def __test_proxy_connection(port_number: int, timeout: float = 10.0): timeout = float(timeout) try: requests.get(f'{Constants.SP_API_BASE_URL}/health', timeout=timeout, proxies=ConnectionController.get_proxies(port_number)) except requests.exceptions.RequestException: raise ConnectionError('The connection could not be established.')