all repos — captcha @ dab967324bac3f9d55fb1664840f86df7643ec5a

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.
  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}