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