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