captcha.go 5.4 KB

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