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