#!/usr/bin/env python3 """DoH (DNS-over-HTTPS) resolver — non-US DNS providers for exchange API hosts.""" from __future__ import annotations import asyncio import logging import socket from typing import Dict, List, Optional import httpx logger = logging.getLogger(__name__) # Prefer EU/global resolvers (not US-centric filtering) DOH_PROVIDERS: List[Dict[str, str]] = [ {"name": "quad9", "url": "https://dns.quad9.net/dns-query"}, {"name": "cloudflare", "url": "https://cloudflare-dns.com/dns-query"}, {"name": "adguard", "url": "https://dns.adguard-dns.com/dns-query"}, {"name": "google", "url": "https://dns.google/resolve"}, ] class ExchangeDNSResolver: """Resolve exchange hostnames via DoH, cache A records.""" def __init__(self) -> None: self._cache: Dict[str, List[str]] = {} async def resolve(self, hostname: str) -> Optional[str]: if hostname in self._cache and self._cache[hostname]: return self._cache[hostname][0] for provider in DOH_PROVIDERS: try: async with httpx.AsyncClient(timeout=6.0) as client: resp = await client.get( provider["url"], params={"name": hostname, "type": "A"}, headers={"accept": "application/dns-json"}, ) if resp.status_code != 200: continue data = resp.json() answers = data.get("Answer") or [] ips = [a["data"] for a in answers if a.get("type") == 1 and a.get("data")] if ips: self._cache[hostname] = ips logger.info("DoH %s: %s -> %s", provider["name"], hostname, ips[0]) return ips[0] except Exception as exc: logger.debug("DoH %s failed for %s: %s", provider["name"], hostname, exc) try: loop = asyncio.get_event_loop() ip = await loop.run_in_executor(None, socket.gethostbyname, hostname) if ip: self._cache[hostname] = [ip] return ip except Exception: pass return None async def fetch_via_doh( self, base_url: str, path: str, params: Optional[dict] = None, timeout: float = 12.0, ) -> Optional[httpx.Response]: """HTTP GET to resolved IP with correct Host header (SNI).""" hostname = base_url.split("://", 1)[-1].split("/")[0] ip = await self.resolve(hostname) if not ip: return None url = f"https://{ip}{path}" headers = {"Host": hostname, "Accept": "application/json"} try: async with httpx.AsyncClient( timeout=timeout, verify=False, headers=headers, ) as client: return await client.get(url, params=params) except Exception as exc: logger.debug("DoH fetch failed %s%s: %s", hostname, path, exc) return None