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 + + + +
+

Type the numbers you see in the picture below:

+

Captcha image

+Reload | Play Audio + +
+ + +
+` 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) + } + } +}