all repos — captcha @ 4b8382af1554c4a28cf9819998a1e99b48908281

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