61 lines
2.5 KiB
Python
61 lines
2.5 KiB
Python
"""HTTP-GET mit DNS-Rückfall für Health-Checks.
|
||
|
||
Der Resolver des Admin-Servers (Coolify/Docker) cacht „Name nicht gefunden“ für neue Hostnamen oft lange – obwohl der Name öffentlich
|
||
längst existiert. Schlägt die Auflösung fehl, fragen wir die Adresse per DNS-over-HTTPS (Cloudflare, dann Google) ab und verbinden uns
|
||
direkt zur IP (Host-Header + SNI = Hostname, Zertifikat wird normal geprüft).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import socket
|
||
from urllib.parse import urlsplit
|
||
|
||
import httpx
|
||
|
||
|
||
def _is_dns_error(exc: BaseException) -> bool:
|
||
seen = set()
|
||
while exc is not None and id(exc) not in seen:
|
||
seen.add(id(exc))
|
||
if isinstance(exc, socket.gaierror):
|
||
return True
|
||
exc = exc.__cause__ or exc.__context__
|
||
return False
|
||
|
||
|
||
def is_dns_failure_text(msg: str) -> bool:
|
||
m = msg.lower()
|
||
return "name or service not known" in m or "temporary failure in name resolution" in m or "nodename nor servname" in m or "gaierror" in m
|
||
|
||
|
||
def doh_resolve(host: str, timeout: float = 5.0) -> list[str]:
|
||
for url in ("https://cloudflare-dns.com/dns-query", "https://dns.google/resolve"):
|
||
try:
|
||
r = httpx.get(url, params={"name": host, "type": "A"}, headers={"accept": "application/dns-json"}, timeout=timeout)
|
||
ips = [a["data"] for a in r.json().get("Answer", []) if a.get("type") == 1]
|
||
if ips:
|
||
return ips
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
return []
|
||
|
||
|
||
def get_json(url: str, timeout: float = 8.0) -> tuple[int, dict]:
|
||
"""GET mit DNS-Rückfall → (status, json-dict oder {}). Bei Rückfall enthält das Dict `_dns_fallback: true`."""
|
||
def parse(r):
|
||
return r.status_code, (r.json() if r.headers.get("content-type", "").startswith("application/json") else {})
|
||
try:
|
||
return parse(httpx.get(url, timeout=timeout, follow_redirects=True))
|
||
except httpx.ConnectError as exc:
|
||
if not _is_dns_error(exc):
|
||
raise
|
||
parts = urlsplit(url)
|
||
for ip in doh_resolve(parts.hostname or ""):
|
||
try:
|
||
r = httpx.get(f"{parts.scheme}://{ip}{parts.path or '/'}", headers={"Host": parts.hostname}, extensions={"sni_hostname": parts.hostname},
|
||
timeout=timeout, follow_redirects=True)
|
||
status, body = parse(r)
|
||
return status, {**body, "_dns_fallback": True}
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
raise
|