| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- 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.
- StdCollectNum = 100
- // Expiration time of captchas.
- StdExpiration = 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 = newStore(StdCollectNum, StdExpiration)
- // 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.saveCaptcha(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.getDigits(id)
- if old == nil {
- return false
- }
- globalStore.saveCaptcha(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.getDigits(id)
- 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.getDigits(id)
- 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.getDigitsClear(id)
- 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.
- 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
- 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.getDigits(id)
- 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)
- }
- }
|