all repos — captcha @ d010e3f940eb4120c858e62195435aa592c1ece5

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)
 12
 13const (
 14	// Standard number of digits in captcha.
 15	StdLength = 6
 16	// The number of captchas created that triggers garbage collection.
 17	StdCollectNum = 100
 18	// Expiration time of captchas.
 19	StdExpiration = 2 * 60 // 2 minutes
 20
 21)
 22
 23var ErrNotFound = os.NewError("captcha with the given id not found")
 24
 25// globalStore is a shared storage for captchas, generated by New function.
 26var globalStore = newStore(StdCollectNum, StdExpiration)
 27
 28// RandomDigits returns a byte slice of the given length containing random
 29// digits in range 0-9.
 30func RandomDigits(length int) []byte {
 31	d := make([]byte, length)
 32	if _, err := io.ReadFull(rand.Reader, d); err != nil {
 33		panic("error reading random source: " + err.String())
 34	}
 35	for i := range d {
 36		d[i] %= 10
 37	}
 38	return d
 39}
 40
 41// New creates a new captcha of the given length, saves it in the internal
 42// storage, and returns its id.
 43func New(length int) (id string) {
 44	id = uniuri.New()
 45	globalStore.saveCaptcha(id, RandomDigits(length))
 46	return
 47}
 48
 49// Reload generates and remembers new digits for the given captcha id.  This
 50// function returns false if there is no captcha with the given id.
 51//
 52// After calling this function, the image or audio presented to a user must be
 53// refreshed to show the new captcha representation (WriteImage and WriteAudio
 54// will write the new one).
 55func Reload(id string) bool {
 56	old := globalStore.getDigits(id)
 57	if old == nil {
 58		return false
 59	}
 60	globalStore.saveCaptcha(id, RandomDigits(len(old)))
 61	return true
 62}
 63
 64// WriteImage writes PNG-encoded image representation of the captcha with the
 65// given id. The image will have the given width and height.
 66func WriteImage(w io.Writer, id string, width, height int) os.Error {
 67	d := globalStore.getDigits(id)
 68	if d == nil {
 69		return ErrNotFound
 70	}
 71	_, err := NewImage(d, width, height).WriteTo(w)
 72	return err
 73}
 74
 75// WriteAudio writes WAV-encoded audio representation of the captcha with the
 76// given id.
 77func WriteAudio(w io.Writer, id string) os.Error {
 78	d := globalStore.getDigits(id)
 79	if d == nil {
 80		return ErrNotFound
 81	}
 82	_, err := NewAudio(d).WriteTo(w)
 83	return err
 84}
 85
 86// Verify returns true if the given digits are the ones that were used to
 87// create the given captcha id.
 88// 
 89// The function deletes the captcha with the given id from the internal
 90// storage, so that the same captcha can't be verified anymore.
 91func Verify(id string, digits []byte) bool {
 92	if digits == nil || len(digits) == 0 {
 93		return false
 94	}
 95	reald := globalStore.getDigitsClear(id)
 96	if reald == nil {
 97		return false
 98	}
 99	return bytes.Equal(digits, reald)
100}
101
102// VerifyString is like Verify, but accepts a string of digits.  It removes
103// spaces and commas from the string, but any other characters, apart from
104// digits and listed above, will cause the function to return false.
105func VerifyString(id string, digits string) bool {
106	if digits == "" {
107		return false
108	}
109	ns := make([]byte, len(digits))
110	for i := range ns {
111		d := digits[i]
112		switch {
113		case '0' <= d && d <= '9':
114			ns[i] = d - '0'
115		case d == ' ' || d == ',':
116			// ignore
117		default:
118			return false
119		}
120	}
121	return Verify(id, ns)
122}
123
124// Collect deletes expired or used captchas from the internal storage. It is
125// called automatically by New function every CollectNum generated captchas,
126// but still exported to enable freeing memory manually if needed.
127//
128// Collection is launched in a new goroutine.
129func Collect() {
130	go globalStore.collect()
131}
132
133type captchaHandler struct {
134	imgWidth  int
135	imgHeight int
136}
137
138// CaptchaServer returns a handler that serves HTTP requests with image or
139// audio representations of captchas. Image dimensions are accepted as
140// arguments. The server decides which captcha to serve based on the last URL
141// path component: file name part must contain a captcha id, file extension —
142// its format (PNG or WAV).
143//
144// For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha
145// with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the
146// same captcha in audio format.
147func Server(w, h int) http.Handler { return &captchaHandler{w, h} }
148
149func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
150	_, file := path.Split(r.URL.Path)
151	ext := path.Ext(file)
152	id := file[:len(file)-len(ext)]
153	if ext == "" || id == "" {
154		http.NotFound(w, r)
155		return
156	}
157	var err os.Error
158	switch ext {
159	case ".png", ".PNG":
160		w.Header().Set("Content-Type", "image/png")
161		err = WriteImage(w, id, h.imgWidth, h.imgHeight)
162	case ".wav", ".WAV":
163		w.Header().Set("Content-Type", "audio/x-wav")
164		err = WriteAudio(w, id)
165	default:
166		err = ErrNotFound
167	}
168	if err != nil {
169		if err == ErrNotFound {
170			http.NotFound(w, r)
171			return
172		}
173		http.Error(w, "error serving captcha", http.StatusInternalServerError)
174	}
175}