captcha.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // Package captcha implements generation and verification of image and audio
  2. // CAPTCHAs.
  3. //
  4. // A captcha solution is the sequence of digits 0-9 with the defined length.
  5. // There are two captcha representations: image and audio.
  6. //
  7. // An image representation is a PNG-encoded image with the solution printed on
  8. // it in such a way that makes it hard for computers to solve it using OCR.
  9. //
  10. // An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound
  11. // with the spoken solution (currently in English). To make it hard for
  12. // computers to solve audio captcha, the voice that pronounces numbers has
  13. // random speed and pitch, and there is a randomly generated background noise
  14. // mixed into the sound.
  15. //
  16. // This package doesn't require external files or libraries to generate captcha
  17. // representations; it is self-contained.
  18. //
  19. // To make captchas one-time, the package includes a memory storage that stores
  20. // captcha ids, their solutions, and expiration time. Used captchas are removed
  21. // from the store immediately after calling Verify or VerifyString, while
  22. // unused captchas (user loaded a page with captcha, but didn't submit the
  23. // form) are collected automatically after the predefined expiration time.
  24. // Developers can also provide custom store (for example, which saves captcha
  25. // ids and solutions in database) by implementing Store interface and
  26. // registering the object with SetCustomStore.
  27. //
  28. // Captchas are created by calling New, which returns the captcha id. Their
  29. // representations, though, are created on-the-fly by calling WriteImage or
  30. // WriteAudio functions. Created representations are not stored anywhere, so
  31. // subsequent calls to these functions with the same id will write the same
  32. // captcha solution, but with a different random representation. Reload
  33. // function will create a new different solution for the provided captcha,
  34. // allowing users to "reload" captcha if they can't solve the displayed one
  35. // without reloading the whole page. Verify and VerifyString are used to
  36. // verify that the given solution is the right one for the given captcha id.
  37. //
  38. // Server provides an http.Handler which can serve image and audio
  39. // representations of captchas automatically from the URL. It can also be used
  40. // to reload captchas. Refer to Server function documentation for details, or
  41. // take a look at the example in "example" subdirectory.
  42. package captcha
  43. import (
  44. "bytes"
  45. "crypto/rand"
  46. "github.com/dchest/uniuri"
  47. "http"
  48. "io"
  49. "os"
  50. "path"
  51. "strconv"
  52. )
  53. const (
  54. // Standard number of digits in captcha.
  55. StdLength = 6
  56. // The number of captchas created that triggers garbage collection used
  57. // by default store.
  58. CollectNum = 100
  59. // Expiration time of captchas used by default store.
  60. Expiration = 10 * 60 // 10 minutes
  61. )
  62. var ErrNotFound = os.NewError("captcha with the given id not found")
  63. // globalStore is a shared storage for captchas, generated by New function.
  64. var globalStore = NewMemoryStore(CollectNum, Expiration)
  65. // SetCustomStore sets custom storage for captchas, replacing the default
  66. // memory store. This function must be called before generating any captchas.
  67. func SetCustomStore(s Store) {
  68. globalStore = s
  69. }
  70. // RandomDigits returns a byte slice of the given length containing random
  71. // digits in range 0-9.
  72. func RandomDigits(length int) []byte {
  73. d := make([]byte, length)
  74. if _, err := io.ReadFull(rand.Reader, d); err != nil {
  75. panic("error reading random source: " + err.String())
  76. }
  77. for i := range d {
  78. d[i] %= 10
  79. }
  80. return d
  81. }
  82. // New creates a new captcha of the given length, saves it in the internal
  83. // storage, and returns its id.
  84. func New(length int) (id string) {
  85. id = uniuri.New()
  86. globalStore.Set(id, RandomDigits(length))
  87. return
  88. }
  89. // Reload generates and remembers new digits for the given captcha id. This
  90. // function returns false if there is no captcha with the given id.
  91. //
  92. // After calling this function, the image or audio presented to a user must be
  93. // refreshed to show the new captcha representation (WriteImage and WriteAudio
  94. // will write the new one).
  95. func Reload(id string) bool {
  96. old := globalStore.Get(id, false)
  97. if old == nil {
  98. return false
  99. }
  100. globalStore.Set(id, RandomDigits(len(old)))
  101. return true
  102. }
  103. // WriteImage writes PNG-encoded image representation of the captcha with the
  104. // given id. The image will have the given width and height.
  105. func WriteImage(w io.Writer, id string, width, height int) os.Error {
  106. d := globalStore.Get(id, false)
  107. if d == nil {
  108. return ErrNotFound
  109. }
  110. _, err := NewImage(d, width, height).WriteTo(w)
  111. return err
  112. }
  113. // WriteAudio writes WAV-encoded audio representation of the captcha with the
  114. // given id.
  115. func WriteAudio(w io.Writer, id string) os.Error {
  116. d := globalStore.Get(id, false)
  117. if d == nil {
  118. return ErrNotFound
  119. }
  120. _, err := NewAudio(d).WriteTo(w)
  121. return err
  122. }
  123. // Verify returns true if the given digits are the ones that were used to
  124. // create the given captcha id.
  125. //
  126. // The function deletes the captcha with the given id from the internal
  127. // storage, so that the same captcha can't be verified anymore.
  128. func Verify(id string, digits []byte) bool {
  129. if digits == nil || len(digits) == 0 {
  130. return false
  131. }
  132. reald := globalStore.Get(id, true)
  133. if reald == nil {
  134. return false
  135. }
  136. return bytes.Equal(digits, reald)
  137. }
  138. // VerifyString is like Verify, but accepts a string of digits. It removes
  139. // spaces and commas from the string, but any other characters, apart from
  140. // digits and listed above, will cause the function to return false.
  141. func VerifyString(id string, digits string) bool {
  142. if digits == "" {
  143. return false
  144. }
  145. ns := make([]byte, len(digits))
  146. for i := range ns {
  147. d := digits[i]
  148. switch {
  149. case '0' <= d && d <= '9':
  150. ns[i] = d - '0'
  151. case d == ' ' || d == ',':
  152. // ignore
  153. default:
  154. return false
  155. }
  156. }
  157. return Verify(id, ns)
  158. }
  159. // Collect deletes expired or used captchas from the internal storage. It is
  160. // called automatically by New function every CollectNum generated captchas,
  161. // but still exported to enable freeing memory manually if needed.
  162. //
  163. // Collection is launched in a new goroutine.
  164. func Collect() {
  165. go globalStore.Collect()
  166. }
  167. type captchaHandler struct {
  168. imgWidth int
  169. imgHeight int
  170. }
  171. // Server returns a handler that serves HTTP requests with image or
  172. // audio representations of captchas. Image dimensions are accepted as
  173. // arguments. The server decides which captcha to serve based on the last URL
  174. // path component: file name part must contain a captcha id, file extension —
  175. // its format (PNG or WAV).
  176. //
  177. // For example, for file name "B9QTvDV1RXbVJ3Ac.png" it serves an image captcha
  178. // with id "B9QTvDV1RXbVJ3Ac", and for "B9QTvDV1RXbVJ3Ac.wav" it serves the
  179. // same captcha in audio format.
  180. //
  181. // To serve an audio captcha as downloadable file, append "?get" to URL.
  182. //
  183. // To reload captcha (get a different solution for the same captcha id), append
  184. // "?reload=x" to URL, where x may be anything (for example, current time or a
  185. // random number to make browsers refetch an image instead of loading it from
  186. // cache).
  187. func Server(w, h int) http.Handler { return &captchaHandler{w, h} }
  188. func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  189. _, file := path.Split(r.URL.Path)
  190. ext := path.Ext(file)
  191. id := file[:len(file)-len(ext)]
  192. if ext == "" || id == "" {
  193. http.NotFound(w, r)
  194. return
  195. }
  196. var err os.Error
  197. if r.FormValue("reload") != "" {
  198. Reload(id)
  199. }
  200. switch ext {
  201. case ".png", ".PNG":
  202. w.Header().Set("Content-Type", "image/png")
  203. err = WriteImage(w, id, h.imgWidth, h.imgHeight)
  204. case ".wav", ".WAV":
  205. if r.URL.RawQuery == "get" {
  206. w.Header().Set("Content-Type", "application/octet-stream")
  207. } else {
  208. w.Header().Set("Content-Type", "audio/x-wav")
  209. }
  210. //err = WriteAudio(w, id)
  211. //XXX(dchest) Workaround for Chrome: it wants content-length,
  212. //or else will start playing NOT from the beginning.
  213. //File issue: http://code.google.com/p/chromium/issues/detail?id=80565
  214. d := globalStore.Get(id, false)
  215. if d == nil {
  216. err = ErrNotFound
  217. } else {
  218. a := NewAudio(d)
  219. w.Header().Set("Content-Length", strconv.Itoa(a.EncodedLen()))
  220. _, err = a.WriteTo(w)
  221. }
  222. default:
  223. err = ErrNotFound
  224. }
  225. if err != nil {
  226. if err == ErrNotFound {
  227. http.NotFound(w, r)
  228. return
  229. }
  230. http.Error(w, "error serving captcha", http.StatusInternalServerError)
  231. }
  232. }