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