ApplicationController transitioned to the HTTPx Client system, instead of using requests library proxy for Tor. Also the GUI's observers now do strings to be more neutral to what object type is being downloaded (application_version objects vs dependency).

This commit is contained in:
SimplifiedPrivacy 2026-08-12 14:19:55 -04:00
parent b84bb2eefa
commit 01bd5f8d3f
4 changed files with 78 additions and 32 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ prototype_client.py
.venv .venv
__pycache__ __pycache__
dist dist
.mypy_cache

View file

@ -3,6 +3,9 @@
# Singbox Setup # Singbox Setup
### Aug 12, 2026 ### Aug 12, 2026
Prepared Singbox setup modules, which includes installation, download, move to sudo folder, and sudo setup scripts. Added a `Dependency` model, endpoint, and the ability to sync that model. (Related note: Server-side prepared the endpoint, and stocked with real data.) And also the sync service modules were adjusted to handle new data types more smoothly, before they had errors. As part of that sync flow change, the CachedSync metadata model was transitioned to ints instead of strings, with default 0 values. This should in theory migrate all clients without further changes needed. Prepared Singbox setup modules, which includes installation, download, move to sudo folder, and sudo setup scripts. Added a `Dependency` model, endpoint, and the ability to sync that model. (Related note: Server-side prepared the endpoint, and stocked with real data.) And also the sync service modules were adjusted to handle new data types more smoothly, before they had errors. As part of that sync flow change, the CachedSync metadata model was transitioned to ints instead of strings, with default 0 values. This should in theory migrate all clients without further changes needed.
### Additional Changes:
ApplicationController transitioned to the HTTPx Client system, instead of using requests library proxy for Tor. Also the GUI's observers now do strings to be more neutral to what object type is being downloaded (application_version objects vs dependency).
<br/> <br/>
# Singbox Orchestration # Singbox Orchestration

View file

@ -1,3 +1,9 @@
from core.services.networking.httpx import httpx_client
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import Configuration, ConnectionChoice
from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError, ApplicationAlreadyInstalledError from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError, ApplicationAlreadyInstalledError
from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationController import ApplicationController
from core.models.session.Application import Application from core.models.session.Application import Application
@ -12,6 +18,7 @@ from core.observers.ConnectionObserver import ConnectionObserver
from core.services.WebServiceApiService import WebServiceApiService from core.services.WebServiceApiService import WebServiceApiService
from core.errors.logger import logger from core.errors.logger import logger
import httpx
from io import BytesIO from io import BytesIO
from typing import Optional from typing import Optional
import hashlib import hashlib
@ -19,6 +26,7 @@ import shutil
import tarfile import tarfile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select from sqlalchemy import select
import os
class ApplicationVersionController: class ApplicationVersionController:
@ -48,8 +56,12 @@ class ApplicationVersionController:
if application_version.is_installed(): if application_version.is_installed():
raise ApplicationAlreadyInstalledError('The application in question is already installed.') raise ApplicationAlreadyInstalledError('The application in question is already installed.')
from core.controllers.ConnectionController import ConnectionController # this used to go through "with_preferred_connection", but now re-uses the same HTTPx client as sync,
ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController.__install, application_version_observer=application_version_observer, connection_observer=connection_observer) ApplicationVersionController.__install(application_version, application_version_observer, connection_observer)
# legacy:
# 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 @staticmethod
def uninstall(application_version: ApplicationVersion): def uninstall(application_version: ApplicationVersion):
@ -72,41 +84,53 @@ class ApplicationVersionController:
ApplicationVersion.save_many(application_versions) ApplicationVersion.save_many(application_versions)
@staticmethod @staticmethod
def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, proxies: Optional[dict] = None): def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None):
target_app_name = application_version.application_code.capitalize()
import requests target_app_version = application_version.version_number
if application_version_observer is not None: if application_version_observer is not None:
application_version_observer.notify('downloading', application_version) application_version_observer.notify('downloading', f"Downloading {target_app_name} {target_app_version}. Connecting..")
# legacy:
# application_version_observer.notify('downloading', application_version)
if proxies is not None: ################################################
response = requests.get(application_version.download_path, stream=True, proxies=proxies) # SETUP HTTP CLIENT
else: ################################################
response = requests.get(application_version.download_path, stream=True) client = httpx_client.get_http_session()
logger.info(f"client type is {type(client)}")
if client is None:
client = _get_httpx_client(target_app_name=target_app_name, connection_observer=connection_observer)
if response.status_code == 200: ################################################
# GET THE DATA
################################################
download_path = application_version.download_path
logger.info(f"download_path is {download_path}")
with client.stream('GET', download_path) as response:
logger.info("doing the stream...")
if response.status_code == 200:
response_size = int(response.headers.get('Content-Length', 0))
response_buffer = BytesIO()
response_size = int(response.headers.get('Content-Length', 0)) block_size = 1024
response_buffer = BytesIO() bytes_written = 0
for data in response.iter_bytes(block_size):
block_size = 1024 bytes_written += len(data)
bytes_written = 0 response_buffer.write(data)
progress = (bytes_written / response_size) * 100 if response_size > 0 else 0
for data in response.iter_content(block_size): if application_version_observer is not None:
application_version_observer.notify('download_progressing', f"Downloading {target_app_name} {progress:.2f}% v: {target_app_version}")
else:
raise ConnectionError('The application version could not be downloaded.')
bytes_written += len(data) application_version_observer.notify('downloaded', f"Downloaded {target_app_name} {target_app_version}")
response_buffer.write(data)
progress = (bytes_written / response_size) * 100 if response_size > 0 else 0
if application_version_observer is not None:
application_version_observer.notify('download_progressing', application_version, dict(
progress=progress
))
application_version_observer.notify('downloaded', application_version)
response_buffer.seek(0) response_buffer.seek(0)
################################################
# VERIFY THE HASH
################################################
file_hash = ApplicationVersionController.__calculate_file_hash(response_buffer) file_hash = ApplicationVersionController.__calculate_file_hash(response_buffer)
if file_hash != application_version.file_hash: if file_hash != application_version.file_hash:
@ -118,9 +142,6 @@ class ApplicationVersionController:
with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file: with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file:
hash_file.write(f'{file_hash}\n') hash_file.write(f'{file_hash}\n')
else:
raise ConnectionError('The application version could not be downloaded.')
@staticmethod @staticmethod
def __calculate_file_hash(file): def __calculate_file_hash(file):
@ -136,6 +157,26 @@ class ApplicationVersionController:
return hasher.hexdigest() return hasher.hexdigest()
# This function is a temporary transition for the connect module to get better public APIs
def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client:
connection_type = ConfigurationController.get_connection_enum()
client = connect.make_client(connection_type, connection_observer)
logger.info(f"client type is {type(client)}")
if isinstance(client, ApiResponse):
if not client.valid:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
if isinstance(client, bool):
if not client:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
else:
logger.info("It's a boolean, getting the client now.")
client = httpx_client.get_http_session()
logger.info(f"client type is {type(client)}")
return client
# legacy: # legacy:
# @staticmethod # @staticmethod

View file

@ -22,6 +22,7 @@ from essentials.observers.ConnectionObserver import ConnectionObserver
from core.errors.logger import logger from core.errors.logger import logger
import httpx import httpx
from typing import Optional
_port_used = None _port_used = None
@ -43,7 +44,7 @@ def single_endpoint(method: str, url: str, observer: ConnectionObserver, payload
######################################################## ########################################################
if client is None: if client is None:
observer.notify('message', "Testing Connection..") observer.notify('message', "Testing Connection..")
made_client = _make_client(connection_type, observer) made_client = make_client(connection_type, observer)
if not made_client and connection_type == ConnectionChoice.TOR: if not made_client and connection_type == ConnectionChoice.TOR:
observer.notify('message', "Tor Bootstrap..") observer.notify('message', "Tor Bootstrap..")
return bootstrap_and_try_again( return bootstrap_and_try_again(
@ -113,7 +114,7 @@ def single_endpoint(method: str, url: str, observer: ConnectionObserver, payload
return initial_result return initial_result
def _make_client(connection_type: str, observer) -> httpx.Client | ApiResponse: def make_client(connection_type: str, observer: Optional[ConnectionObserver] = None) -> httpx.Client | ApiResponse:
""" """
Rank: Rank:
Coordinator Coordinator