captcha.go 5.2 KB

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