captcha.go 5.4 KB

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