File size: 5,363 Bytes
76b253b
bab413c
 
 
 
07f8298
76b253b
 
bab413c
 
 
07f8298
bab413c
07f8298
76b253b
 
07f8298
bab413c
 
 
 
 
 
07f8298
76b253b
07f8298
bab413c
07f8298
bab413c
 
07f8298
bab413c
 
 
 
 
 
 
 
 
07f8298
 
bab413c
 
 
07f8298
 
bab413c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07f8298
 
bab413c
 
 
 
 
 
 
 
 
 
b0bf7d8
 
bab413c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0bf7d8
bab413c
 
b0bf7d8
bab413c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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)}