From cbdd1a933c92336c5458de5580b1fe627d92431f Mon Sep 17 00:00:00 2001 From: Jeremy Lin Date: Sun, 6 Oct 2024 06:30:33 -0700 Subject: [PATCH] feat(publicip): `cloudflare` API support (#2502) --- internal/publicip/api/api.go | 7 ++- internal/publicip/api/cloudflare.go | 91 +++++++++++++++++++++++++++++ internal/publicip/api/errors.go | 1 + 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 internal/publicip/api/cloudflare.go diff --git a/internal/publicip/api/api.go b/internal/publicip/api/api.go index e5e19252..6f12439c 100644 --- a/internal/publicip/api/api.go +++ b/internal/publicip/api/api.go @@ -19,6 +19,7 @@ type API interface { type Provider string const ( + Cloudflare Provider = "cloudflare" IPInfo Provider = "ipinfo" IP2Location Provider = "ip2location" ) @@ -26,6 +27,8 @@ const ( func New(provider Provider, client *http.Client, token string) ( //nolint:ireturn a API, err error) { switch provider { + case Cloudflare: + return newCloudflare(client), nil case IPInfo: return newIPInfo(client, token), nil case IP2Location: @@ -41,12 +44,14 @@ var ( func ParseProvider(s string) (provider Provider, err error) { switch strings.ToLower(s) { + case "cloudflare": + return Cloudflare, nil case "ipinfo": return IPInfo, nil case "ip2location": return IP2Location, nil default: - return "", fmt.Errorf(`%w: %q can only be "ipinfo" or "ip2location"`, + return "", fmt.Errorf(`%w: %q can only be "cloudflare", "ipinfo", or "ip2location"`, ErrProviderNotValid, s) } } diff --git a/internal/publicip/api/cloudflare.go b/internal/publicip/api/cloudflare.go new file mode 100644 index 00000000..867e7595 --- /dev/null +++ b/internal/publicip/api/cloudflare.go @@ -0,0 +1,91 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/netip" + "strings" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" +) + +type cloudflare struct { + client *http.Client +} + +func newCloudflare(client *http.Client) *cloudflare { + return &cloudflare{ + client: client, + } +} + +// FetchInfo obtains information on the public IP address of the machine, +// and returns an error if the `ip` argument is set since the Cloudflare API +// can only be used to provide details about the current machine public IP. +func (c *cloudflare) FetchInfo(ctx context.Context, ip netip.Addr) ( + result models.PublicIP, err error) { + url := "https://speed.cloudflare.com/meta" + if ip.IsValid() { + return result, fmt.Errorf("%w: cloudflare cannot provide information on the arbitrary IP address %s", + ErrServiceLimited, ip) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return result, err + } + + response, err := c.client.Do(request) + if err != nil { + return result, err + } + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusOK: + case http.StatusTooManyRequests: + return result, fmt.Errorf("%w from %s: %d %s", + ErrTooManyRequests, url, response.StatusCode, response.Status) + default: + return result, fmt.Errorf("%w from %s: %d %s", + ErrBadHTTPStatus, url, response.StatusCode, response.Status) + } + + decoder := json.NewDecoder(response.Body) + var data struct { + Hostname string `json:"hostname,omitempty"` + ClientIP netip.Addr `json:"clientIp,omitempty"` + ASOrganization string `json:"asOrganization,omitempty"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + Latitude string `json:"latitude,omitempty"` + Longitude string `json:"longitude,omitempty"` + } + if err := decoder.Decode(&data); err != nil { + return result, fmt.Errorf("decoding response: %w", err) + } + + countryCode := strings.ToLower(data.Country) + country, ok := constants.CountryCodes()[countryCode] + if ok { + data.Country = country + } + + result = models.PublicIP{ + IP: data.ClientIP, + Region: data.Region, + Country: data.Country, + City: data.City, + Hostname: data.Hostname, + Location: data.Latitude + "," + data.Longitude, + Organization: data.ASOrganization, + PostalCode: data.PostalCode, + Timezone: "", // no timezone + } + return result, nil +} diff --git a/internal/publicip/api/errors.go b/internal/publicip/api/errors.go index 7ef8f1fb..0b479551 100644 --- a/internal/publicip/api/errors.go +++ b/internal/publicip/api/errors.go @@ -6,4 +6,5 @@ var ( ErrTokenNotValid = errors.New("token is not valid") ErrTooManyRequests = errors.New("too many requests sent for this month") ErrBadHTTPStatus = errors.New("bad HTTP status received") + ErrServiceLimited = errors.New("service is limited") )