// Package captcha implements generation and verification of image and audio // CAPTCHAs. package captcha import ( "bytes" "crypto/rand" "github.com/dchest/uniuri" "http" "io" "os" "path" "strconv" ) const ( // Standard number of digits in captcha. StdLength = 6 // The number of captchas created that triggers garbage collection used // by default store. CollectNum = 100 // Expiration time of captchas used by default store. Expiration = 10 * 60 // 10 minutes ) var ErrNotFound = os.NewError("captcha with the given id not found") // globalStore is a shared storage for captchas, generated by New function. var globalStore = NewMemoryStore(CollectNum, Expiration) // SetCustomStore sets custom storage for captchas, replacing the default // memory store. This function must be called before generating any captchas. func SetCustomStore(s Store) { globalStore = s } // RandomDigits returns a byte slice of the given length containing random // digits in range 0-9. func RandomDigits(length int) []byte { d := make([]byte, length) if _, err := io.ReadFull(rand.Reader, d); err != nil { panic("error reading random source: " + err.String()) } for i := range d { d[i] %= 10 } return d } // New creates a new captcha of the given length, saves it in the internal // storage, and returns its id. func New(length int) (id string) { id = uniuri.New() globalStore.Set(id, RandomDigits(length)) return } // Reload generates and remembers new digits for the given captcha id. This // function returns false if there is no captcha with the given id. // // After calling this function, the image or audio presented to a user must be // refreshed to show the new captcha representation (WriteImage and WriteAudio // will write the new one). func Reload(id string) bool { old := globalStore.Get(id, false) if old == nil { return false } globalStore.Set(id, RandomDigits(len(old))) return true } // WriteImage writes PNG-encoded image representation of the captcha with the // given id. The image will have the given width and height. func WriteImage(w io.Writer, id string, width, height int) os.Error { d := globalStore.Get(id, false) if d == nil { return ErrNotFound } _, err := NewImage(d, width, height).WriteTo(w) return err } // WriteAudio writes WAV-encoded audio representation of the captcha with the // given id. func WriteAudio(w io.Writer, id string) os.Error { d := globalStore.Get(id, false) if d == nil { return ErrNotFound } _, err := NewAudio(d).WriteTo(w) return err } // Verify returns true if the given digits are the ones that were used to // create the given captcha id. // // The function deletes the captcha with the given id from the internal // storage, so that the same captcha can't be verified anymore. func Verify(id string, digits []byte) bool { if digits == nil || len(digits) == 0 { return false } reald := globalStore.Get(id, true) if reald == nil { return false } return bytes.Equal(digits, reald) } // VerifyString is like Verify, but accepts a string of digits. It removes // spaces and commas from the string, but any other characters, apart from // digits and listed above, will cause the function to return false. func VerifyString(id string, digits string) bool { if digits == "" { return false } ns := make([]byte, len(digits)) for i := range ns { d := digits[i] switch { case '0' <= d && d <= '9': ns[i] = d - '0' case d == ' ' || d == ',': // ignore default: return false } } return Verify(id, ns) } // Collect deletes expired or used captchas from the internal storage. It is // called automatically by New function every CollectNum generated captchas, // but still exported to enable freeing memory manually if needed. // // Collection is launched in a new goroutine. func Collect() { go globalStore.Collect() } type captchaHandler struct { imgWidth int imgHeight int } // Server returns a handler that serves HTTP requests with image or // audio representations of captchas. Image dimensions are accepted as // arguments. The server decides which captcha to serve based on the last URL // path component: file name part must contain a captcha id, file extension — // its format (PNG or WAV). // // For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha // with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the // same captcha in audio format. // // To serve an audio captcha as downloadable file, append "?get" to URL. // // To reload captcha (get a different solution for the same captcha id), append // "?reload=x" to URL, where x may be anything (for example, current time or a // random number to make browsers refetch an image instead of loading it from // cache). func Server(w, h int) http.Handler { return &captchaHandler{w, h} } func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { _, file := path.Split(r.URL.Path) ext := path.Ext(file) id := file[:len(file)-len(ext)] if ext == "" || id == "" { http.NotFound(w, r) return } var err os.Error if r.FormValue("reload") != "" { Reload(id) } switch ext { case ".png", ".PNG": w.Header().Set("Content-Type", "image/png") err = WriteImage(w, id, h.imgWidth, h.imgHeight) case ".wav", ".WAV": if r.URL.RawQuery == "get" { w.Header().Set("Content-Type", "application/octet-stream") } else { w.Header().Set("Content-Type", "audio/x-wav") } //err = WriteAudio(buf, id) //XXX(dchest) Workaround for Chrome: it wants content-length, //or else will start playing NOT from the beginning. d := globalStore.Get(id, false) if d == nil { err = ErrNotFound } else { a := NewAudio(d) w.Header().Set("Content-Length", strconv.Itoa(a.EncodedLen())) _, err = a.WriteTo(w) } default: err = ErrNotFound } if err != nil { if err == ErrNotFound { http.NotFound(w, r) return } http.Error(w, "error serving captcha", http.StatusInternalServerError) } }