322 lines
14 KiB
Python
322 lines
14 KiB
Python
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 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):
|
|
|
|
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 = 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 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_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():
|
|
|
|
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.')
|