all repos — captcha @ aa0588b4c9adf2cd98941eb25a8797644c22c863

Go package captcha implements generation and verification of image and audio CAPTCHAs.

Refactor store into its own file.
Dmitry Chestnykh dmitry@codingrobots.com
Thu, 21 Apr 2011 22:28:52 +0200
commit

aa0588b4c9adf2cd98941eb25a8797644c22c863

parent

0674e88f74f59174b585f767a122945803bf90f9

4 files changed, 126 insertions(+), 70 deletions(-)

jump to
M MakefileMakefile

@@ -3,6 +3,7 @@

TARG=github.com/dchest/captcha GOFILES=\ captcha.go\ + store.go\ font.go\ image.go
M captcha.gocaptcha.go

@@ -8,47 +8,19 @@ "time"

crand "crypto/rand" "github.com/dchest/uniuri" "io" - "container/list" - "sync" ) -const ( - // Expiration time for captchas - Expiration = 2 * 60 // 2 minutes - // The number of captchas created that triggers garbage collection - CollectNum = 100 -) +// Standard number of numbers in captcha +const StdLength = 6 -// expValue stores timestamp and id of captchas. It is used in a list inside -// storage for indexing generated captchas by timestamp to enable garbage -// collection of expired captchas. -type expValue struct { - timestamp int64 - id string -} - -// storage is an internal storage for captcha ids and their values. -type storage struct { - mu sync.RWMutex - ids map[string][]byte - exp *list.List - // Number of items stored after last collection - colNum int -} - -func newStore() *storage { - s := new(storage) - s.ids = make(map[string][]byte) - s.exp = list.New() - return s -} - -var store = newStore() +var globalStore = newStore() func init() { rand.Seed(time.Seconds()) } +// randomNumbers return a byte slice of the given length containing random +// numbers in range 0-9. func randomNumbers(length int) []byte { n := make([]byte, length) if _, err := io.ReadFull(crand.Reader, n); err != nil {

@@ -62,28 +34,30 @@ }

// New creates a new captcha of the given length, saves it in the internal // storage, and returns its id. -func New(length int) string { - ns := randomNumbers(length) - id := uniuri.New() - store.mu.Lock() - defer store.mu.Unlock() - store.ids[id] = ns - store.exp.PushBack(expValue{time.Seconds(), id}) - store.colNum++ - if store.colNum > CollectNum { - go Collect() - store.colNum = 0 +func New(length int) (id string) { + id = uniuri.New() + globalStore.saveCaptcha(id, randomNumbers(length)) + return +} + +// Reload generates new numbers for the given captcha id. This function does +// nothing if there is no captcha with the given id. +// +// After calling this function, the image presented to a user must be refreshed +// to show the new captcha (WriteImage will write the new one). +func Reload(id string) { + oldns := globalStore.getNumbers(id) + if oldns == nil { + return } - return id + globalStore.saveCaptcha(id, randomNumbers(len(oldns))) } // WriteImage writes PNG-encoded captcha image of the given width and height // with the given captcha id into the io.Writer. func WriteImage(w io.Writer, id string, width, height int) os.Error { - store.mu.RLock() - defer store.mu.RUnlock() - ns, ok := store.ids[id] - if !ok { + ns := globalStore.getNumbers(id) + if ns == nil { return os.NewError("captcha id not found") } return NewImage(ns, width, height).PNGEncode(w)

@@ -95,13 +69,10 @@ //

// The function deletes the captcha with the given id from the internal // storage, so that the same captcha can't be used anymore. func Verify(id string, numbers []byte) bool { - store.mu.Lock() - defer store.mu.Unlock() - realns, ok := store.ids[id] - if !ok { + realns := globalStore.getNumbersClear(id) + if realns == nil { return false } - store.ids[id] = nil, false return bytes.Equal(numbers, realns) }

@@ -109,20 +80,9 @@ // Collect deletes expired and used captchas from the internal

// storage. It is called automatically by New function every CollectNum // generated captchas, but still exported to enable freeing memory manually if // needed. +// +// Collection is launched in a new goroutine, so this function returns +// immediately. func Collect() { - now := time.Seconds() - store.mu.Lock() - defer store.mu.Unlock() - for e := store.exp.Front(); e != nil; e = e.Next() { - ev, ok := e.Value.(expValue) - if !ok { - return - } - if ev.timestamp+Expiration < now { - store.ids[ev.id] = nil, false - store.exp.Remove(e) - } else { - return - } - } + go globalStore.collect() }
M cmd/main.gocmd/main.go

@@ -6,6 +6,6 @@ "os"

) func main() { - img, _ := captcha.NewRandomImage(captcha.StdWidth, captcha.StdHeight) + img, _ := captcha.NewRandomImage(captcha.StdLength, captcha.StdWidth, captcha.StdHeight) img.PNGEncode(os.Stdout) }
A store.go

@@ -0,0 +1,95 @@

+package captcha + +import ( + "container/list" + "sync" + "time" +) + +const ( + // Expiration time for captchas + Expiration = 2 * 60 // 2 minutes + // The number of captchas created that triggers garbage collection + CollectNum = 100 +) + +// expValue stores timestamp and id of captchas. It is used in a list inside +// store for indexing generated captchas by timestamp to enable garbage +// collection of expired captchas. +type expValue struct { + timestamp int64 + id string +} + +// store is an internal store for captcha ids and their values. +type store struct { + mu sync.RWMutex + ids map[string][]byte + exp *list.List + // Number of items stored after last collection + colNum int +} + +// newStore initializes and returns a new store. +func newStore() *store { + s := new(store) + s.ids = make(map[string][]byte) + s.exp = list.New() + return s +} + +// saveCaptcha saves the captcha id and the corresponding numbers. +func (s *store) saveCaptcha(id string, ns []byte) { + s.mu.Lock() + defer s.mu.Unlock() + s.ids[id] = ns + s.exp.PushBack(expValue{time.Seconds(), id}) + s.colNum++ + if s.colNum > CollectNum { + go s.collect() + s.colNum = 0 + } +} + +// getNumbers returns the numbers for the given id. +func (s *store) getNumbers(id string) (ns []byte) { + s.mu.RLock() + defer s.mu.RUnlock() + ns, _ = s.ids[id] + return +} + +// getNumbersClear returns the numbers for the given id, and removes them from +// the store. +func (s *store) getNumbersClear(id string) (ns []byte) { + s.mu.Lock() + defer s.mu.Unlock() + ns, ok := s.ids[id] + if !ok { + return + } + s.ids[id] = nil, false + // XXX(dchest) Index (s.exp) will be cleaned when collecting expired + // captchas. Can't clean it here, because we don't store reference to + // expValue in the map. Maybe store it? + return +} + +// collect deletes expired captchas from the store. +func (s *store) collect() { + now := time.Seconds() + s.mu.Lock() + defer s.mu.Unlock() + for e := s.exp.Front(); e != nil; e = e.Next() { + ev, ok := e.Value.(expValue) + if !ok { + return + } + if ev.timestamp+Expiration < now { + s.ids[ev.id] = nil, false + s.exp.Remove(e) + } else { + return + } + } +}