|
@@ -8,47 +8,19 @@ import (
|
|
|
crand "crypto/rand"
|
|
crand "crypto/rand"
|
|
|
"github.com/dchest/uniuri"
|
|
"github.com/dchest/uniuri"
|
|
|
"io"
|
|
"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
|
|
|
|
|
-)
|
|
|
|
|
-
|
|
|
|
|
-// 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
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// Standard number of numbers in captcha
|
|
|
|
|
+const StdLength = 6
|
|
|
|
|
|
|
|
-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() {
|
|
func init() {
|
|
|
rand.Seed(time.Seconds())
|
|
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 {
|
|
func randomNumbers(length int) []byte {
|
|
|
n := make([]byte, length)
|
|
n := make([]byte, length)
|
|
|
if _, err := io.ReadFull(crand.Reader, n); err != nil {
|
|
if _, err := io.ReadFull(crand.Reader, n); err != nil {
|
|
@@ -62,28 +34,30 @@ func randomNumbers(length int) []byte {
|
|
|
|
|
|
|
|
// New creates a new captcha of the given length, saves it in the internal
|
|
// New creates a new captcha of the given length, saves it in the internal
|
|
|
// storage, and returns its id.
|
|
// 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
|
|
// WriteImage writes PNG-encoded captcha image of the given width and height
|
|
|
// with the given captcha id into the io.Writer.
|
|
// with the given captcha id into the io.Writer.
|
|
|
func WriteImage(w io.Writer, id string, width, height int) os.Error {
|
|
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 os.NewError("captcha id not found")
|
|
|
}
|
|
}
|
|
|
return NewImage(ns, width, height).PNGEncode(w)
|
|
return NewImage(ns, width, height).PNGEncode(w)
|
|
@@ -95,13 +69,10 @@ func WriteImage(w io.Writer, id string, width, height int) os.Error {
|
|
|
// The function deletes the captcha with the given id from the internal
|
|
// The function deletes the captcha with the given id from the internal
|
|
|
// storage, so that the same captcha can't be used anymore.
|
|
// storage, so that the same captcha can't be used anymore.
|
|
|
func Verify(id string, numbers []byte) bool {
|
|
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
|
|
return false
|
|
|
}
|
|
}
|
|
|
- store.ids[id] = nil, false
|
|
|
|
|
return bytes.Equal(numbers, realns)
|
|
return bytes.Equal(numbers, realns)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -109,20 +80,9 @@ func Verify(id string, numbers []byte) bool {
|
|
|
// storage. It is called automatically by New function every CollectNum
|
|
// storage. It is called automatically by New function every CollectNum
|
|
|
// generated captchas, but still exported to enable freeing memory manually if
|
|
// generated captchas, but still exported to enable freeing memory manually if
|
|
|
// needed.
|
|
// needed.
|
|
|
|
|
+//
|
|
|
|
|
+// Collection is launched in a new goroutine, so this function returns
|
|
|
|
|
+// immediately.
|
|
|
func Collect() {
|
|
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()
|
|
|
}
|
|
}
|