diff --git a/pkg/publicip/info/ip2location.go b/pkg/publicip/info/ip2location.go new file mode 100644 index 000000000..9fbf43b60 --- /dev/null +++ b/pkg/publicip/info/ip2location.go @@ -0,0 +1,78 @@ +package info + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/netip" +) + +func newIP2Location(client *http.Client) *ip2Location { + return &ip2Location{ + client: client, + } +} + +type ip2Location struct { + client *http.Client +} + +func (p *ip2Location) get(ctx context.Context, ip netip.Addr) ( + result Result, err error) { + result.Source = string(Ipinfo) + + url := "https://api.ip2location.io/" + if ip.IsValid() { + url += "?ip=" + ip.String() + } + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return result, fmt.Errorf("creating request: %w", err) + } + + response, err := p.client.Do(request) + if err != nil { + return result, fmt.Errorf("doing request: %w", err) + } + + switch response.StatusCode { + case http.StatusOK: + case http.StatusForbidden, http.StatusTooManyRequests: + bodyString := bodyToSingleLine(response.Body) + _ = response.Body.Close() + return result, fmt.Errorf("%w (%s)", ErrTooManyRequests, bodyString) + default: + bodyString := bodyToSingleLine(response.Body) + _ = response.Body.Close() + return result, fmt.Errorf("%w: %d %s (%s)", ErrBadHTTPStatus, + response.StatusCode, response.Status, bodyString) + } + + decoder := json.NewDecoder(response.Body) + var data struct { + IP netip.Addr `json:"ip"` + RegionName string `json:"region_name"` + CountryName string `json:"country_name"` + CityName string `json:"city_name"` + // More fields available see https://www.ip2location.io/ip2location-documentation + } + err = decoder.Decode(&data) + if err != nil { + return result, fmt.Errorf("decoding JSON response: %w", err) + } + + result.IP = data.IP + if data.RegionName != "" { + result.Region = stringPtr(data.RegionName) + } + if data.CityName != "" { + result.City = stringPtr(data.CityName) + } + if data.CountryName != "" { + country := countryCodeToName(data.CountryName) + result.Country = stringPtr(country) + } + + return result, nil +} diff --git a/pkg/publicip/info/provider.go b/pkg/publicip/info/provider.go index 3414eb43f..6f78ae5d7 100644 --- a/pkg/publicip/info/provider.go +++ b/pkg/publicip/info/provider.go @@ -11,12 +11,14 @@ import ( type Provider string const ( - Ipinfo Provider = "ipinfo" + Ipinfo Provider = "ipinfo" + IP2Location Provider = "ip2location" ) func ListProviders() []Provider { return []Provider{ Ipinfo, + IP2Location, } } @@ -39,6 +41,8 @@ func newProvider(providerName Provider, client *http.Client) provider { //nolint switch providerName { case Ipinfo: return newIpinfo(client) + case IP2Location: + return newIP2Location(client) default: panic(fmt.Sprintf("provider %s not implemented", providerName)) }