wip
at main 162 lines 3.7 kB view raw
1package ipinfo 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net" 8 "net/http" 9 "net/url" 10 "sync" 11 "time" 12) 13 14type Client struct { 15 httpClient *http.Client 16 baseURL string 17 mu sync.RWMutex 18 backoffUntil time.Time 19 backoffDuration time.Duration 20} 21 22func NewClient() *Client { 23 return &Client{ 24 httpClient: &http.Client{ 25 Timeout: 10 * time.Second, 26 }, 27 baseURL: "https://api.ipapi.is", 28 backoffDuration: 5 * time.Minute, 29 } 30} 31 32// IsInBackoff checks if we're currently in backoff period 33func (c *Client) IsInBackoff() bool { 34 c.mu.RLock() 35 defer c.mu.RUnlock() 36 return time.Now().Before(c.backoffUntil) 37} 38 39// SetBackoff sets the backoff period 40func (c *Client) SetBackoff() { 41 c.mu.Lock() 42 defer c.mu.Unlock() 43 c.backoffUntil = time.Now().Add(c.backoffDuration) 44} 45 46// ClearBackoff clears the backoff (on successful request) 47func (c *Client) ClearBackoff() { 48 c.mu.Lock() 49 defer c.mu.Unlock() 50 c.backoffUntil = time.Time{} 51} 52 53// GetIPInfo fetches IP information from ipapi.is 54func (c *Client) GetIPInfo(ctx context.Context, ip string) (map[string]interface{}, error) { 55 // Check if we're in backoff period 56 if c.IsInBackoff() { 57 c.mu.RLock() 58 remaining := time.Until(c.backoffUntil) 59 c.mu.RUnlock() 60 return nil, fmt.Errorf("in backoff period, retry in %v", remaining.Round(time.Second)) 61 } 62 63 // Build URL with IP parameter 64 reqURL := fmt.Sprintf("%s/?q=%s", c.baseURL, url.QueryEscape(ip)) 65 66 req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 67 if err != nil { 68 return nil, fmt.Errorf("failed to create request: %w", err) 69 } 70 71 resp, err := c.httpClient.Do(req) 72 if err != nil { 73 // Set backoff on network errors (timeout, etc) 74 c.SetBackoff() 75 return nil, fmt.Errorf("failed to fetch IP info: %w", err) 76 } 77 defer resp.Body.Close() 78 79 if resp.StatusCode == http.StatusTooManyRequests { 80 // Set backoff on rate limit 81 c.SetBackoff() 82 return nil, fmt.Errorf("rate limited (429), backing off for %v", c.backoffDuration) 83 } 84 85 if resp.StatusCode != http.StatusOK { 86 // Set backoff on other errors too 87 c.SetBackoff() 88 return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 89 } 90 91 var ipInfo map[string]interface{} 92 if err := json.NewDecoder(resp.Body).Decode(&ipInfo); err != nil { 93 return nil, fmt.Errorf("failed to decode response: %w", err) 94 } 95 96 // Clear backoff on successful request 97 c.ClearBackoff() 98 99 return ipInfo, nil 100} 101 102// IPAddresses holds both IPv4 and IPv6 addresses 103type IPAddresses struct { 104 IPv4 string 105 IPv6 string 106} 107 108// ExtractIPsFromEndpoint extracts both IPv4 and IPv6 from endpoint URL 109func ExtractIPsFromEndpoint(endpoint string) (*IPAddresses, error) { 110 // Parse URL 111 parsedURL, err := url.Parse(endpoint) 112 if err != nil { 113 return nil, fmt.Errorf("failed to parse endpoint URL: %w", err) 114 } 115 116 host := parsedURL.Hostname() 117 if host == "" { 118 return nil, fmt.Errorf("no hostname in endpoint") 119 } 120 121 result := &IPAddresses{} 122 123 // Check if host is already an IP 124 if ip := net.ParseIP(host); ip != nil { 125 if ip.To4() != nil { 126 result.IPv4 = host 127 } else { 128 result.IPv6 = host 129 } 130 return result, nil 131 } 132 133 // Resolve hostname to IPs 134 ips, err := net.LookupIP(host) 135 if err != nil { 136 return nil, fmt.Errorf("failed to resolve hostname: %w", err) 137 } 138 139 if len(ips) == 0 { 140 return nil, fmt.Errorf("no IPs found for hostname") 141 } 142 143 // Extract both IPv4 and IPv6 144 for _, ip := range ips { 145 if ipv4 := ip.To4(); ipv4 != nil { 146 if result.IPv4 == "" { 147 result.IPv4 = ipv4.String() 148 } 149 } else { 150 if result.IPv6 == "" { 151 result.IPv6 = ip.String() 152 } 153 } 154 } 155 156 // Must have at least one IP 157 if result.IPv4 == "" && result.IPv6 == "" { 158 return nil, fmt.Errorf("no valid IPs found") 159 } 160 161 return result, nil 162}