Live video on the AT Protocol
1package aqhttp
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "net/url"
10 "sync"
11 "time"
12)
13
14const (
15 TypeA = 1 // IPv4
16 TypeAAAA = 28 // IPv6
17)
18
19type dnsRecord struct {
20 ips []string
21 expiresAt time.Time
22}
23
24type DoHResolver struct {
25 Server string
26 Client *http.Client
27 invalidRanges []*net.IPNet
28 cache map[string]*dnsRecord
29 mu sync.RWMutex
30}
31
32func NewDoHResolver(server string) *DoHResolver {
33 if server == "" {
34 server = "https://1.1.1.1/dns-query"
35 }
36 ipv4Bogons := []string{
37 "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
38 "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24",
39 "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24",
40 "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32",
41 }
42
43 ipv6Bogons := []string{
44 "::/128", // Unspecified
45 "::1/128", // Loopback
46 "100::/64", // Discard prefix
47 "2001::/32", // TEREDO
48 "2001:10::/28", // Deprecated (ORCHID)
49 "2001:db8::/32", // Documentation
50 "fc00::/7", // Unique local addresses (ULA)
51 "fe80::/10", // Link-local
52 "ff00::/8", // Multicast
53 }
54
55 ranges := append(ipv4Bogons, ipv6Bogons...)
56 var invalidRanges []*net.IPNet
57 for _, cidr := range ranges {
58 _, network, err := net.ParseCIDR(cidr)
59 if err == nil {
60 invalidRanges = append(invalidRanges, network)
61 }
62 }
63
64 return &DoHResolver{
65 Server: server,
66 Client: &http.Client{
67 Timeout: 10 * time.Second,
68 },
69 invalidRanges: invalidRanges,
70 cache: make(map[string]*dnsRecord),
71 }
72}
73
74type DoHResponse struct {
75 Status int `json:"Status"`
76 Answer []struct {
77 Name string `json:"name"`
78 Type int `json:"type"`
79 TTL int `json:"TTL"`
80 Data string `json:"data"`
81 } `json:"Answer"`
82}
83
84func (r *DoHResolver) Resolve(domain string, recordType int) ([]string, error) {
85 cacheKey := fmt.Sprintf("%s:%d", domain, recordType)
86
87 r.mu.RLock()
88 if record, ok := r.cache[cacheKey]; ok {
89 if time.Now().Before(record.expiresAt) {
90 defer r.mu.RUnlock()
91 return record.ips, nil
92 }
93 }
94 r.mu.RUnlock()
95
96 reqURL := fmt.Sprintf("%s?name=%s&type=%d", r.Server, url.QueryEscape(domain), recordType)
97
98 req, err := http.NewRequest("GET", reqURL, nil)
99 if err != nil {
100 return nil, fmt.Errorf("failed to create request: %w", err)
101 }
102
103 req.Header.Set("Accept", "application/dns-json")
104
105 resp, err := r.Client.Do(req)
106 if err != nil {
107 return nil, fmt.Errorf("failed to execute request: %w", err)
108 }
109 defer resp.Body.Close()
110
111 if resp.StatusCode != http.StatusOK {
112 return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
113 }
114
115 body, err := io.ReadAll(resp.Body)
116 if err != nil {
117 return nil, fmt.Errorf("failed to read response: %w", err)
118 }
119
120 var dohResp DoHResponse
121 if err := json.Unmarshal(body, &dohResp); err != nil {
122 return nil, fmt.Errorf("failed to parse JSON response: %w", err)
123 }
124
125 var results []string
126 var minTTL = 3600
127 for _, answer := range dohResp.Answer {
128 if answer.Type == recordType {
129 results = append(results, answer.Data)
130 if answer.TTL < minTTL {
131 minTTL = answer.TTL
132 }
133 }
134 }
135
136 if len(results) > 0 {
137 r.mu.Lock()
138 r.cache[cacheKey] = &dnsRecord{
139 ips: results,
140 expiresAt: time.Now().Add(time.Duration(minTTL) * time.Second),
141 }
142 r.mu.Unlock()
143 }
144
145 return results, nil
146}
147
148// check if the given IP address is within known invalid ranges
149func (r *DoHResolver) IsInvalidIP(ip string) bool {
150 pip := net.ParseIP(ip)
151 if pip == nil {
152 return true // unparseable IPs are invalid
153 }
154 for _, nw := range r.invalidRanges {
155 if nw.Contains(pip) {
156 return true
157 }
158 }
159 return false
160}
161
162// validates a HTTPS URL and returns a safe IP address to use for the request.
163func (r *DoHResolver) ValidateAndGetIP(urlStr string) (string, *url.URL, error) {
164 parsedURL, err := url.Parse(urlStr)
165 if err != nil {
166 return "", nil, fmt.Errorf("failed to parse URL: %w", err)
167 }
168
169 if parsedURL.Scheme != "https" {
170 return "", nil, fmt.Errorf("only HTTPS URLs are allowed, got: %s", parsedURL.Scheme)
171 }
172
173 hostname := parsedURL.Hostname()
174 if hostname == "" {
175 return "", nil, fmt.Errorf("URL has no hostname")
176 }
177
178 ipv4Addrs, err := r.Resolve(hostname, TypeA)
179 if err == nil && len(ipv4Addrs) > 0 {
180 for _, ip := range ipv4Addrs {
181 if !r.IsInvalidIP(ip) {
182 return ip, parsedURL, nil
183 }
184 }
185 }
186
187 ipv6Addrs, err := r.Resolve(hostname, TypeAAAA)
188 if err == nil && len(ipv6Addrs) > 0 {
189 for _, ip := range ipv6Addrs {
190 if !r.IsInvalidIP(ip) {
191 return ip, parsedURL, nil
192 }
193 }
194 }
195
196 return "", nil, fmt.Errorf("no valid IP addresses found for %s (all resolved to internal/bogon addresses)", hostname)
197}