ipfetcher.go 1.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  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. // FetchPublicIPFrom fetches the public IP by trying each URL in order.
  19. // If urls is nil or empty, the built-in default providers are used.
  20. func FetchPublicIPFrom(ctx context.Context, urls []string) (string, error) {
  21. slog.Debug("fetching public IP...")
  22. if len(urls) == 0 {
  23. urls = defaultProviders
  24. }
  25. var lastErr error
  26. for _, url := range urls {
  27. ip, err := fetchFrom(ctx, url)
  28. if err != nil {
  29. lastErr = err
  30. continue
  31. }
  32. slog.Default().Debug("public IP fetched", "ip", ip)
  33. return ip, nil
  34. }
  35. return "", fmt.Errorf("all IP providers failed, last error: %w", lastErr)
  36. }
  37. func fetchFrom(ctx context.Context, url string) (string, error) {
  38. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  39. if err != nil {
  40. return "", err
  41. }
  42. resp, err := httpClient.Do(req)
  43. if err != nil {
  44. return "", err
  45. }
  46. defer resp.Body.Close()
  47. if resp.StatusCode != http.StatusOK {
  48. return "", fmt.Errorf("%s returned status %d", url, resp.StatusCode)
  49. }
  50. body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
  51. if err != nil {
  52. return "", err
  53. }
  54. ip := strings.TrimSpace(string(body))
  55. if net.ParseIP(ip) == nil {
  56. return "", fmt.Errorf("%s returned invalid IP: %q", url, ip)
  57. }
  58. return ip, nil
  59. }