Added Support for Retries of Tor DNS failures with a custom HTTPX DNS resolver using DoH Quad9 via Tor, to then go direct. And setup ticket and sync modules to use this new error solver.

This commit is contained in:
SimplifiedPrivacy 2026-07-13 15:50:04 -04:00
parent f919957e48
commit 1722905e62
18 changed files with 614 additions and 95 deletions

View file

@ -55,14 +55,11 @@ class ClientController:
@staticmethod @staticmethod
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None): def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
if client_observer is not None: if client_observer is not None:
client_observer.notify('synchronizing', "Fetching list of new data ..") client_observer.notify('synchronizing', "Fetching list of new data ..")
result = coordinate_cache_sync(client_observer, connection_observer) result = coordinate_cache_sync(client_observer, connection_observer)
# logger.info(f"We got a Result from the API of {result}")
# Outright Error: # Outright Error:
if not result["success"]: if not result["success"]:
error_msg = result["error"] error_msg = result["error"]

View file

@ -9,8 +9,8 @@ from core.models.invoice.TicketInvoice import TicketInvoice
from core.services.prepare_tickets.get_pub_key import get_pub_key from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing
# from core.services.payment_phase.check_if_paid import _check_if_paid
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.step5_solve_api_problems import solve_api_problems
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
@ -186,15 +186,23 @@ def check_if_paid(
# literally send: # literally send:
api_reply_object = send_data_to_server(payload, url, connection_observer) api_reply_object = send_data_to_server(payload, url, connection_observer)
if not api_reply_object.valid:
api_reply_object = solve_api_problems(
api_reply_object=api_reply_object,
get_or_post="post",
url=url,
payload=payload,
connection_observer=connection_observer,
client_observer=None
)
if not api_reply_object.valid:
# 2nd try, return the error of why we have no payload:
error_msg = f"Connection/API Error: {api_reply_object.message}"
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return {"valid": False, "message": error_msg}
# return the payload with GUI/CLI to interpret results: # return the payload with GUI/CLI to interpret results:
if api_reply_object.valid:
reply_dict = api_reply_object.data reply_dict = api_reply_object.data
logger.debug(f"inside ticketpay controller the reply is {reply_dict}") logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")
return reply_dict return reply_dict
# return the error of why we have no payload:
else:
error_msg = api_reply_object.message
error_msg = f"Connection/API Error: {error_msg}"
logger.error(f"inside ticketpay controller there was an a {error_msg}")
return {"valid": False, "message": error_msg}

View file

@ -9,6 +9,7 @@ from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.networking.api_requests.step1_get_or_post import get_data_from_api from core.services.networking.api_requests.step1_get_or_post import get_data_from_api
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.step5_solve_api_problems import solve_api_problems
from core.errors.logger import logger from core.errors.logger import logger
@ -31,12 +32,24 @@ def sync_ticket_prices(
url = f"{base_url}/sync" url = f"{base_url}/sync"
try: try:
sync_results = get_data_from_api(url, None, connection_observer) api_result = get_data_from_api(url, None, connection_observer)
if sync_results.valid: if not api_result.valid:
return {"valid": True, "data": sync_results.data} error_msg = api_result.message
else: logger.error(f"[TICKET SYNC Controller] There's an issue with the sync of endpoint {url} the API Reply: {error_msg}")
api_result = solve_api_problems(
api_reply_object=api_result,
get_or_post="get",
url=url,
payload=None,
connection_observer=connection_observer,
client_observer=None
)
# 2nd try:
if not api_result.valid:
return {"valid": False, "error_code": "sync_failed"} return {"valid": False, "error_code": "sync_failed"}
return {"valid": True, "data": api_result.data}
except: except:
return {"valid": False, "error_code": "sync_failed"} return {"valid": False, "error_code": "sync_failed"}

View file

@ -6,12 +6,15 @@ class ErrorType(Enum):
"""Classified error categories.""" """Classified error categories."""
SUCCESS = "success" SUCCESS = "success"
TOR_NOT_WORKING = "tor_not_working" TOR_NOT_WORKING = "tor_not_working"
TOR_DNS_BLOCKED = "tor_dns_blocked"
DNS_RESOLUTION = "dns_resolution" DNS_RESOLUTION = "dns_resolution"
QUAD9_DNS_RESOLUTION = "quad9_dns_resolution"
NO_INTERNET = "no_internet" NO_INTERNET = "no_internet"
NETWORK_ERROR = "network_error" NETWORK_ERROR = "network_error"
INVALID_INPUT = "invalid_input" INVALID_INPUT = "invalid_input"
RATE_LIMITED = "rate_limited" RATE_LIMITED = "rate_limited"
SERVER_ERROR = "server_error" SERVER_ERROR = "server_error"
DEVELOPER_ERROR = "developer_error"
UNKNOWN = "unknown" UNKNOWN = "unknown"
@dataclass @dataclass
@ -25,6 +28,7 @@ class ApiResponse:
retry_later: bool = False retry_later: bool = False
tor: bool = False tor: bool = False
ip_address: str = None ip_address: str = None
ask_clearweb: bool = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert to dict for backwards compatibility.""" """Convert to dict for backwards compatibility."""
@ -36,6 +40,7 @@ class ApiResponse:
"retry_now": self.retry_now, "retry_now": self.retry_now,
"retry_later": self.retry_later, "retry_later": self.retry_later,
"tor": self.tor, "tor": self.tor,
"ask_clearweb": self.ask_clearweb
} }
def get(self, key: str, default=None): def get(self, key: str, default=None):

View file

@ -13,8 +13,6 @@ def classify_http_response(response) -> ApiResponse:
""" """
import requests import requests
print("We're inside classify http")
if not isinstance(response, requests.Response): if not isinstance(response, requests.Response):
return ApiResponse( return ApiResponse(
valid=False, valid=False,

View file

@ -25,7 +25,10 @@ def classify_request_error(
client_observer: Optional[ClientObserver] = None, client_observer: Optional[ClientObserver] = None,
) -> ApiResponse: ) -> ApiResponse:
""" """
Classify all error requests, Tor or Not. Classify all error requests,
Tor Connection.
DNS
Internet Connection
""" """
domain_only = extract_domain(url) domain_only = extract_domain(url)

View file

@ -0,0 +1,60 @@
from core.errors.logger import logger
from core.Constants import Constants
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from essentials.observers.ConnectionObserver import ConnectionObserver
from essentials.services.ConnectionService import ConnectionService
from core.observers.ClientObserver import ClientObserver
# tools to solve:
from core.services.networking.api_requests.subtools.direct_dns_tools import get_DNS_then_use_it
# generic
from typing import Optional
def solve_api_problems(
api_reply_object: ApiResponse,
get_or_post: str,
url: str,
payload: str = None,
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
) -> ApiResponse:
if api_reply_object.valid:
logger.debug(f"[API SOLVER] The API call worked, so there's no reason to try to solve it.")
return api_reply_object
reason_for_error = api_reply_object.error_type
cant_be_solved = [ErrorType.QUAD9_DNS_RESOLUTION, ErrorType.UNKNOWN, ErrorType.NO_INTERNET]
if reason_for_error in cant_be_solved:
logger.debug(f"[API SOLVER] This can't be solved if the reason is {reason_for_error}")
return api_reply_object
elif reason_for_error == ErrorType.DEVELOPER_ERROR:
logger.error(f"[API SOLVER] The developer made an error {api_reply_object.message}")
return api_reply_object
elif reason_for_error == ErrorType.DNS_RESOLUTION:
logger.debug(f"[API SOLVER] We're solving a DNS resolution error {api_reply_object.message}")
if api_reply_object.tor:
dns_fix = get_DNS_then_use_it(get_or_post, url, payload, connection_observer, client_observer)
return dns_fix
else:
logger.error(f"[API SOLVER] We have NOT yet setup clearweb DNS solutions")
return api_reply_object
elif reason_for_error == ErrorType.TOR_NOT_WORKING:
api_reply_object.ask_clearweb = True
return api_reply_object
elif reason_for_error == ErrorType.NETWORK_ERROR:
if api_reply_object.tor:
api_reply_object.ask_clearweb = True
return api_reply_object
else:
logger.error(f"[API SOLVER] Unknown reason {reason_for_error} with message: {api_reply_object.message}")
return api_reply_object

View file

@ -0,0 +1,46 @@
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,343 @@
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
# 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
# pip install pip install httpx[http2]
# pip install httpx[socks]
import httpx
import dns.message
import dns.name
import dns.rdatatype
import base64
import json
from typing import Optional
def get_DNS_then_use_it(
get_or_post: str,
full_url: str,
payload: str = None,
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
) -> ApiResponse:
if not full_url:
return ApiResponse(
valid=False,
error_type=ErrorType.DEVELOPER_ERROR,
tor=True,
message="Dev Error! Invalid Inputs to Function"
)
# ================= Setup Tor ===============
if client_observer:
status_update = "Evading Tor DNS block..."
client_observer.notify('synchronizing', status_update)
port_number = None
tor_module = None
client = None
try:
port_number = ConnectionService.get_random_available_port_number()
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
tor_proxy = f"socks5h://127.0.0.1:{port_number}"
logger.debug(f"[DNS TOOLS] Tor Proxy Setup on Port {port_number}")
# ================= Do Quad9 Query ===============
domain = extract_domain(full_url)
logger.debug(f"[DNS TOOLS] We'll be doing a DNS lookup to the domain {domain}..")
ip_address = quad9_proxy_dns_lookup(domain, tor_proxy)
if not ip_address:
return ApiResponse(
valid=False,
error_type=ErrorType.QUAD9_DNS_RESOLUTION,
tor=True,
message=f"Cannot resolve {domain} via Tor or Quad9"
)
logger.debug(f"[DNS TOOLS] We got an IP address from Quad9 for {domain} of: {ip_address}")
# ================= Use Custom DNS ===============
client = create_httpx_client_with_custom_dns({domain: ip_address}, tor_proxy)
url_with_ip = swap_domain_for_ip(domain, full_url, ip_address)
print(f"url_with_ip is {url_with_ip}")
print(f"we are doing a {get_or_post} request")
if get_or_post == "get":
response = client.get(url_with_ip, headers={"Host": domain})
elif get_or_post == "post":
if payload:
response = client.post(url_with_ip, json=payload, headers={"Host": domain})
else:
return ApiResponse(
valid=False,
error_type=ErrorType.DEVELOPER_ERROR,
ip_address=ip_address,
tor=True,
message=f"Dev Error! No Payload for POST request"
)
else:
return ApiResponse(
valid=False,
error_type=ErrorType.DEVELOPER_ERROR,
ip_address=ip_address,
tor=True,
message=f"Dev Error! Invalid Input of get_or_post: {get_or_post}"
)
# print(response.json())
if not response:
error_msg = f"Could not connect to {domain} via {ip_address}"
return ApiResponse(
valid=False,
error_type=ErrorType.NETWORK_ERROR,
ip_address=ip_address,
tor=True,
message=error_msg
)
response_status_code = response.status_code
if 200 <= response_status_code < 300:
try:
data = response.json()
return ApiResponse(
valid=True,
error_type=ErrorType.TOR_DNS_BLOCKED,
ip_address=ip_address,
tor=True,
data=data
)
except ValueError:
print("We could not get the value from it.")
return ApiResponse(
valid=True,
error_type=ErrorType.SERVER_ERROR,
ip_address=ip_address,
tor=True,
data=response.text
)
else:
error_msg = f"Server replied with {response_status_code} at {ip_address}"
return ApiResponse(
valid=False,
error_type=ErrorType.SERVER_ERROR,
ip_address=ip_address,
tor=True,
message=error_msg
)
except Exception as e:
error_msg = f"Tor failed: {type(e).__name__}: {e}"
logger.error(f"[DNS] {error_msg}")
return ApiResponse(
valid=False,
error_type=ErrorType.NETWORK_ERROR,
tor=True,
message=error_msg
)
finally:
if client:
client.close()
if tor_module and port_number:
try:
tor_module.destroy_session(port_number)
except Exception as e:
logger.warning(f"[DNS] Error destroying Tor session: {e}")
# This version is passed a "generic" proxy with external Tor:
def quad9_proxy_dns_lookup(
domain: str,
custom_proxy: str,
timeout: int = 10,
) -> str:
logger.debug("Doing a Proxy Quad9 DNS lookup")
# ================= 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=timeout) as client:
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] 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]}")
if client_observer:
status_update = f"Quad9 Failed to Resolve {domain}"
client_observer.notify('synchronizing', status_update)
return False
# This version does Tor & the lookup same time.
def quad9_tor_dns_lookup(
domain: str,
connection_observer: Optional[ConnectionObserver] = None,
client_observer: Optional[ClientObserver] = None,
timeout: int = 10,
) -> str:
import httpx
import dns.message
import dns.name
import dns.rdatatype
import base64
logger.debug("Doing a Tor Quad9 DNS lookup")
# ================= Setup Tor ===============
if client_observer:
status_update = "Initializing Tor connection..."
client_observer.notify('synchronizing', status_update)
port_number = None
tor_module = None
try:
port_number = ConnectionService.get_random_available_port_number()
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer)
tor_proxy = f"socks5h://127.0.0.1:{port_number}"
logger.debug(f"[QUAD9-DNS] Tor Proxy Setup on Port {port_number}")
# ================= 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=tor_proxy, timeout=timeout) as client:
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] 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]}")
if client_observer:
status_update = f"Quad9 Failed to Resolve {domain}"
client_observer.notify('synchronizing', status_update)
return False
except Exception as e:
logger.debug(f"[QUAD9-DNS] Tor Quad9 GET failed: {type(e).__name__}: {e}")
return None
finally:
if tor_module and port_number:
try:
tor_module.destroy_session(port_number)
except Exception as e:
logger.warning(f"[QUAD9-DNS] Error destroying Tor session: {e}")
return result
def quad9_clearweb_dns_lookup(domain: str) -> str:
import httpx
import dns.message
import dns.name
import dns.rdatatype
import base64
logger.debug("Doing a Clearweb Quad9 DNS lookup")
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('=')
try:
# Use httpx with HTTP/2
with httpx.Client(http2=True) as client:
response = client.get(
"https://dns.quad9.net/dns-query",
params={"dns": encoded},
headers={"Accept": "application/dns-message"}
)
logger.debug(f"Status: {response.status_code}")
logger.debug(f"Headers: {response.headers}")
if response.status_code == 200:
result = dns.message.from_wire(response.content)
logger.debug(f"Quad9 Returned {result}")
return result
else:
logger.error(f"Quad9 Body: {response.text[:500]}")
return False
except:
logger.error("Regular Quad9 request failed.")
return None

View file

@ -1,10 +1,12 @@
# used in the 2nd function, not the first,
import re
def extract_domain(url: str) -> str:
""" """
Purpose:
Extract domain from URL, stripping protocol and path. Extract domain from URL, stripping protocol and path.
If exact TLD match isn't found, tries to return the best guess. If exact TLD match isn't found, tries to return the best guess.
""" """
def extract_domain(url: str) -> str:
common_domains = [".com", ".net", ".org", ".is"] common_domains = [".com", ".net", ".org", ".is"]
# Remove protocol # Remove protocol
@ -29,3 +31,24 @@ def extract_domain(url: str) -> str:
# Fallback: return everything before the first slash (best guess) # Fallback: return everything before the first slash (best guess)
return url.split("/")[0] return url.split("/")[0]
def swap_domain_for_ip(raw_domain: str, full_url: str, ip_address: str):
"""
Purpose:
Switch the real domain for the raw IP address,
But keep the rest of the domain intact.
Rank:
Helper
Raises Errors?
False
"""
# Escape the domain in case it has regex special characters (like dots)
escaped_domain = re.escape(raw_domain)
# Replace only the first instance of the domain with the IP
url_with_ip = re.sub(escaped_domain, ip_address, full_url, count=1)
return url_with_ip

View file

@ -9,6 +9,7 @@ from core.models.invoice.TicketInvoice import TicketInvoice
from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.services.networking.api_requests.step1_get_or_post import send_data_to_server
# from core.services.networking.send_data_to_server import send_data_to_server # from core.services.networking.send_data_to_server import send_data_to_server
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.step5_solve_api_problems import solve_api_problems
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
@ -48,6 +49,15 @@ def save_and_send_intitial_billing(
url = make_url(which_endpoint) url = make_url(which_endpoint)
api_reply_object = send_data_to_server(payload, url, connection_observer) api_reply_object = send_data_to_server(payload, url, connection_observer)
if not api_reply_object.valid:
api_reply_object = solve_api_problems(
api_reply_object=api_reply_object,
get_or_post="post",
url=url,
payload=payload,
connection_observer=connection_observer,
client_observer=None
)
if not api_reply_object.valid: if not api_reply_object.valid:
return api_reply_object return api_reply_object

View file

@ -7,6 +7,7 @@ if TYPE_CHECKING:
# services # services
from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.services.networking.api_requests.step1_get_or_post import send_data_to_server
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.step5_solve_api_problems import solve_api_problems
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.services.helpers.get_which_billing_key import get_which_billing_key from core.services.helpers.get_which_billing_key import get_which_billing_key
@ -50,10 +51,19 @@ def send_blind_commitments(
# did API call work? # did API call work?
if not api_reply_object.valid: if not api_reply_object.valid:
error_msg = api_reply_object.message final_error_msg = f"[SEND BLIND COMMITMENTS] First Post request failed {api_reply_object.message}"
final_error_msg = f"API/Connection Error {error_msg}"
logger.error(final_error_msg) logger.error(final_error_msg)
ticket_observer.notify("error", subject=final_error_msg) api_reply_object = solve_api_problems(
api_reply_object=api_reply_object,
get_or_post="post",
url=url,
payload=payload,
connection_observer=connection_observer,
client_observer=None
)
if not api_reply_object.valid:
logger.error(f"[SEND BLIND COMMITMENTS] Second Post request failed. {api_reply_object.message}")
ticket_observer.notify("error", subject=api_reply_object.message)
return None return None
# assuming it worked, extract the data: # assuming it worked, extract the data:

View file

@ -1,6 +1,8 @@
# from core.services.sync.get_data_from_api import get_data_from_api # from core.services.sync.get_data_from_api import get_data_from_api
from core.services.networking.api_requests.step1_get_or_post import get_data_from_api from core.services.networking.api_requests.step1_get_or_post import get_data_from_api
from core.services.networking.api_requests.ApiResponseModel import ApiResponse from core.services.networking.api_requests.ApiResponseModel import ApiResponse
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.models.manage.insert import insert_into_model from core.models.manage.insert import insert_into_model
from core.models.manage.denormalize import denormalize from core.models.manage.denormalize import denormalize
@ -22,14 +24,24 @@ def sync_one_orm_model(
) -> dict: ) -> dict:
full_request_url = f"{Constants.SP_API_BASE_URL}/{which_endpoint}" full_request_url = f"{Constants.SP_API_BASE_URL}/{which_endpoint}"
api_result = get_data_from_api(full_request_url, ClientObserver, ConnectionObserver) api_result = get_data_from_api(full_request_url, client_observer, connection_observer)
if not api_result.valid: if not api_result.valid:
error_msg = api_result.message error_msg = api_result.message
logger.error(f"API sync failed for {which_endpoint} with: {error_msg}") logger.error(f"[ONE ORM SYNC] There's an issue with the sync of endpoint {which_endpoint} the API Reply: {error_msg}")
api_result = solve_api_problems(
api_reply_object=api_result,
get_or_post="get",
url=full_request_url,
payload=None,
connection_observer=connection_observer,
client_observer=client_observer
)
# 2nd try:
if not api_result.valid:
return { return {
"success": False, "success": False,
"error": error_msg "error": api_result.message
} }
new_data = api_result.data new_data = api_result.data

View file

@ -4,7 +4,8 @@ from core.services.sync.compare_tables import compare_tables
# from core.services.sync.get_data_from_api import get_data_from_api # from core.services.sync.get_data_from_api import get_data_from_api
from core.services.networking.api_requests.step1_get_or_post import get_data_from_api from core.services.networking.api_requests.step1_get_or_post import get_data_from_api
from core.services.networking.api_requests.ApiResponseModel import ApiResponse from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
# ORM for metadata: # ORM for metadata:
from core.models.manage.session_management import get_session from core.models.manage.session_management import get_session
@ -153,21 +154,32 @@ def coordinate_cache_sync(
full_request_url = f"{Constants.SP_API_BASE_URL}/cachedsync" full_request_url = f"{Constants.SP_API_BASE_URL}/cachedsync"
api_result = get_data_from_api(full_request_url, client_observer, connection_observer) api_result = get_data_from_api(full_request_url, client_observer, connection_observer)
# ================== SOLVE API CALL PROBLEMS ==================
# we trust the 'get_data_from_api' function to give us a dictionary: # we trust the 'get_data_from_api' function to give us a dictionary:
if not api_result.valid: if not api_result.valid:
print(f"Sync service says its not a valid reply")
error_msg = api_result.message error_msg = api_result.message
logger.error(f"API sync failed: {error_msg}") logger.error(f"[SYNC SERVICE] There's an issue with the API Reply: {error_msg}")
api_result = solve_api_problems(
api_reply_object=api_result,
get_or_post="get",
url=full_request_url,
payload=None,
connection_observer=connection_observer,
client_observer=client_observer
)
# 2nd try:
if not api_result.valid:
return { return {
"success": False, "success": False,
"changed_tables": [], "changed_tables": [],
"new_model_types": [], "new_model_types": [],
"error": error_msg, "error": api_result.message,
} }
# Extract the data from the client's own trusted function, # ================== VALIDATE DATA ==================
# Extract the data from our own trusted function without type checking,
new_data = api_result.data new_data = api_result.data
print(f"inside sync_service, new_data is {new_data}")
if new_data: if new_data:
logger.debug(f"We got an API response with some data, let's check if it's a valid format...") logger.debug(f"We got an API response with some data, let's check if it's a valid format...")

View file

@ -7,6 +7,7 @@ if TYPE_CHECKING:
# services & helpers # services & helpers
from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.services.networking.api_requests.step1_get_or_post import send_data_to_server
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.step5_solve_api_problems import solve_api_problems
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.services.helpers.get_which_billing_key import get_which_billing_key from core.services.helpers.get_which_billing_key import get_which_billing_key
@ -49,9 +50,20 @@ def send_unblinded_ticket_to_server(
# send it: # send it:
which_endpoint = "validate" which_endpoint = "validate"
url = make_url(which_endpoint) url = make_url(which_endpoint)
reply_object = send_data_to_server(payload, url, connection_observer) api_reply_object = send_data_to_server(payload, different_url, connection_observer)
return reply_object # This is if the API failed, not if it's not a valid ticket:
if not api_reply_object.valid:
api_reply_object = solve_api_problems(
api_reply_object=api_reply_object,
get_or_post="post",
url=url,
payload=payload,
connection_observer=connection_observer,
client_observer=None
)
return api_reply_object
except Exception as e: except Exception as e:
human_readable_error_msg = f"The send_unblinded_ticket_to_server function's try-except block failed for ticket {which_ticket}. Returning False..." human_readable_error_msg = f"The send_unblinded_ticket_to_server function's try-except block failed for ticket {which_ticket}. Returning False..."

View file

@ -39,12 +39,14 @@ def use_ticket_orchestrator(
raw_data_dict = api_reply_object.data raw_data_dict = api_reply_object.data
logger.debug(f"raw_data_dict is {raw_data_dict}") # logger.debug(f"[USE TICKET ORCHESTRATOR] We have received a raw dictionary of {raw_data_dict}")
if not isinstance(raw_data_dict, dict): if not isinstance(raw_data_dict, dict):
logger.debug(f"[USE TICKET ORCHESTRATOR] Invalid Data format returned from the API server.")
return {"valid": False, "message": "invalid_format"} return {"valid": False, "message": "invalid_format"}
if "valid" not in raw_data_dict: if "valid" not in raw_data_dict:
logger.debug(f"[USE TICKET ORCHESTRATOR] Invalid Data format returned from the API server.")
return {"valid": False, "message": "invalid_format"} return {"valid": False, "message": "invalid_format"}
is_it_valid = raw_data_dict.get("valid") is_it_valid = raw_data_dict.get("valid")
@ -57,6 +59,8 @@ def use_ticket_orchestrator(
""" """
if is_it_valid: if is_it_valid:
logger.debug(f"[USE TICKET ORCHESTRATOR] The API server returned a valid billing code.")
# update the ticket tracker: # update the ticket tracker:
status_update = update_value_in_json_with_two_values( status_update = update_value_in_json_with_two_values(
ticket_tracker_path, which_ticket, "status", "used" ticket_tracker_path, which_ticket, "status", "used"
@ -79,6 +83,8 @@ def use_ticket_orchestrator(
message = raw_data_dict.get("message") message = raw_data_dict.get("message")
if message == "already_used": if message == "already_used":
logger.debug(f"[USE TICKET ORCHESTRATOR] This billing code has already been used, but it is still potentially still valid.")
# update the ticket tracker to reflect that it's already used with that billing code: # update the ticket tracker to reflect that it's already used with that billing code:
status_update = update_value_in_json_with_two_values( status_update = update_value_in_json_with_two_values(
ticket_tracker_path, which_ticket, "status", "used" ticket_tracker_path, which_ticket, "status", "used"

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

@ -45,6 +45,9 @@ dependencies = [
"typing-inspection==0.4.2", "typing-inspection==0.4.2",
"typing_extensions==4.15.0", "typing_extensions==4.15.0",
"urllib3==2.6.3", "urllib3==2.6.3",
"httpx==0.28.1",
"dnspython==2.8.0",
"socksio==1.0.0",
] ]
[project.urls] [project.urls]