diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index c416061..17298db 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -23,7 +23,7 @@ jobs: with: go-version: ${{ steps.vars.outputs.go_version }} - name: Bump version and push tag - uses: anothrNick/github-tag-action@1.26.0 + uses: anothrNick/github-tag-action@1.39.0 id: tagging env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2f06425..afae683 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,8 +11,6 @@ jobs: steps: - name: Install Go uses: actions/setup-go@v2 - with: - go-version: '^1.17.0' - name: Checkout code uses: actions/checkout@v2 - name: Test @@ -23,7 +21,5 @@ jobs: - uses: actions/checkout@v2 - name: Install Go uses: actions/setup-go@v2 - with: - go-version: '^1.17.0' - name: golangci-lint uses: golangci/golangci-lint-action@v2 diff --git a/README.md b/README.md index 0fa2a19..f515e41 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/circa10a/go-geofence)](https://pkg.go.dev/github.com/circa10a/go-geofence?tab=overview) [![Go Report Card](https://goreportcard.com/badge/github.com/circa10a/go-geofence)](https://goreportcard.com/report/github.com/circa10a/go-geofence) -A small library to detect if an IP address is close to yours or another of your choosing using https://freegeoip.app/ +A small library to detect if an IP address is close to yours or another of your choosing using https://ipbase.com/ ## Usage -First you will need a free API Token from [freegeoip.app](https://freegeoip.app/) +First you will need a free API Token from [ipbase.com](https://ipbase.com/) ```bash go get github.com/circa10a/go-geofence @@ -29,8 +29,8 @@ func main() { geofence, err := geofence.New(&geofence.Config{ // Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter IPAddress: "", - // freegeoip.app API token - Token: "YOUR_FREEGEOIP_API_TOKEN", + // ipbase.com API token + Token: "YOUR_IPBASE_API_TOKEN", // Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with isAddressNearby // 1 kilometer Radius: 1.0, diff --git a/geofence.go b/geofence.go index fd68426..ea08c8a 100644 --- a/geofence.go +++ b/geofence.go @@ -11,7 +11,7 @@ import ( ) const ( - freeGeoIPBaseURL = "https://api.freegeoip.app/json" + ipBaseBaseURL = "https://api.ipbase.com/v2" deleteExpiredCacheItemsInternal = 10 * time.Minute ) @@ -24,36 +24,105 @@ type Config struct { CacheTTL time.Duration } -// Geofence holds a freegeoip.app client, cache and user supplied config +// Geofence holds a ipbase.com client, cache and user supplied config type Geofence struct { - Cache *cache.Cache - FreeGeoIPClient *resty.Client + Cache *cache.Cache + IPBaseClient *resty.Client Config Latitude float64 Longitude float64 } -// FreeGeoIPResponse is the json response from freegeoip.app -type FreeGeoIPResponse struct { - IP string `json:"ip"` - CountryCode string `json:"country_code"` - CountryName string `json:"country_name"` - RegionCode string `json:"region_code"` - RegionName string `json:"region_name"` - City string `json:"city"` - ZipCode string `json:"zip_code"` - TimeZone string `json:"time_zone"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - MetroCode int `json:"metro_code"` +// ipBaseResponse is the json response from ipbase.com +type ipBaseResponse struct { + Data data `json:"data"` } -// FreeGeoIPError is the json response when there is an error from freegeoip.app -type FreeGeoIPError struct { +type timezone struct { + Id string `json:"id"` + CurrentTime string `json:"current_time"` + Code string `json:"code"` + IDaylightSaving bool `json:"is_daylight_saving"` + GmtOffset int `json:"gmt_offset"` +} + +type connection struct { + Organization string `json:"organization"` + Isp string `json:"isp"` + Asn int `json:"asn"` +} + +type continent struct { + Code string `json:"code"` + Name string `json:"name"` + NameTranslated string `json:"name_translated"` +} + +type currencies struct { + Symbol string `json:"symbol"` + Name string `json:"name"` + SymbolNative string `json:"symbol_native"` + Code string `json:"code"` + NamePlural string `json:"name_plural"` + DecimalDigits int `json:"decimal_digits"` + Rounding int `json:"rounding"` +} + +type languages struct { + Name string `json:"name"` + NameNative string `json:"name_native"` +} +type country struct { + Alpha2 string `json:"alpha2"` + Alpha3 string `json:"alpha3"` + CallingCodes []string `json:"calling_codes"` + Currencies []currencies `json:"currencies"` + Emoji string `json:"emoji"` + Ioc string `json:"ioc"` + Languages []languages `json:"languages"` + Name string `json:"name"` + NameTranslated string `json:"name_translated"` + Timezones []string `json:"timezones"` + IsInEuropeanUnion bool `json:"is_in_european_union"` +} + +type city struct { + Name string `json:"name"` + NameTranslated string `json:"name_translated"` +} + +type region struct { + Fips interface{} `json:"fips"` + Alpha2 interface{} `json:"alpha2"` + Name string `json:"name"` + NameTranslated string `json:"name_translated"` +} + +type location struct { + GeonamesID interface{} `json:"geonames_id"` + Region region `json:"region"` + Continent continent `json:"continent"` + City city `json:"city"` + Zip string `json:"zip"` + Country country `json:"country"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type data struct { + Timezone timezone `json:"timezone"` + IP string `json:"ip"` + Type string `json:"type"` + Connection connection `json:"connection"` + Location location `json:"location"` +} + +// IPBaseError is the json response when there is an error from ipbase.com +type IPBaseError struct { Message string `json:"message"` } -func (e *FreeGeoIPError) Error() string { +func (e *IPBaseError) Error() string { return e.Message } @@ -68,41 +137,42 @@ func validateIPAddress(ipAddress string) error { return nil } -// getIPGeoData fetches geolocation data for specified IP address from https://freegeoip.app -func (g *Geofence) getIPGeoData(ipAddress string) (*FreeGeoIPResponse, error) { - freeGeoIPResponse := &FreeGeoIPResponse{} - freeGeoIPError := &FreeGeoIPError{} +// getIPGeoData fetches geolocation data for specified IP address from https://ipbase.com +func (g *Geofence) getIPGeoData(ipAddress string) (*ipBaseResponse, error) { + response := &ipBaseResponse{} + ipBaseError := &IPBaseError{} - resp, err := g.FreeGeoIPClient.R(). + resp, err := g.IPBaseClient.R(). SetHeader("Accept", "application/json"). SetQueryParam("apikey", g.Token). - SetResult(freeGeoIPResponse). - SetError(freeGeoIPError). - Get(ipAddress) + SetQueryParam("ip", ipAddress). + SetResult(response). + SetError(ipBaseError). + Get("/info") if err != nil { - return freeGeoIPResponse, err + return response, err } // If api gives back status code >399, report error to user if resp.IsError() { - return freeGeoIPResponse, freeGeoIPError + return response, ipBaseError } - return resp.Result().(*FreeGeoIPResponse), nil + return resp.Result().(*ipBaseResponse), nil } // New creates a new geofence for the IP address specified. // Use "" as the ip address to geofence the machine your application is running on -// Token comes from https://freegeoip.app/ +// Token comes from https://ipbase.com/ func New(c *Config) (*Geofence, error) { - // Create new client for freegeoip.app - freeGeoIPClient := resty.New().SetBaseURL(freeGeoIPBaseURL) + // Create new client for ipbase.com + IPBaseClient := resty.New().SetBaseURL(ipBaseBaseURL) // New Geofence object geofence := &Geofence{ - Config: *c, - FreeGeoIPClient: freeGeoIPClient, - Cache: cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal), + Config: *c, + IPBaseClient: IPBaseClient, + Cache: cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal), } // Get current location of specified IP address @@ -114,8 +184,8 @@ func New(c *Config) (*Geofence, error) { } // Set the location of our geofence to compare against looked up IP's - geofence.Latitude = ipAddressLookupDetails.Latitude - geofence.Longitude = ipAddressLookupDetails.Longitude + geofence.Latitude = ipAddressLookupDetails.Data.Location.Latitude + geofence.Longitude = ipAddressLookupDetails.Data.Location.Longitude return geofence, nil } @@ -148,7 +218,7 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) { // Format our IP coordinates and the clients currentCoordinates := geo.NewCoordinatesFromDegrees(g.Latitude, g.Longitude) - clientCoordinates := geo.NewCoordinatesFromDegrees(ipAddressLookupDetails.Latitude, ipAddressLookupDetails.Longitude) + clientCoordinates := geo.NewCoordinatesFromDegrees(ipAddressLookupDetails.Data.Location.Latitude, ipAddressLookupDetails.Data.Location.Longitude) // Get distance in kilometers distance := currentCoordinates.Distance(clientCoordinates) diff --git a/geofence_test.go b/geofence_test.go index 8c8b21c..14b57d8 100644 --- a/geofence_test.go +++ b/geofence_test.go @@ -11,6 +11,10 @@ import ( "github.com/stretchr/testify/assert" ) +var ( + endpointStrTemplate = "%s/info?apikey=%s&ip=%s" +) + func TestValidateIPAddress(t *testing.T) { tests := []struct { expected error @@ -51,7 +55,7 @@ func TestGeofenceNear(t *testing.T) { fakeLatitude := 37.751 fakeLongitude := -97.822 fakeRadius := 0.0 - fakeEndpoint := fmt.Sprintf("%s/%s?apikey=%s", freeGeoIPBaseURL, fakeIPAddress, fakeApiToken) + fakeEndpoint := fmt.Sprintf(endpointStrTemplate, ipBaseBaseURL, fakeApiToken, fakeIPAddress) // new geofence geofence, _ := New(&Config{ @@ -63,17 +67,25 @@ func TestGeofenceNear(t *testing.T) { geofence.Latitude = fakeLatitude geofence.Longitude = fakeLongitude - httpmock.ActivateNonDefault(geofence.FreeGeoIPClient.GetClient()) + httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient()) defer httpmock.DeactivateAndReset() // mock json rsponse - response := &FreeGeoIPResponse{ - IP: fakeIPAddress, - CountryCode: "US", - CountryName: "United States", - TimeZone: "America/Chicago", - Latitude: fakeLatitude, - Longitude: fakeLongitude, + response := &ipBaseResponse{ + Data: data{ + IP: fakeIPAddress, + Location: location{ + Latitude: fakeLatitude, + Longitude: fakeLongitude, + Country: country{ + Ioc: "USA", + Name: "United States", + }, + }, + Timezone: timezone{ + Id: "America/Chicago", + }, + }, } // mock freegeoip.app response @@ -105,7 +117,7 @@ func TestGeofencePrivateIP(t *testing.T) { fakeLatitude := 37.751 fakeLongitude := -97.822 fakeRadius := 0.0 - fakeEndpoint := fmt.Sprintf("%s/%s?apikey=%s", freeGeoIPBaseURL, fakeIPAddress, fakeApiToken) + fakeEndpoint := fmt.Sprintf(endpointStrTemplate, ipBaseBaseURL, fakeApiToken, fakeIPAddress) // new geofence geofence, _ := New(&Config{ @@ -118,17 +130,25 @@ func TestGeofencePrivateIP(t *testing.T) { geofence.Latitude = fakeLatitude geofence.Longitude = fakeLongitude - httpmock.ActivateNonDefault(geofence.FreeGeoIPClient.GetClient()) + httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient()) defer httpmock.DeactivateAndReset() // mock json rsponse - response := &FreeGeoIPResponse{ - IP: fakeIPAddress, - CountryCode: "US", - CountryName: "United States", - TimeZone: "America/Chicago", - Latitude: fakeLatitude, - Longitude: fakeLongitude, + response := &ipBaseResponse{ + Data: data{ + IP: fakeIPAddress, + Location: location{ + Latitude: fakeLatitude, + Longitude: fakeLongitude, + Country: country{ + Ioc: "USA", + Name: "United States", + }, + }, + Timezone: timezone{ + Id: "America/Chicago", + }, + }, } // mock freegeoip.app response @@ -160,7 +180,7 @@ func TestGeofenceNotNear(t *testing.T) { fakeLatitude := 37.751 fakeLongitude := -98.822 fakeRadius := 0.0 - fakeEndpoint := fmt.Sprintf("%s/%s?apikey=%s", freeGeoIPBaseURL, fakeIPAddress, fakeApiToken) + fakeEndpoint := fmt.Sprintf(endpointStrTemplate, ipBaseBaseURL, fakeApiToken, fakeIPAddress) // new geofence geofence, _ := New(&Config{ @@ -172,17 +192,25 @@ func TestGeofenceNotNear(t *testing.T) { geofence.Latitude = fakeLatitude + 1 geofence.Longitude = fakeLongitude + 1 - httpmock.ActivateNonDefault(geofence.FreeGeoIPClient.GetClient()) + httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient()) defer httpmock.DeactivateAndReset() // mock json rsponse - response := &FreeGeoIPResponse{ - IP: fakeIPAddress, - CountryCode: "US", - CountryName: "United States", - TimeZone: "America/Chicago", - Latitude: fakeLatitude, - Longitude: fakeLongitude, + response := &ipBaseResponse{ + Data: data{ + IP: fakeIPAddress, + Location: location{ + Latitude: fakeLatitude, + Longitude: fakeLongitude, + Country: country{ + Ioc: "USA", + Name: "United States", + }, + }, + Timezone: timezone{ + Id: "America/Chicago", + }, + }, } // mock freegeoip.app response diff --git a/go.mod b/go.mod index 0248e97..57c76bf 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect + golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 7dab68a..7f3b041 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc= -golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=