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, style string) (*image.Image, error) { if c, ok := cdb.DB[id]; ok { return c.GetImage(style), 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 }