captcha.go 4.1 KB

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