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