captcha.go (view raw)
1package captcha
2
3import (
4 "bytes"
5 "crypto/rand"
6 "github.com/dchest/uniuri"
7 "http"
8 "io"
9 "os"
10 "path"
11)
12
13const (
14 // Standard number of digits in captcha.
15 StdLength = 6
16 // The number of captchas created that triggers garbage collection.
17 StdCollectNum = 100
18 // Expiration time of captchas.
19 StdExpiration = 2 * 60 // 2 minutes
20
21)
22
23var ErrNotFound = os.NewError("captcha with the given id not found")
24
25// globalStore is a shared storage for captchas, generated by New function.
26var globalStore = newStore(StdCollectNum, StdExpiration)
27
28// RandomDigits returns a byte slice of the given length containing random
29// digits in range 0-9.
30func RandomDigits(length int) []byte {
31 d := make([]byte, length)
32 if _, err := io.ReadFull(rand.Reader, d); err != nil {
33 panic("error reading random source: " + err.String())
34 }
35 for i := range d {
36 d[i] %= 10
37 }
38 return d
39}
40
41// New creates a new captcha of the given length, saves it in the internal
42// storage, and returns its id.
43func New(length int) (id string) {
44 id = uniuri.New()
45 globalStore.saveCaptcha(id, RandomDigits(length))
46 return
47}
48
49// Reload generates and remembers new digits for the given captcha id. This
50// function returns false if there is no captcha with the given id.
51//
52// After calling this function, the image or audio presented to a user must be
53// refreshed to show the new captcha representation (WriteImage and WriteAudio
54// will write the new one).
55func Reload(id string) bool {
56 old := globalStore.getDigits(id)
57 if old == nil {
58 return false
59 }
60 globalStore.saveCaptcha(id, RandomDigits(len(old)))
61 return true
62}
63
64// WriteImage writes PNG-encoded image representation of the captcha with the
65// given id. The image will have the given width and height.
66func WriteImage(w io.Writer, id string, width, height int) os.Error {
67 d := globalStore.getDigits(id)
68 if d == nil {
69 return ErrNotFound
70 }
71 _, err := NewImage(d, width, height).WriteTo(w)
72 return err
73}
74
75// WriteAudio writes WAV-encoded audio representation of the captcha with the
76// given id.
77func WriteAudio(w io.Writer, id string) os.Error {
78 d := globalStore.getDigits(id)
79 if d == nil {
80 return ErrNotFound
81 }
82 _, err := NewAudio(d).WriteTo(w)
83 return err
84}
85
86// Verify returns true if the given digits are the ones that were used to
87// create the given captcha id.
88//
89// The function deletes the captcha with the given id from the internal
90// storage, so that the same captcha can't be verified anymore.
91func Verify(id string, digits []byte) bool {
92 if digits == nil || len(digits) == 0 {
93 return false
94 }
95 reald := globalStore.getDigitsClear(id)
96 if reald == nil {
97 return false
98 }
99 return bytes.Equal(digits, reald)
100}
101
102// VerifyString is like Verify, but accepts a string of digits. It removes
103// spaces and commas from the string, but any other characters, apart from
104// digits and listed above, will cause the function to return false.
105func VerifyString(id string, digits string) bool {
106 if digits == "" {
107 return false
108 }
109 ns := make([]byte, len(digits))
110 for i := range ns {
111 d := digits[i]
112 switch {
113 case '0' <= d && d <= '9':
114 ns[i] = d - '0'
115 case d == ' ' || d == ',':
116 // ignore
117 default:
118 return false
119 }
120 }
121 return Verify(id, ns)
122}
123
124// Collect deletes expired or used captchas from the internal storage. It is
125// called automatically by New function every CollectNum generated captchas,
126// but still exported to enable freeing memory manually if needed.
127//
128// Collection is launched in a new goroutine.
129func Collect() {
130 go globalStore.collect()
131}
132
133type captchaHandler struct {
134 imgWidth int
135 imgHeight int
136}
137
138// CaptchaServer returns a handler that serves HTTP requests with image or
139// audio representations of captchas. Image dimensions are accepted as
140// arguments. The server decides which captcha to serve based on the last URL
141// path component: file name part must contain a captcha id, file extension —
142// its format (PNG or WAV).
143//
144// For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha
145// with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the
146// same captcha in audio format.
147func Server(w, h int) http.Handler { return &captchaHandler{w, h} }
148
149func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
150 _, file := path.Split(r.URL.Path)
151 ext := path.Ext(file)
152 id := file[:len(file)-len(ext)]
153 if ext == "" || id == "" {
154 http.NotFound(w, r)
155 return
156 }
157 var err os.Error
158 switch ext {
159 case ".png", ".PNG":
160 w.Header().Set("Content-Type", "image/png")
161 err = WriteImage(w, id, h.imgWidth, h.imgHeight)
162 case ".wav", ".WAV":
163 w.Header().Set("Content-Type", "audio/x-wav")
164 err = WriteAudio(w, id)
165 default:
166 err = ErrNotFound
167 }
168 if err != nil {
169 if err == ErrNotFound {
170 http.NotFound(w, r)
171 return
172 }
173 http.Error(w, "error serving captcha", http.StatusInternalServerError)
174 }
175}