justcaptcha/pkg/captcha/db.go

119 lines
2.6 KiB
Go

package captcha
import (
"crypto/sha256"
"encoding/base64"
"image"
"strconv"
"sync"
"time"
)
var expiredScanInterval = 60 * time.Second
type ID string
// NewID generates an ID as a sha256 hash of additionalData, current time
// and answer encoded with base64 in raw URL variant.
func NewID(additionalData string, answer Answer) ID {
idHash := sha256.New()
idHash.Write([]byte(additionalData))
idHash.Write([]byte(strconv.FormatInt(time.Now().UnixMicro(), 16)))
idHash.Write([]byte(answer))
return ID(base64.RawURLEncoding.EncodeToString(idHash.Sum(nil)))
}
type ICaptchaDB interface {
New(data string) (ICaptcha, ID)
SetExpiry(expiry time.Duration)
GetExpiry() 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
ExpireIn time.Duration
sync.Mutex
}
// SetExpiry stores expire value and starts a goroutine
// that checks for expired CAPTCHAs every minute.
func (cdb *CaptchaDB) SetExpiry(expire time.Duration) {
cdb.ExpireIn = expire
if expire < expiredScanInterval {
expiredScanInterval = expire
}
go func() {
for {
sleepFor := expiredScanInterval - (time.Duration(time.Now().Second()) % expiredScanInterval)
time.Sleep(sleepFor)
for id, captcha := range cdb.DB {
if time.Now().Sub(captcha.Expiry()) <= 0 {
cdb.Lock()
defer cdb.Unlock()
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) {
id := NewID(data, captcha.GetAnswer())
cdb.Lock()
defer cdb.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.Lock()
defer cdb.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.Lock()
defer cdb.Unlock()
if c, ok := cdb.DB[id]; ok {
ok = c.IsSolved()
delete(cdb.DB, id)
return ok, nil
}
return false, errorNotFound
}
func (cdb *CaptchaDB) GetExpiry() time.Duration {
return cdb.ExpireIn
}