Introduced Connect Module, which manages HTTPx Clients, solves network/DNS issues, and coordinates Tor Bootstraps. Introduced async & single endpoint full workflows. This version is stable and tested for DNS & Tor problems. Also modified Configuration to be Pydantic, instead of @dataclass_json, and changed the connection type to enums. Further, there's a new enum function to get the new enum types, but the legacy function exists for backwards compatability.

This commit is contained in:
SimplifiedPrivacy 2026-08-03 16:59:39 -04:00
parent fb43228f2f
commit ed055f338d
17 changed files with 1275 additions and 136 deletions

View file

@ -1,6 +1,10 @@
# Major Change Log: # Major Change Log:
# Connect Module & Async
### August 3, 2026
Introduced Connect Module, which manages HTTPx Clients, solves network/DNS issues, and coordinates Tor Bootstraps. Introduced async & single endpoint full workflows. This version is stable and tested for DNS & Tor problems. Also modified Configuration to be Pydantic, instead of @dataclass_json, and changed the connection type to enums. Further, there's a new enum function to get the new enum types, but the legacy function exists for backwards compatability.
# HTTPx Client, ThreadPool, & Tor Management # HTTPx Client, ThreadPool, & Tor Management
### August 2, 2026 ### August 2, 2026
Elaborate HTTPx feature-rich strategy with HTTPx Client Reuse, Tor Management, Tor bootstrap monitor & retry. HTTPx clients can be managed across modules and reused. There's an elaborate HTTPx retry strategy based on status codes. Tor management tries to use the existing Tor default ports, then checks the Tor config, before finally bootstrapping. There is now a ThreadPool futures executor, which even works with Tor, to batch API requests. Elaborate HTTPx feature-rich strategy with HTTPx Client Reuse, Tor Management, Tor bootstrap monitor & retry. HTTPx clients can be managed across modules and reused. There's an elaborate HTTPx retry strategy based on status codes. Tor management tries to use the existing Tor default ports, then checks the Tor config, before finally bootstrapping. There is now a ThreadPool futures executor, which even works with Tor, to batch API requests.

View file

@ -1,14 +1,23 @@
from core.Errors import UnknownConnectionTypeError from core.Errors import UnknownConnectionTypeError
from core.models.Configuration import Configuration from core.models.Configuration import Configuration, ConnectionChoice
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
class ConfigurationController: class ConfigurationController:
_config: Optional[Configuration] = None
@staticmethod @staticmethod
def get(): def get():
return Configuration.get() if ConfigurationController._config is None:
ConfigurationController._config = Configuration.get()
return ConfigurationController._config
# return Configuration.get()
@staticmethod
def reload_from_disk():
ConfigurationController._config = None
@staticmethod @staticmethod
def get_or_new(): def get_or_new():
@ -24,16 +33,26 @@ class ConfigurationController:
def get_connection(): def get_connection():
configuration = ConfigurationController.get() configuration = ConfigurationController.get()
return configuration.connection.value
if configuration is None or configuration.connection not in ('system', 'tor'): @staticmethod
raise UnknownConnectionTypeError('The preferred connection type could not be determined.') def get_connection_enum():
configuration = ConfigurationController.get()
return configuration.connection return configuration.connection
@staticmethod @staticmethod
def set_connection(connection: Optional[str] = None): def set_connection(connection_string: Optional[str] = None):
configuration = ConfigurationController.get_or_new() configuration = ConfigurationController.get_or_new()
if connection_string == "tor":
connection = ConnectionChoice.TOR
elif connection_string == "system":
connection = ConnectionChoice.SYSTEM
else:
raise UnknownConnectionTypeError(f'The choice of {connection_string} is not valid.')
configuration.connection = connection configuration.connection = connection
configuration.save() configuration.save()

View file

@ -1,113 +1,75 @@
from core.errors.logger import logger from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
from core.Helpers import write_atomically from core.Helpers import write_atomically
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config #######################
from enum import Enum
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
from datetime import datetime from datetime import datetime
from marshmallow import fields
from typing import Optional, Self
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import dataclasses_json from typing import Optional, Self
import json import json
import os import os
import sys import sys
@dataclass_json class ConnectionChoice(str, Enum):
@dataclass TOR = "tor"
class Configuration: SYSTEM = "system"
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
)
)
endpoint_verification_enabled: Optional[bool] = field(
default=False,
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
)
)
firewall: Optional[bool] = field( class Configuration(BaseModel):
default=False, connection: Optional[ConnectionChoice] = None
metadata=config( auto_sync_enabled: Optional[bool] = None
undefined=dataclasses_json.Undefined.EXCLUDE, endpoint_verification_enabled: Optional[bool] = False
exclude=lambda value: value is None last_synced_at: Optional[datetime] = None
) firewall: Optional[bool] = False
dns: Optional[bool] = False
did_sudo_setup: Optional[bool] = False
model_config = ConfigDict(
extra='ignore', # Ignore unknown fields in JSON
exclude_none=True # Don't serialize None values
) )
@field_validator('last_synced_at', mode='before')
@classmethod
def parse_datetime(cls, v):
if isinstance(v, str):
v = v.replace('Z', '+00:00') # Z → +00:00 for parsing
return v
@field_serializer('last_synced_at')
def serialize_datetime(self, value: datetime) -> str:
if value:
value = value.replace(tzinfo=ZoneInfo('UTC'))
return value.isoformat().replace('+00:00', 'Z') # +00:00 → Z for JSON
return None
dns: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
did_sudo_setup: Optional[bool] = field(
default=False,
metadata=config(
undefined=dataclasses_json.Undefined.EXCLUDE,
exclude=lambda value: value is None
)
)
def save(self: Self):
config_file_contents = f'{self.to_json(indent=4)}\n'
os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
config_file_path = f'{Constants.HV_CONFIG_HOME}/config.json'
write_atomically(config_file_path, config_file_contents)
@staticmethod @staticmethod
def get(): def get():
try: try:
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read() with open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r') as f:
config_file_contents = f.read()
except FileNotFoundError: except FileNotFoundError:
return None return None
try: try:
configuration = json.loads(config_file_contents) configuration_dict = json.loads(config_file_contents)
except ValueError: except ValueError:
sys.exit(1) sys.exit(1)
return Configuration(**configuration_dict) # Pydantic validates on init
# noinspection PyUnresolvedReferences
configuration = Configuration.from_dict(configuration)
return configuration def save(self: Self):
config_file_contents = f'{self.model_dump_json(indent=4)}\n'
@staticmethod os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
def _iso_format(datetime_instance: datetime):
datetime_instance = datetime_instance.replace(tzinfo=ZoneInfo('UTC')) config_file_path = f'{Constants.HV_CONFIG_HOME}/config.json'
return datetime.isoformat(datetime_instance).replace('+00:00', 'Z') write_atomically(config_file_path, config_file_contents)
@staticmethod
def _from_iso_format(datetime_string: str):
date_string = datetime_string.replace('Z', '+00:00')
return datetime.fromisoformat(date_string)
def read_config(): def read_config():

View file

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional, Any from typing import Optional, Any
@ -39,9 +39,11 @@ class ErrorType(Enum):
DNS_PERMANENT = "dns_permanent" DNS_PERMANENT = "dns_permanent"
QUAD9_DNS_RESOLUTION = "quad9_dns_resolution" QUAD9_DNS_RESOLUTION = "quad9_dns_resolution"
NO_INTERNET = "no_internet" NO_INTERNET = "no_internet"
CONNECTION_ERROR = "connection_error"
INVALID_INPUT = "invalid_input" INVALID_INPUT = "invalid_input"
PERMISSION_ERROR = "permission_error" # duplicate PERMISSION_ERROR = "permission_error" # duplicate
DEVELOPER_ERROR = "developer_error" DEVELOPER_ERROR = "developer_error"
PORT_NOT_LISTENING = "port_not_listening"
UNKNOWN = "unknown" UNKNOWN = "unknown"
@dataclass @dataclass
@ -50,6 +52,7 @@ class ApiResponse:
valid: bool valid: bool
error_type: Optional[ErrorType] = None error_type: Optional[ErrorType] = None
data: Optional[Any] = None data: Optional[Any] = None
failures: dict = field(default_factory=dict)
message: Optional[str] = None message: Optional[str] = None
backoff_strategy: BackoffStrategy = BackoffStrategy.NO_RETRY backoff_strategy: BackoffStrategy = BackoffStrategy.NO_RETRY
retry_now: bool = False # legacy retry_now: bool = False # legacy

View file

@ -0,0 +1,119 @@
from httpx_socks import AsyncProxyTransport
from core.services.networking.httpx.async_request import make_async_request
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.errors.logger import logger
import json
import time
import asyncio
import httpx
async def create_async_clearweb_client() -> httpx.AsyncClient:
"""Create a CLEARWEB AsyncClient ready for API requests."""
client = httpx.AsyncClient(http2=True, timeout=30)
return client
async def create_async_tor_client(port: int) -> httpx.AsyncClient:
logger.info(f"Creating async client on port {port}")
"""Create a TOR AsyncClient ready for API requests."""
transport = AsyncProxyTransport.from_url(f"socks5://127.0.0.1:{port}")
client = httpx.AsyncClient(transport=transport, http2=True, timeout=30)
# for spoofing errors if we don't want to run more checks:
# return client
try:
response = await make_async_request(
method="get",
url="https://check.torproject.org/api/ip",
client=client,
)
if response.valid:
data = response.data
is_tor = data.get("IsTor", False)
if is_tor:
logger.info(f"Tor session initialized successfully on port {port}")
return client
else:
await client.aclose()
logger.warning("Tor connectivity check failed: IsTor returned False")
return None
else:
await client.aclose()
logger.error(f"Error with reaching tor verification: {response.error_type}")
return None
except Exception as e:
await client.aclose()
logger.error(f"The HTTP call failed: {e}")
return None
async def _async_parallel(desired_endpoints: dict, port: int|None = None) -> dict:
# Tor
if port is not None:
client = await create_async_tor_client(port)
if client is None:
return ApiResponse(valid=False, error_type=ErrorType.TOR_NOT_WORKING)
# clearweb:
else:
client = await create_async_clearweb_client()
start_total = time.time()
try:
# Create all coroutines
tasks = [
make_async_request(method="get", url=url, client=client)
for url in desired_endpoints.values()
]
# Run them concurrently
results_list = await asyncio.gather(*tasks)
# Map back to keys
results = dict(zip(desired_endpoints.keys(), results_list))
total_parallel = time.time() - start_total
logger.info(f"\nTotal time for all API calls: {total_parallel:.2f}s")
return ApiResponse(valid=True, data=results)
finally:
await client.aclose()
def async_parallel(desired_endpoints: dict, port: int|None = None) -> dict:
logger.info(f"We got port of {port}")
return asyncio.run(_async_parallel(desired_endpoints, port))
# has_failures = any(not result.valid for result in results.values())
# return ApiResponse(valid=not has_failures, data=results)
# def format_results_for_return(results):
# # Separate working from failing
# working_results = {}
# failures = {}
# for endpoint_key, result in results.items():
# if result.valid:
# working_results[endpoint_key] = result
# else:
# print(f"The failures result for {endpoint_key} is {result}")
# failures[endpoint_key] = result
# has_failures = len(failures) > 0
# if not has_failures:
# # All succeeded
# return ApiResponse(valid=True, data=results)
# logger.info(f"These are the failures: {failures}")
# # Some failed, some passed. We give the dict of success with data. And a list of the failures.
# return ApiResponse(valid=False, data=working_results, failures=failures)

View file

@ -0,0 +1,176 @@
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType, BackoffStrategy
from core.errors.logger import logger
from core.services.networking.httpx.classify_response import classify_response
from typing import Optional
import httpx
import asyncio
import json
import time
import socket
from python_socks._errors import ProxyError
async def make_async_request(
method: str,
url: str,
client: httpx.Client,
payload: Optional[dict] = None,
) -> ApiResponse:
if method == "post" and not payload:
return ApiResponse(valid=False, error_type=ErrorType.INVALID_INPUT, message="Can't have a POST request without a payload")
initial_result = await _make_async_request(
method=method,
url=url,
client=client,
payload=payload
)
if initial_result.valid:
return initial_result
logger.error(f"{method.upper()} request failed: {initial_result.error_type}")
# Immediate Retry
if initial_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE:
time.sleep(3)
second_result = await _make_async_request(
method=method,
url=url,
client=client,
payload=payload
)
if second_result.valid:
return second_result
if second_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE:
logger.error("We are avoiding retrying immediately twice.")
second_result.backoff_strategy = BackoffStrategy.RETRY_EXPONENTIAL
return second_result
# Delayed Retry
elif initial_result.backoff_strategy == BackoffStrategy.RETRY_EXPONENTIAL:
time.sleep(10)
second_result = await _make_async_request(
method=method,
url=url,
client=client,
payload=payload
)
return second_result
# Invalid:
elif initial_result.backoff_strategy == BackoffStrategy.FIX_CLIENT_SIDE_INFO and initial_result.error_type == ErrorType.INVALID_REQUEST:
logger.error("This was classified Invalid. This might be a mistake of doing GET when it's POST")
return switch_get_and_post(method=method, url=url, client=client, payload=payload)
return initial_result
error_spoofer = 0
async def _make_async_request(
method: str,
url: str,
client: httpx.Client,
payload: Optional[dict] = None,
) -> ApiResponse:
logger.debug(f"Executing {method.upper()} to {url}")
try:
if method.lower() == "get":
response = await client.get(url)
else:
response = await client.post(url, json=payload)
return classify_response(response)
except httpx.TimeoutException as e:
return ApiResponse(
valid=False,
error_type=ErrorType.NETWORK_ERROR,
message=f"Request timeout: {str(e)}",
backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL
)
except httpx.ConnectError as e:
logger.error(f"Error: {e}")
if isinstance(e.__cause__, socket.gaierror):
gaierror = e.__cause__
if gaierror.errno in (-3, -11): # EAI_AGAIN
# Transient DNS failure
return ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY, backoff_strategy=BackoffStrategy.NO_RETRY) # this is going to go to DNS resolver.
else:
# Permanent DNS failure (bad domain)
return ApiResponse(valid=False, error_type=ErrorType.DNS_PERMANENT, backoff_strategy=BackoffStrategy.NO_RETRY)
else:
# Non-DNS connection issue,
error_type = ErrorType.CONNECTION_ERROR
return ApiResponse(valid=False, error_type=error_type, backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL)
# except httpx.ProxyError as e:
# return ApiResponse(
# valid=False,
# error_type=ErrorType.TOR_NOT_WORKING,
# message=f"Proxy error: {str(e)}",
# backoff_strategy=BackoffStrategy.NO_RETRY
# )
except (httpx.ProxyError, ProxyError) as e:
# Check if it's a DNS-like failure from Tor
if "host unreachable" in str(e).lower() or "name resolution" in str(e).lower():
return ApiResponse(
valid=False,
error_type=ErrorType.DNS_PERMANENT,
backoff_strategy=BackoffStrategy.NO_RETRY
)
else:
# Actual Tor infrastructure problem
return ApiResponse(
valid=False,
error_type=ErrorType.TOR_NOT_WORKING,
backoff_strategy=BackoffStrategy.NO_RETRY
)
except httpx.RequestError as e:
# Catches any other httpx request errors not covered above
return ApiResponse(
valid=False,
error_type=ErrorType.NETWORK_ERROR,
message=f"Request error: {str(e)}",
backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL
)
except Exception as e:
# Catch anything that slipped through
logger.error(f"Unexpected error in _make_async_request: {type(e).__name__}: {e}")
return ApiResponse(
valid=False,
error_type=ErrorType.UNKNOWN,
message=f"Unexpected error: {str(e)}",
backoff_strategy=BackoffStrategy.NO_RETRY
)
async def switch_get_and_post(method: str, url: str, client: httpx.Client, payload: dict) -> ApiResponse:
if method == "get":
new_method = "post"
else:
new_method = "get"
second_result = await _make_async_request(
method=new_method,
url=url,
client=client,
payload=payload
)
if second_result.valid:
logger.info("Our strategy worked! This is a mistake of GET/POST")
return second_result
else:
# nevermind,
logger.info("Our strategy of switching GET/POST did NOT work.")
return initial_result

View file

@ -16,7 +16,7 @@ import json
def classify_response(response) -> ApiResponse: def classify_response(response) -> ApiResponse:
if not isinstance(response, httpx.Response): if not isinstance(response, httpx.Response):
error_msg = "Library Error: HTTPx library returned an invalid object response." error_msg = "invalid object response, its not even HTTPx"
logger.error(error_msg) logger.error(error_msg)
return ApiResponse( return ApiResponse(
valid=False, valid=False,

View file

@ -0,0 +1,363 @@
from core.services.networking.httpx import httpx_client
from core.services.networking.httpx.make_request import make_request
from core.services.networking.tor_tools.tor_orchestrator import establish_tor_connection
from core.services.networking.tor_tools.tor_dns import setup_SINGLE_use_resolver
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.httpx.async_batch_requests import async_parallel
from core.services.networking.tor_tools.pre_bootstrap import get_bootstrap_port
from core.services.networking.api_requests.subtools.get_connection_type import get_connection_type
from core.services.networking.api_requests.subtools.extract_domain import extract_domain, swap_domain_for_ip
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import Configuration, ConnectionChoice
from core.services.networking.httpx.endpoints import get_endpoints
from core.services.networking.httpx.parallel_threading import parallel_thread
from core.services.networking.tor_tools import ports
from core.Constants import Constants
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.errors.logger import logger
import httpx
_port_used = None
def single_endpoint(method: str, url: str, observer: ConnectionObserver, payload: dict = None) -> ApiResponse:
"""
Rank:
Orchestrator
Purpose:
Try a request to a single endpoint using either an existing client, or creating a new one, then error handling.
"""
global _port_used
connection_type = ConfigurationController.get_connection_enum()
client = httpx_client.get_http_session()
########################################################
# NO CLIENT
########################################################
if client is None:
made_client = _make_client(connection_type, observer)
if not made_client and connection_type == ConnectionChoice.TOR:
return bootstrap_and_try_again(
method=method,
url=url,
observer=observer,
payload=payload
)
elif not made_client and connection_type == ConnectionChoice.SYSTEM:
return ApiResponse(valid=False, error_type=ErrorType.DEVELOPER_ERROR, message="Can't make a local non-Tor HTTPx client. This error should not go off.")
# now get it, regardless of Tor or Not:
client = httpx_client.get_http_session()
########################################################
# FROM THIS POINT ON, WE HAVE A CLIENT
########################################################
# #### SPOOF
# _port_used = 9050
# initial_result = ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY)
initial_result = make_request(
method=method,
url=url,
client=client,
payload=payload
)
if initial_result.valid:
return initial_result
########################################################
# FROM THIS POINT ON, WE HAVE PROBLEMS
########################################################
if connection_type == ConnectionChoice.SYSTEM:
# We have no solutions for clearweb that weren't tried already.
# But may add more in the future
return initial_result
########################################################
# TOR NOT WORKING. BOOTSTRAP
########################################################
if initial_result.error_type == ErrorType.TOR_NOT_WORKING:
return bootstrap_and_try_again(
method=method,
url=url,
observer=observer,
payload=payload
)
########################################################
# DNS BLOCK
########################################################
if initial_result.error_type == ErrorType.DNS_TEMPORARY:
return custom_dns_resolver_for_SINGLE_ENDPOINT(
method=method,
url=url,
observer=observer,
payload=payload
)
# nothing we can do here
else:
return initial_result
def _make_client(connection_type: str, observer) -> httpx.Client | ApiResponse:
"""
Rank:
Coordinator
Purpose:
Create an HTTPx Client for either kind of transport
"""
global _port_used
# Clearweb
if connection_type == ConnectionChoice.SYSTEM:
return httpx_client.init_session() # this is not the client, its a boolean
if _port_used is None:
_port_used = Constants.DEFAULT_TOR_PORT
# Tor:
# check if port is even listening:
listening = ports.is_port_in_use(_port_used)
if listening:
client = httpx_client.init_tor_session(_port_used)
if client:
return client
else:
logger.error(f"Could NOT create a Tor HTTPx client on port {_port_used}.. Bootstrapping..")
return False
def bootstrap_and_try_again(method: str, url: str, observer: ConnectionObserver, payload: dict = None):
"""
Rank:
Coordinator
Purpose:
Get a port
Test it
Make a client with it
Use it to do the request again
Called by:
single_endpoint
"""
global _port_used
if _port_used is None:
_port_used = Constants.DEFAULT_TOR_PORT
bootstrap_results = get_bootstrap_port(port_tried=_port_used, observer=observer)
# BOOTSTRAP FAILURE
if not bootstrap_results.valid and bootstrap_results.port:
return bootstrap_results
# BOOTSTRAP SUCCESS
_port_used = bootstrap_results.port
made_client = httpx_client.init_tor_session(_port_used)
if not made_client:
return bootstrap_results
client = httpx_client.get_http_session()
return make_request(
method=method,
url=url,
client=client,
payload=payload
)
def bulk_async(wanted_list: list, observer: ConnectionObserver) -> ApiResponse:
"""
Rank:
Orchestrator
Purpose:
Do a bulk async data fetch, & Error handle it
"""
global _port_used
########################################################
# PREP SETTINGS & GOAL
########################################################
connection_type = ConfigurationController.get_connection_enum()
desired_endpoints = get_endpoints(wanted_list=wanted_list, dns_resolver=False)
########################################################
# CLEARWEB "regular" INTERNET
########################################################
if connection_type == ConnectionChoice.SYSTEM:
return async_parallel(desired_endpoints=desired_endpoints, port=None)
########################################################
# TOR. CHECK THE TOR PORTS
########################################################
if _port_used is None:
_port_used = Constants.DEFAULT_TOR_PORT
# check if that (default) port is even listening:
listening = ports.is_port_in_use(_port_used)
if listening:
results = async_parallel(desired_endpoints=desired_endpoints, port=_port_used)
else:
new_port = just_only_bootstrap_port(port_used=_port_used, observer=observer)
if new_port:
_port_used = new_port # Set this as the port for next time
results = async_parallel(desired_endpoints=desired_endpoints, port=new_port)
else:
return ApiResponse(valid=False, error_type=ErrorType.TOR_NOT_WORKING)
########################################################
# EVALUATE RESULTS.
########################################################
if results is None:
return ApiResponse(valid=False, error_type=ErrorType.UNKNOWN)
return _evaluate_results(results, observer=observer)
def just_only_bootstrap_port(port_tried: int, observer: ConnectionObserver) -> ApiResponse|int:
bootstrap_results = get_bootstrap_port(port_tried=port_tried, observer=observer)
if bootstrap_results.valid and bootstrap_results.port:
return bootstrap_results.port
else:
logger.error(f"Bootstrap Failed. The reason is likely ALREADY in the error log, and there is NOTHING WE CAN DO ABOUT IT NOW. but here it is again after retries: {bootstrap_results.error_type} and {bootstrap_results.message}")
return False
def _evaluate_results(results: ApiResponse, observer: ConnectionObserver) -> ApiResponse:
"""
Purpose:
ALL async bulk results flow through here. Good or bad.
Then we categorize individual failures and attempt a group recovery.
Rank:
Mini-Coordinator
Called by:
bulk_async
"""
########################################################
# PREP TRACKERS & DATA
########################################################
all_data = results.data
working_results = {}
tor_problem_list = []
dns_problem_list = []
########################################################
# Categorize each result
########################################################
for key, reply in all_data.items():
logger.info(f"DEBUG: Processing failure key={key}. with the reply={reply}, reply.error_type={reply.error_type if hasattr(reply, 'error_type') else 'NO ATTR'}")
if reply.valid:
working_results[key] = reply
continue
if reply.error_type == ErrorType.DNS_TEMPORARY:
logger.info(f"Adding {key} to DNS problem list (recoverable).")
dns_problem_list.append(key)
elif reply.error_type == ErrorType.TOR_NOT_WORKING:
logger.info(f"Adding {key} to Tor problem list (can bootstrap new port).")
tor_problem_list.append(key)
else:
logger.info(f"Skipping {key} — error type {reply.error_type} is not recoverable.")
########################################################
# DNS FAILURE. SOLUTION: CUSTOM RESOLVER
########################################################
if dns_problem_list:
logger.info(f"Attempting DNS resolution recovery for: {dns_problem_list}")
recovered_results = custom_dns_resolver_for_BULK_THREADING(observer=observer, dns_problem_list=dns_problem_list) # returns a dict
if recovered_results:
return ApiResponse(valid=True, data=recovered_results, error_type=ErrorType.TOR_DNS_BLOCKED)
else:
# Preserve working results—caller sees what *did* work
return ApiResponse(valid=False, data=working_results, error_type=ErrorType.TOR_DNS_BLOCKED)
########################################################
# TOR NOT WORKING. SOLUTION: NEW BOOTSTRAP
########################################################
if tor_problem_list:
logger.info(f"Attempting Tor bootstrap recovery for: {tor_problem_list}")
tor_endpoints = get_endpoints(wanted_list=tor_problem_list, dns_resolver=False)
new_port = just_only_bootstrap_port(port_tried=_port_used, observer=observer)
if new_port:
return async_parallel(desired_endpoints=tor_endpoints, port=new_port)
else:
# Preserve working results—caller sees what *did* work
return ApiResponse(valid=False, data=working_results, error_type=ErrorType.TOR_NOT_WORKING)
########################################################
# IF IT MADE IT HERE THEN: Either it worked, or we can't solve it.
########################################################
# Send working results — Caller can't solve the failure if we can't.
return ApiResponse(valid=True, data=working_results)
def custom_dns_resolver_for_BULK_THREADING(dns_problem_list: list, observer: ConnectionObserver) -> dict:
# now the real one with the IP address hardcoded:
client = httpx_client.setup_tor_session_WITH_CUSTOM_DNS(port=_port_used)
# now we have the IP address stored in the httpx_client global to call upon inside this function:
dns_endpoints = get_endpoints(wanted_list=dns_problem_list, dns_resolver=True)
return parallel_thread(client, desired_endpoints=dns_endpoints) # threading returns a dict!
def custom_dns_resolver_for_SINGLE_ENDPOINT(method: str, url: str, observer: ConnectionObserver, payload: dict = None) -> dict:
########################################################
# MAKE DNS RESOLVER
########################################################
domain = extract_domain(url)
# is this our API? If so, we're making a client to persist across sessions,
if domain == extract_domain(Constants.SP_API_BASE_URL):
client = httpx_client.setup_tor_session_WITH_CUSTOM_DNS(domain=domain, port=_port_used)
ip_address = httpx_client.get_cached_ip_address()
logger.info(f"Got IP of {ip_address} for {domain}")
else:
# new domain, make only a one time session outside of the httpx_client module:
client, ip_address = setup_SINGLE_use_resolver(domain=domain, port=_port_used)
########################################################
# USE DNS RESOLVER
########################################################
# swap the single url:
url_with_ip = swap_domain_for_ip(domain, url, ip_address)
return make_request(
method=method,
url=url_with_ip,
client=client,
payload=payload
)
# def merge_dicts(dict1, dict2):
# """Merge two dictionaries. dict1 values take precedence on key collision."""
# return {**dict2, **dict1}

View file

@ -0,0 +1,48 @@
from core.errors.logger import logger
import json
import httpx
import ssl
import certifi
from unittest.mock import patch
import httpcore
def create_httpx_client_with_custom_dns(hostname_to_ip_map: dict, custom_proxy: str):
"""
Purpose:
Creates an httpx.Client that connects to IPs while verifying certificates
against the original hostnames (via SNI).
"""
ssl_context = ssl.create_default_context(cafile=certifi.where())
ssl_context.check_hostname = True
ssl_context.verify_mode = ssl.CERT_REQUIRED
# Store the original wrap_socket method
original_wrap_socket = ssl.SSLContext.wrap_socket
def patched_wrap_socket(self, sock, *args, **kwargs):
# Inject server_hostname for IPs in our map
for hostname, ip in hostname_to_ip_map.items():
try:
peer_addr = sock.getpeername()
if peer_addr[0] == ip:
kwargs['server_hostname'] = hostname
break
except OSError:
pass
return original_wrap_socket(self, sock, *args, **kwargs)
# Apply the patch PERMANENTLY
ssl.SSLContext.wrap_socket = patched_wrap_socket
# Build mounts
transport = httpx.HTTPTransport(verify=ssl_context, http2=True)
mounts = {}
for hostname, ip in hostname_to_ip_map.items():
mounts[f"https://{hostname}"] = transport
mounts[f"https://{ip}"] = transport
# Create and return client
client = httpx.Client(mounts=mounts, http2=True, proxy=custom_proxy)
return client

View file

@ -0,0 +1,27 @@
from core.services.networking.httpx.httpx_client import get_cached_ip_address
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.Constants import Constants
def switch_endpoint_domain(domain: str) -> dict:
return {
"locations": f"https://{domain}/api/v1/locations",
"operators": f"https://{domain}/api/v1/operators",
"client": f"https://{domain}/api/v1/platforms/linux-x86_64/appimage/client-versions",
"sub_plans": f"https://{domain}/api/v1/subscription-plans",
"applications": f"https://{domain}/api/v1/platforms/linux-x86_64/applications",
}
def get_endpoints(wanted_list: list, dns_resolver: bool = False) -> dict:
if dns_resolver:
ip_address = get_cached_ip_address() # from when the HTTPx Client was setup
all_endpoints = switch_endpoint_domain(domain=ip_address)
else:
all_endpoints = switch_endpoint_domain(domain=extract_domain(Constants.SP_API_BASE_URL))
# Which ones do you want?
if wanted_list == ["all"]:
return all_endpoints
else:
return {key: all_endpoints[key] for key in wanted_list if key in all_endpoints}

View file

@ -1,10 +1,19 @@
""" """
Module Purpose: Module Purpose:
Manage and reuse an HTTPx Client across modules Manage and reuse an HTTPx Client across modules.
init_tor_session - Setup a setup a tor proxy client
setup_tor_session_WITH_CUSTOM_DNS - Manually lookup IP addresses through a Tor Proxy, then use them with SSL, and verify SSL with a custom local resolver.
""" """
from core.services.networking.tor_tools import tor_dns
from core.services.networking.httpx.dns_resolver import create_httpx_client_with_custom_dns
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.errors.logger import logger from core.errors.logger import logger
from core.Constants import Constants
import httpx import httpx
from httpx_socks import AsyncProxyTransport from httpx_socks import AsyncProxyTransport
@ -12,8 +21,19 @@ import httpx_socks
from httpx_socks import ProxyType from httpx_socks import ProxyType
_http_client = None _http_client = None
_ip_address = None
def init_session() -> bool:
global _http_client
try:
_http_client = httpx.Client(http2=True, timeout=10)
return True
except Exception as e:
logger.error(f"Unknown error with creating an non-Tor httpx client: {e}")
return False
# test tor & get a basic client
def init_tor_session(port: int = 9050) -> bool: def init_tor_session(port: int = 9050) -> bool:
""" """
Purpose: Purpose:
@ -58,6 +78,37 @@ def init_tor_session(port: int = 9050) -> bool:
return False return False
def setup_tor_session_WITH_CUSTOM_DNS(
port: int = 9050,
domain: str = extract_domain(Constants.SP_API_BASE_URL)
) -> bool:
"""
Purpose:
Sets up a Custom HTTPx Proxy Client,
using IP addresses instead of DNS, and then verifying SSL locally.
"""
# Step 1 is done for us prior to calling this function, (called init_tor_session)
global _http_client
global _ip_address
# setup proxy for the DNS resolver:
tor_proxy = f"socks5h://127.0.0.1:{port}"
# Step 2, get the raw IP address:
_ip_address = tor_dns.quad9_lookup(domain=domain, custom_proxy=tor_proxy)
logger.info(f"Got IP address of {_ip_address}")
# Step 3, feed the IP address/proxy, and domain into the DNS resolver:
_http_client = create_httpx_client_with_custom_dns(
hostname_to_ip_map={domain: _ip_address},
custom_proxy=tor_proxy
)
logger.info("Setup Custom DNS HTTPx Client!")
return _http_client
def init_untested_tor(port: int = 9050) -> bool: def init_untested_tor(port: int = 9050) -> bool:
""" """
@ -78,9 +129,16 @@ def init_untested_tor(port: int = 9050) -> bool:
def get_http_session() -> httpx.Client: def get_http_session() -> httpx.Client:
"""Return the global _http_client or raise RuntimeError.""" """Return the global _http_client or raise RuntimeError."""
if _http_client is None: if _http_client is None:
raise RuntimeError("HTTP session not initialized. Call init_tor_session(port) first.") return None
# raise RuntimeError("HTTP session not initialized. Call init_tor_session(port) first.")
return _http_client return _http_client
def get_cached_ip_address() -> str:
"""Return the global _ip_address or raise RuntimeError."""
if _ip_address is None:
raise RuntimeError("Custom DNS Resolver was never initialized. Call setup_tor_session_WITH_CUSTOM_DNS first.")
return _ip_address
def close_http_session(): def close_http_session():
"""Close and reset the global _http_client.""" """Close and reset the global _http_client."""

View file

@ -8,6 +8,7 @@ import asyncio
import json import json
import time import time
import socket import socket
from python_socks._errors import ProxyError
def make_request( def make_request(
method: str, method: str,
@ -91,35 +92,53 @@ def _make_request(
except httpx.ConnectError as e: except httpx.ConnectError as e:
logger.error(f"Error: {e}")
if isinstance(e.__cause__, socket.gaierror): if isinstance(e.__cause__, socket.gaierror):
gaierror = e.__cause__ gaierror = e.__cause__
if gaierror.errno in (-3, -11): # EAI_AGAIN if gaierror.errno in (-3, -11): # EAI_AGAIN
# Transient DNS failure if "Name or service not known" in e:
return ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY, backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL) # Permanent DNS failure (bad domain)
else: return ApiResponse(valid=False, error_type=ErrorType.DNS_PERMANENT, backoff_strategy=BackoffStrategy.NO_RETRY)
# Permanent DNS failure (bad domain) else:
return ApiResponse(valid=False, error_type=ErrorType.DNS_PERMANENT, backoff_strategy=BackoffStrategy.NO_RETRY) # Transient DNS failure
return ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY, backoff_strategy=BackoffStrategy.NO_RETRY) # this is going to go to DNS resolver.
# else:
# # Transient DNS failure
# return ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY, backoff_strategy=BackoffStrategy.NO_RETRY) # this is going to go to DNS resolver.
else: else:
# Non-DNS connection issue, # Non-DNS connection issue,
error_type = ErrorType.CONNECT_ERROR error_type = ErrorType.CONNECTION_ERROR
return ApiResponse(valid=False, error_type=error_type, backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL) return ApiResponse(valid=False, error_type=error_type, backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL)
except httpx.ProxyError as e: except httpx.ProxyError as e:
return ApiResponse( logger.error(f"Error: {e}")
valid=False, if isinstance(e.__cause__, socket.gaierror):
error_type=ErrorType.TOR_NOT_WORKING, gaierror = e.__cause__
message=f"Proxy error: {str(e)}", if gaierror.errno in (-3, -11): # EAI_AGAIN
backoff_strategy=BackoffStrategy.NO_RETRY if "Host unreachable" in e:
) # Permanent DNS failure (bad domain)
return ApiResponse(valid=False, error_type=ErrorType.DNS_PERMANENT, backoff_strategy=BackoffStrategy.NO_RETRY)
else:
# Transient DNS failure
return ApiResponse(valid=False, error_type=ErrorType.DNS_TEMPORARY, backoff_strategy=BackoffStrategy.NO_RETRY) # this is going to go to DNS resolver.
else:
return ApiResponse(
valid=False,
error_type=ErrorType.TOR_NOT_WORKING,
message=f"Proxy error: {str(e)}",
backoff_strategy=BackoffStrategy.NO_RETRY
)
print("this should not print")
except httpx.SSLError as e:
return ApiResponse(
valid=False, # except httpx.SSLError as e:
error_type=ErrorType.NETWORK_ERROR, # return ApiResponse(
message=f"SSL/certificate error: {str(e)}", # valid=False,
backoff_strategy=BackoffStrategy.NO_RETRY # Permanent cert issue # error_type=ErrorType.NETWORK_ERROR,
) # message=f"SSL/certificate error: {str(e)}",
# backoff_strategy=BackoffStrategy.NO_RETRY # Permanent cert issue
# )
except httpx.RequestError as e: except httpx.RequestError as e:
# Catches any other httpx request errors not covered above # Catches any other httpx request errors not covered above
@ -130,6 +149,15 @@ def _make_request(
backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL backoff_strategy=BackoffStrategy.RETRY_EXPONENTIAL
) )
except Exception as e:
return ApiResponse(
valid=False,
error_type=ErrorType.UNKNOWN,
message=f"Request error: {str(e)}",
backoff_strategy=BackoffStrategy.NO_RETRY
)
def switch_get_and_post(method: str, url: str, client: httpx.Client, payload: dict) -> ApiResponse: def switch_get_and_post(method: str, url: str, client: httpx.Client, payload: dict) -> ApiResponse:
if method == "get": if method == "get":

View file

@ -1,6 +1,8 @@
from core.services.networking.httpx.make_request import make_request from core.services.networking.httpx.make_request import make_request
# from core.services.networking.httpx.endpoints import get_endpoints
from core.services.networking.httpx.httpx_client import init_tor_session, get_http_session, init_untested_tor from core.services.networking.httpx.httpx_client import init_tor_session, get_http_session, init_untested_tor
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType, BackoffStrategy from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType, BackoffStrategy
from core.errors.logger import logger from core.errors.logger import logger
import time import time
@ -10,26 +12,10 @@ import httpx
import httpx_socks # pip install httpx-socks import httpx_socks # pip install httpx-socks
from httpx_socks import ProxyType from httpx_socks import ProxyType
def if_a_new_client_is_needed():
transport = httpx_socks.SyncProxyTransport(
proxy_type=ProxyType.SOCKS5,
proxy_host="127.0.0.1",
proxy_port=9050,
)
client = httpx.Client(transport=transport, http2=True, timeout=10)
return client
def parallel_thread(client: httpx.Client, desired_endpoints: dict) -> dict:
ENDPOINTS = { # desired_endpoints = get_endpoints(wanted_list)
"locations": "https://api.hydraveil.net/api/v1/locations",
"operators": "https://api.hydraveil.net/api/v1/operators",
"client": "https://api.hydraveil.net/api/v1/platforms/linux-x86_64/appimage/client-versions",
"sub_plans": "https://api.hydraveil.net/api/v1/subscription-plans",
"applications": "https://api.hydraveil.net/api/v1/platforms/linux-x86_64/applications",
}
def parallel_thread(client: httpx.Client, endpoints: dict = ENDPOINTS) -> dict:
start_total = time.time() start_total = time.time()
with ThreadPoolExecutor(max_workers=5) as executor: with ThreadPoolExecutor(max_workers=5) as executor:
@ -40,7 +26,7 @@ def parallel_thread(client: httpx.Client, endpoints: dict = ENDPOINTS) -> dict:
url=url, url=url,
client=client client=client
) )
for key, url in endpoints.items() for key, url in desired_endpoints.items()
} }
# Wait for all jobs to finish and collect results (BLOCKING) # Wait for all jobs to finish and collect results (BLOCKING)
@ -50,3 +36,14 @@ def parallel_thread(client: httpx.Client, endpoints: dict = ENDPOINTS) -> dict:
logger.info(f"\nTotal time for all API calls: {total_parallel:.2f}s") logger.info(f"\nTotal time for all API calls: {total_parallel:.2f}s")
return results return results
# def if_a_new_client_is_needed():
# transport = httpx_socks.SyncProxyTransport(
# proxy_type=ProxyType.SOCKS5,
# proxy_host="127.0.0.1",
# proxy_port=9050,
# )
# client = httpx.Client(transport=transport, http2=True, timeout=10)
# return client

View file

@ -0,0 +1,204 @@
from core.services.networking.httpx.httpx_client import init_tor_session, get_http_session, init_untested_tor
from core.services.networking.tor_tools.evaluate_tor import evaluate_pre_existing_tor, is_installed
from core.services.networking.tor_tools.install_tor import install_tor
from core.services.networking.tor_tools import tor_files
from core.services.networking.tor_tools import ports
from core.services.networking.tor_tools.bootstrap import bootstrap
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.utils.run_commands import run_generic_command
from core.Constants import Constants
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.errors.logger import logger
import asyncio
import httpx
from httpx_socks import AsyncProxyTransport
# pip install httpx-socks
# we eliminate by saying what did NOT work
def check_if_port_works(port: int) -> bool:
listening = ports.is_port_in_use(port)
if not listening:
return False
worked = init_tor_session(port=port)
if worked:
return True
else:
return False
def get_bootstrap_port(port_tried: int, observer: ConnectionObserver):
# diagnosis = diagnose_tor_port(port_tried, observer)
# if diagnosis.valid:
# port = diagnosis.port
# else:
# port = port_tried
# while port == port_tried:
# Remove the lock
removed_lock = tor_files.remove_lock_file()
# random port:
port = ports.get_random_available_port()
return bootstrap(
port=port,
observer=observer,
use_new_folder=False
)
# if bootstrap_results.valid:
# WORKING_PORT = bootstrap_results.port
# logger.info(f"worked!! got a tor session on {WORKING_PORT}")
# return bootstrap_results
# # working_on_default = init_tor_session(port=bootstrap_results.port)
# if working_on_default:
# return ApiResponse(valid=True, port=bootstrap_results.port)
# else:
# return ApiResponse(valid=False, error_type=ErrorType.UNKNOWN)
# we eliminate by saying what WORKED
def diagnose_tor_port(port_tried: int, observer: ConnectionObserver) -> ApiResponse:
# Step 1) Is Tor Installed?
if not is_installed('tor'):
installed = install_tor()
if installed:
return ApiResponse(valid=True, error_type=ErrorType.TOR_NOT_INSTALLED, port=Constants.DEFAULT_TOR_PORT)
else:
return ApiResponse(valid=False, error_type=ErrorType.REFUSAL_TO_INSTALL_TOR)
# Get info from Tor's torrc config:
config = tor_files.diagnose_config()
# config testing,
if config.valid and config.port:
config_works = check_if_port_works(port=config.port)
if config_works:
return ApiResponse(valid=True, port=config.port)
# default testing
if port_tried != Constants.DEFAULT_TOR_PORT:
default_works = check_if_port_works(port=Constants.DEFAULT_TOR_PORT)
if default_works:
return ApiResponse(valid=True, error_type=ErrorType.TOR_ON_DIFFERENT_PORT, port=Constants.DEFAULT_TOR_PORT)
# Is the port you were using even listening?
that_port_is_listening = ports.is_port_in_use(port_tried)
if not that_port_is_listening:
logger.info(f"The port {port_tried} is NOT listening")
return ApiResponse(valid=False, error_type=ErrorType.PORT_NOT_LISTENING)
# screw it, let's get a random port,
return ApiResponse(valid=False, error_type=ErrorType.UNKNOWN)
# # # Step 2) Defaults?
# # if port_tried == Constants.DEFAULT_TOR_PORT:
# # return ApiResponse(valid=False, error_type=ErrorType.DEFAULT_TOR_PORT_DEAD)
# # # Step 3) is the default listening?
# logger.info(f"The port {port_tried} is at least listening")
# if
# valid=True, error_type=ErrorType.TOR_ON_DIFFERENT_PORT
# return ApiResponse(valid=False, error_type=ErrorType.TOR_INSTALLED_BUT_DEAD, port=config.port)
# if result.valid:
# return result
# logger.info(f"Pre-existing Tor did not work, here's what category we have so far: {result.error_type}")
# # Step 2) Install Tor if needed.
# if result.error_type == ErrorType.TOR_NOT_INSTALLED:
# installed = install_tor()
# if not installed:
# return ApiResponse(valid=False, error_type=ErrorType.REFUSAL_TO_INSTALL_TOR)
# # Step 4) Get a new port:
# port = ports.get_random_available_port()
# # BOOTSTRAP TIME!
# logger.info(f"""
# Ladies and Gentlemen, this is your captain speaking,
# We are strapped in for bootstrapping, please fasten your seats & tables to the upright position.
# Pre-flight Checks Perfomed:
# 1) Tor is installed!
# 2) Default port doesn't work of {Constants.DEFAULT_TOR_PORT}
# 3) Config port checked of {result.port}
# 4) Did we remove the lock file? {removal_result}
# 5) We got a new port of {port}
# Have a good flight...
# """)
# bootstrap_results = bootstrap(
# port=port,
# observer=observer,
# use_new_folder=False
# )
# if bootstrap_results.valid:
# working_on_default = init_tor_session(port=bootstrap_results.port)
# if working_on_default:
# print(f"worked!! got a tor session on {port}")
# return ApiResponse(valid=True, port=bootstrap_results.port)
# else:
# return ApiResponse(valid=False, error_type=ErrorType.UNKNOWN)
# # If I need this later:
# # async def create_async_tor_client(port: int) -> httpx.AsyncClient:
# # """Create an AsyncClient ready for API requests."""
# # transport = AsyncProxyTransport.from_url(f"socks5://127.0.0.1:{port}")
# # return httpx.AsyncClient(transport=transport, http2=True, timeout=30)
# # potential: pip install httpx-socks[asyncio] if the async at bottom is used.

View file

@ -0,0 +1,130 @@
from core.errors.logger import logger
from core.Constants import Constants
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.subtools.custom_httpx_dns_resolver import create_httpx_client_with_custom_dns
from core.services.networking.api_requests.subtools.extract_domain import extract_domain, swap_domain_for_ip
from essentials.observers.ConnectionObserver import ConnectionObserver
import httpx
import dns.message
import dns.name
import dns.rdatatype
import base64
import json
# This version is passed a "generic" proxy with external Tor:
def quad9_lookup(
domain: str,
custom_proxy: str
) -> str:
# ================= Use Proxy for Quad9 ===============
qname = dns.name.from_text(domain)
query = dns.message.make_query(qname, dns.rdatatype.A)
wire_format = query.to_wire()
encoded = base64.urlsafe_b64encode(wire_format).decode().rstrip('=')
# Use httpx with HTTP/2
with httpx.Client(http2=True, proxy=custom_proxy, timeout=20) as client:
response = client.get(
"https://dns.quad9.net/dns-query",
params={"dns": encoded},
headers={"Accept": "application/dns-message"}
)
logger.debug(f"[QUAD9-DNS] http_version: {response.http_version}")
logger.debug(f"[QUAD9-DNS] Status: {response.status_code}")
logger.debug(f"[QUAD9-DNS] Headers: {response.headers}")
if response.status_code == 200:
# ================= Filter Results ===============
result = dns.message.from_wire(response.content)
logger.debug(f"[QUAD9-DNS] Quad9 Returned {result}")
ip_address = None
for rrset in result.answer:
for rr in rrset:
if rr.rdtype == dns.rdatatype.A:
ip_address = rr.address # Get just the IP address
break
if ip_address:
break
return ip_address
else:
logger.error(f"Quad9's Invalid Response Body: {response.text[:500]}")
return False
def setup_SINGLE_use_resolver(
domain: str,
port: int = 9050
) -> bool:
"""
Purpose:
This is a duplicate of what's in httpx_client module,,
But it's because this designed to not-persist across sessions.
Sets up a Custom HTTPx Proxy Client,
using IP addresses instead of DNS, and then verifying SSL locally.
"""
# Step 1. setup proxy for the DNS resolver:
tor_proxy = f"socks5h://127.0.0.1:{port}"
# Step 2, get the raw IP address:
ip_address = quad9_lookup(domain=domain, custom_proxy=tor_proxy)
logger.info(f"Got IP address of {ip_address}")
# Step 3, feed the IP address/proxy, and domain into the DNS resolver:
client = create_httpx_client_with_custom_dns(
hostname_to_ip_map={domain: ip_address},
custom_proxy=tor_proxy
)
return client, ip_address
def quad9_lookup_WITH_SAME_CLIENT(
domain: str,
client: httpx.Client
) -> str:
# ================= Use Proxy for Quad9 ===============
qname = dns.name.from_text(domain)
query = dns.message.make_query(qname, dns.rdatatype.A)
wire_format = query.to_wire()
encoded = base64.urlsafe_b64encode(wire_format).decode().rstrip('=')
response = client.get(
"https://dns.quad9.net/dns-query",
params={"dns": encoded},
headers={"Accept": "application/dns-message"}
)
logger.debug(f"[QUAD9-DNS] Status: {response.status_code}")
logger.debug(f"[QUAD9-DNS] http_version: {response.http_version}")
logger.debug(f"[QUAD9-DNS] Headers: {response.headers}")
if response.status_code == 200:
# ================= Filter Results ===============
result = dns.message.from_wire(response.content)
logger.debug(f"[QUAD9-DNS] Quad9 Returned {result}")
ip_address = None
for rrset in result.answer:
for rr in rrset:
if rr.rdtype == dns.rdatatype.A:
ip_address = rr.address # Get just the IP address
break
if ip_address:
break
return ip_address
else:
logger.error(f"Quad9's Invalid Response Body: {response.text[:500]}")
return False

View file

@ -23,7 +23,7 @@ def establish_tor_connection(observer: ConnectionObserver) -> ApiResponse:
King Orchestrator King Orchestrator
Purpose: Purpose:
Get a working port for a Tor connection, via any available means. Get a working port for a REGULAR non-async Tor connection, via any available means.
""" """
# Step 1) Find out if the defaults or any others work. # Step 1) Find out if the defaults or any others work.
result = evaluate_pre_existing_tor(observer) result = evaluate_pre_existing_tor(observer)

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-core" name = "sp-hydra-veil-core"
version = "2.5.4" version = "2.5.5"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]
@ -46,6 +46,7 @@ dependencies = [
"typing_extensions==4.15.0", "typing_extensions==4.15.0",
"urllib3==2.6.3", "urllib3==2.6.3",
"httpx[http2]==0.28.1", "httpx[http2]==0.28.1",
"httpx-socks==0.11.0",
"dnspython==2.8.0", "dnspython==2.8.0",
"socksio==1.0.0", "socksio==1.0.0",
] ]