Skip to content

Commit

Permalink
#minor: support redis for caching
Browse files Browse the repository at this point in the history
  • Loading branch information
circa10a committed Sep 1, 2022
1 parent 7a94016 commit 9bfa193
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# behavior.
before:
hooks:
- go mod tidy
- go mod tidy -compat=1.17
builds:
- skip: true
changelog:
Expand Down
58 changes: 56 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ func main() {
IPAddress: "",
// 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
// Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with IsIPAddressNear()
// 1 kilometer
Radius: 1.0,
// Allow 192.X, 172.X, 10.X and loopback addresses
AllowPrivateIPAddresses: true
AllowPrivateIPAddresses: true,
// How long to cache if any ip address is nearby
CacheTTL: 7 * (24 * time.Hour), // 1 week
})
Expand All @@ -50,3 +50,57 @@ func main() {
fmt.Println("Address nearby: ", isAddressNearby)
}
```

## Caching

To cache keys indefinitely, set `CacheTTL: -1`

### Local (in-memory)

By default, the library will use an in-memory cache that will be used to reduce the number of calls to ipbase.com and increase performance. If no `CacheTTL` value is set (`0`), the in-memory cache is disabled.

### Persistent

If you need a persistent cache to live outside of your application, [Redis](https://redis.io/) is supported by this library. To have the library cache addres proximity using a Redis instance, simply provide a `redis.RedisOptions` struct to `geofence.Config.RedisOptions`. If `RedisOptions` is configured, the in-memory cache will not be used.

> Note: Only Redis 7 is currently supported at the time of this writing.
#### Example Redis Usage

```go
package main

import (
"fmt"
"log"
"time"

"github.com/circa10a/go-geofence"
"github.com/go-redis/redis/v9"
)

func main() {
geofence, err := geofence.New(&geofence.Config{
IPAddress: "",
Token: "YOUR_IPBASE_API_TOKEN",
Radius: 1.0,
AllowPrivateIPAddresses: true,
CacheTTL: 7 * (24 * time.Hour), // 1 week
// Use Redis for caching
RedisOptions: &redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
},
})
if err != nil {
log.Fatal(err)
}
isAddressNearby, err := geofence.IsIPAddressNear("8.8.8.8")
if err != nil {
log.Fatal(err)
}
// Address nearby: false
fmt.Println("Address nearby: ", isAddressNearby)
}
```
114 changes: 93 additions & 21 deletions geofence.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,45 @@ package geofence
import (
"fmt"
"net"
"strconv"
"time"

"github.com/EpicStep/go-simple-geo/v2/geo"
"github.com/go-redis/redis/v9"
"github.com/go-resty/resty/v2"
"github.com/patrickmn/go-cache"
"golang.org/x/net/context"
)

const (
ipBaseBaseURL = "https://api.ipbase.com/v2"
ipBaseBaseURL = "https://api.ipbase.com/v2"
// For in-memory cache
deleteExpiredCacheItemsInternal = 10 * time.Minute
)

// Config holds the user configuration to setup a new geofence
type Config struct {
RedisOptions *redis.Options
IPAddress string
Token string
Radius float64
AllowPrivateIPAddresses bool
CacheTTL time.Duration
AllowPrivateIPAddresses bool
}

// Geofence holds a ipbase.com client, cache and user supplied config
// Geofence holds a ipbase.com client, redis client, in-memory cache and user supplied config
type Geofence struct {
Cache *cache.Cache
IPBaseClient *resty.Client
cache *cache.Cache
ipbaseClient *resty.Client
redisClient *redis.Client
ctx context.Context
Config
Latitude float64
Longitude float64
}

// ipBaseResponse is the json response from ipbase.com
type ipBaseResponse struct {
type ipbaseResponse struct {
Data data `json:"data"`
}

Expand Down Expand Up @@ -129,6 +136,9 @@ func (e *IPBaseError) Error() string {
// ErrInvalidIPAddress is the error raised when an invalid IP address is provided
var ErrInvalidIPAddress = fmt.Errorf("invalid IP address provided")

// ErrCacheNotConfigured is the error raised when the cache was not set up correctly
var ErrCacheNotConfigured = fmt.Errorf("cache no configured")

// validateIPAddress ensures valid ip address
func validateIPAddress(ipAddress string) error {
if net.ParseIP(ipAddress) == nil {
Expand All @@ -138,41 +148,49 @@ func validateIPAddress(ipAddress string) error {
}

// getIPGeoData fetches geolocation data for specified IP address from https://ipbase.com
func (g *Geofence) getIPGeoData(ipAddress string) (*ipBaseResponse, error) {
response := &ipBaseResponse{}
ipBaseError := &IPBaseError{}
func (g *Geofence) getIPGeoData(ipAddress string) (*ipbaseResponse, error) {
response := &ipbaseResponse{}
ipbaseError := &IPBaseError{}

resp, err := g.IPBaseClient.R().
resp, err := g.ipbaseClient.R().
SetHeader("Accept", "application/json").
SetQueryParam("apikey", g.Token).
SetQueryParam("ip", ipAddress).
SetResult(response).
SetError(ipBaseError).
SetError(ipbaseError).
Get("/info")
if err != nil {
return response, err
}

// If api gives back status code >399, report error to user
if resp.IsError() {
return response, ipBaseError
return response, ipbaseError
}

return resp.Result().(*ipBaseResponse), 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://ipbase.com/
func New(c *Config) (*Geofence, error) {
// Create new client for ipbase.com
IPBaseClient := resty.New().SetBaseURL(ipBaseBaseURL)
ipbaseClient := resty.New().SetBaseURL(ipBaseBaseURL)

// New Geofence object
geofence := &Geofence{
Config: *c,
IPBaseClient: IPBaseClient,
Cache: cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal),
ipbaseClient: ipbaseClient,
ctx: context.Background(),
}

// Set up redis client if options are provided
// Else we create a local in-memory cache
if c.RedisOptions != nil {
geofence.redisClient = redis.NewClient(c.RedisOptions)
} else {
geofence.cache = cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal)
}

// Get current location of specified IP address
Expand Down Expand Up @@ -206,8 +224,12 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) {
}

// Check if ipaddress has been looked up before and is in cache
if isIPAddressNear, found := g.Cache.Get(ipAddress); found {
return isIPAddressNear.(bool), nil
isIPAddressNear, found, err := g.cacheGet(ipAddress)
if err != nil {
return false, err
}
if found {
return isIPAddressNear, nil
}

// If not in cache, lookup IP and compare
Expand All @@ -224,11 +246,61 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) {
distance := currentCoordinates.Distance(clientCoordinates)

// Compare coordinates
// distance must be less than or equal to the configured radius to be near
// Distance must be less than or equal to the configured radius to be near
isNear := distance <= g.Radius

// Insert ip address and it's status into the cache if user instantiated a cache
g.Cache.Set(ipAddress, isNear, cache.DefaultExpiration)
err = g.cacheSet(ipAddress, isNear)
if err != nil {
return false, err
}

return isNear, nil
}

func (g *Geofence) cacheGet(ipAddress string) (bool, bool, error) {
// Use redis if configured
if g.redisClient != nil {
val, err := g.redisClient.Get(g.ctx, ipAddress).Result()
if err != nil {
// If key is not in redis
if err == redis.Nil {
return false, false, nil
}
return false, false, err
}
isIPAddressNear, err := strconv.ParseBool(val)
if err != nil {
return false, false, err
}
return isIPAddressNear, true, nil
}

// Use in memory cache if configured
if g.cache != nil {
if isIPAddressNear, found := g.cache.Get(ipAddress); found {
return isIPAddressNear.(bool), found, nil
} else {
return false, false, nil
}
}

return false, false, ErrCacheNotConfigured
}

func (g *Geofence) cacheSet(ipAddress string, isNear bool) error {
// Use redis if configured
if g.redisClient != nil {
// Redis stores false as 0 for whatever reason, so we'll store as a string and parse out in cacheGet
err := g.redisClient.Set(g.ctx, ipAddress, strconv.FormatBool(isNear), g.Config.CacheTTL).Err()
if err != nil {
return err
}
}

// Use in memory cache if configured
if g.cache != nil {
g.cache.Set(ipAddress, isNear, g.Config.CacheTTL)
}

return nil
}
12 changes: 6 additions & 6 deletions geofence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ func TestGeofenceNear(t *testing.T) {
geofence.Latitude = fakeLatitude
geofence.Longitude = fakeLongitude

httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
defer httpmock.DeactivateAndReset()

// mock json rsponse
response := &ipBaseResponse{
response := &ipbaseResponse{
Data: data{
IP: fakeIPAddress,
Location: location{
Expand Down Expand Up @@ -130,11 +130,11 @@ func TestGeofencePrivateIP(t *testing.T) {
geofence.Latitude = fakeLatitude
geofence.Longitude = fakeLongitude

httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
defer httpmock.DeactivateAndReset()

// mock json rsponse
response := &ipBaseResponse{
response := &ipbaseResponse{
Data: data{
IP: fakeIPAddress,
Location: location{
Expand Down Expand Up @@ -192,11 +192,11 @@ func TestGeofenceNotNear(t *testing.T) {
geofence.Latitude = fakeLatitude + 1
geofence.Longitude = fakeLongitude + 1

httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
defer httpmock.DeactivateAndReset()

// mock json rsponse
response := &ipBaseResponse{
response := &ipbaseResponse{
Data: data{
IP: fakeIPAddress,
Location: location{
Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ go 1.17

require (
github.com/EpicStep/go-simple-geo/v2 v2.0.1
github.com/go-redis/redis/v9 v9.0.0-beta.2
github.com/go-resty/resty/v2 v2.7.0
github.com/jarcoal/httpmock v1.0.8
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
)

require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
25 changes: 18 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
github.com/EpicStep/go-simple-geo/v2 v2.0.1 h1:+suZRwgZVZCuH8NXNE/D+7EH0iY90dqx2eA3faQ2v7c=
github.com/EpicStep/go-simple-geo/v2 v2.0.1/go.mod h1:ELLmk0tgdNH4BLiL+jrSg+X6nz3aMgZrTRnHPWsaXvQ=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4=
github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -14,18 +25,18 @@ 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-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw=
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit 9bfa193

Please sign in to comment.