| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
- package ipfetcher
- import (
- "context"
- "fmt"
- "io"
- "log/slog"
- "net"
- "net/http"
- "strings"
- "time"
- )
- var defaultProviders = []string{
- "https://api.ipify.org",
- "https://ifconfig.me/ip",
- "https://icanhazip.com",
- }
- var httpClient = &http.Client{Timeout: 5 * time.Second}
- // Result holds the outcome of a successful public IP fetch.
- type Result struct {
- IP string // The detected public IP.
- Source string // The URL that returned the IP (e.g. https://api.ipify.org).
- }
- // FetchPublicIPFrom fetches the public IP by trying each URL in order.
- // If urls is nil or empty, the built-in default providers are used.
- func FetchPublicIPFrom(ctx context.Context, urls []string) (*Result, error) {
- slog.Debug("fetching public IP...")
- if len(urls) == 0 {
- urls = defaultProviders
- }
- var lastErr error
- for _, url := range urls {
- ip, err := fetchFrom(ctx, url)
- if err != nil {
- lastErr = err
- continue
- }
- slog.Default().Debug("public IP fetched", "ip", ip, "source", url)
- return &Result{IP: ip, Source: url}, nil
- }
- return nil, fmt.Errorf("all IP providers failed, last error: %w", lastErr)
- }
- func fetchFrom(ctx context.Context, url string) (string, error) {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
- if err != nil {
- return "", err
- }
- resp, err := httpClient.Do(req)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("%s returned status %d", url, resp.StatusCode)
- }
- body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
- if err != nil {
- return "", err
- }
- ip := strings.TrimSpace(string(body))
- if net.ParseIP(ip) == nil {
- return "", fmt.Errorf("%s returned invalid IP: %q", url, ip)
- }
- return ip, nil
- }
|