Skip to content

Commit 9bfa193

Browse files
committed
#minor: support redis for caching
1 parent 7a94016 commit 9bfa193

File tree

6 files changed

+179
-39
lines changed

6 files changed

+179
-39
lines changed

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# behavior.
33
before:
44
hooks:
5-
- go mod tidy
5+
- go mod tidy -compat=1.17
66
builds:
77
- skip: true
88
changelog:

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ func main() {
3131
IPAddress: "",
3232
// ipbase.com API token
3333
Token: "YOUR_IPBASE_API_TOKEN",
34-
// Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with isAddressNearby
34+
// Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with IsIPAddressNear()
3535
// 1 kilometer
3636
Radius: 1.0,
3737
// Allow 192.X, 172.X, 10.X and loopback addresses
38-
AllowPrivateIPAddresses: true
38+
AllowPrivateIPAddresses: true,
3939
// How long to cache if any ip address is nearby
4040
CacheTTL: 7 * (24 * time.Hour), // 1 week
4141
})
@@ -50,3 +50,57 @@ func main() {
5050
fmt.Println("Address nearby: ", isAddressNearby)
5151
}
5252
```
53+
54+
## Caching
55+
56+
To cache keys indefinitely, set `CacheTTL: -1`
57+
58+
### Local (in-memory)
59+
60+
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.
61+
62+
### Persistent
63+
64+
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.
65+
66+
> Note: Only Redis 7 is currently supported at the time of this writing.
67+
68+
#### Example Redis Usage
69+
70+
```go
71+
package main
72+
73+
import (
74+
"fmt"
75+
"log"
76+
"time"
77+
78+
"github.com/circa10a/go-geofence"
79+
"github.com/go-redis/redis/v9"
80+
)
81+
82+
func main() {
83+
geofence, err := geofence.New(&geofence.Config{
84+
IPAddress: "",
85+
Token: "YOUR_IPBASE_API_TOKEN",
86+
Radius: 1.0,
87+
AllowPrivateIPAddresses: true,
88+
CacheTTL: 7 * (24 * time.Hour), // 1 week
89+
// Use Redis for caching
90+
RedisOptions: &redis.Options{
91+
Addr: "localhost:6379",
92+
Password: "", // no password set
93+
DB: 0, // use default DB
94+
},
95+
})
96+
if err != nil {
97+
log.Fatal(err)
98+
}
99+
isAddressNearby, err := geofence.IsIPAddressNear("8.8.8.8")
100+
if err != nil {
101+
log.Fatal(err)
102+
}
103+
// Address nearby: false
104+
fmt.Println("Address nearby: ", isAddressNearby)
105+
}
106+
```

geofence.go

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,45 @@ package geofence
33
import (
44
"fmt"
55
"net"
6+
"strconv"
67
"time"
78

89
"github.com/EpicStep/go-simple-geo/v2/geo"
10+
"github.com/go-redis/redis/v9"
911
"github.com/go-resty/resty/v2"
1012
"github.com/patrickmn/go-cache"
13+
"golang.org/x/net/context"
1114
)
1215

1316
const (
14-
ipBaseBaseURL = "https://api.ipbase.com/v2"
17+
ipBaseBaseURL = "https://api.ipbase.com/v2"
18+
// For in-memory cache
1519
deleteExpiredCacheItemsInternal = 10 * time.Minute
1620
)
1721

1822
// Config holds the user configuration to setup a new geofence
1923
type Config struct {
24+
RedisOptions *redis.Options
2025
IPAddress string
2126
Token string
2227
Radius float64
23-
AllowPrivateIPAddresses bool
2428
CacheTTL time.Duration
29+
AllowPrivateIPAddresses bool
2530
}
2631

27-
// Geofence holds a ipbase.com client, cache and user supplied config
32+
// Geofence holds a ipbase.com client, redis client, in-memory cache and user supplied config
2833
type Geofence struct {
29-
Cache *cache.Cache
30-
IPBaseClient *resty.Client
34+
cache *cache.Cache
35+
ipbaseClient *resty.Client
36+
redisClient *redis.Client
37+
ctx context.Context
3138
Config
3239
Latitude float64
3340
Longitude float64
3441
}
3542

3643
// ipBaseResponse is the json response from ipbase.com
37-
type ipBaseResponse struct {
44+
type ipbaseResponse struct {
3845
Data data `json:"data"`
3946
}
4047

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

139+
// ErrCacheNotConfigured is the error raised when the cache was not set up correctly
140+
var ErrCacheNotConfigured = fmt.Errorf("cache no configured")
141+
132142
// validateIPAddress ensures valid ip address
133143
func validateIPAddress(ipAddress string) error {
134144
if net.ParseIP(ipAddress) == nil {
@@ -138,41 +148,49 @@ func validateIPAddress(ipAddress string) error {
138148
}
139149

140150
// getIPGeoData fetches geolocation data for specified IP address from https://ipbase.com
141-
func (g *Geofence) getIPGeoData(ipAddress string) (*ipBaseResponse, error) {
142-
response := &ipBaseResponse{}
143-
ipBaseError := &IPBaseError{}
151+
func (g *Geofence) getIPGeoData(ipAddress string) (*ipbaseResponse, error) {
152+
response := &ipbaseResponse{}
153+
ipbaseError := &IPBaseError{}
144154

145-
resp, err := g.IPBaseClient.R().
155+
resp, err := g.ipbaseClient.R().
146156
SetHeader("Accept", "application/json").
147157
SetQueryParam("apikey", g.Token).
148158
SetQueryParam("ip", ipAddress).
149159
SetResult(response).
150-
SetError(ipBaseError).
160+
SetError(ipbaseError).
151161
Get("/info")
152162
if err != nil {
153163
return response, err
154164
}
155165

156166
// If api gives back status code >399, report error to user
157167
if resp.IsError() {
158-
return response, ipBaseError
168+
return response, ipbaseError
159169
}
160170

161-
return resp.Result().(*ipBaseResponse), nil
171+
return resp.Result().(*ipbaseResponse), nil
162172
}
163173

164174
// New creates a new geofence for the IP address specified.
165175
// Use "" as the ip address to geofence the machine your application is running on
166176
// Token comes from https://ipbase.com/
167177
func New(c *Config) (*Geofence, error) {
168178
// Create new client for ipbase.com
169-
IPBaseClient := resty.New().SetBaseURL(ipBaseBaseURL)
179+
ipbaseClient := resty.New().SetBaseURL(ipBaseBaseURL)
170180

171181
// New Geofence object
172182
geofence := &Geofence{
173183
Config: *c,
174-
IPBaseClient: IPBaseClient,
175-
Cache: cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal),
184+
ipbaseClient: ipbaseClient,
185+
ctx: context.Background(),
186+
}
187+
188+
// Set up redis client if options are provided
189+
// Else we create a local in-memory cache
190+
if c.RedisOptions != nil {
191+
geofence.redisClient = redis.NewClient(c.RedisOptions)
192+
} else {
193+
geofence.cache = cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal)
176194
}
177195

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

208226
// Check if ipaddress has been looked up before and is in cache
209-
if isIPAddressNear, found := g.Cache.Get(ipAddress); found {
210-
return isIPAddressNear.(bool), nil
227+
isIPAddressNear, found, err := g.cacheGet(ipAddress)
228+
if err != nil {
229+
return false, err
230+
}
231+
if found {
232+
return isIPAddressNear, nil
211233
}
212234

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

226248
// Compare coordinates
227-
// distance must be less than or equal to the configured radius to be near
249+
// Distance must be less than or equal to the configured radius to be near
228250
isNear := distance <= g.Radius
229251

230-
// Insert ip address and it's status into the cache if user instantiated a cache
231-
g.Cache.Set(ipAddress, isNear, cache.DefaultExpiration)
252+
err = g.cacheSet(ipAddress, isNear)
253+
if err != nil {
254+
return false, err
255+
}
232256

233257
return isNear, nil
234258
}
259+
260+
func (g *Geofence) cacheGet(ipAddress string) (bool, bool, error) {
261+
// Use redis if configured
262+
if g.redisClient != nil {
263+
val, err := g.redisClient.Get(g.ctx, ipAddress).Result()
264+
if err != nil {
265+
// If key is not in redis
266+
if err == redis.Nil {
267+
return false, false, nil
268+
}
269+
return false, false, err
270+
}
271+
isIPAddressNear, err := strconv.ParseBool(val)
272+
if err != nil {
273+
return false, false, err
274+
}
275+
return isIPAddressNear, true, nil
276+
}
277+
278+
// Use in memory cache if configured
279+
if g.cache != nil {
280+
if isIPAddressNear, found := g.cache.Get(ipAddress); found {
281+
return isIPAddressNear.(bool), found, nil
282+
} else {
283+
return false, false, nil
284+
}
285+
}
286+
287+
return false, false, ErrCacheNotConfigured
288+
}
289+
290+
func (g *Geofence) cacheSet(ipAddress string, isNear bool) error {
291+
// Use redis if configured
292+
if g.redisClient != nil {
293+
// Redis stores false as 0 for whatever reason, so we'll store as a string and parse out in cacheGet
294+
err := g.redisClient.Set(g.ctx, ipAddress, strconv.FormatBool(isNear), g.Config.CacheTTL).Err()
295+
if err != nil {
296+
return err
297+
}
298+
}
299+
300+
// Use in memory cache if configured
301+
if g.cache != nil {
302+
g.cache.Set(ipAddress, isNear, g.Config.CacheTTL)
303+
}
304+
305+
return nil
306+
}

geofence_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ func TestGeofenceNear(t *testing.T) {
6767
geofence.Latitude = fakeLatitude
6868
geofence.Longitude = fakeLongitude
6969

70-
httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
70+
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
7171
defer httpmock.DeactivateAndReset()
7272

7373
// mock json rsponse
74-
response := &ipBaseResponse{
74+
response := &ipbaseResponse{
7575
Data: data{
7676
IP: fakeIPAddress,
7777
Location: location{
@@ -130,11 +130,11 @@ func TestGeofencePrivateIP(t *testing.T) {
130130
geofence.Latitude = fakeLatitude
131131
geofence.Longitude = fakeLongitude
132132

133-
httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
133+
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
134134
defer httpmock.DeactivateAndReset()
135135

136136
// mock json rsponse
137-
response := &ipBaseResponse{
137+
response := &ipbaseResponse{
138138
Data: data{
139139
IP: fakeIPAddress,
140140
Location: location{
@@ -192,11 +192,11 @@ func TestGeofenceNotNear(t *testing.T) {
192192
geofence.Latitude = fakeLatitude + 1
193193
geofence.Longitude = fakeLongitude + 1
194194

195-
httpmock.ActivateNonDefault(geofence.IPBaseClient.GetClient())
195+
httpmock.ActivateNonDefault(geofence.ipbaseClient.GetClient())
196196
defer httpmock.DeactivateAndReset()
197197

198198
// mock json rsponse
199-
response := &ipBaseResponse{
199+
response := &ipbaseResponse{
200200
Data: data{
201201
IP: fakeIPAddress,
202202
Location: location{

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ go 1.17
44

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

1315
require (
16+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
1417
github.com/davecgh/go-spew v1.1.0 // indirect
18+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1519
github.com/pmezard/go-difflib v1.0.0 // indirect
16-
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect
17-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
20+
gopkg.in/yaml.v3 v3.0.1 // indirect
1821
)

go.sum

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
github.com/EpicStep/go-simple-geo/v2 v2.0.1 h1:+suZRwgZVZCuH8NXNE/D+7EH0iY90dqx2eA3faQ2v7c=
22
github.com/EpicStep/go-simple-geo/v2 v2.0.1/go.mod h1:ELLmk0tgdNH4BLiL+jrSg+X6nz3aMgZrTRnHPWsaXvQ=
3+
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
4+
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
35
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
46
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
8+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
9+
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
10+
github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4=
11+
github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o=
512
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
613
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
14+
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
715
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
816
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
17+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
18+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
19+
github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q=
920
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
1021
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
1122
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -14,18 +25,18 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
1425
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
1526
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
1627
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
17-
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw=
18-
golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
28+
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
29+
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
1930
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
2031
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22-
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
32+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
2333
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
24-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
2534
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
26-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
35+
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
2736
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
2837
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2938
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
30-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
39+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
3140
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
41+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
42+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)