all repos — captcha @ c833d164be8ae91753e83315c2d9f1a347cf9af8

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