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