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