all repos — captcha @ 316aacb9c04c6035277402f39fa8e40688258a7d

Go package captcha implements generation and verification of image and audio CAPTCHAs.

captcha.go (view raw)

  1// Package captcha implements generation and verification of image and audio
  2// CAPTCHAs.
  3//
  4// A captcha solution is the sequence of digits 0-9 with the defined length.
  5// There are two captcha representations: image and audio.
  6//
  7// An image representation is a PNG-encoded image with the solution printed on
  8// it in such a way that makes it hard for computers to solve it using OCR.
  9//
 10// An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound
 11// with the spoken solution (currently in English). To make it hard for
 12// computers to solve audio captcha, the voice that pronounces numbers has
 13// random speed and pitch, and there is a randomly generated background noise
 14// mixed into the sound.
 15//
 16// This package doesn't require external files or libraries to generate captcha
 17// representations; it is self-contained.
 18//
 19// To make captchas one-time, the package includes a memory storage that stores
 20// captcha ids, their solutions, and expiration time. Used captchas are removed
 21// from the store immediately after calling Verify or VerifyString, while
 22// unused captchas (user loaded a page with captcha, but didn't submit the
 23// form) are collected automatically after the predefined expiration time.
 24// Developers can also provide custom store (for example, which saves captcha
 25// ids and solutions in database) by implementing Store interface and
 26// registering the object with SetCustomStore.
 27//
 28// Captchas are created by calling New, which returns the captcha id.  Their
 29// representations, though, are created on-the-fly by calling WriteImage or
 30// WriteAudio functions. Created representations are not stored anywhere, so
 31// subsequent calls to these functions with the same id will write the same
 32// captcha solution, but with a different random representation. Reload
 33// function will create a new different solution for the provided captcha,
 34// allowing users to "reload" captcha if they can't solve the displayed one
 35// without reloading the whole page.  Verify and VerifyString are used to
 36// verify that the given solution is the right one for the given captcha id.
 37//
 38// Server provides an http.Handler which can serve image and audio
 39// representations of captchas automatically from the URL. It can also be used
 40// to reload captchas.  Refer to Server function documentation for details, or
 41// take a look at the example in "example" subdirectory.
 42package captcha
 43
 44import (
 45	"bytes"
 46	"crypto/rand"
 47	"github.com/dchest/uniuri"
 48	"http"
 49	"io"
 50	"os"
 51	"path"
 52	"strconv"
 53)
 54
 55const (
 56	// Standard number of digits in captcha.
 57	StdLength = 6
 58	// The number of captchas created that triggers garbage collection used
 59	// by default store.
 60	CollectNum = 100
 61	// Expiration time of captchas used by default store.
 62	Expiration = 10 * 60 // 10 minutes
 63
 64)
 65
 66var ErrNotFound = os.NewError("captcha with the given id not found")
 67
 68// globalStore is a shared storage for captchas, generated by New function.
 69var globalStore = NewMemoryStore(CollectNum, Expiration)
 70
 71// SetCustomStore sets custom storage for captchas, replacing the default
 72// memory store. This function must be called before generating any captchas.
 73func SetCustomStore(s Store) {
 74	globalStore = s
 75}
 76
 77// RandomDigits returns a byte slice of the given length containing random
 78// digits in range 0-9.
 79func RandomDigits(length int) []byte {
 80	d := make([]byte, length)
 81	if _, err := io.ReadFull(rand.Reader, d); err != nil {
 82		panic("error reading random source: " + err.String())
 83	}
 84	for i := range d {
 85		d[i] %= 10
 86	}
 87	return d
 88}
 89
 90// New creates a new captcha of the given length, saves it in the internal
 91// storage, and returns its id.
 92func New(length int) (id string) {
 93	id = uniuri.New()
 94	globalStore.Set(id, RandomDigits(length))
 95	return
 96}
 97
 98// Reload generates and remembers new digits for the given captcha id.  This
 99// function returns false if there is no captcha with the given id.
100//
101// After calling this function, the image or audio presented to a user must be
102// refreshed to show the new captcha representation (WriteImage and WriteAudio
103// will write the new one).
104func Reload(id string) bool {
105	old := globalStore.Get(id, false)
106	if old == nil {
107		return false
108	}
109	globalStore.Set(id, RandomDigits(len(old)))
110	return true
111}
112
113// WriteImage writes PNG-encoded image representation of the captcha with the
114// given id. The image will have the given width and height.
115func WriteImage(w io.Writer, id string, width, height int) os.Error {
116	d := globalStore.Get(id, false)
117	if d == nil {
118		return ErrNotFound
119	}
120	_, err := NewImage(d, width, height).WriteTo(w)
121	return err
122}
123
124// WriteAudio writes WAV-encoded audio representation of the captcha with the
125// given id.
126func WriteAudio(w io.Writer, id string) os.Error {
127	d := globalStore.Get(id, false)
128	if d == nil {
129		return ErrNotFound
130	}
131	_, err := NewAudio(d).WriteTo(w)
132	return err
133}
134
135// Verify returns true if the given digits are the ones that were used to
136// create the given captcha id.
137// 
138// The function deletes the captcha with the given id from the internal
139// storage, so that the same captcha can't be verified anymore.
140func Verify(id string, digits []byte) bool {
141	if digits == nil || len(digits) == 0 {
142		return false
143	}
144	reald := globalStore.Get(id, true)
145	if reald == nil {
146		return false
147	}
148	return bytes.Equal(digits, reald)
149}
150
151// VerifyString is like Verify, but accepts a string of digits.  It removes
152// spaces and commas from the string, but any other characters, apart from
153// digits and listed above, will cause the function to return false.
154func VerifyString(id string, digits string) bool {
155	if digits == "" {
156		return false
157	}
158	ns := make([]byte, len(digits))
159	for i := range ns {
160		d := digits[i]
161		switch {
162		case '0' <= d && d <= '9':
163			ns[i] = d - '0'
164		case d == ' ' || d == ',':
165			// ignore
166		default:
167			return false
168		}
169	}
170	return Verify(id, ns)
171}
172
173// Collect deletes expired or used captchas from the internal storage. It is
174// called automatically by New function every CollectNum generated captchas,
175// but still exported to enable freeing memory manually if needed.
176//
177// Collection is launched in a new goroutine.
178func Collect() {
179	go globalStore.Collect()
180}
181
182type captchaHandler struct {
183	imgWidth  int
184	imgHeight int
185}
186
187// Server returns a handler that serves HTTP requests with image or
188// audio representations of captchas. Image dimensions are accepted as
189// arguments. The server decides which captcha to serve based on the last URL
190// path component: file name part must contain a captcha id, file extension —
191// its format (PNG or WAV).
192//
193// For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha
194// with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the
195// same captcha in audio format.
196//
197// To serve an audio captcha as downloadable file, append "?get" to URL.
198//
199// To reload captcha (get a different solution for the same captcha id), append
200// "?reload=x" to URL, where x may be anything (for example, current time or a
201// random number to make browsers refetch an image instead of loading it from
202// cache).
203func Server(w, h int) http.Handler { return &captchaHandler{w, h} }
204
205func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
206	_, file := path.Split(r.URL.Path)
207	ext := path.Ext(file)
208	id := file[:len(file)-len(ext)]
209	if ext == "" || id == "" {
210		http.NotFound(w, r)
211		return
212	}
213	if r.FormValue("reload") != "" {
214		Reload(id)
215	}
216	var err os.Error
217	switch ext {
218	case ".png":
219		w.Header().Set("Content-Type", "image/png")
220		err = WriteImage(w, id, h.imgWidth, h.imgHeight)
221	case ".wav":
222		if r.URL.RawQuery == "get" {
223			w.Header().Set("Content-Type", "application/octet-stream")
224		} else {
225			w.Header().Set("Content-Type", "audio/x-wav")
226		}
227		//err = WriteAudio(w, id)
228		//XXX(dchest) Workaround for Chrome: it wants content-length,
229		//or else will start playing NOT from the beginning.
230		//Filed issue: http://code.google.com/p/chromium/issues/detail?id=80565
231		d := globalStore.Get(id, false)
232		if d == nil {
233			err = ErrNotFound
234		} else {
235			a := NewAudio(d)
236			w.Header().Set("Content-Length", strconv.Itoa(a.EncodedLen()))
237			_, err = a.WriteTo(w)
238		}
239	default:
240		err = ErrNotFound
241	}
242	if err != nil {
243		if err == ErrNotFound {
244			http.NotFound(w, r)
245			return
246		}
247		http.Error(w, "error serving captcha", http.StatusInternalServerError)
248	}
249}