from core.Constants import Constants from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ProfileActivationError 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 _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 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 profile.subscription.expires_at is None: 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 profile.subscription.expires_at is None: 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): 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 ProfileActivationError('The profile could not be enabled.') 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 = 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 = session_directory + '/wireguard' Path(wireguard_session_directory).mkdir(exist_ok=True, mode=0o700) wireproxy_configuration_file_path = wireguard_session_directory + '/wireproxy.conf' Path(wireproxy_configuration_file_path).touch(exist_ok=True, mode=0o600) with open(wireproxy_configuration_file_path, 'w') as output_file: print(f'WGConfig = {profile.get_wireguard_configuration_path()}\n\n[Socks5]\nBindAddress = 127.0.0.1:{str(port_number)}\n', file=output_file) return subprocess.Popen((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 = 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 = 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}' with open(Constants.SP_DATA_HOME + '/proxychains.ptpl', 'r') as proxychains_template: proxychains_configuration_file_path = proxy_session_directory + '/proxychains.conf' Path(proxychains_configuration_file_path).touch(exist_ok=True, mode=0o600) with open(proxychains_configuration_file_path, 'w') as output_file: output_file_contents = proxychains_template.read().format(proxy_list=proxychains_proxy_list) output_file.write(output_file_contents) return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @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_amount_of_retries = 10 retry_interval = 5.0 for retry_count in range(maximum_amount_of_retries): if connection_observer is not None: connection_observer.notify('connecting', dict( retry_interval=retry_interval, maximum_amount_of_retries=maximum_amount_of_retries, 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(): 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 _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.')