diff --git a/internal/configuration/settings/publicip.go b/internal/configuration/settings/publicip.go index 5e2c30ed..9cb642aa 100644 --- a/internal/configuration/settings/publicip.go +++ b/internal/configuration/settings/publicip.go @@ -21,7 +21,8 @@ type PublicIP struct { // internal state IPFilepath *string // API is the API name to use to fetch public IP information. - // It can be ipinfo or ip2location. It defaults to ipinfo. + // It can be cloudflare, ifconfigco, ip2location or ipinfo. + // It defaults to ipinfo. API string // APIToken is the token to use for the IP data service // such as ipinfo.io. It can be the empty string to diff --git a/internal/publicip/api/api.go b/internal/publicip/api/api.go index 6f12439c..3ffb3ea6 100644 --- a/internal/publicip/api/api.go +++ b/internal/publicip/api/api.go @@ -20,6 +20,7 @@ type Provider string const ( Cloudflare Provider = "cloudflare" + IfConfigCo Provider = "ifconfigco" IPInfo Provider = "ipinfo" IP2Location Provider = "ip2location" ) @@ -29,6 +30,8 @@ func New(provider Provider, client *http.Client, token string) ( //nolint:iretur switch provider { case Cloudflare: return newCloudflare(client), nil + case IfConfigCo: + return newIfConfigCo(client), nil case IPInfo: return newIPInfo(client, token), nil case IP2Location: @@ -46,12 +49,14 @@ func ParseProvider(s string) (provider Provider, err error) { switch strings.ToLower(s) { case "cloudflare": return Cloudflare, nil + case string(IfConfigCo): + return IfConfigCo, nil case "ipinfo": return IPInfo, nil case "ip2location": return IP2Location, nil default: - return "", fmt.Errorf(`%w: %q can only be "cloudflare", "ipinfo", or "ip2location"`, + return "", fmt.Errorf(`%w: %q can only be "cloudflare", "ifconfigco", "ip2location" or "ipinfo"`, ErrProviderNotValid, s) } } diff --git a/internal/publicip/api/ifconfigco.go b/internal/publicip/api/ifconfigco.go new file mode 100644 index 00000000..245f22bc --- /dev/null +++ b/internal/publicip/api/ifconfigco.go @@ -0,0 +1,85 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/netip" + + "github.com/qdm12/gluetun/internal/models" +) + +type ifConfigCo struct { + client *http.Client +} + +func newIfConfigCo(client *http.Client) *ifConfigCo { + return &ifConfigCo{ + client: client, + } +} + +// FetchInfo obtains information on the ip address provided +// using the ifconfig.co/json API. If the ip is the zero value, +// the public IP address of the machine is used as the IP. +func (i *ifConfigCo) FetchInfo(ctx context.Context, ip netip.Addr) ( + result models.PublicIP, err error) { + url := "https://ifconfig.co/json" + if ip.IsValid() { + url += "?ip=" + ip.String() + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return result, err + } + + response, err := i.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: %s", + ErrTooManyRequests, url, response.Status) + default: + return result, fmt.Errorf("%w from %s: %s", + ErrBadHTTPStatus, url, response.Status) + } + + decoder := json.NewDecoder(response.Body) + var data struct { + IP netip.Addr `json:"ip,omitempty"` + Country string `json:"country,omitempty"` + RegionName string `json:"region_name,omitempty"` + ZipCode string `json:"zip_code,omitempty"` + City string `json:"city,omitempty"` + Latitude float32 `json:"latitude,omitempty"` + Longitude float32 `json:"longitude,omitempty"` + Hostname string `json:"hostname,omitempty"` + // Timezone in the form America/Montreal + Timezone string `json:"time_zone,omitempty"` + AsnOrg string `json:"asn_org,omitempty"` + } + err = decoder.Decode(&data) + if err != nil { + return result, fmt.Errorf("decoding response: %w", err) + } + + result = models.PublicIP{ + IP: data.IP, + Region: data.RegionName, + Country: data.Country, + City: data.City, + Hostname: data.Hostname, + Location: fmt.Sprintf("%f,%f", data.Latitude, data.Longitude), + Organization: data.AsnOrg, + PostalCode: data.ZipCode, + Timezone: data.Timezone, + } + return result, nil +}