Initial commit
This commit is contained in:
commit
d6c9664345
46 changed files with 2747 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/.idea
|
115
README.md
Normal file
115
README.md
Normal file
|
@ -0,0 +1,115 @@
|
|||
# sp-client-core (`v0.1.0`)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Application Data
|
||||
|
||||
* `xdg-data-resources.tar.gz`
|
||||
* `~/.local/share/simplified-privacy`
|
||||
|
||||
### System Packages
|
||||
|
||||
```bash
|
||||
sudo apt install bubblewrap iproute2 microsocks proxychains4 ratpoison tor wireguard xserver-xephyr
|
||||
```
|
||||
|
||||
### Python Packages
|
||||
|
||||
All external Python dependencies can be found in `requirements.txt`.
|
||||
|
||||
## Command Line Interface
|
||||
|
||||
For testing purposes, this version contains a command line interface that will eventually be extracted.
|
||||
|
||||
> **Important:** At the same time, said `cli` serves as a reference implementation of the library. Do not rely on `main.py` when writing your own implementation. In addition, please do not interact with logic outside the controllers.
|
||||
|
||||
All commands and subcommands feature help messages (accessible through `-h` or `--help`).
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Get Connection
|
||||
|
||||
```bash
|
||||
python3 main.py get connection
|
||||
```
|
||||
|
||||
#### Set Connection
|
||||
|
||||
```bash
|
||||
# Example 1
|
||||
python3 main.py set connection system
|
||||
|
||||
# Example 2
|
||||
python3 main.py set connection tor
|
||||
```
|
||||
|
||||
### Sync
|
||||
|
||||
```bash
|
||||
python3 main.py sync
|
||||
```
|
||||
|
||||
### Profiles
|
||||
|
||||
#### Create System Profile
|
||||
|
||||
```bash
|
||||
python3 main.py profile create system -i 1 -n 'Primary' -l 'md' -c 'wireguard'
|
||||
```
|
||||
|
||||
#### Create Session Profile
|
||||
|
||||
```bash
|
||||
# Example 1
|
||||
python3 main.py profile create session -i 2 -n 'Research' -l 'md' -a 'brave:1.63.165' -c 'tor' -r '1024x768'
|
||||
|
||||
# Example 2
|
||||
python3 main.py profile create session -i 3 -n 'Entertainment' -l 'md' -a 'chromium:122.0.6261.94-1' -c 'system' -r '1600x900'
|
||||
|
||||
# Example 3
|
||||
python3 main.py profile create session -i 4 -n 'Banking' -l 'md' -a 'firefox:123.0' -c 'wireguard' -m
|
||||
```
|
||||
|
||||
#### List Profiles
|
||||
|
||||
```bash
|
||||
python3 main.py profile list
|
||||
```
|
||||
|
||||
#### Show Profile
|
||||
|
||||
```bash
|
||||
python3 main.py profile show -i 4
|
||||
```
|
||||
|
||||
#### Destroy Profile
|
||||
|
||||
```bash
|
||||
python3 main.py profile destroy -i 4
|
||||
```
|
||||
|
||||
#### Enable Profile
|
||||
|
||||
```bash
|
||||
# Enable (halt in case of state conflicts and/or potential security issues).
|
||||
python3 main.py profile enable -i 4
|
||||
|
||||
# Enable (ignore state conflicts and/or potential security issues).
|
||||
python3 main.py profile enable -i 4 -f
|
||||
```
|
||||
|
||||
#### Disable Profile
|
||||
|
||||
```bash
|
||||
# Disable (halt in case of state conflicts and/or potential security issues).
|
||||
python3 main.py profile disable -i 4
|
||||
|
||||
# Disable (ignore state conflicts and/or potential security issues).
|
||||
python3 main.py profile disable -i 4 -f
|
||||
```
|
||||
|
||||
### Version Information
|
||||
|
||||
```bash
|
||||
python3 main.py --version
|
||||
```
|
29
core/Constants.py
Normal file
29
core/Constants.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
import os
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Constants:
|
||||
|
||||
CLIENT_VERSION: Final[str] = '1.0.0'
|
||||
HOME: Final[str] = os.path.expanduser('~')
|
||||
|
||||
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME') or os.path.join(HOME, '.config')
|
||||
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME') or os.path.join(HOME, '.local/share')
|
||||
STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME') or os.path.join(HOME, '.local/state')
|
||||
|
||||
SP_CONFIG_HOME: Final[str] = CONFIG_HOME + '/simplified-privacy'
|
||||
SP_DATA_HOME: Final[str] = DATA_HOME + '/simplified-privacy'
|
||||
SP_STATE_HOME: Final[str] = STATE_HOME + '/simplified-privacy'
|
||||
|
||||
SP_PROFILE_CONFIG_HOME: Final[str] = SP_CONFIG_HOME + '/profiles'
|
||||
SP_PROFILE_DATA_HOME: Final[str] = SP_DATA_HOME + '/profiles'
|
||||
|
||||
SP_APPLICATION_DATA_HOME: Final[str] = SP_DATA_HOME + '/applications'
|
||||
|
||||
SP_SESSION_STATE_HOME: Final[str] = SP_STATE_HOME + '/sessions'
|
||||
|
||||
SP_STORAGE_DATABASE: Final[str] = SP_DATA_HOME + '/storage.db'
|
||||
|
||||
SP_API_BASE_URL: Final[str] = 'https://api.simplifiedprivacy.is/api/v1'
|
38
core/Errors.py
Normal file
38
core/Errors.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
class UnknownConnectionTypeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileStateConflictError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileActivationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileDeactivationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingSubscriptionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSubscriptionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceExpiredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoicePaymentFailedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionUnprotectedError(Exception):
|
||||
pass
|
99
core/controllers/ApplicationController.py
Normal file
99
core/controllers/ApplicationController.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from core.Constants import Constants
|
||||
from core.controllers.SessionStateController import SessionStateController
|
||||
from core.models.session.Application import Application
|
||||
from core.models.session.ApplicationVersion import ApplicationVersion
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.session.SessionState import SessionState
|
||||
from core.observers.ProfileObserver import ProfileObserver
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
|
||||
class ApplicationController:
|
||||
|
||||
@staticmethod
|
||||
def get(code: str):
|
||||
return Application.find(code)
|
||||
|
||||
@staticmethod
|
||||
def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, profile_observer: Optional[ProfileObserver] = None):
|
||||
|
||||
persistent_state_path = f'{profile.get_data_path()}/persistent-state'
|
||||
|
||||
if not os.path.isdir(persistent_state_path) or len(os.listdir(persistent_state_path)) == 0:
|
||||
shutil.copytree(f'{version.installation_path}/resources/initial-state', persistent_state_path)
|
||||
|
||||
display = ApplicationController._find_unused_display()
|
||||
|
||||
base_initialization_file_template = open(f'/{Constants.SP_DATA_HOME}/.init.ptpl', 'r').read()
|
||||
base_initialization_file_contents = base_initialization_file_template.format(display=display, time_zone=profile.location.time_zone, sp_data_home=Constants.SP_DATA_HOME)
|
||||
|
||||
application_initialization_file_template = open(f'/{version.installation_path}/.init.ptpl', 'r').read()
|
||||
application_initialization_file_contents = application_initialization_file_template.format(application_version_home=version.installation_path, port_number=port_number or -1, home=Constants.HOME, profile_data_path=profile.get_data_path(), config_home=Constants.CONFIG_HOME, application_version_number=version.version_number)
|
||||
|
||||
session_state = SessionStateController.get_or_new(profile.id)
|
||||
SessionStateController.update_or_create(session_state)
|
||||
|
||||
initialization_file_contents = base_initialization_file_contents + application_initialization_file_contents
|
||||
initialization_file_path = f'{session_state.get_state_path()}/.init'
|
||||
|
||||
Path(initialization_file_path).touch(exist_ok=True, mode=0o600 | stat.S_IEXEC)
|
||||
open(initialization_file_path, 'w').write(initialization_file_contents)
|
||||
|
||||
fork_process_id = os.fork()
|
||||
|
||||
if not fork_process_id:
|
||||
|
||||
process = subprocess.Popen(('xinit', initialization_file_path, '--', '/usr/bin/Xephyr', '-ac', '-title', f'Simplified Privacy - {profile.name or "Unnamed Profile"}', '-screen', profile.resolution, '-no-host-grab', display), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
|
||||
session_state = SessionState(session_state.id, session_state.network_port_numbers, [process.pid])
|
||||
SessionStateController.update_or_create(session_state)
|
||||
|
||||
os.waitpid(process.pid, 0)
|
||||
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
ProfileController.disable(profile, False, profile_observer=profile_observer)
|
||||
|
||||
@staticmethod
|
||||
def fetch(proxies: Optional[dict] = None):
|
||||
|
||||
applications = WebServiceApiService.get_applications(proxies)
|
||||
supported_applications = []
|
||||
|
||||
for application in applications:
|
||||
if application.code in ('firefox', 'brave', 'chromium'):
|
||||
supported_applications.append(application)
|
||||
|
||||
Application.truncate()
|
||||
Application.save_many(applications)
|
||||
|
||||
@staticmethod
|
||||
def _find_unused_display():
|
||||
|
||||
file_names = os.listdir('/tmp/.X11-unix')
|
||||
active_displays = []
|
||||
|
||||
for file_name in file_names:
|
||||
|
||||
match_object = re.search(r'X(\d+)', file_name)
|
||||
|
||||
if match_object:
|
||||
|
||||
detected_display = int(match_object.group(1))
|
||||
|
||||
if 170 <= detected_display <= 198:
|
||||
active_displays.append(detected_display)
|
||||
|
||||
if len(active_displays) > 0:
|
||||
|
||||
unused_display = sorted(active_displays)[-1] + 1
|
||||
return ':' + str(unused_display)
|
||||
|
||||
else:
|
||||
return ':170'
|
59
core/controllers/ApplicationVersionController.py
Normal file
59
core/controllers/ApplicationVersionController.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from core.models.session.ApplicationVersion import ApplicationVersion
|
||||
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
import requests
|
||||
import shutil
|
||||
import tarfile
|
||||
|
||||
|
||||
class ApplicationVersionController:
|
||||
|
||||
@staticmethod
|
||||
def get(application_code: str, version_number: str):
|
||||
return ApplicationVersion.find(application_code, version_number)
|
||||
|
||||
@staticmethod
|
||||
def fetch(proxies: Optional[dict] = None):
|
||||
|
||||
application_versions = []
|
||||
|
||||
for application_code in 'firefox', 'brave', 'chromium':
|
||||
|
||||
application_version_subset = WebServiceApiService.get_application_versions(application_code, proxies)
|
||||
|
||||
for application_version in application_version_subset:
|
||||
application_versions.append(application_version)
|
||||
|
||||
ApplicationVersion.truncate()
|
||||
ApplicationVersion.save_many(application_versions)
|
||||
|
||||
@staticmethod
|
||||
def install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController._install, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||
|
||||
@staticmethod
|
||||
def uninstall(application_version: ApplicationVersion):
|
||||
shutil.rmtree(application_version.installation_path, ignore_errors=True)
|
||||
|
||||
@staticmethod
|
||||
def _install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, proxies: Optional[dict] = None):
|
||||
|
||||
if application_version_observer is not None:
|
||||
application_version_observer.notify('downloading', application_version)
|
||||
|
||||
if proxies is not None:
|
||||
response = requests.get(application_version.download_path, stream=True, proxies=proxies)
|
||||
else:
|
||||
response = requests.get(application_version.download_path, stream=True)
|
||||
|
||||
if response.status_code == 200:
|
||||
with tarfile.open(fileobj=BytesIO(response.content), mode='r:gz') as file:
|
||||
file.extractall(application_version.installation_path)
|
||||
|
||||
else:
|
||||
raise ConnectionError('The application version could not be downloaded.')
|
48
core/controllers/ClientController.py
Normal file
48
core/controllers/ClientController.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from core.Constants import Constants
|
||||
from core.controllers.ApplicationController import ApplicationController
|
||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||
from core.controllers.ClientVersionController import ClientVersionController
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
from core.controllers.LocationController import LocationController
|
||||
from core.controllers.SubscriptionPlanController import SubscriptionPlanController
|
||||
from core.observers import ClientObserver
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from os import path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ClientController:
|
||||
|
||||
@staticmethod
|
||||
def get_working_directory():
|
||||
|
||||
import main
|
||||
return str(path.dirname(path.abspath(main.__file__)))
|
||||
|
||||
@staticmethod
|
||||
def get_version():
|
||||
return ClientVersionController.get(Constants.CLIENT_VERSION)
|
||||
|
||||
@staticmethod
|
||||
def can_be_updated():
|
||||
return not ClientVersionController.is_latest(ClientController.get_version())
|
||||
|
||||
@staticmethod
|
||||
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
ConnectionController.with_preferred_connection(task=ClientController._sync, client_observer=client_observer, connection_observer=connection_observer)
|
||||
|
||||
@staticmethod
|
||||
def _sync(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
|
||||
|
||||
if client_observer is not None:
|
||||
client_observer.notify('synchronizing')
|
||||
|
||||
ApplicationController.fetch(proxies)
|
||||
ApplicationVersionController.fetch(proxies)
|
||||
ClientVersionController.fetch(proxies)
|
||||
LocationController.fetch(proxies)
|
||||
SubscriptionPlanController.fetch(proxies)
|
||||
|
||||
ConfigurationController.update_last_synced_at()
|
30
core/controllers/ClientVersionController.py
Normal file
30
core/controllers/ClientVersionController.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from core.models.ClientVersion import ClientVersion
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ClientVersionController:
|
||||
|
||||
@staticmethod
|
||||
def get_latest():
|
||||
return ClientVersion.latest()
|
||||
|
||||
@staticmethod
|
||||
def is_latest(client_version):
|
||||
return client_version.version_number == ClientVersion.latest().version_number
|
||||
|
||||
@staticmethod
|
||||
def get(version_number: str):
|
||||
return ClientVersion.find(version_number)
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return ClientVersion.all()
|
||||
|
||||
@staticmethod
|
||||
def fetch(proxies: Optional[dict] = None):
|
||||
|
||||
client_versions = WebServiceApiService.get_client_versions(proxies)
|
||||
|
||||
ClientVersion.truncate()
|
||||
ClientVersion.save_many(client_versions)
|
76
core/controllers/ConfigurationController.py
Normal file
76
core/controllers/ConfigurationController.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from core.Errors import UnknownConnectionTypeError
|
||||
from core.models.Configuration import Configuration
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ConfigurationController:
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
return Configuration.get()
|
||||
|
||||
@staticmethod
|
||||
def get_or_new():
|
||||
|
||||
configuration = ConfigurationController.get()
|
||||
|
||||
if configuration is None:
|
||||
return Configuration()
|
||||
|
||||
return configuration
|
||||
|
||||
@staticmethod
|
||||
def get_connection():
|
||||
|
||||
configuration = ConfigurationController.get()
|
||||
|
||||
if configuration is None or configuration.connection not in ['system', 'tor']:
|
||||
raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
|
||||
|
||||
return configuration.connection
|
||||
|
||||
@staticmethod
|
||||
def set_connection(connection: Optional[str] = None):
|
||||
|
||||
configuration = ConfigurationController.get_or_new()
|
||||
configuration.connection = connection
|
||||
configuration.save(configuration)
|
||||
|
||||
@staticmethod
|
||||
def get_auto_sync_enabled():
|
||||
|
||||
configuration = ConfigurationController.get()
|
||||
|
||||
if configuration is None:
|
||||
return False
|
||||
|
||||
return configuration.auto_sync_enabled
|
||||
|
||||
@staticmethod
|
||||
def set_auto_sync_enabled(enable_auto_sync: Optional[bool] = None):
|
||||
|
||||
configuration = ConfigurationController.get_or_new()
|
||||
configuration.auto_sync_enabled = enable_auto_sync
|
||||
configuration.save(configuration)
|
||||
|
||||
@staticmethod
|
||||
def get_last_synced_at():
|
||||
|
||||
configuration = ConfigurationController.get()
|
||||
|
||||
if configuration is None:
|
||||
return None
|
||||
|
||||
return configuration.last_synced_at
|
||||
|
||||
@staticmethod
|
||||
def update_last_synced_at():
|
||||
|
||||
configuration = ConfigurationController.get_or_new()
|
||||
configuration.last_synced_at = datetime.now(timezone.utc)
|
||||
Configuration.save(configuration)
|
||||
|
||||
@staticmethod
|
||||
def update_or_create(configuration):
|
||||
Configuration.save(configuration)
|
293
core/controllers/ConnectionController.py
Normal file
293
core/controllers/ConnectionController.py
Normal file
|
@ -0,0 +1,293 @@
|
|||
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.')
|
79
core/controllers/InvoiceController.py
Normal file
79
core/controllers/InvoiceController.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
from core.Errors import InvoiceExpiredError, InvoicePaymentFailedError, InvoiceNotFoundError
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.observers.InvoiceObserver import InvoiceObserver
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
|
||||
class InvoiceController:
|
||||
|
||||
@staticmethod
|
||||
def get(billing_code: str, proxies: Optional[dict] = None):
|
||||
return WebServiceApiService.get_invoice(billing_code, proxies)
|
||||
|
||||
@staticmethod
|
||||
def handle_payment(billing_code: str, invoice_observer: InvoiceObserver = None, connection_observer: ConnectionObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
return ConnectionController.with_preferred_connection(billing_code, task=InvoiceController._handle_payment, invoice_observer=invoice_observer, connection_observer=connection_observer)
|
||||
|
||||
@staticmethod
|
||||
def _handle_payment(billing_code: str, invoice_observer: Optional[InvoiceObserver] = None, proxies: Optional[dict] = None):
|
||||
|
||||
invoice = None
|
||||
|
||||
for index in range(1, 10):
|
||||
|
||||
invoice = WebServiceApiService.get_invoice(billing_code, proxies)
|
||||
|
||||
if invoice is None:
|
||||
time.sleep(5.0)
|
||||
else:
|
||||
break
|
||||
|
||||
if invoice is None:
|
||||
raise InvoiceNotFoundError('The invoice in question could not be found.')
|
||||
|
||||
invoice.status = 'new'
|
||||
|
||||
if invoice_observer is not None:
|
||||
invoice_observer.notify('retrieved', invoice)
|
||||
|
||||
while invoice.status == 'new':
|
||||
|
||||
invoice = WebServiceApiService.get_invoice(billing_code, proxies)
|
||||
time.sleep(15.0)
|
||||
|
||||
if invoice.status == 'expired':
|
||||
raise InvoiceExpiredError('The invoice in question has expired.')
|
||||
|
||||
if invoice.status == 'processing':
|
||||
|
||||
if invoice_observer is not None:
|
||||
invoice_observer.notify('processing', invoice)
|
||||
|
||||
while invoice.status == 'processing':
|
||||
|
||||
invoice = WebServiceApiService.get_invoice(billing_code, proxies)
|
||||
time.sleep(15.0)
|
||||
|
||||
if invoice.status != 'settled':
|
||||
raise InvoicePaymentFailedError('The invoice payment has failed. Please contact support.')
|
||||
|
||||
else:
|
||||
|
||||
if invoice_observer is not None:
|
||||
invoice_observer.notify('settled', invoice)
|
||||
|
||||
for attempt in range(1, 10):
|
||||
|
||||
subscription = WebServiceApiService.get_subscription(billing_code, proxies)
|
||||
|
||||
if subscription is not None:
|
||||
return subscription
|
||||
|
||||
else:
|
||||
time.sleep(15.0)
|
||||
|
||||
return None
|
22
core/controllers/LocationController.py
Normal file
22
core/controllers/LocationController.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from core.models.Location import Location
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LocationController:
|
||||
|
||||
@staticmethod
|
||||
def get(code: str):
|
||||
return Location.find(code)
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return Location.all()
|
||||
|
||||
@staticmethod
|
||||
def fetch(proxies: Optional[dict] = None):
|
||||
|
||||
locations = WebServiceApiService.get_locations(proxies)
|
||||
|
||||
Location.truncate()
|
||||
Location.save_many(locations)
|
222
core/controllers/ProfileController.py
Normal file
222
core/controllers/ProfileController.py
Normal file
|
@ -0,0 +1,222 @@
|
|||
from core.Errors import ProfileStateConflictError, InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ProfileDeactivationError
|
||||
from core.controllers.ApplicationController import ApplicationController
|
||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||
from core.controllers.SessionStateController import SessionStateController
|
||||
from core.controllers.SystemStateController import SystemStateController
|
||||
from core.models.BaseProfile import BaseProfile as Profile
|
||||
from core.models.Subscription import Subscription
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.observers.ProfileObserver import ProfileObserver
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Union, Optional
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
class ProfileController:
|
||||
|
||||
@staticmethod
|
||||
def get(id: int) -> Union[SessionProfile, SystemProfile, None]:
|
||||
return Profile.find_by_id(id)
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return Profile.all()
|
||||
|
||||
@staticmethod
|
||||
def create(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
|
||||
|
||||
Profile.save(profile)
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('created', profile)
|
||||
|
||||
@staticmethod
|
||||
def enable(profile: Union[SessionProfile, SystemProfile], force: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
if ProfileController.is_enabled(profile):
|
||||
|
||||
if not force:
|
||||
raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.')
|
||||
else:
|
||||
ProfileController.disable(profile)
|
||||
|
||||
if profile.is_session_profile():
|
||||
|
||||
application_version = profile.application_version
|
||||
|
||||
if not application_version.installed:
|
||||
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||
|
||||
port_number = ConnectionController.establish_connection(profile, force=force, connection_observer=connection_observer)
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('enabled', profile)
|
||||
|
||||
ApplicationController.launch(application_version, profile, port_number, profile_observer=profile_observer)
|
||||
|
||||
if profile.is_system_profile():
|
||||
|
||||
ConnectionController.establish_connection(profile, force=force, connection_observer=connection_observer)
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('enabled', profile)
|
||||
|
||||
@staticmethod
|
||||
def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, force: bool = False, profile_observer: ProfileObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
if profile.is_session_profile():
|
||||
|
||||
if SessionStateController.exists(profile.id):
|
||||
|
||||
session_state = SessionStateController.get(profile.id)
|
||||
session_state.dissolve(session_state.id)
|
||||
|
||||
if profile.is_system_profile():
|
||||
|
||||
profiles = ProfileController.get_all()
|
||||
|
||||
for key, value in profiles.items():
|
||||
|
||||
if type(value).__name__ == 'SessionProfile':
|
||||
|
||||
if value.connection.is_unprotected() and ProfileController.is_enabled(value) and not force:
|
||||
raise ConnectionUnprotectedError('Disabling this system connection would leave one or more sessions exposed.')
|
||||
|
||||
if SystemStateController.exists():
|
||||
|
||||
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 ProfileDeactivationError('The profile could not be disabled.')
|
||||
|
||||
if profile_observer is not None:
|
||||
|
||||
profile_observer.notify('disabled', profile, dict(
|
||||
explicitly=explicitly,
|
||||
))
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
@staticmethod
|
||||
def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None):
|
||||
|
||||
ProfileController.disable(profile)
|
||||
Profile.delete(profile)
|
||||
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('destroyed', profile)
|
||||
|
||||
@staticmethod
|
||||
def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription):
|
||||
|
||||
profile.subscription = subscription
|
||||
profile.save(profile)
|
||||
|
||||
@staticmethod
|
||||
def activate_subscription(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
if profile.subscription is not None:
|
||||
|
||||
subscription = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
|
||||
|
||||
if subscription is not None:
|
||||
|
||||
profile.subscription = subscription
|
||||
profile.save(profile)
|
||||
|
||||
else:
|
||||
raise InvalidSubscriptionError()
|
||||
|
||||
else:
|
||||
raise MissingSubscriptionError()
|
||||
|
||||
@staticmethod
|
||||
def is_enabled(profile: Union[SessionProfile, SystemProfile]):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
if profile.is_session_profile():
|
||||
|
||||
session_state = SessionStateController.get_or_new(profile.id)
|
||||
return len(session_state.network_port_numbers) > 0 or len(session_state.process_ids) > 0
|
||||
|
||||
if profile.is_system_profile():
|
||||
|
||||
if SystemStateController.get() is not None:
|
||||
return ConnectionController.system_uses_wireguard_interface()
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_invoice(profile: Union[SessionProfile, SystemProfile]):
|
||||
|
||||
if profile.subscription is None:
|
||||
return None
|
||||
|
||||
return WebServiceApiService.get_invoice(profile.subscription.billing_code)
|
||||
|
||||
@staticmethod
|
||||
def attach_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
|
||||
|
||||
if type(profile).__name__ != 'SessionProfile':
|
||||
return None
|
||||
|
||||
if profile.subscription is None:
|
||||
return None
|
||||
|
||||
proxy_configuration = WebServiceApiService.get_proxy_configuration(profile.subscription.billing_code)
|
||||
|
||||
if proxy_configuration is not None:
|
||||
profile.attach_proxy_configuration(proxy_configuration)
|
||||
|
||||
@staticmethod
|
||||
def get_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
|
||||
|
||||
if type(profile).__name__ != 'SessionProfile':
|
||||
return None
|
||||
|
||||
profile.get_proxy_configuration()
|
||||
|
||||
@staticmethod
|
||||
def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
|
||||
profile.has_proxy_configuration()
|
||||
|
||||
@staticmethod
|
||||
def register_wireguard_session(profile: Union[SessionProfile, SystemProfile]):
|
||||
|
||||
if profile.location is None:
|
||||
return None
|
||||
|
||||
if profile.subscription is None:
|
||||
return None
|
||||
|
||||
wireguard_configuration = WebServiceApiService.post_wireguard_session(profile.location.code, profile.subscription.billing_code)
|
||||
|
||||
if wireguard_configuration is not None:
|
||||
profile.attach_wireguard_configuration(wireguard_configuration)
|
||||
|
||||
@staticmethod
|
||||
def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]):
|
||||
return profile.get_wireguard_configuration_path()
|
||||
|
||||
@staticmethod
|
||||
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
|
||||
return profile.has_wireguard_configuration()
|
26
core/controllers/SessionStateController.py
Normal file
26
core/controllers/SessionStateController.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from core.models.session.SessionState import SessionState
|
||||
|
||||
|
||||
class SessionStateController:
|
||||
|
||||
@staticmethod
|
||||
def get(id: int):
|
||||
return SessionState.find_by_id(id)
|
||||
|
||||
@staticmethod
|
||||
def get_or_new(id: int):
|
||||
|
||||
session_state = SessionStateController.get(id)
|
||||
|
||||
if session_state is None:
|
||||
return SessionState(id)
|
||||
|
||||
return session_state
|
||||
|
||||
@staticmethod
|
||||
def exists(id: int):
|
||||
return SessionState.exists(id)
|
||||
|
||||
@staticmethod
|
||||
def update_or_create(session_state):
|
||||
SessionState.save(session_state)
|
21
core/controllers/SubscriptionController.py
Normal file
21
core/controllers/SubscriptionController.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from core.models.SubscriptionPlan import SubscriptionPlan
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Union
|
||||
|
||||
|
||||
class SubscriptionController:
|
||||
|
||||
@staticmethod
|
||||
def get(billing_code: str, connection_observer: ConnectionObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
return ConnectionController.with_preferred_connection(billing_code, task=WebServiceApiService.get_subscription, connection_observer=connection_observer)
|
||||
|
||||
@staticmethod
|
||||
def create(subscription_plan: SubscriptionPlan, profile: Union[SessionProfile, SystemProfile], connection_observer: ConnectionObserver = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
return ConnectionController.with_preferred_connection(subscription_plan.id, profile.location.id, task=WebServiceApiService.post_subscription, connection_observer=connection_observer)
|
24
core/controllers/SubscriptionPlanController.py
Normal file
24
core/controllers/SubscriptionPlanController.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from core.models.SubscriptionPlan import SubscriptionPlan
|
||||
from core.models.session.SessionConnection import SessionConnection
|
||||
from core.models.system.SystemConnection import SystemConnection
|
||||
from core.services.WebServiceApiService import WebServiceApiService
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
class SubscriptionPlanController:
|
||||
|
||||
@staticmethod
|
||||
def get(connection: Union[SessionConnection, SystemConnection], duration: int):
|
||||
return SubscriptionPlan.find(connection, duration)
|
||||
|
||||
@staticmethod
|
||||
def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None):
|
||||
return SubscriptionPlan.all(connection)
|
||||
|
||||
@staticmethod
|
||||
def fetch(proxies: Optional[dict] = None):
|
||||
|
||||
subscription_plans = WebServiceApiService.get_subscription_plans(proxies)
|
||||
|
||||
SubscriptionPlan.truncate()
|
||||
SubscriptionPlan.save_many(subscription_plans)
|
26
core/controllers/SystemStateController.py
Normal file
26
core/controllers/SystemStateController.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from core.models.system.SystemState import SystemState
|
||||
|
||||
|
||||
class SystemStateController:
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
return SystemState.get()
|
||||
|
||||
@staticmethod
|
||||
def get_or_new(profile_id: int):
|
||||
|
||||
system_state = SystemStateController.get()
|
||||
|
||||
if system_state is None:
|
||||
return SystemState(profile_id)
|
||||
|
||||
return system_state
|
||||
|
||||
@staticmethod
|
||||
def exists():
|
||||
return SystemState.exists()
|
||||
|
||||
@staticmethod
|
||||
def update_or_create(system_state):
|
||||
SystemState.save(system_state)
|
11
core/models/BaseConnection.py
Normal file
11
core/models/BaseConnection.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class BaseConnection:
|
||||
code: str
|
||||
|
||||
def needs_wireguard_configuration(self):
|
||||
return self.code == 'wireguard'
|
149
core/models/BaseProfile.py
Normal file
149
core/models/BaseProfile.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
from core.Constants import Constants
|
||||
from core.models.Location import Location
|
||||
from core.models.Subscription import Subscription
|
||||
from core.models.session.ApplicationVersion import ApplicationVersion
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, Exclude, dataclass_json
|
||||
from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class BaseProfile:
|
||||
id: int = field(
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
name: str
|
||||
subscription: Optional[Subscription]
|
||||
location: Optional[Location]
|
||||
|
||||
def get_config_path(self):
|
||||
return BaseProfile._get_config_path(self.id)
|
||||
|
||||
def get_data_path(self):
|
||||
return BaseProfile._get_data_path(self.id)
|
||||
|
||||
def has_subscription(self):
|
||||
return self.subscription is not None
|
||||
|
||||
def attach_wireguard_configuration(self, wireguard_configuration):
|
||||
|
||||
with open(self.get_wireguard_configuration_path(), 'w') as output_file:
|
||||
output_file.write(wireguard_configuration)
|
||||
|
||||
def get_wireguard_configuration_path(self):
|
||||
return self.get_config_path() + '/wg.conf'
|
||||
|
||||
def has_wireguard_configuration(self):
|
||||
return os.path.exists(self.get_config_path() + '/wg.conf')
|
||||
|
||||
def is_session_profile(self):
|
||||
return type(self).__name__ == 'SessionProfile'
|
||||
|
||||
def is_system_profile(self):
|
||||
return type(self).__name__ == 'SystemProfile'
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
|
||||
try:
|
||||
config_file_contents = open(BaseProfile._get_config_path(id) + '/config.json', 'r').read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
try:
|
||||
profile = json.loads(config_file_contents)
|
||||
except JSONDecodeError:
|
||||
return None
|
||||
|
||||
profile['id'] = id
|
||||
|
||||
if profile['location'] is not None:
|
||||
|
||||
location = Location.find(profile['location']['code'] or None)
|
||||
|
||||
if location is not None:
|
||||
|
||||
if profile['location']['time_zone'] is not None:
|
||||
location.time_zone = profile['location']['time_zone']
|
||||
|
||||
profile['location'] = location
|
||||
|
||||
if 'application_version' in profile:
|
||||
|
||||
if profile['application_version'] is not None:
|
||||
application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None)
|
||||
|
||||
if application_version is not None:
|
||||
profile['application_version'] = application_version
|
||||
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
# noinspection PyUnresolvedReferences
|
||||
profile = SessionProfile.from_dict(profile)
|
||||
|
||||
else:
|
||||
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
# noinspection PyUnresolvedReferences
|
||||
profile = SystemProfile.from_dict(profile)
|
||||
|
||||
return profile
|
||||
|
||||
@staticmethod
|
||||
def exists(id: int):
|
||||
return os.path.isdir(BaseProfile._get_config_path(id)) and re.match(r'^\d+$', str(id))
|
||||
|
||||
@staticmethod
|
||||
def all():
|
||||
|
||||
profiles = {}
|
||||
|
||||
for id in map(int, sorted(os.listdir(Constants.SP_PROFILE_CONFIG_HOME))):
|
||||
|
||||
if BaseProfile.exists(id):
|
||||
|
||||
if os.path.exists(BaseProfile._get_config_path(id) + '/config.json'):
|
||||
|
||||
profile = BaseProfile.find_by_id(id)
|
||||
|
||||
if profile is not None:
|
||||
profiles[id] = profile
|
||||
|
||||
return profiles
|
||||
|
||||
@staticmethod
|
||||
def save(profile):
|
||||
|
||||
if profile.is_session_profile() and profile.application_version is not None:
|
||||
|
||||
persistent_state_path = f'{BaseProfile._get_data_path(profile.id)}/persistent-state'
|
||||
|
||||
if os.path.isdir(persistent_state_path):
|
||||
shutil.rmtree(persistent_state_path)
|
||||
|
||||
config_file_contents = profile.to_json(indent=4) + '\n'
|
||||
|
||||
os.makedirs(BaseProfile._get_config_path(profile.id), exist_ok=True)
|
||||
os.makedirs(BaseProfile._get_data_path(profile.id), exist_ok=True)
|
||||
|
||||
text_io_wrapper = open(BaseProfile._get_config_path(profile.id) + '/config.json', 'w')
|
||||
text_io_wrapper.write(config_file_contents)
|
||||
|
||||
@staticmethod
|
||||
def delete(profile):
|
||||
|
||||
shutil.rmtree(BaseProfile._get_config_path(profile.id))
|
||||
shutil.rmtree(BaseProfile._get_data_path(profile.id))
|
||||
|
||||
@staticmethod
|
||||
def _get_config_path(id: int):
|
||||
return Constants.SP_PROFILE_CONFIG_HOME + '/' + str(id)
|
||||
|
||||
@staticmethod
|
||||
def _get_data_path(id: int):
|
||||
return Constants.SP_PROFILE_DATA_HOME + '/' + str(id)
|
80
core/models/ClientVersion.py
Normal file
80
core/models/ClientVersion.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from core.models.Model import Model
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, dataclass_json, Exclude
|
||||
from datetime import datetime
|
||||
from dateutil.parser import isoparse
|
||||
from marshmallow import fields
|
||||
from typing import Optional
|
||||
|
||||
_table_name: str = 'client_versions'
|
||||
|
||||
_table_definition: str = """
|
||||
'id' int UNIQUE,
|
||||
'version_number' varchar UNIQUE,
|
||||
'released_at' varchar,
|
||||
'download_path' varchar UNIQUE
|
||||
"""
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class ClientVersion(Model):
|
||||
version_number: str
|
||||
released_at: datetime = field(
|
||||
metadata=config(
|
||||
encoder=datetime.isoformat,
|
||||
decoder=datetime.fromisoformat,
|
||||
mm_field=fields.DateTime(format='iso')
|
||||
)
|
||||
)
|
||||
id: Optional[int] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
download_path: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM client_versions WHERE id = ? LIMIT 1', ClientVersion.factory, [id])
|
||||
|
||||
@staticmethod
|
||||
def find(version_number: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM client_versions WHERE version_number = ? LIMIT 1', ClientVersion.factory, [version_number])
|
||||
|
||||
@staticmethod
|
||||
def latest():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM client_versions ORDER BY CAST(released_at as datetime) DESC LIMIT 1', ClientVersion.factory)
|
||||
|
||||
@staticmethod
|
||||
def all():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_all('SELECT * FROM client_versions', ClientVersion.factory)
|
||||
|
||||
@staticmethod
|
||||
def truncate():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
|
||||
|
||||
@staticmethod
|
||||
def save_many(client_versions):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
Model._insert_many('INSERT INTO client_versions VALUES(?, ?, ?, ?)', ClientVersion.tuple_factory, client_versions)
|
||||
|
||||
@staticmethod
|
||||
def factory(cursor, row):
|
||||
|
||||
database_fields = [column[0] for column in cursor.description]
|
||||
|
||||
client_version = ClientVersion(**{key: value for key, value in zip(database_fields, row)})
|
||||
client_version.released_at = isoparse(str(client_version.released_at))
|
||||
|
||||
return client_version
|
||||
|
||||
@staticmethod
|
||||
def tuple_factory(client_version):
|
||||
return client_version.id, client_version.version_number, client_version.released_at, client_version.download_path
|
77
core/models/Configuration.py
Normal file
77
core/models/Configuration.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from core.Constants import Constants
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import dataclass_json, config
|
||||
from datetime import datetime
|
||||
from json import JSONDecodeError
|
||||
from marshmallow import fields
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
import dataclasses_json
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Configuration:
|
||||
connection: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(
|
||||
undefined=dataclasses_json.Undefined.EXCLUDE,
|
||||
exclude=lambda value: value is None
|
||||
)
|
||||
)
|
||||
auto_sync_enabled: Optional[bool] = field(
|
||||
default=None,
|
||||
metadata=config(
|
||||
undefined=dataclasses_json.Undefined.EXCLUDE,
|
||||
exclude=lambda value: value is None
|
||||
)
|
||||
)
|
||||
last_synced_at: Optional[datetime] = field(
|
||||
default=None,
|
||||
metadata=config(
|
||||
encoder=lambda datetime_instance: Configuration._iso_format(datetime_instance),
|
||||
decoder=lambda datetime_string: Configuration._from_iso_format(datetime_string),
|
||||
mm_field=fields.DateTime(format='iso'),
|
||||
undefined=dataclasses_json.Undefined.EXCLUDE,
|
||||
exclude=lambda value: value is None
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
|
||||
try:
|
||||
config_file_contents = open(Constants.SP_CONFIG_HOME + '/config.json', 'r').read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
try:
|
||||
configuration = json.loads(config_file_contents)
|
||||
except JSONDecodeError:
|
||||
exit(1)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
configuration = Configuration.from_dict(configuration)
|
||||
|
||||
return configuration
|
||||
|
||||
@staticmethod
|
||||
def save(configuration):
|
||||
|
||||
config_file_contents = configuration.to_json(indent=4) + '\n'
|
||||
os.makedirs(Constants.SP_CONFIG_HOME, exist_ok=True)
|
||||
|
||||
text_io_wrapper = open(Constants.SP_CONFIG_HOME + '/config.json', 'w')
|
||||
text_io_wrapper.write(config_file_contents)
|
||||
|
||||
@staticmethod
|
||||
def _iso_format(datetime_instance: datetime):
|
||||
datetime_instance = datetime_instance.replace(tzinfo=ZoneInfo('UTC'))
|
||||
return datetime.isoformat(datetime_instance).replace('+00:00', 'Z')
|
||||
|
||||
@staticmethod
|
||||
def _from_iso_format(datetime_string: str):
|
||||
date_string = datetime_string.replace('Z', '+00:00')
|
||||
return datetime.fromisoformat(date_string)
|
6
core/models/Event.py
Normal file
6
core/models/Event.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
class Event:
|
||||
|
||||
def __init__(self, subject = None, meta = None):
|
||||
|
||||
self.subject = subject
|
||||
self.meta = meta or {}
|
76
core/models/Location.py
Normal file
76
core/models/Location.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from core.models.Model import Model
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, Exclude
|
||||
from pytz import country_timezones
|
||||
from typing import Optional
|
||||
|
||||
_table_name: str = 'locations'
|
||||
|
||||
_table_definition: str = """
|
||||
'id' int UNIQUE,
|
||||
'code' varchar UNIQUE,
|
||||
'name' varchar UNIQUE
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location(Model):
|
||||
code: str
|
||||
id: Optional[int] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
name: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
time_zone: str = None
|
||||
available: Optional[bool] = field(
|
||||
default=False,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.time_zone is None:
|
||||
self.time_zone = country_timezones[self.code][0]
|
||||
|
||||
self.available = self.exists(self.code)
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM locations WHERE id = ? LIMIT 1', Location.factory, [id])
|
||||
|
||||
@staticmethod
|
||||
def find(code: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM locations WHERE code = ? LIMIT 1', Location.factory, [code])
|
||||
|
||||
@staticmethod
|
||||
def exists(code: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_exists('SELECT * FROM locations WHERE code = ?', [code])
|
||||
|
||||
@staticmethod
|
||||
def all():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_all('SELECT * FROM locations', Location.factory)
|
||||
|
||||
@staticmethod
|
||||
def truncate():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
|
||||
|
||||
@staticmethod
|
||||
def save_many(locations):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
Model._insert_many('INSERT INTO locations VALUES(?, ?, ?)', Location.tuple_factory, locations)
|
||||
|
||||
@staticmethod
|
||||
def factory(cursor, row):
|
||||
local_fields = [column[0] for column in cursor.description]
|
||||
return Location(**{key: value for key, value in zip(local_fields, row)})
|
||||
|
||||
@staticmethod
|
||||
def tuple_factory(location):
|
||||
return location.id, location.code, location.name
|
73
core/models/Model.py
Normal file
73
core/models/Model.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from core.Constants import Constants
|
||||
import sqlite3
|
||||
|
||||
|
||||
class Model:
|
||||
|
||||
@staticmethod
|
||||
def _create_table_if_not_exists(table_name: str, table_definition: str, drop_existing: bool = False):
|
||||
|
||||
connection = sqlite3.connect(Constants.SP_STORAGE_DATABASE)
|
||||
cursor = connection.cursor()
|
||||
|
||||
if drop_existing:
|
||||
cursor.execute('DROP TABLE IF EXISTS ' + table_name)
|
||||
|
||||
cursor.execute('CREATE TABLE IF NOT EXISTS ' + table_name + '(' + table_definition + ')')
|
||||
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
@staticmethod
|
||||
def _query_one(query: str, row_factory, parameters=None):
|
||||
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
|
||||
connection = sqlite3.connect(Constants.SP_STORAGE_DATABASE)
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.row_factory = row_factory
|
||||
|
||||
results = cursor.execute(query, parameters).fetchone()
|
||||
connection.close()
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _query_exists(query: str, parameters):
|
||||
|
||||
connection = sqlite3.connect(Constants.SP_STORAGE_DATABASE)
|
||||
cursor = connection.cursor()
|
||||
|
||||
response = cursor.execute('SELECT EXISTS(' + query + ')', parameters).fetchone()
|
||||
connection.close()
|
||||
|
||||
return response[0] == 1
|
||||
|
||||
@staticmethod
|
||||
def _query_all(query: str, row_factory, parameters=None):
|
||||
|
||||
if parameters is None:
|
||||
parameters = []
|
||||
|
||||
connection = sqlite3.connect(Constants.SP_STORAGE_DATABASE)
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.row_factory = row_factory
|
||||
|
||||
results = cursor.execute(query, parameters).fetchall()
|
||||
connection.close()
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _insert_many(query: str, tuple_factory, items):
|
||||
|
||||
connection = sqlite3.connect(Constants.SP_STORAGE_DATABASE)
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.executemany(query, map(tuple_factory, items))
|
||||
|
||||
connection.commit()
|
||||
connection.close()
|
31
core/models/Subscription.py
Normal file
31
core/models/Subscription.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, dataclass_json
|
||||
from datetime import datetime
|
||||
from marshmallow import fields
|
||||
from typing import Optional
|
||||
import dataclasses_json
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Subscription:
|
||||
billing_code: str
|
||||
expires_at: Optional[datetime] = field(
|
||||
default=None,
|
||||
metadata=config(
|
||||
encoder=lambda datetime_instance: Subscription._iso_format(datetime_instance),
|
||||
decoder=lambda datetime_string: Subscription.from_iso_format(datetime_string),
|
||||
mm_field=fields.DateTime(format='iso'),
|
||||
undefined=dataclasses_json.Undefined.EXCLUDE,
|
||||
exclude=lambda value: value is None
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _iso_format(datetime_instance: datetime):
|
||||
return datetime.isoformat(datetime_instance).replace('+00:00', 'Z')
|
||||
|
||||
@staticmethod
|
||||
def from_iso_format(datetime_string: str):
|
||||
date_string = datetime_string.replace('Z', '+00:00')
|
||||
return datetime.fromisoformat(date_string)
|
89
core/models/SubscriptionPlan.py
Normal file
89
core/models/SubscriptionPlan.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
from core.models.Model import Model
|
||||
from core.models.session.SessionConnection import SessionConnection
|
||||
from core.models.system.SystemConnection import SystemConnection
|
||||
from dataclasses import dataclass
|
||||
from typing import Union, Optional
|
||||
|
||||
_table_name: str = 'subscription_plans'
|
||||
|
||||
_table_definition: str = """
|
||||
'id' int UNIQUE,
|
||||
'code' varchar UNIQUE,
|
||||
'wireguard_session_limit' int,
|
||||
'duration' int,
|
||||
'price' int,
|
||||
'features_proxy' bool,
|
||||
'features_wireguard' bool
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubscriptionPlan(Model):
|
||||
id: int
|
||||
code: str
|
||||
wireguard_session_limit: int
|
||||
duration: int
|
||||
price: int
|
||||
features_proxy: bool
|
||||
features_wireguard: bool
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM subscription_plans WHERE id = ? LIMIT 1', SubscriptionPlan.factory, [id])
|
||||
|
||||
@staticmethod
|
||||
def find(connection: Union[SessionConnection, SystemConnection], duration: int):
|
||||
|
||||
features_proxy = False
|
||||
features_wireguard = False
|
||||
|
||||
if type(connection).__name__ == 'SessionConnection':
|
||||
if connection.masked:
|
||||
features_proxy = True
|
||||
|
||||
if connection.code == 'wireguard':
|
||||
features_wireguard = True
|
||||
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ? AND duration = ? LIMIT 1', SubscriptionPlan.factory, [features_proxy, features_wireguard, duration])
|
||||
|
||||
@staticmethod
|
||||
def all(connection: Optional[Union[SessionConnection, SystemConnection]] = None):
|
||||
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
|
||||
if connection is None:
|
||||
return Model._query_all('SELECT * FROM subscription_plans', SubscriptionPlan.factory)
|
||||
|
||||
else:
|
||||
|
||||
features_proxy = False
|
||||
features_wireguard = False
|
||||
|
||||
if type(connection).__name__ == 'SessionConnection':
|
||||
if connection.masked:
|
||||
features_proxy = True
|
||||
|
||||
if connection.code == 'wireguard':
|
||||
features_wireguard = True
|
||||
|
||||
return Model._query_all('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ?', SubscriptionPlan.factory, [features_proxy, features_wireguard])
|
||||
|
||||
@staticmethod
|
||||
def truncate():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
|
||||
|
||||
@staticmethod
|
||||
def save_many(subscription_plans):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
Model._insert_many('INSERT INTO subscription_plans VALUES(?, ?, ?, ?, ?, ?, ?)', SubscriptionPlan.tuple_factory, subscription_plans)
|
||||
|
||||
@staticmethod
|
||||
def factory(cursor, row):
|
||||
local_fields = [column[0] for column in cursor.description]
|
||||
return SubscriptionPlan(**{key: value for key, value in zip(local_fields, row)})
|
||||
|
||||
@staticmethod
|
||||
def tuple_factory(subscription_plan):
|
||||
return subscription_plan.id, subscription_plan.code, subscription_plan.wireguard_session_limit, subscription_plan.duration, subscription_plan.price, subscription_plan.features_proxy, subscription_plan.features_wireguard
|
23
core/models/invoice/Invoice.py
Normal file
23
core/models/invoice/Invoice.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from core.models.invoice import PaymentMethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Invoice:
|
||||
billing_code: str
|
||||
status: str
|
||||
expires_at: datetime
|
||||
payment_methods: tuple[PaymentMethod]
|
||||
|
||||
def is_new(self):
|
||||
return self.status == 'new'
|
||||
|
||||
def is_rejected(self):
|
||||
return self.status == 'invalid' or self.status == 'expired'
|
||||
|
||||
def is_processing(self):
|
||||
return self.status == 'processing'
|
||||
|
||||
def is_settled(self):
|
||||
return self.status == 'settled'
|
12
core/models/invoice/PaymentMethod.py
Normal file
12
core/models/invoice/PaymentMethod.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentMethod:
|
||||
code: str
|
||||
name: str
|
||||
address: str
|
||||
payment_link: str
|
||||
rate: float
|
||||
amount: float
|
||||
due: float
|
53
core/models/session/Application.py
Normal file
53
core/models/session/Application.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from core.models.Model import Model
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, Exclude
|
||||
from typing import Optional
|
||||
|
||||
_table_name: str = 'applications'
|
||||
|
||||
_table_definition: str = """
|
||||
'id' int UNIQUE,
|
||||
'code' varchar UNIQUE,
|
||||
'name' varchar UNIQUE
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Application(Model):
|
||||
code: str
|
||||
name: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
id: Optional[int] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM applications WHERE id = ? LIMIT 1', Application.factory, [id])
|
||||
|
||||
@staticmethod
|
||||
def find(code: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM applications WHERE code = ? LIMIT 1', Application.factory, [code])
|
||||
|
||||
@staticmethod
|
||||
def truncate():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
|
||||
|
||||
@staticmethod
|
||||
def save_many(applications):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
Model._insert_many('INSERT INTO applications VALUES(?, ?, ?)', Application.tuple_factory, applications)
|
||||
|
||||
@staticmethod
|
||||
def factory(cursor, row):
|
||||
database_fields = [column[0] for column in cursor.description]
|
||||
return Application(**{key: value for key, value in zip(database_fields, row)})
|
||||
|
||||
@staticmethod
|
||||
def tuple_factory(application):
|
||||
return application.id, application.code, application.name
|
98
core/models/session/ApplicationVersion.py
Normal file
98
core/models/session/ApplicationVersion.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
from core.Constants import Constants
|
||||
from core.models.Model import Model
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, Exclude
|
||||
from datetime import datetime
|
||||
from dateutil.parser import isoparse
|
||||
from marshmallow import fields
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
_table_name: str = 'application_versions'
|
||||
|
||||
_table_definition: str = """
|
||||
'id' int UNIQUE,
|
||||
'application_code' varchar,
|
||||
'version_number' varchar,
|
||||
'download_path' varchar UNIQUE,
|
||||
'released_at' varchar,
|
||||
UNIQUE(application_code, version_number)
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationVersion(Model):
|
||||
application_code: str
|
||||
version_number: str
|
||||
id: Optional[int] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
download_path: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
released_at: Optional[datetime] = field(
|
||||
default=None,
|
||||
metadata=config(
|
||||
encoder=datetime.isoformat,
|
||||
decoder=datetime.fromisoformat,
|
||||
mm_field=fields.DateTime(format='iso'),
|
||||
exclude=Exclude.ALWAYS
|
||||
)
|
||||
)
|
||||
installation_path: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
installed: Optional[bool] = field(
|
||||
default=False,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
supported: Optional[bool] = field(
|
||||
default=False,
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
self.installation_path = Constants.SP_APPLICATION_DATA_HOME + '/' + self.application_code + '/' + self.version_number
|
||||
self.installed = os.path.isdir(self.installation_path) and len(os.listdir(self.installation_path)) > 0
|
||||
self.supported = self.exists(self.application_code, self.version_number)
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM application_versions WHERE id = ? LIMIT 1', ApplicationVersion.factory, [id])
|
||||
|
||||
@staticmethod
|
||||
def find(application_code: str, version_number: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_one('SELECT * FROM application_versions WHERE application_code = ? AND version_number = ? LIMIT 1', ApplicationVersion.factory, [application_code, version_number])
|
||||
|
||||
@staticmethod
|
||||
def exists(application_code: str, version_number: str):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
return Model._query_exists('SELECT * FROM application_versions WHERE application_code = ? AND version_number = ?', [application_code, version_number])
|
||||
|
||||
@staticmethod
|
||||
def truncate():
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition, drop_existing=True)
|
||||
|
||||
@staticmethod
|
||||
def save_many(application_versions):
|
||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||
Model._insert_many('INSERT INTO application_versions VALUES(?, ?, ?, ?, ?)', ApplicationVersion.tuple_factory, application_versions)
|
||||
|
||||
@staticmethod
|
||||
def factory(cursor, row):
|
||||
|
||||
database_fields = [column[0] for column in cursor.description]
|
||||
|
||||
application_version = ApplicationVersion(**{key: value for key, value in zip(database_fields, row)})
|
||||
application_version.released_at = isoparse(str(application_version.released_at))
|
||||
|
||||
return application_version
|
||||
|
||||
@staticmethod
|
||||
def tuple_factory(application_version):
|
||||
return application_version.id, application_version.application_code, application_version.version_number, application_version.download_path, application_version.released_at
|
11
core/models/session/ProxyConfiguration.py
Normal file
11
core/models/session/ProxyConfiguration.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class ProxyConfiguration:
|
||||
ip_address: str
|
||||
port_number: int
|
||||
username: str
|
||||
password: str
|
18
core/models/session/SessionConnection.py
Normal file
18
core/models/session/SessionConnection.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from core.models.BaseConnection import BaseConnection
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConnection(BaseConnection):
|
||||
masked: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code not in ('system', 'tor', 'wireguard'):
|
||||
raise ValueError('Invalid connection code.')
|
||||
|
||||
def is_unprotected(self):
|
||||
return self.code == 'system' and self.masked is False
|
||||
|
||||
def needs_proxy_configuration(self):
|
||||
return self.masked is True
|
47
core/models/session/SessionProfile.py
Normal file
47
core/models/session/SessionProfile.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
from core.Constants import Constants
|
||||
from core.models.BaseProfile import BaseProfile
|
||||
from core.models.session.ApplicationVersion import ApplicationVersion
|
||||
from core.models.session.ProxyConfiguration import ProxyConfiguration
|
||||
from core.models.session.SessionConnection import SessionConnection
|
||||
from dataclasses import dataclass
|
||||
from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionProfile(BaseProfile):
|
||||
resolution: str
|
||||
application_version: Optional[ApplicationVersion]
|
||||
connection: Optional[SessionConnection]
|
||||
|
||||
def attach_proxy_configuration(self, proxy_configuration):
|
||||
|
||||
config_file_contents = proxy_configuration.to_json(indent=4) + '\n'
|
||||
os.makedirs(Constants.SP_CONFIG_HOME, exist_ok=True)
|
||||
|
||||
text_io_wrapper = open(self.get_proxy_configuration_path(), 'w')
|
||||
text_io_wrapper.write(config_file_contents)
|
||||
|
||||
def get_proxy_configuration_path(self):
|
||||
return self.get_config_path() + '/proxy.json'
|
||||
|
||||
def get_proxy_configuration(self):
|
||||
|
||||
try:
|
||||
config_file_contents = open(self.get_proxy_configuration_path(), 'r').read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
try:
|
||||
proxy_configuration = json.loads(config_file_contents)
|
||||
except JSONDecodeError:
|
||||
return None
|
||||
|
||||
proxy_configuration = ProxyConfiguration.from_dict(proxy_configuration)
|
||||
|
||||
return proxy_configuration
|
||||
|
||||
def has_proxy_configuration(self):
|
||||
return os.path.exists(self.get_config_path() + '/proxy.json')
|
102
core/models/session/SessionState.py
Normal file
102
core/models/session/SessionState.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
from core.Constants import Constants
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses_json import config, Exclude, dataclass_json
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import psutil
|
||||
import re
|
||||
import shutil
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class SessionState:
|
||||
id: int = field(
|
||||
metadata=config(exclude=Exclude.ALWAYS)
|
||||
)
|
||||
network_port_numbers: list[int] = field(default_factory=list)
|
||||
process_ids: list[int] = field(default_factory=list)
|
||||
|
||||
def get_state_path(self):
|
||||
return SessionState._get_state_path(self.id)
|
||||
|
||||
@staticmethod
|
||||
def find_by_id(id: int):
|
||||
|
||||
try:
|
||||
session_state_file_contents = open(SessionState._get_state_path(id) + '/state.json', 'r').read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
try:
|
||||
session_state = json.loads(session_state_file_contents)
|
||||
except JSONDecodeError:
|
||||
return None
|
||||
|
||||
session_state['id'] = id
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
return SessionState.from_dict(session_state)
|
||||
|
||||
@staticmethod
|
||||
def exists(id: int):
|
||||
return os.path.isdir(SessionState._get_state_path(id)) and re.match(r'^\d+$', str(id))
|
||||
|
||||
@staticmethod
|
||||
def save(session_state):
|
||||
|
||||
session_state_file_contents = session_state.to_json(indent=4) + '\n'
|
||||
os.makedirs(SessionState._get_state_path(session_state.id), exist_ok=True, mode=0o700)
|
||||
|
||||
session_state_file_path = SessionState._get_state_path(session_state.id) + '/state.json'
|
||||
Path(session_state_file_path).touch(exist_ok=True, mode=0o600)
|
||||
|
||||
text_io_wrapper = open(session_state_file_path, 'w')
|
||||
text_io_wrapper.write(session_state_file_contents)
|
||||
|
||||
@staticmethod
|
||||
def dissolve(id: int):
|
||||
|
||||
session_state = SessionState.find_by_id(id)
|
||||
|
||||
if session_state is not None:
|
||||
|
||||
session_state_path = session_state.get_state_path()
|
||||
|
||||
SessionState._kill_associated_processes(session_state)
|
||||
shutil.rmtree(session_state_path, ignore_errors=True)
|
||||
|
||||
@staticmethod
|
||||
def _kill_associated_processes(session_state):
|
||||
|
||||
associated_process_ids = list(session_state.process_ids)
|
||||
network_connections = psutil.net_connections()
|
||||
|
||||
for network_port_number in session_state.network_port_numbers:
|
||||
|
||||
for network_connection in network_connections:
|
||||
|
||||
if network_connection.laddr != tuple() and network_connection.laddr.port == network_port_number:
|
||||
|
||||
if network_connection.pid is not None:
|
||||
associated_process_ids.append(network_connection.pid)
|
||||
|
||||
if network_connection.raddr != tuple() and network_connection.raddr.port == network_port_number:
|
||||
|
||||
if network_connection.pid is not None:
|
||||
associated_process_ids.append(network_connection.pid)
|
||||
|
||||
for process in psutil.process_iter():
|
||||
|
||||
if process.pid in associated_process_ids and process.is_running():
|
||||
|
||||
for child_process in process.children(True):
|
||||
child_process.kill()
|
||||
|
||||
process.kill()
|
||||
|
||||
@staticmethod
|
||||
def _get_state_path(id: int):
|
||||
return Constants.SP_SESSION_STATE_HOME + '/' + str(id)
|
15
core/models/system/SystemConnection.py
Normal file
15
core/models/system/SystemConnection.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from core.models.BaseConnection import BaseConnection
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemConnection (BaseConnection):
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code != 'wireguard':
|
||||
raise ValueError('Invalid connection code.')
|
||||
|
||||
@staticmethod
|
||||
def needs_proxy_configuration():
|
||||
return False
|
9
core/models/system/SystemProfile.py
Normal file
9
core/models/system/SystemProfile.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from core.models.BaseProfile import BaseProfile
|
||||
from core.models.system.SystemConnection import SystemConnection
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemProfile(BaseProfile):
|
||||
connection: Optional[SystemConnection]
|
59
core/models/system/SystemState.py
Normal file
59
core/models/system/SystemState.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from core.Constants import Constants
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class SystemState:
|
||||
profile_id: int
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
|
||||
try:
|
||||
system_state_file_contents = open(SystemState._get_state_path() + '/system.json', 'r').read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
try:
|
||||
system_state = json.loads(system_state_file_contents)
|
||||
except JSONDecodeError:
|
||||
return None
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
return SystemState.from_dict(system_state)
|
||||
|
||||
@staticmethod
|
||||
def exists():
|
||||
return os.path.isfile(SystemState._get_state_path() + '/system.json')
|
||||
|
||||
@staticmethod
|
||||
def save(system_state):
|
||||
|
||||
system_state_file_contents = system_state.to_json(indent=4) + '\n'
|
||||
os.makedirs(SystemState._get_state_path(), exist_ok=True, mode=0o700)
|
||||
|
||||
system_state_file_path = SystemState._get_state_path() + '/system.json'
|
||||
Path(system_state_file_path).touch(exist_ok=True, mode=0o600)
|
||||
|
||||
text_io_wrapper = open(system_state_file_path, 'w')
|
||||
text_io_wrapper.write(system_state_file_contents)
|
||||
|
||||
@staticmethod
|
||||
def dissolve():
|
||||
|
||||
system_state = SystemState.get()
|
||||
|
||||
if system_state is not None:
|
||||
|
||||
system_state_path = SystemState._get_state_path() + '/system.json'
|
||||
pathlib.Path.unlink(Path(system_state_path), missing_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_state_path():
|
||||
return Constants.SP_STATE_HOME
|
7
core/observers/ApplicationVersionObserver.py
Normal file
7
core/observers/ApplicationVersionObserver.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from core.observers.BaseObserver import BaseObserver
|
||||
|
||||
|
||||
class ApplicationVersionObserver(BaseObserver):
|
||||
|
||||
def __init__(self):
|
||||
self.on_downloading = []
|
25
core/observers/BaseObserver.py
Normal file
25
core/observers/BaseObserver.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from core.models.Event import Event
|
||||
|
||||
|
||||
class BaseObserver:
|
||||
|
||||
def subscribe(self, topic, callback):
|
||||
|
||||
callbacks = getattr(self, 'on_' + topic, None)
|
||||
|
||||
if callbacks is None:
|
||||
return
|
||||
|
||||
callbacks.append(callback)
|
||||
|
||||
def notify(self, topic, subject = None, meta = None):
|
||||
|
||||
callbacks = getattr(self, 'on_' + topic, None)
|
||||
|
||||
if callbacks is None:
|
||||
return
|
||||
|
||||
event = Event(subject, meta)
|
||||
|
||||
for callback in callbacks:
|
||||
callback(event)
|
7
core/observers/ClientObserver.py
Normal file
7
core/observers/ClientObserver.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from core.observers.BaseObserver import BaseObserver
|
||||
|
||||
|
||||
class ClientObserver(BaseObserver):
|
||||
|
||||
def __init__(self):
|
||||
self.on_synchronizing = []
|
7
core/observers/ConnectionObserver.py
Normal file
7
core/observers/ConnectionObserver.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from core.observers.BaseObserver import BaseObserver
|
||||
|
||||
|
||||
class ConnectionObserver(BaseObserver):
|
||||
|
||||
def __init__(self):
|
||||
self.on_connecting = []
|
10
core/observers/InvoiceObserver.py
Normal file
10
core/observers/InvoiceObserver.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from core.observers.BaseObserver import BaseObserver
|
||||
|
||||
|
||||
class InvoiceObserver(BaseObserver):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.on_retrieved = []
|
||||
self.on_processing = []
|
||||
self.on_settled = []
|
11
core/observers/ProfileObserver.py
Normal file
11
core/observers/ProfileObserver.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from core.observers.BaseObserver import BaseObserver
|
||||
|
||||
|
||||
class ProfileObserver(BaseObserver):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.on_created = []
|
||||
self.on_destroyed = []
|
||||
self.on_disabled = []
|
||||
self.on_enabled = []
|
167
core/services/WebServiceApiService.py
Normal file
167
core/services/WebServiceApiService.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
from core.Constants import Constants
|
||||
from core.models.ClientVersion import ClientVersion
|
||||
from core.models.Location import Location
|
||||
from core.models.Subscription import Subscription
|
||||
from core.models.SubscriptionPlan import SubscriptionPlan
|
||||
from core.models.invoice.Invoice import Invoice
|
||||
from core.models.invoice.PaymentMethod import PaymentMethod
|
||||
from core.models.session.Application import Application
|
||||
from core.models.session.ApplicationVersion import ApplicationVersion
|
||||
from core.models.session.ProxyConfiguration import ProxyConfiguration
|
||||
from typing import Optional
|
||||
import re
|
||||
import requests
|
||||
|
||||
|
||||
class WebServiceApiService:
|
||||
|
||||
@staticmethod
|
||||
def get_applications(proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/platforms/linux-x86_64/applications', None, proxies)
|
||||
applications = []
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
for application in response.json()['data']:
|
||||
applications.append(Application(application['code'], application['name'], application['id']))
|
||||
|
||||
return applications
|
||||
|
||||
@staticmethod
|
||||
def get_application_versions(code: str, proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/platforms/linux-x86_64/applications/' + code + '/application-versions', None, proxies)
|
||||
application_versions = []
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
for application_version in response.json()['data']:
|
||||
application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['id'], application_version['download_path'], application_version['released_at']))
|
||||
|
||||
return application_versions
|
||||
|
||||
@staticmethod
|
||||
def get_client_versions(proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/platforms/linux-x86_64/client-versions', None, proxies)
|
||||
client_versions = []
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
for client_version in response.json()['data']:
|
||||
client_versions.append(ClientVersion(client_version['version_number'], client_version['released_at'], client_version['id'], client_version['download_path']))
|
||||
|
||||
return client_versions
|
||||
|
||||
@staticmethod
|
||||
def get_locations(proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/locations', None, proxies)
|
||||
locations = []
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
for location in response.json()['data']:
|
||||
locations.append(Location(location['code'], location['id'], location['name']))
|
||||
|
||||
return locations
|
||||
|
||||
@staticmethod
|
||||
def get_subscription_plans(proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/subscription-plans', None, proxies)
|
||||
subscription_plans = []
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
for subscription_plan in response.json()['data']:
|
||||
subscription_plans.append(SubscriptionPlan(subscription_plan['id'], subscription_plan['code'], subscription_plan['wireguard_session_limit'], subscription_plan['duration'], subscription_plan['price'], subscription_plan['features_proxy'], subscription_plan['features_wireguard']))
|
||||
|
||||
return subscription_plans
|
||||
|
||||
@staticmethod
|
||||
def post_subscription(subscription_plan_id, location_id, proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._post('/subscriptions', None, {
|
||||
'subscription_plan_id': subscription_plan_id,
|
||||
'location_id': location_id
|
||||
}, proxies)
|
||||
|
||||
if response.status_code == requests.codes.created:
|
||||
return Subscription(response.headers['X-Billing-Code'])
|
||||
|
||||
@staticmethod
|
||||
def get_subscription(billing_code: str, proxies: Optional[dict] = None):
|
||||
|
||||
if billing_code is not None:
|
||||
|
||||
billing_code = billing_code.replace('-', '').upper()
|
||||
billing_code_fragments = re.findall('....?', billing_code)
|
||||
billing_code = '-'.join(billing_code_fragments)
|
||||
|
||||
response = WebServiceApiService._get('/subscriptions/current', billing_code, proxies)
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
|
||||
subscription = response.json()['data']
|
||||
return Subscription(billing_code, Subscription.from_iso_format(subscription['expires_at']))
|
||||
|
||||
@staticmethod
|
||||
def get_invoice(billing_code: str, proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/invoices/current', billing_code, proxies)
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
|
||||
response_data = response.json()['data']
|
||||
|
||||
invoice = {
|
||||
'status': response_data['status'],
|
||||
'expires_at': response_data['expires_at']
|
||||
}
|
||||
|
||||
payment_methods = []
|
||||
|
||||
for payment_method in response_data['payment_methods']:
|
||||
payment_methods.append(PaymentMethod(payment_method['code'], payment_method['name'], payment_method['address'], payment_method['payment_link'], payment_method['rate'], payment_method['amount'], payment_method['due']))
|
||||
|
||||
return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods))
|
||||
|
||||
@staticmethod
|
||||
def get_proxy_configuration(billing_code: str, proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._get('/proxy-configurations/current', billing_code, proxies)
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
|
||||
proxy_configuration = response.json()['data']
|
||||
return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'])
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def post_wireguard_session(location_code: str, billing_code: str, proxies: Optional[dict] = None):
|
||||
|
||||
response = WebServiceApiService._post('/locations/' + location_code + '/wireguard-sessions', billing_code, proxies)
|
||||
|
||||
if response.status_code == requests.codes.created:
|
||||
return response.text
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get(path, billing_code: Optional[str] = None, proxies: Optional[dict] = None):
|
||||
|
||||
if billing_code is not None:
|
||||
headers = {'X-Billing-Code': billing_code}
|
||||
else:
|
||||
headers = None
|
||||
|
||||
return requests.get(Constants.SP_API_BASE_URL + path, headers=headers, proxies=proxies)
|
||||
|
||||
@staticmethod
|
||||
def _post(path, billing_code: Optional[str] = None, body: Optional[dict] = None, proxies: Optional[dict] = None):
|
||||
|
||||
if billing_code is not None:
|
||||
headers = {'X-Billing-Code': billing_code}
|
||||
else:
|
||||
headers = None
|
||||
|
||||
return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies)
|
259
main.py
Normal file
259
main.py
Normal file
|
@ -0,0 +1,259 @@
|
|||
from core.Constants import Constants
|
||||
from core.Errors import MissingSubscriptionError, InvalidSubscriptionError
|
||||
from core.controllers.ApplicationController import ApplicationController
|
||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||
from core.controllers.ClientController import ClientController
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
from core.controllers.InvoiceController import InvoiceController
|
||||
from core.controllers.LocationController import LocationController
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
from core.controllers.SubscriptionController import SubscriptionController
|
||||
from core.controllers.SubscriptionPlanController import SubscriptionPlanController
|
||||
from core.models.session.SessionConnection import SessionConnection
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.system.SystemConnection import SystemConnection
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
||||
from core.observers.ClientObserver import ClientObserver
|
||||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.observers.InvoiceObserver import InvoiceObserver
|
||||
from core.observers.ProfileObserver import ProfileObserver
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import pprint
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Path(Constants.SP_CONFIG_HOME).mkdir(parents=True, exist_ok=True)
|
||||
Path(Constants.SP_DATA_HOME).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
application_version_observer = ApplicationVersionObserver()
|
||||
client_observer = ClientObserver()
|
||||
connection_observer = ConnectionObserver()
|
||||
invoice_observer = InvoiceObserver()
|
||||
profile_observer = ProfileObserver()
|
||||
|
||||
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...\n'))
|
||||
client_observer.subscribe('synchronizing', lambda event: print('Synchronizing...\n'))
|
||||
connection_observer.subscribe('connecting', lambda event: print(f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_amount_of_retries")}] Performing connection attempt...\n'))
|
||||
|
||||
invoice_observer.subscribe('retrieved', lambda event: print(f'\n{pprint.pp(event.subject)}\n'))
|
||||
invoice_observer.subscribe('processing', lambda event: print('A payment has been detected and is being verified...\n'))
|
||||
invoice_observer.subscribe('settled', lambda event: print('The payment has been successfully verified.\n'))
|
||||
|
||||
profile_observer.subscribe('created', lambda event: pprint.pp((event.subject, 'Created')))
|
||||
profile_observer.subscribe('destroyed', lambda event: pprint.pp((event.subject, 'Destroyed')))
|
||||
profile_observer.subscribe('disabled', lambda event: pprint.pp((event.subject, 'Disabled')) if event.meta.get('explicitly') else None)
|
||||
profile_observer.subscribe('enabled', lambda event: pprint.pp((event.subject, 'Enabled')))
|
||||
|
||||
safe_parser = argparse.ArgumentParser(add_help=False)
|
||||
safe_parser.add_argument('--force', '-f', action='store_true')
|
||||
|
||||
main_parser = argparse.ArgumentParser(prog='simplified-privacy')
|
||||
main_parser.add_argument('--version', '-v', action='version', version='simplified-privacy v0.1.0')
|
||||
main_subparsers = main_parser.add_subparsers(title='commands', dest='command')
|
||||
|
||||
profile_parser = main_subparsers.add_parser('profile')
|
||||
profile_subparsers = profile_parser.add_subparsers(title='subcommands', dest='subcommand')
|
||||
|
||||
profile_base_parser = argparse.ArgumentParser(add_help=False)
|
||||
profile_base_parser.add_argument('--id', '-i', type=int, required=True)
|
||||
|
||||
profile_subparsers.add_parser('list')
|
||||
|
||||
profile_subparsers.add_parser('show', parents=[profile_base_parser])
|
||||
|
||||
profile_create_parser = profile_subparsers.add_parser('create')
|
||||
profile_create_subparsers = profile_create_parser.add_subparsers(title='profile_types', dest='profile_type')
|
||||
|
||||
session_profile_create_parser = profile_create_subparsers.add_parser('session', parents=[profile_base_parser, safe_parser])
|
||||
|
||||
session_profile_create_parser.add_argument('--name', '-n', default='')
|
||||
session_profile_create_parser.add_argument('--location', '-l', dest='location_code', default=None)
|
||||
session_profile_create_parser.add_argument('--application', '-a', required=True)
|
||||
session_profile_create_parser.add_argument('--connection', '-c', dest='connection_type', choices=['system', 'tor', 'wireguard'], default='system')
|
||||
session_profile_create_parser.add_argument('--mask-connection', '-m', action='store_true')
|
||||
session_profile_create_parser.add_argument('--resolution', '-r', default='1280x720')
|
||||
|
||||
system_profile_create_parser = profile_create_subparsers.add_parser('system', parents=[profile_base_parser, safe_parser])
|
||||
|
||||
system_profile_create_parser.add_argument('--name', '-n', default='')
|
||||
system_profile_create_parser.add_argument('--location', '-l', dest='location_code', default=None)
|
||||
system_profile_create_parser.add_argument('--connection', '-c', dest='connection_type', choices=['wireguard'], default='wireguard')
|
||||
|
||||
profile_subparsers.add_parser('destroy', parents=[profile_base_parser, safe_parser])
|
||||
|
||||
profile_subparsers.add_parser('enable', parents=[profile_base_parser, safe_parser])
|
||||
profile_subparsers.add_parser('disable', parents=[profile_base_parser, safe_parser])
|
||||
|
||||
get_parser = main_subparsers.add_parser('get')
|
||||
get_subparsers = get_parser.add_subparsers(title='subcommands', dest='subcommand')
|
||||
|
||||
get_connection_parser = get_subparsers.add_parser('connection')
|
||||
|
||||
set_parser = main_subparsers.add_parser('set')
|
||||
set_subparsers = set_parser.add_subparsers(title='subcommands', dest='subcommand')
|
||||
|
||||
set_connection_parser = set_subparsers.add_parser('connection')
|
||||
set_connection_parser.add_argument('connection_type', choices=['system', 'tor'])
|
||||
|
||||
sync_parser = main_subparsers.add_parser('sync')
|
||||
|
||||
arguments = main_parser.parse_args()
|
||||
|
||||
if arguments.command is None:
|
||||
main_parser.print_help()
|
||||
|
||||
elif arguments.command == 'profile':
|
||||
|
||||
if arguments.subcommand is None:
|
||||
profile_parser.print_help()
|
||||
|
||||
elif arguments.subcommand == 'list':
|
||||
pprint.pp(ProfileController.get_all())
|
||||
|
||||
elif arguments.subcommand == 'show':
|
||||
pprint.pp(ProfileController.get(arguments.id))
|
||||
|
||||
elif arguments.subcommand == 'create':
|
||||
|
||||
location = LocationController.get(arguments.location_code)
|
||||
|
||||
if location is None:
|
||||
main_parser.error('the following argument should be a valid reference: --location/-l')
|
||||
|
||||
if arguments.profile_type == 'session':
|
||||
|
||||
if not arguments.application:
|
||||
arguments.application = ':'
|
||||
|
||||
application_details = arguments.application.split(':', 1)
|
||||
application_version = ApplicationVersionController.get(application_details[0], application_details[1])
|
||||
|
||||
if application_version is None:
|
||||
main_parser.error('the following argument should be a valid reference: --application/-a')
|
||||
|
||||
connection = SessionConnection(arguments.connection_type, arguments.mask_connection)
|
||||
profile = SessionProfile(arguments.id, arguments.name, None, location, arguments.resolution, application_version, connection)
|
||||
ProfileController.create(profile, profile_observer=profile_observer)
|
||||
|
||||
else:
|
||||
|
||||
connection = SystemConnection(arguments.connection_type)
|
||||
profile = SystemProfile(arguments.id, arguments.name, None, location, connection)
|
||||
ProfileController.create(profile, profile_observer=profile_observer)
|
||||
|
||||
elif arguments.subcommand == 'destroy':
|
||||
|
||||
profile = ProfileController.get(arguments.id)
|
||||
|
||||
if profile is not None:
|
||||
ProfileController.destroy(profile, profile_observer=profile_observer)
|
||||
|
||||
else:
|
||||
main_parser.error('the following argument should be a valid reference: --id/-i')
|
||||
|
||||
elif arguments.subcommand == 'enable':
|
||||
|
||||
profile = ProfileController.get(arguments.id)
|
||||
|
||||
if profile is not None:
|
||||
|
||||
try:
|
||||
ProfileController.enable(profile, arguments.force, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||
|
||||
except (InvalidSubscriptionError, MissingSubscriptionError) as exception:
|
||||
|
||||
if type(exception).__name__ == 'InvalidSubscriptionError':
|
||||
print('The profile\'s subscription appears to be invalid.\n')
|
||||
|
||||
if type(exception).__name__ == 'MissingSubscriptionError':
|
||||
print('The profile is not tied to a subscription.\n')
|
||||
|
||||
manage_subscription_input = None
|
||||
|
||||
while manage_subscription_input not in ['1', '2', '3', '']:
|
||||
|
||||
print('Please select from the following:\n')
|
||||
|
||||
print(' 1) Request new subscription')
|
||||
print(' 2) Enter billing code')
|
||||
|
||||
print('\n 3) Exit')
|
||||
|
||||
manage_subscription_input = input('\nEnter your choice [1]: ')
|
||||
|
||||
if manage_subscription_input == '1' or manage_subscription_input == '':
|
||||
|
||||
print('\nCreating subscription...\n')
|
||||
|
||||
subscription_plan = SubscriptionPlanController.get(profile.connection, 720)
|
||||
|
||||
if subscription_plan is None:
|
||||
raise RuntimeError('No compatible subscription plan was found. Please contact support.')
|
||||
|
||||
potential_subscription = SubscriptionController.create(subscription_plan, profile, connection_observer=connection_observer)
|
||||
|
||||
if potential_subscription is not None:
|
||||
ProfileController.attach_subscription(profile, potential_subscription)
|
||||
|
||||
else:
|
||||
raise RuntimeError('The subscription could not be created. Please try again later.')
|
||||
|
||||
subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer)
|
||||
|
||||
if subscription is not None:
|
||||
ProfileController.attach_subscription(profile, subscription)
|
||||
|
||||
else:
|
||||
raise RuntimeError('The subscription could not be activated. Please try again later.')
|
||||
|
||||
ProfileController.enable(profile, force=arguments.force, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||
|
||||
elif manage_subscription_input == '2':
|
||||
|
||||
billing_code = input('\nEnter your billing code: ')
|
||||
print()
|
||||
|
||||
subscription = SubscriptionController.get(billing_code, connection_observer=connection_observer)
|
||||
|
||||
if subscription is not None:
|
||||
|
||||
ProfileController.attach_subscription(profile, subscription)
|
||||
ProfileController.enable(profile, arguments.force, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||
|
||||
else:
|
||||
|
||||
print('\nThe billing code appears to be invalid.\n')
|
||||
manage_subscription_input = None
|
||||
|
||||
elif manage_subscription_input == '3':
|
||||
pass
|
||||
|
||||
else:
|
||||
print('\nInput appears to be invalid. Please try again.\n')
|
||||
|
||||
else:
|
||||
main_parser.error('the following argument should be a valid reference: --id/-i')
|
||||
|
||||
elif arguments.subcommand == 'disable':
|
||||
|
||||
profile = ProfileController.get(arguments.id)
|
||||
|
||||
if profile is not None:
|
||||
ProfileController.disable(profile, force=arguments.force, profile_observer=profile_observer)
|
||||
else:
|
||||
main_parser.error('the following argument should be a valid reference: --id/-i')
|
||||
|
||||
elif arguments.command == 'sync':
|
||||
ClientController.sync(client_observer=client_observer, connection_observer=connection_observer)
|
||||
|
||||
elif arguments.command == 'get':
|
||||
|
||||
if arguments.subcommand == 'connection':
|
||||
print(ConfigurationController.get_connection())
|
||||
|
||||
elif arguments.command == 'set':
|
||||
|
||||
if arguments.subcommand == 'connection':
|
||||
ConfigurationController.set_connection(arguments.connection_type)
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
dataclasses-json~=0.6.4
|
||||
marshmallow~=3.21.1
|
||||
psutil~=5.9.8
|
||||
pysocks~=1.7.1
|
||||
python-dateutil~=2.9.0.post0
|
||||
pytz~=2024.1
|
||||
requests~=2.31.0
|
Loading…
Reference in a new issue