Compare commits

..

No commits in common. "632cc16ba153df055cd4d7ac4b26343a33228269" and "c6a189c360835d9fcae671f6a15dea03b83f9a6e" have entirely different histories.

10 changed files with 115 additions and 349 deletions

View file

@ -5,6 +5,5 @@ July 11, 2026
Features: Revamp of GET/POST API requests for both Tor and Clearweb. Features: Revamp of GET/POST API requests for both Tor and Clearweb.
Why: Move towards more clear Tor API error feedback. And reduce redundancy. Why: Move towards more clear Tor API error feedback. And reduce redundancy.
Status: Stable, works for both clear/tor and GET/POST. Status: Stable, works for both clear/tor and GET/POST.
Still Needs: UI feedback, DNS Tor improvements
<br/> <br/>

View file

@ -23,9 +23,7 @@ class ApiResponse:
message: Optional[str] = None message: Optional[str] = None
retry_now: bool = False retry_now: bool = False
retry_later: bool = False retry_later: bool = False
tor: bool = False
ip_address: str = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert to dict for backwards compatibility.""" """Convert to dict for backwards compatibility."""
return { return {
@ -35,7 +33,6 @@ class ApiResponse:
"message": self.message, "message": self.message,
"retry_now": self.retry_now, "retry_now": self.retry_now,
"retry_later": self.retry_later, "retry_later": self.retry_later,
"tor": self.tor,
} }
def get(self, key: str, default=None): def get(self, key: str, default=None):

View file

@ -18,7 +18,6 @@ def get_data_from_api(
full_request_url: str, full_request_url: str,
client_observer: Optional[ClientObserver] = None, client_observer: Optional[ClientObserver] = None,
connection_observer: Optional[ConnectionObserver] = None, connection_observer: Optional[ConnectionObserver] = None,
clearweb_resolved_ip: str = None,
) -> ApiResponse: ) -> ApiResponse:
""" """
GET wrapper. Desktop passes observer for updates; Android passes None. GET wrapper. Desktop passes observer for updates; Android passes None.
@ -40,8 +39,7 @@ def get_data_from_api(
None, # get request has no payload None, # get request has no payload
connection_observer, connection_observer,
client_observer, client_observer,
timeout=10, timeout=10
clearweb_resolved_ip=clearweb_resolved_ip
) )
else: else:
# Regular, no tor: # Regular, no tor:
@ -66,7 +64,6 @@ def send_data_to_server(
original_url: str, original_url: str,
connection_observer: Optional[ConnectionObserver] = None, connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None, client_observer: Optional[ClientObserver] = None,
clearweb_resolved_ip: str = None,
) -> ApiResponse: ) -> ApiResponse:
""" """
POST wrapper. Same pattern as GET. POST wrapper. Same pattern as GET.
@ -81,8 +78,7 @@ def send_data_to_server(
payload, payload,
connection_observer, connection_observer,
client_observer, client_observer,
timeout=10, timeout=10
clearweb_resolved_ip=clearweb_resolved_ip
) )
else: else:
result_object = _execute_regular_request( result_object = _execute_regular_request(
@ -98,5 +94,5 @@ def send_data_to_server(
if not result_object.valid and client_observer: if not result_object.valid and client_observer:
error_msg = result_object.message error_msg = result_object.message
client_observer.notify('synchronizing', f"Connection Issue: {error_msg}") client_observer.notify('synchronizing', f"Connection Issue: {error_msg}")
return result_object return result_object

View file

@ -3,13 +3,13 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
import requests import requests
from core.Constants import Constants from core.Constants import Constants
# Networking Group # Networking Group
from core.services.networking.api_requests.step3_classify_http import classify_http_response from core.services.networking.api_requests.step3_classify_http import classify_http_response
from core.services.networking.api_requests.step4_error_classifier import classify_request_error from core.services.networking.api_requests.step4_error_classifier import classify_request_error
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.subtools.custom_dns import prep_url_with_known_ip, load_custom_dns_resolver
# tor # tor
from essentials.modules.TorModule import TorModule from essentials.modules.TorModule import TorModule
@ -27,8 +27,6 @@ import json, os
from typing import Any from typing import Any
import time import time
def _execute_tor_request( def _execute_tor_request(
method: str, method: str,
url: str, url: str,
@ -36,7 +34,6 @@ def _execute_tor_request(
connection_observer: Optional[ConnectionObserver] = None, connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None, client_observer: Optional[ClientObserver] = None,
timeout: int = 6, timeout: int = 6,
clearweb_resolved_ip: str = None # if DNS via Tor is blocked
) -> ApiResponse: ) -> ApiResponse:
""" """
Tor request with optional intermediate UI feedback (desktop-only). Tor request with optional intermediate UI feedback (desktop-only).
@ -60,32 +57,12 @@ def _execute_tor_request(
proxies = {"http": f"socks5h://127.0.0.1:{port_number}", proxies = {"http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}"} "https": f"socks5h://127.0.0.1:{port_number}"}
# ================= OPTION: DNS OUTSIDE TOR. (IF DNS VIA TOR IS BLOCKED) =============== if method.lower() == "get":
if clearweb_resolved_ip: response = requests.get(url, proxies=proxies, timeout=timeout)
CustomHostnameAdapter = load_custom_dns_resolver()
url_with_ip, original_hostname = prep_url_with_known_ip(clearweb_resolved_ip, url)
# Set up session with custom adapter for DNS resolution:
session = requests.Session()
session.mount('https://', CustomHostnameAdapter(original_hostname))
session.headers.update({"Host": original_hostname})
# Make the request to the raw IP, but tell the server it's the original domain
if method.lower() == "get":
response = session.get(url_with_ip, proxies=proxies, timeout=timeout, verify=True)
else:
response = requests.post(url_with_ip, json=payload, proxies=proxies, timeout=timeout, verify=True)
else: else:
# ================= USING DNS VIA TOR (normal-use) =============== response = requests.post(url, json=payload, proxies=proxies, timeout=timeout)
if method.lower() == "get":
response = requests.get(url, proxies=proxies, timeout=timeout)
else:
response = requests.post(url, json=payload, proxies=proxies, timeout=timeout)
# ================= Working Response ===============
result = classify_http_response(response) result = classify_http_response(response)
except Exception as e: except Exception as e:

View file

@ -1,3 +1,9 @@
# from __future__ import annotations
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
@ -6,7 +12,7 @@ from core.services.networking.api_requests.ApiResponseModel import ApiResponse,
# diagnosis services # diagnosis services
from core.services.networking.api_requests.subtools.internet_test import do_we_have_internet from core.services.networking.api_requests.subtools.internet_test import do_we_have_internet
from core.services.networking.api_requests.subtools.dns_evaluation import is_dns_problem, dns_works_via_tor from core.services.networking.api_requests.subtools.is_dns_problem import is_dns_problem, is_dns_problem_via_tor
from core.services.networking.api_requests.subtools.extract_domain import extract_domain from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.services.networking.api_requests.subtools.is_tor_working import is_tor_working from core.services.networking.api_requests.subtools.is_tor_working import is_tor_working
@ -25,7 +31,8 @@ def classify_request_error(
client_observer: Optional[ClientObserver] = None, client_observer: Optional[ClientObserver] = None,
) -> ApiResponse: ) -> ApiResponse:
""" """
Classify all error requests, Tor or Not. Unified diagnostic tree for all request failures.
Replaces evaluate_tor_networking_problem() and evaluate_regular_networking_problem().
""" """
domain_only = extract_domain(url) domain_only = extract_domain(url)
@ -36,36 +43,23 @@ def classify_request_error(
return ApiResponse( return ApiResponse(
valid=False, valid=False,
error_type=ErrorType.TOR_NOT_WORKING, error_type=ErrorType.TOR_NOT_WORKING,
tor=True,
message="Tor connection failed" message="Tor connection failed"
) )
ip_via_tor = dns_works_via_tor(domain_only, connection_observer) logger.debug("Skipping DNS resolution via Tor due to PySocks support.")
# if is_dns_problem_via_tor(domain_only, connection_observer):
if not ip_via_tor: # logger.debug("DNS resolution via Tor failed")
logger.debug("DNS resolution via Tor failed") # return ApiResponse(
return ApiResponse( # valid=False,
valid=False, # error_type=ErrorType.DNS_RESOLUTION,
error_type=ErrorType.DNS_RESOLUTION, # message=f"Cannot resolve {domain_only} via Tor"
tor=True, # )
message=f"Cannot resolve {domain_only} via Tor"
)
else:
return ApiResponse(
valid=False,
error_type=ErrorType.UNKNOWN,
tor=True,
ip_address=ip_via_tor,
message=f"For Unknown reasons, we can't connect, but we can resolve {domain_only} to {ip_via_tor} via Tor"
)
else: else:
# Regular (non-Tor) diagnostics # Regular (non-Tor) diagnostics
if not do_we_have_internet(): if not do_we_have_internet():
logger.error("No internet connection") logger.error("No internet connection")
return ApiResponse( return ApiResponse(
valid=False, valid=False,
tor=False,
error_type=ErrorType.NO_INTERNET, error_type=ErrorType.NO_INTERNET,
message="No internet connectivity" message="No internet connectivity"
) )
@ -74,7 +68,6 @@ def classify_request_error(
logger.error("Local DNS resolution failed") logger.error("Local DNS resolution failed")
return ApiResponse( return ApiResponse(
valid=False, valid=False,
tor=False,
error_type=ErrorType.DNS_RESOLUTION, error_type=ErrorType.DNS_RESOLUTION,
message=f"Cannot resolve {domain_only}" message=f"Cannot resolve {domain_only}"
) )

View file

@ -1,108 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import requests
# errors
from core.errors.exceptions import *
from core.errors.logger import logger
from urllib.parse import urlparse, urlunparse
def prep_url_with_known_ip(known_ip: str, original_url: str):
"""
Purpose:
Prepares URLs for custom DNS checking
Rank:
Helper
Called by:
step2_execute's _execute_tor_request
Raises Errors?
False
"""
# Parse the original URL to extract the hostname
parsed = urlparse(original_url)
original_hostname = parsed.hostname
# Reconstruct the URL with the IP instead of the domain
url_with_ip = original_url.replace(parsed.hostname, known_ip)
return url_with_ip, original_hostname
def load_custom_dns_resolver():
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
import ssl
import certifi
class CustomHostnameAdapter(HTTPAdapter):
"""
Purpose:
If Tor (or ANY) DNS server is blocked, we can go directly to the IP,
and use this module to verify the SSL against local certs.
Method:
Adapter that connects to an IP but verifies the SSL cert against the original hostname (via SNI).
then uses that with the proxy, essentially intercepting/modifying the proxy behavior
Rank:
Coordinator for the SSL verification Task,
But the caller is orchestrating the actual API calls.
Called by:
step2_execute's _execute_tor_request
Raises Errors?
False
"""
def __init__(self, original_hostname, *args, **kwargs):
logger.debug(f"\n[ADAPTER INIT] Creating CustomHostnameAdapter")
logger.debug(f" original_hostname: {original_hostname}")
self.original_hostname = original_hostname
super().__init__(*args, **kwargs)
logger.debug(f"[ADAPTER INIT] ✓ Adapter initialized")
def init_poolmanager(self, *args, **kwargs):
logger.debug(f"\n[POOLMANAGER INIT] Creating standard pool manager (for direct connections)")
ctx = create_urllib3_context()
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(certifi.where())
kwargs['ssl_context'] = ctx
logger.debug(f" ✓ Standard pool manager configured")
return super().init_poolmanager(*args, **kwargs)
def proxy_manager_for(self, proxy, **proxy_kwargs):
"""
This is called when making requests through a proxy.
This is where we inject the custom SSL context.
"""
logger.debug(f"\n[PROXY MANAGER] Creating proxy manager")
logger.debug(f" proxy: {proxy}")
logger.debug(f" original_hostname (for verification): {self.original_hostname}")
ctx = create_urllib3_context()
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(certifi.where())
# KEY: Set server_hostname on the context for SNI
logger.debug(f" Setting server_hostname for SNI: {self.original_hostname}")
proxy_kwargs['ssl_context'] = ctx
proxy_kwargs['server_hostname'] = self.original_hostname
logger.debug(f" ✓ Proxy manager will verify cert against: {self.original_hostname}")
result = super().proxy_manager_for(proxy, **proxy_kwargs)
logger.debug(f" ✓ Proxy manager created successfully")
return result
return CustomHostnameAdapter

View file

@ -1,133 +0,0 @@
# for tor:
from essentials.modules.TorModule import TorModule
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
from core.observers.ClientObserver import ClientObserver
from core.Constants import Constants
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.errors.logger import logger
from core.errors.exceptions import *
import dns.resolver
import dns.exception
import dns.rdatatype
from typing import Optional
import os
import socket
import socks
import subprocess
"""
Check if there's a DNS problem with a domain.
Returns True if DNS fails (problem exists), False if DNS resolves successfully.
"""
def is_dns_problem(url: str) -> bool:
"""Regular DNS check for non-Tor domains"""
domain = extract_domain(url)
try:
socket.gethostbyname(domain)
return False # DNS resolved successfully
except socket.gaierror:
return True # DNS resolution failed
except Exception:
return True
def dns_works_via_tor(domain, connection_observer, client_observer=None):
"""
Purpose:
Check if domain's resolver is blocking Tor by querying via proxychains.
Method:
Dynamically configures proxychains with the given port.
Uses this to query the DNS via dig.
Dependencies:
Proxychains being installed.
Called by:
step4_error_classifier's classify_request_error
Raises Errors?
False
"""
port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"[DNS PROBLEM OF TOR SERVICE] Using Tor on port number {port_number}")
if client_observer is not None:
client_observer.notify('synchronizing', f"Using Tor on Port {port_number}..")
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
logger.debug(f"[DNS PROBLEM OF TOR SERVICE] Tor Started a Session")
if client_observer is not None:
client_observer.notify('synchronizing', f"Initialized Tor..")
except TorServiceInitializationError as e:
error_msg = f"[DNS PROBLEM OF TOR SERVICE] Tor failed to start: {e}"
tor_module.destroy_session(port_number)
logger.error(error_msg, exc_info=True)
if client_observer is not None:
client_observer.notify('synchronizing', f"Tor Failed to Initialize")
proxies = {
"http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}",
}
config_path = f"{Constants.HV_CONFIG_HOME}/proxychains_dynamic.conf"
try:
# Write proxychains config on the fly
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
f.write(f"""
strict_chain
proxy_dns
tcp_read_time_out 10000
tcp_connect_time_out 10000
[ProxyList]
socks5 127.0.0.1 {port_number}
""")
f.flush()
os.fsync(f.fileno()) # Force write to disk
print("running command...")
# Run dig through proxychains with the dynamic config
try:
result = subprocess.run(
['proxychains4', '-f', config_path, 'dig', domain, '+short'],
capture_output=True,
text=True,
timeout=10
)
logger.debug(f"result: {result}")
except:
logger.error(f"[DNS PROBLEM] Proxychains not installed on this computer")
return False
output = result.stdout.strip()
logger.debug(f"output of stripping the IP out of the result: {output}")
if not output or result.returncode != 0:
logger.debug(f"[DNS PROBLEM] {domain}: No response")
return False
logger.debug(f"[DNS OK] {domain}: {output}")
return output
except subprocess.TimeoutExpired:
logger.debug(f"[DNS PROBLEM] {domain}: Timeout")
return False
except Exception as e:
logger.debug(f"[DNS PROBLEM] {domain}: {type(e).__name__}: {e}")
return False
finally:
tor_module.destroy_session(port_number)

View file

@ -0,0 +1,87 @@
# for tor:
from essentials.modules.TorModule import TorModule
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
from core.observers.ClientObserver import ClientObserver
from core.Constants import Constants
import json, os
from typing import Optional
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.errors.logger import logger
import socket
import socks
"""
Check if there's a DNS problem with a domain.
Returns True if DNS fails (problem exists), False if DNS resolves successfully.
"""
def is_dns_problem(url: str) -> bool:
domain = extract_domain(url)
try:
socket.gethostbyname(domain)
return False # DNS resolved successfully, no problem
except socket.gaierror:
return True # DNS resolution failed, problem exists
except Exception:
return True # Any other error is treated as a DNS problem
def is_dns_problem_via_tor(
domain: str,
connection_observer: ConnectionObserver | None = None,
client_observer: Optional[ClientObserver] = None,
) -> bool:
logger.debug("[DNS PROBLEM OF TOR SERVICE] We've triggered EVALUATING IF it's a DNS problem via Tor")
port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"[DNS PROBLEM OF TOR SERVICE] Using Tor on port number {port_number}")
if client_observer is not None:
client_observer.notify('synchronizing', f"Using Tor on Port {port_number}..")
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
logger.debug(f"[DNS PROBLEM OF TOR SERVICE] Tor Started a Session")
if client_observer is not None:
client_observer.notify('synchronizing', f"Initialized Tor..")
except TorServiceInitializationError as e:
error_msg = f"[DNS PROBLEM OF TOR SERVICE] Tor failed to start: {e}"
tor_module.destroy_session(port_number)
logger.error(error_msg, exc_info=True)
if client_observer is not None:
client_observer.notify('synchronizing', f"Tor Failed to Initialize")
# proxies = {
# "http": f"socks5h://127.0.0.1:{port_number}",
# "https": f"socks5h://127.0.0.1:{port_number}",
# }
logger.debug(f"[DNS PROBLEM OF TOR SERVICE] About to try to apply the proxy but may get a PySocks error...")
try:
# Set up SOCKS5 proxy using the same port as the existing Tor proxy,
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", port_number)
socket.socket = socks.socksocket
except:
logger.error(f"[DNS PROBLEM OF TOR SERVICE] Error with setting up the socks5 proxy using socks.set_deault0")
tor_module.destroy_session(port_number)
return True
# DNS query check
try:
logger.debug("checking dns via socket..")
socket.gethostbyname(domain)
tor_module.destroy_session(port_number)
return False # DNS resolved successfully, no problem
except socket.gaierror:
return True # DNS resolution failed, problem exists
except Exception:
return True # Any other error is treated as a DNS problem

View file

@ -1,42 +0,0 @@
print("hi")
import httpx
import ssl
import certifi
def load_custom_dns_resolver():
"""Create a client with custom transports for Tor DNS bypass."""
def create_client_with_custom_dns(hostname_to_ip_map: dict):
"""
hostname_to_ip_map: {"example.com": "203.0.113.45", "other.com": "203.0.113.46"}
"""
mounts = {}
for hostname, ip in hostname_to_ip_map.items():
ctx = ssl.create_default_context(cafile=certifi.where())
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
# transport = httpx.HTTPTransport(ssl_context=ctx, http2=True)
transport = httpx.HTTPTransport(ssl_context=context)
# Mount for both hostname and IP patterns
mounts[f"https://{hostname}"] = transport
mounts[f"https://{ip}"] = transport
# client = httpx.Client(mounts=mounts, http2=True)
client = httpx.Client(mounts=mounts)
return client
return create_client_with_custom_dns
# Usage
create_client = load_custom_dns_resolver()
client = create_client({"api.hydraveil.net": "185.165.169.91"})
# Connect to IP, but Host header and cert verification use original hostname
response = client.get("https://185.165.169.91/api/v1/cachedsync", headers={"Host": "api.hydraveil.net"})
print(response)
print(response.json)

View file

@ -18,7 +18,7 @@ dependencies = [
"pysocks ~= 1.7.1", "pysocks ~= 1.7.1",
"python-dateutil ~= 2.9.0.post0", "python-dateutil ~= 2.9.0.post0",
"requests ~= 2.32.5", "requests ~= 2.32.5",
"sp-essentials ~= 1.2.1", "sp-essentials ~= 1.2.0",
"annotated-types==0.7.0", "annotated-types==0.7.0",
"certifi==2026.4.22", "certifi==2026.4.22",
"charset-normalizer==3.4.7", "charset-normalizer==3.4.7",