ipfetcher.go 1.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. package ipfetcher
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "net"
  8. "net/http"
  9. "strings"
  10. "time"
  11. )
  12. var defaultProviders = []string{
  13. "https://api.ipify.org",
  14. "https://ifconfig.me/ip",
  15. "https://icanhazip.com",
  16. }
  17. var httpClient = &http.Client{Timeout: 5 * time.Second}
  18. // Result holds the outcome of a successful public IP fetch.
  19. type Result struct {
  20. IP string // The detected public IP.
  21. Source string // The URL that returned the IP (e.g. https://api.ipify.org).
  22. }
  23. // FetchPublicIPFrom fetches the public IP by trying each URL in order.
  24. // If urls is nil or empty, the built-in default providers are used.
  25. func FetchPublicIPFrom(ctx context.Context, urls []string) (*Result, error) {
  26. slog.Debug("fetching public IP...")
  27. if len(urls) == 0 {
  28. urls = defaultProviders
  29. }
  30. var lastErr error
  31. for _, url := range urls {
  32. ip, err := fetchFrom(ctx, url)
  33. if err != nil {
  34. lastErr = err
  35. continue
  36. }
  37. slog.Default().Debug("public IP fetched", "ip", ip, "source", url)
  38. return &Result{IP: ip, Source: url}, nil
  39. }
  40. return nil, fmt.Errorf("all IP providers failed, last error: %w", lastErr)
  41. }
  42. func fetchFrom(ctx context.Context, url string) (string, error) {
  43. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  44. if err != nil {
  45. return "", err
  46. }
  47. resp, err := httpClient.Do(req)
  48. if err != nil {
  49. return "", err
  50. }
  51. defer resp.Body.Close()
  52. if resp.StatusCode != http.StatusOK {
  53. return "", fmt.Errorf("%s returned status %d", url, resp.StatusCode)
  54. }
  55. body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
  56. if err != nil {
  57. return "", err
  58. }
  59. ip := strings.TrimSpace(string(body))
  60. if net.ParseIP(ip) == nil {
  61. return "", fmt.Errorf("%s returned invalid IP: %q", url, ip)
  62. }
  63. return ip, nil
  64. }