Live video on the AT Protocol
at natb/block-javascript-protocol 197 lines 4.6 kB view raw
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}