Spaces:
Runtime error
Runtime error
| # PATH: bot/integrations/http.py | |
| import ssl | |
| import json as _json | |
| from typing import Any, Dict, Optional, Tuple, List | |
| from urllib.parse import urlparse, urlunparse | |
| import httpx | |
| _DEFAULT_TIMEOUT = httpx.Timeout(20.0, connect=10.0) | |
| _client: Optional[httpx.AsyncClient] = None | |
| _client_insecure: Optional[httpx.AsyncClient] = None | |
| def get_client() -> httpx.AsyncClient: | |
| global _client | |
| if _client is None: | |
| _client = httpx.AsyncClient( | |
| timeout=_DEFAULT_TIMEOUT, | |
| follow_redirects=True, | |
| http2=True, | |
| ) | |
| return _client | |
| def get_client_insecure() -> httpx.AsyncClient: | |
| """ | |
| Insecure client used only for IP fallback (TLS verify off), | |
| because we connect to https://<IP> with Host header. | |
| """ | |
| global _client_insecure | |
| if _client_insecure is None: | |
| _client_insecure = httpx.AsyncClient( | |
| timeout=_DEFAULT_TIMEOUT, | |
| follow_redirects=True, | |
| http2=True, | |
| verify=False, | |
| ) | |
| return _client_insecure | |
| def _is_dns_error(e: Exception) -> bool: | |
| s = str(e).lower() | |
| return ("no address associated with hostname" in s) or ("gaierror" in s) or ("name or service not known" in s) | |
| async def doh_resolve_a(host: str) -> List[str]: | |
| """ | |
| Resolve A records via DNS-over-HTTPS. | |
| Uses dns.google (since google.com resolves fine in your env). | |
| """ | |
| c = get_client() | |
| try: | |
| r = await c.get("https://dns.google/resolve", params={"name": host, "type": "A"}) | |
| if r.status_code != 200: | |
| return [] | |
| j = r.json() | |
| ans = j.get("Answer") or [] | |
| ips = [] | |
| for a in ans: | |
| if a.get("type") == 1 and a.get("data"): | |
| ips.append(str(a["data"])) | |
| # unique keep order | |
| out = [] | |
| for ip in ips: | |
| if ip not in out: | |
| out.append(ip) | |
| return out | |
| except Exception: | |
| return [] | |
| def _split_url(url: str) -> Tuple[str, str, str]: | |
| """ | |
| Returns (scheme, host, rest(path+query+fragment)) | |
| """ | |
| u = urlparse(url) | |
| scheme = u.scheme or "https" | |
| host = u.netloc | |
| rest = urlunparse(("", "", u.path or "/", u.params, u.query, u.fragment)) | |
| return scheme, host, rest | |
| async def fetch_status(url: str) -> str: | |
| """ | |
| Returns status code as string, or ERR:... | |
| Uses same DNS fallback logic. | |
| """ | |
| try: | |
| r = await request_text("GET", url) | |
| return str(r[0]) | |
| except Exception as e: | |
| return f"ERR:{type(e).__name__}:{e}" | |
| async def request_text(method: str, url: str, headers: Optional[Dict[str, str]] = None) -> Tuple[int, str]: | |
| """ | |
| Low-level text request with DNS fallback. | |
| Returns (status_code, text) | |
| """ | |
| headers = dict(headers or {}) | |
| c = get_client() | |
| try: | |
| r = await c.request(method, url, headers=headers) | |
| return r.status_code, r.text | |
| except Exception as e: | |
| # DNS fail -> DoH fallback | |
| if not _is_dns_error(e): | |
| raise | |
| scheme, host, rest = _split_url(url) | |
| if not host: | |
| raise | |
| ips = await doh_resolve_a(host) | |
| if not ips: | |
| raise | |
| ic = get_client_insecure() | |
| last_err = e | |
| for ip in ips: | |
| try: | |
| # connect to IP but keep routing via Host header | |
| h2 = dict(headers) | |
| h2["Host"] = host | |
| r = await ic.request(method, f"{scheme}://{ip}{rest}", headers=h2) | |
| return r.status_code, r.text | |
| except Exception as e2: | |
| last_err = e2 | |
| continue | |
| raise last_err | |
| async def post_json(url: str, headers: Optional[Dict[str, str]] = None, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: | |
| """ | |
| POST JSON helper with DNS fallback. | |
| Returns dict: {ok, status, data?, err?} | |
| """ | |
| headers = dict(headers or {}) | |
| headers.setdefault("Content-Type", "application/json") | |
| c = get_client() | |
| payload = payload or {} | |
| try: | |
| r = await c.post(url, headers=headers, json=payload) | |
| try: | |
| data = r.json() | |
| except Exception: | |
| data = {"raw": r.text} | |
| return {"ok": r.status_code < 400, "status": r.status_code, "data": data} | |
| except Exception as e: | |
| if not _is_dns_error(e): | |
| return {"ok": False, "status": 0, "err": str(e)} | |
| # DNS fail -> DoH -> IP fallback (verify off + Host header) | |
| scheme, host, rest = _split_url(url) | |
| if not host: | |
| return {"ok": False, "status": 0, "err": str(e)} | |
| ips = await doh_resolve_a(host) | |
| if not ips: | |
| return {"ok": False, "status": 0, "err": str(e)} | |
| ic = get_client_insecure() | |
| last_err: Exception = e | |
| for ip in ips: | |
| try: | |
| h2 = dict(headers) | |
| h2["Host"] = host | |
| r = await ic.post(f"{scheme}://{ip}{rest}", headers=h2, json=payload) | |
| try: | |
| data = r.json() | |
| except Exception: | |
| data = {"raw": r.text} | |
| return {"ok": r.status_code < 400, "status": r.status_code, "data": data} | |
| except Exception as e2: | |
| last_err = e2 | |
| continue | |
| return {"ok": False, "status": 0, "err": str(last_err)} |