justcaptcha/internal/captcha/db.go

105 lines
2.4 KiB
Go

package captcha
import (
"crypto/sha256"
"encoding/base64"
"image"
"strconv"
"sync"
"time"
)
var expiredScanInterval = 60 * time.Second
type ICaptchaDB interface {
New(data string) (ICaptcha, ID)
SetExpiration(expire time.Duration)
Image(id ID) (*image.Image, error)
Solve(id ID, answer Answer) (bool, error)
IsSolved(id ID) bool
}
type CaptchaDB struct {
DB map[ID]ICaptcha
ExpireInterval time.Duration
mut sync.Mutex
}
// SetExpiration stores expire value and starts a goroutine
// that checks for expired CAPTCHAs every minute.
func (cdb *CaptchaDB) SetExpiration(expire time.Duration) {
cdb.ExpireInterval = expire
if expire < expiredScanInterval {
expiredScanInterval = expire
}
go func() {
for {
n := time.Now().Second()
sleepFor := expiredScanInterval - (time.Duration(n) * time.Second % expiredScanInterval)
time.Sleep(sleepFor)
for id, captcha := range cdb.DB {
if time.Now().Sub(captcha.Expire()) <= 0 {
delete(cdb.DB, id)
}
}
}
}()
}
// New accepts an ICaptcha instance, generates an ID and store it in a database.
// `data` string is an additional random data used to generate an ID,
// e.g. IP-address.
func (cdb *CaptchaDB) New(data string, captcha ICaptcha) (ICaptcha, ID) {
idHash := sha256.New()
idHash.Write([]byte(data))
idHash.Write([]byte(strconv.FormatInt(time.Now().UnixMicro(), 16)))
idHash.Write([]byte(captcha.GetAnswer()))
id := ID(base64.RawURLEncoding.EncodeToString(idHash.Sum(nil)))
cdb.mut.Lock()
defer cdb.mut.Unlock()
cdb.DB[id] = captcha
return captcha, id
}
// Image returns image for a captcha.
func (cdb *CaptchaDB) Image(id ID) (*image.Image, error) {
if c, ok := cdb.DB[id]; ok {
return c.GetImage(), nil
}
return nil, errorNotFound
}
// Solve compares given answer with a stored one and if failed
// deletes a captcha from database.
func (cdb *CaptchaDB) Solve(id ID, answer Answer) (bool, error) {
cdb.mut.Lock()
defer cdb.mut.Unlock()
if c, ok := cdb.DB[id]; ok {
ok = c.Solve(answer)
if !ok {
delete(cdb.DB, id)
}
return ok, nil
}
return false, errorNotFound
}
// IsSolved checks if captcha was solved and removes it
// from a database.
func (cdb *CaptchaDB) IsSolved(id ID) (bool, error) {
cdb.mut.Lock()
defer cdb.mut.Unlock()
if c, ok := cdb.DB[id]; ok {
ok = c.IsSolved()
delete(cdb.DB, id)
return ok, nil
}
return false, errorNotFound
}