captcha.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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, replacing the default
  27. // memory store. This function must be called before generating any captchas.
  28. func SetCustomStore(s Store) {
  29. globalStore = s
  30. }
  31. // RandomDigits returns a byte slice of the given length containing random
  32. // digits in range 0-9.
  33. func RandomDigits(length int) []byte {
  34. d := make([]byte, length)
  35. if _, err := io.ReadFull(rand.Reader, d); err != nil {
  36. panic("error reading random source: " + err.String())
  37. }
  38. for i := range d {
  39. d[i] %= 10
  40. }
  41. return d
  42. }
  43. // New creates a new captcha of the given length, saves it in the internal
  44. // storage, and returns its id.
  45. func New(length int) (id string) {
  46. id = uniuri.New()
  47. globalStore.Set(id, RandomDigits(length))
  48. return
  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).
  56. func Reload(id string) bool {
  57. old := globalStore.Get(id, false)
  58. if old == nil {
  59. return false
  60. }
  61. globalStore.Set(id, RandomDigits(len(old)))
  62. return true
  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.
  66. func WriteImage(w io.Writer, id string, width, height int) os.Error {
  67. d := globalStore.Get(id, false)
  68. if d == nil {
  69. return ErrNotFound
  70. }
  71. _, err := NewImage(d, width, height).WriteTo(w)
  72. return err
  73. }
  74. // WriteAudio writes WAV-encoded audio representation of the captcha with the
  75. // given id.
  76. func WriteAudio(w io.Writer, id string) os.Error {
  77. d := globalStore.Get(id, false)
  78. if d == nil {
  79. return ErrNotFound
  80. }
  81. _, err := NewAudio(d).WriteTo(w)
  82. return err
  83. }
  84. // Verify returns true if the given digits are the ones that were used to
  85. // create the given captcha id.
  86. //
  87. // The function deletes the captcha with the given id from the internal
  88. // storage, so that the same captcha can't be verified anymore.
  89. func Verify(id string, digits []byte) bool {
  90. if digits == nil || len(digits) == 0 {
  91. return false
  92. }
  93. reald := globalStore.Get(id, true)
  94. if reald == nil {
  95. return false
  96. }
  97. return bytes.Equal(digits, reald)
  98. }
  99. // VerifyString is like Verify, but accepts a string of digits. It removes
  100. // spaces and commas from the string, but any other characters, apart from
  101. // digits and listed above, will cause the function to return false.
  102. func VerifyString(id string, digits string) bool {
  103. if digits == "" {
  104. return false
  105. }
  106. ns := make([]byte, len(digits))
  107. for i := range ns {
  108. d := digits[i]
  109. switch {
  110. case '0' <= d && d <= '9':
  111. ns[i] = d - '0'
  112. case d == ' ' || d == ',':
  113. // ignore
  114. default:
  115. return false
  116. }
  117. }
  118. return Verify(id, ns)
  119. }
  120. // Collect deletes expired or used captchas from the internal storage. It is
  121. // called automatically by New function every CollectNum generated captchas,
  122. // but still exported to enable freeing memory manually if needed.
  123. //
  124. // Collection is launched in a new goroutine.
  125. func Collect() {
  126. go globalStore.Collect()
  127. }
  128. type captchaHandler struct {
  129. imgWidth int
  130. imgHeight int
  131. }
  132. // Server returns a handler that serves HTTP requests with image or
  133. // audio representations of captchas. Image dimensions are accepted as
  134. // arguments. The server decides which captcha to serve based on the last URL
  135. // path component: file name part must contain a captcha id, file extension —
  136. // its format (PNG or WAV).
  137. //
  138. // For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha
  139. // with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the
  140. // same captcha in audio format.
  141. //
  142. // To serve an audio captcha as downloadable file, append "?get" to URL.
  143. //
  144. // To reload captcha (get a different solution for the same captcha id), append
  145. // "?reload=x" to URL, where x may be anything (for example, current time or a
  146. // random number to make browsers refetch an image instead of loading it from
  147. // cache).
  148. func Server(w, h int) http.Handler { return &captchaHandler{w, h} }
  149. func (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. if r.FormValue("reload") != "" {
  159. Reload(id)
  160. }
  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.Get(id, false)
  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. }