Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add captcha redis storage adapter and example #36

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 113 additions & 0 deletions capexample/redisexample/main.go
Original file line number Diff line number Diff line change
@@ -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, "<br><a href='/'>Try another one</a>")
}

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 = `<!doctype html>
<head><title>Captcha Example</title></head>
<body>
<script>
function setSrcQuery(e, q) {
var src = e.src;
var p = src.indexOf('?');
if (p >= 0) {
src = src.substr(0, p);
}
e.src = src + "?" + q
}

function playAudio() {
var le = document.getElementById("lang");
var lang = le.options[le.selectedIndex].value;
var e = document.getElementById('audio')
setSrcQuery(e, "lang=" + lang)
e.style.display = 'block';
e.autoplay = 'true';
return false;
}

function changeLang() {
var e = document.getElementById('audio')
if (e.style.display == 'block') {
playAudio();
}
}

function reload() {
setSrcQuery(document.getElementById('image'), "reload=" + (new Date()).getTime());
setSrcQuery(document.getElementById('audio'), (new Date()).getTime());
return false;
}
</script>
<select id="lang" onchange="changeLang()">
<option value="en">English</option>
<option value="ja">Japanese</option>
<option value="ru">Russian</option>
<option value="zh">Chinese</option>
</select>
<form action="/process" method=post>
<p>Type the numbers you see in the picture below:</p>
<p><img id=image src="/captcha/{{.CaptchaId}}.png" alt="Captcha image"></p>
<a href="#" onclick="reload()">Reload</a> | <a href="#" onclick="playAudio()">Play Audio</a>
<audio id=audio controls style="display:none" src="/captcha/{{.CaptchaId}}.wav" preload=none>
You browser doesn't support audio.
<a href="/captcha/download/{{.CaptchaId}}.wav">Download file</a> to play it in the external player.
</audio>
<input type=hidden name=captchaId value="{{.CaptchaId}}"><br>
<input name=captchaSolution>
<input type=submit value=Submit>
</form>
`
78 changes: 78 additions & 0 deletions store_redis.go
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions store_redis_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}