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