diff --git a/pkg/captcha/inmemdb/inmemdb.go b/pkg/captcha/inmemdb/inmemdb.go new file mode 100644 index 0000000..400e193 --- /dev/null +++ b/pkg/captcha/inmemdb/inmemdb.go @@ -0,0 +1,157 @@ +package inmemdb + +import ( + "image" + "justcaptcha/pkg/captcha" + "sync" + "time" +) + +type InMemoryCaptchaDB struct { + sync.Mutex + + db map[captcha.ID]captcha.Captcha + expiry time.Duration + expiryScanInterval time.Duration +} + +// NewInMemoryCaptchaDB returns an initialised instance of an InMemoryCaptchaDB. +// An expiry duration is a time for how long CAPTCHA will be valid. +func NewInMemoryCaptchaDB(expiry time.Duration) *InMemoryCaptchaDB { + db := &InMemoryCaptchaDB{ + db: make(map[captcha.ID]captcha.Captcha), + expiry: expiry} + + if expiry < captcha.DefaultExpiredScanInterval { + db.expiryScanInterval = expiry + } else { + db.expiryScanInterval = captcha.DefaultExpiredScanInterval + } + + go db.cleanExpired() + + return db +} + +// New accepts a CAPTHA instance, generates an ID and store it in a database. +// A data string is an additional random data used to generate an ID, +// e.g. an IP-address. +func (imcdb *InMemoryCaptchaDB) New(data string, cptcha captcha.Captcha) (captcha.Captcha, captcha.ID) { + id := captcha.NewID(data, cptcha.Answer()) + + imcdb.Lock() + imcdb.db[id] = cptcha + imcdb.Unlock() + + return cptcha, id +} + +// GetExpiry returns time for how long CAPTCHA will last. +func (imcdb *InMemoryCaptchaDB) GetExpiry() time.Duration { + return imcdb.expiry +} + +// SetExpiry changes an expiry duration to a new one. +func (imcdb *InMemoryCaptchaDB) SetExpiry(expiry time.Duration) { + if expiry < captcha.DefaultExpiredScanInterval { + imcdb.expiryScanInterval = expiry + } else { + imcdb.expiryScanInterval = captcha.DefaultExpiredScanInterval + } +} + +// Image returns a freshly generated image for a CAPTCHA. +func (imcdb *InMemoryCaptchaDB) Image(id captcha.ID, style string) (*image.Image, error) { + imcdb.Lock() + defer imcdb.Unlock() + if c, ok := imcdb.db[id]; ok { + return c.Image(style), nil + } + return nil, captcha.ErrorNotFound +} + +// Solve compares given answer with a stored one and if failed +// deletes a CAPTCHA from database. +func (imcdb *InMemoryCaptchaDB) Solve(id captcha.ID, answer captcha.Answer) (bool, error) { + imcdb.Lock() + defer imcdb.Unlock() + if c, ok := imcdb.db[id]; ok { + ok = c.Solve(answer) + if !ok { + delete(imcdb.db, id) + } + return ok, nil + } + return false, captcha.ErrorNotFound +} + +// IsSolved checks if CAPTCHA was solved and removes it +// from a database. +func (imcdb *InMemoryCaptchaDB) IsSolved(id captcha.ID) (bool, error) { + imcdb.Lock() + defer imcdb.Unlock() + if c, ok := imcdb.db[id]; ok { + delete(imcdb.db, id) + return c.IsSolved(), nil + } + return false, captcha.ErrorNotFound +} + +// Remove a CAPTCHA from a database. +func (imcdb *InMemoryCaptchaDB) Remove(id captcha.ID) error { + imcdb.Lock() + defer imcdb.Unlock() + if _, ok := imcdb.db[id]; ok { + delete(imcdb.db, id) + return nil + } + return captcha.ErrorNotFound +} + +// cleanExpired removes expired CAPTCHAs in a loop. +func (imcdb *InMemoryCaptchaDB) cleanExpired() { + for { + sleepFor := imcdb.expiryScanInterval - (time.Duration(time.Now().Second()) % imcdb.expiryScanInterval) + time.Sleep(sleepFor) + + imcdb.Lock() + for id, captcha := range imcdb.db { + if time.Since(captcha.Expiry()) >= imcdb.expiry { + delete(imcdb.db, id) + } + } + imcdb.Unlock() + } +} + +// An instance of InMemoryCaptchaDB + +var imcdb = NewInMemoryCaptchaDB(captcha.DefaultExpiredScanInterval) + +func GetExpiry() time.Duration { + return imcdb.GetExpiry() +} + +func SetExpiry(expiry time.Duration) { + imcdb.SetExpiry(expiry) +} + +func New(data string, captcha captcha.Captcha) (captcha.Captcha, captcha.ID) { + return imcdb.New(data, captcha) +} + +func Image(id captcha.ID, style string) (*image.Image, error) { + return imcdb.Image(id, style) +} + +func Solve(id captcha.ID, answer captcha.Answer) (bool, error) { + return imcdb.Solve(id, answer) +} + +func IsSolved(id captcha.ID) (bool, error) { + return imcdb.IsSolved(id) +} + +func Remove(id captcha.ID) error { + return imcdb.Remove(id) +}