wip
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}