all repos — captcha @ 8a2b5ae5df96f92bcc8bbf57acc7381b5116edde

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

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}