diff --git a/capexample/redisexample/main.go b/capexample/redisexample/main.go
new file mode 100644
index 0000000..53bd1ca
--- /dev/null
+++ b/capexample/redisexample/main.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/dchest/captcha"
+ "github.com/go-redis/redis"
+)
+
+var formTemplate = template.Must(template.New("example").Parse(formTemplateSrc))
+
+func showFormHandler(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ d := struct {
+ CaptchaId string
+ }{
+ captcha.New(),
+ }
+ if err := formTemplate.Execute(w, &d); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func processFormHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if !captcha.VerifyString(r.FormValue("captchaId"), r.FormValue("captchaSolution")) {
+ io.WriteString(w, "Wrong captcha solution! No robots allowed!\n")
+ } else {
+ io.WriteString(w, "Great job, human! You solved the captcha.\n")
+ }
+ io.WriteString(w, "
Try another one")
+}
+
+func main() {
+ // redis store
+ s, err := captcha.NewRedisStore(&redis.Options{Addr: "localhost:6379", DB: 0}, captcha.Expiration, captcha.DefaultMaxRedisKeys, captcha.DefaultRedisPrefixKey)
+ if err != nil {
+ panic(err.Error())
+ }
+ captcha.SetCustomStore(s)
+
+ // http
+ http.HandleFunc("/", showFormHandler)
+ http.HandleFunc("/process", processFormHandler)
+ http.Handle("/captcha/", captcha.Server(captcha.StdWidth, captcha.StdHeight))
+ fmt.Println("Server is at localhost:8666")
+ if err := http.ListenAndServe("localhost:8666", nil); err != nil {
+ log.Fatal(err)
+ }
+}
+
+const formTemplateSrc = `
+
Captcha Example
+
+
+
+
+`
diff --git a/store_redis.go b/store_redis.go
new file mode 100644
index 0000000..b5ecf17
--- /dev/null
+++ b/store_redis.go
@@ -0,0 +1,78 @@
+// Contributed 2020 by Hari
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package captcha
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-redis/redis"
+)
+
+const (
+ // DefaultMaxRedisKeys default max redis keys per expiration
+ DefaultMaxRedisKeys = 500000
+ // DefaultRedisPrefixKey default redis prefix key
+ DefaultRedisPrefixKey = "captcha"
+)
+
+// redisStore is an internal store for captcha ids and their values.
+type redisStore struct {
+ redisClient *redis.Client
+ expiration time.Duration
+ maxKeys int64
+ prefixKey string
+}
+
+// NewRedisStore returns new Redis memory store
+func NewRedisStore(redisOptions *redis.Options, expiration time.Duration, maxKeys int64, prefixKey string) (Store, error) {
+ if redisOptions == nil {
+ return nil, fmt.Errorf("invalid redis options: %v", redisOptions)
+ }
+ s := new(redisStore)
+ s.redisClient = redis.NewClient(redisOptions)
+ s.expiration = expiration
+ s.maxKeys = maxKeys
+ if s.maxKeys <= 100 {
+ s.maxKeys = DefaultMaxRedisKeys
+ }
+ s.prefixKey = prefixKey
+ if s.prefixKey == "" {
+ s.prefixKey = DefaultRedisPrefixKey
+ }
+
+ return s, nil
+}
+
+func (s *redisStore) Set(id string, digits []byte) {
+ c, err := s.redisClient.DbSize().Result()
+ if err != nil {
+ panic(err)
+ }
+ if c > s.maxKeys {
+ panic(fmt.Errorf("to many keys > %v", s.maxKeys))
+ }
+
+ id = fmt.Sprintf("%s.%s", s.prefixKey, id)
+ _, err = s.redisClient.Get(id).Result()
+ if err == redis.Nil {
+ s.redisClient.Set(id, digits, s.expiration)
+ }
+}
+
+func (s *redisStore) Get(id string, clear bool) (digits []byte) {
+ id = fmt.Sprintf("%s.%s", s.prefixKey, id)
+ val, err := s.redisClient.Get(id).Result()
+ if err == redis.Nil {
+ return digits
+ }
+ digits = []byte(val)
+ if clear {
+ if err != redis.Nil {
+ s.redisClient.Del(id)
+ }
+ }
+ return digits
+}
diff --git a/store_redis_test.go b/store_redis_test.go
new file mode 100644
index 0000000..de2418d
--- /dev/null
+++ b/store_redis_test.go
@@ -0,0 +1,66 @@
+// Contributed 2020 by Hari
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package captcha
+
+import (
+ "bytes"
+ "testing"
+ "time"
+
+ "github.com/go-redis/redis"
+)
+
+func TestRedisSetGet(t *testing.T) {
+ s, err := NewRedisStore(&redis.Options{Addr: "localhost:6379", DB: 0}, 1*time.Minute, DefaultMaxRedisKeys, DefaultRedisPrefixKey)
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+ id := "redis-id-no-clear"
+ d := RandomDigits(10)
+ s.Set(id, d)
+ d2 := s.Get(id, false)
+ if d2 == nil || !bytes.Equal(d, d2) {
+ t.Errorf("saved %v, getDigits returned got %v", d, d2)
+ }
+}
+
+func TestRedisGetClear(t *testing.T) {
+ s, err := NewRedisStore(&redis.Options{Addr: "localhost:6379", DB: 0}, Expiration, DefaultMaxRedisKeys, DefaultRedisPrefixKey)
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+ id := "redis-id"
+ d := RandomDigits(10)
+ s.Set(id, d)
+ d2 := s.Get(id, true)
+ if d2 == nil || !bytes.Equal(d, d2) {
+ t.Errorf("saved %v, getDigits returned got %v", d, d2)
+ }
+ d2 = s.Get(id, false)
+ if d2 != nil {
+ t.Errorf("getDigitClear didn't clear (%q=%v)", id, d2)
+ }
+}
+
+func BenchmarkRedisMaxKeys(b *testing.B) {
+ maxKeys := 101
+
+ b.StopTimer()
+ d := RandomDigits(10)
+ s, err := NewRedisStore(&redis.Options{Addr: "localhost:6379", DB: 0}, 1*time.Minute, int64(maxKeys), DefaultRedisPrefixKey)
+ if err != nil {
+ b.Errorf(err.Error())
+ }
+ ids := make([]string, maxKeys)
+ for i := range ids {
+ ids[i] = randomId()
+ }
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ for j := 0; j < maxKeys; j++ {
+ s.Set(ids[j], d)
+ }
+ }
+}