captcha.go (view raw)
1package captcha
2
3import (
4 "bytes"
5 "os"
6 "rand"
7 "time"
8 crand "crypto/rand"
9 "github.com/dchest/uniuri"
10 "io"
11 "container/list"
12 "sync"
13)
14
15const (
16 // Expiration time for captchas
17 Expiration = 2 * 60 // 2 minutes
18 // The number of captchas created that triggers garbage collection
19 CollectNum = 100
20)
21
22// expValue stores timestamp and id of captchas. It is used in a list inside
23// storage for indexing generated captchas by timestamp to enable garbage
24// collection of expired captchas.
25type expValue struct {
26 timestamp int64
27 id string
28}
29
30// storage is an internal storage for captcha ids and their values.
31type storage struct {
32 mu sync.RWMutex
33 ids map[string][]byte
34 exp *list.List
35 // Number of items stored after last collection
36 colNum int
37}
38
39func newStore() *storage {
40 s := new(storage)
41 s.ids = make(map[string][]byte)
42 s.exp = list.New()
43 return s
44}
45
46var store = newStore()
47
48func init() {
49 rand.Seed(time.Seconds())
50}
51
52func randomNumbers(length int) []byte {
53 n := make([]byte, length)
54 if _, err := io.ReadFull(crand.Reader, n); err != nil {
55 panic(err)
56 }
57 for i := range n {
58 n[i] %= 10
59 }
60 return n
61}
62
63// New creates a new captcha of the given length, saves it in the internal
64// storage, and returns its id.
65func New(length int) string {
66 ns := randomNumbers(length)
67 id := uniuri.New()
68 store.mu.Lock()
69 defer store.mu.Unlock()
70 store.ids[id] = ns
71 store.exp.PushBack(expValue{time.Seconds(), id})
72 store.colNum++
73 if store.colNum > CollectNum {
74 go Collect()
75 store.colNum = 0
76 }
77 return id
78}
79
80// WriteImage writes PNG-encoded captcha image of the given width and height
81// with the given captcha id into the io.Writer.
82func WriteImage(w io.Writer, id string, width, height int) os.Error {
83 store.mu.RLock()
84 defer store.mu.RUnlock()
85 ns, ok := store.ids[id]
86 if !ok {
87 return os.NewError("captcha id not found")
88 }
89 return NewImage(ns, width, height).PNGEncode(w)
90}
91
92// Verify returns true if the given numbers are the numbers that were used to
93// create the given captcha id.
94//
95// The function deletes the captcha with the given id from the internal
96// storage, so that the same captcha can't be used anymore.
97func Verify(id string, numbers []byte) bool {
98 store.mu.Lock()
99 defer store.mu.Unlock()
100 realns, ok := store.ids[id]
101 if !ok {
102 return false
103 }
104 store.ids[id] = nil, false
105 return bytes.Equal(numbers, realns)
106}
107
108// Collect deletes expired and used captchas from the internal
109// storage. It is called automatically by New function every CollectNum
110// generated captchas, but still exported to enable freeing memory manually if
111// needed.
112func Collect() {
113 now := time.Seconds()
114 store.mu.Lock()
115 defer store.mu.Unlock()
116 for e := store.exp.Front(); e != nil; e = e.Next() {
117 ev, ok := e.Value.(expValue)
118 if !ok {
119 return
120 }
121 if ev.timestamp+Expiration < now {
122 store.ids[ev.id] = nil, false
123 store.exp.Remove(e)
124 } else {
125 return
126 }
127 }
128}