You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
6.4 KiB

  1. // Copyright 2013 Beego Authors
  2. // Copyright 2014 The Macaron Authors
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License"): you may
  5. // not use this file except in compliance with the License. You may obtain
  6. // a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. // License for the specific language governing permissions and limitations
  14. // under the License.
  15. // Package captcha a middleware that provides captcha service for Macaron.
  16. package captcha
  17. import (
  18. "fmt"
  19. "html/template"
  20. "image/color"
  21. "path"
  22. "strings"
  23. "gitea.com/macaron/cache"
  24. "gitea.com/macaron/macaron"
  25. "github.com/unknwon/com"
  26. )
  27. const _VERSION = "0.1.0"
  28. func Version() string {
  29. return _VERSION
  30. }
  31. var (
  32. defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  33. )
  34. // Captcha represents a captcha service.
  35. type Captcha struct {
  36. store cache.Cache
  37. SubURL string
  38. URLPrefix string
  39. FieldIdName string
  40. FieldCaptchaName string
  41. StdWidth int
  42. StdHeight int
  43. ChallengeNums int
  44. Expiration int64
  45. CachePrefix string
  46. ColorPalette color.Palette
  47. }
  48. // generate key string
  49. func (c *Captcha) key(id string) string {
  50. return c.CachePrefix + id
  51. }
  52. // generate rand chars with default chars
  53. func (c *Captcha) genRandChars() string {
  54. return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...))
  55. }
  56. // CreateHTML outputs HTML for display and fetch new captcha images.
  57. func (c *Captcha) CreateHTML() template.HTML {
  58. value, err := c.CreateCaptcha()
  59. if err != nil {
  60. panic(fmt.Errorf("fail to create captcha: %v", err))
  61. }
  62. return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s">
  63. <a class="captcha" href="javascript:" tabindex="-1">
  64. <img onclick="this.src=('%[3]s%[4]s%[2]s.png?reload='+(new Date()).getTime())" class="captcha-img" src="%[3]s%[4]s%[2]s.png">
  65. </a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix))
  66. }
  67. // DEPRECATED
  68. func (c *Captcha) CreateHtml() template.HTML {
  69. return c.CreateHTML()
  70. }
  71. // create a new captcha id
  72. func (c *Captcha) CreateCaptcha() (string, error) {
  73. id := string(com.RandomCreateBytes(15))
  74. if err := c.store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil {
  75. return "", err
  76. }
  77. return id, nil
  78. }
  79. // verify from a request
  80. func (c *Captcha) VerifyReq(req macaron.Request) bool {
  81. req.ParseForm()
  82. return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName))
  83. }
  84. // direct verify id and challenge string
  85. func (c *Captcha) Verify(id string, challenge string) bool {
  86. if len(challenge) == 0 || len(id) == 0 {
  87. return false
  88. }
  89. var chars string
  90. key := c.key(id)
  91. if v, ok := c.store.Get(key).(string); ok {
  92. chars = v
  93. } else {
  94. return false
  95. }
  96. defer c.store.Delete(key)
  97. if len(chars) != len(challenge) {
  98. return false
  99. }
  100. // verify challenge
  101. for i, c := range []byte(chars) {
  102. if c != challenge[i]-48 {
  103. return false
  104. }
  105. }
  106. return true
  107. }
  108. type Options struct {
  109. // Suburl path. Default is empty.
  110. SubURL string
  111. // URL prefix of getting captcha pictures. Default is "/captcha/".
  112. URLPrefix string
  113. // Hidden input element ID. Default is "captcha_id".
  114. FieldIdName string
  115. // User input value element name in request form. Default is "captcha".
  116. FieldCaptchaName string
  117. // Challenge number. Default is 6.
  118. ChallengeNums int
  119. // Captcha image width. Default is 240.
  120. Width int
  121. // Captcha image height. Default is 80.
  122. Height int
  123. // Captcha expiration time in seconds. Default is 600.
  124. Expiration int64
  125. // Cache key prefix captcha characters. Default is "captcha_".
  126. CachePrefix string
  127. // ColorPalette holds a collection of primary colors used for
  128. // the captcha's text. If not defined, a random color will be generated.
  129. ColorPalette color.Palette
  130. }
  131. func prepareOptions(options []Options) Options {
  132. var opt Options
  133. if len(options) > 0 {
  134. opt = options[0]
  135. }
  136. opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
  137. // Defaults.
  138. if len(opt.URLPrefix) == 0 {
  139. opt.URLPrefix = "/captcha/"
  140. } else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' {
  141. opt.URLPrefix += "/"
  142. }
  143. if len(opt.FieldIdName) == 0 {
  144. opt.FieldIdName = "captcha_id"
  145. }
  146. if len(opt.FieldCaptchaName) == 0 {
  147. opt.FieldCaptchaName = "captcha"
  148. }
  149. if opt.ChallengeNums == 0 {
  150. opt.ChallengeNums = 6
  151. }
  152. if opt.Width == 0 {
  153. opt.Width = stdWidth
  154. }
  155. if opt.Height == 0 {
  156. opt.Height = stdHeight
  157. }
  158. if opt.Expiration == 0 {
  159. opt.Expiration = 600
  160. }
  161. if len(opt.CachePrefix) == 0 {
  162. opt.CachePrefix = "captcha_"
  163. }
  164. return opt
  165. }
  166. // NewCaptcha initializes and returns a captcha with given options.
  167. func NewCaptcha(opt Options) *Captcha {
  168. return &Captcha{
  169. SubURL: opt.SubURL,
  170. URLPrefix: opt.URLPrefix,
  171. FieldIdName: opt.FieldIdName,
  172. FieldCaptchaName: opt.FieldCaptchaName,
  173. StdWidth: opt.Width,
  174. StdHeight: opt.Height,
  175. ChallengeNums: opt.ChallengeNums,
  176. Expiration: opt.Expiration,
  177. CachePrefix: opt.CachePrefix,
  178. ColorPalette: opt.ColorPalette,
  179. }
  180. }
  181. // Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain.
  182. // An single variadic captcha.Options struct can be optionally provided to configure.
  183. // This should be register after cache.Cacher.
  184. func Captchaer(options ...Options) macaron.Handler {
  185. return func(ctx *macaron.Context, cache cache.Cache) {
  186. cpt := NewCaptcha(prepareOptions(options))
  187. cpt.store = cache
  188. if strings.HasPrefix(ctx.Req.URL.Path, cpt.URLPrefix) {
  189. var chars string
  190. id := path.Base(ctx.Req.URL.Path)
  191. if i := strings.Index(id, "."); i > -1 {
  192. id = id[:i]
  193. }
  194. key := cpt.key(id)
  195. // Reload captcha.
  196. if len(ctx.Query("reload")) > 0 {
  197. chars = cpt.genRandChars()
  198. if err := cpt.store.Put(key, chars, cpt.Expiration); err != nil {
  199. ctx.Status(500)
  200. ctx.Write([]byte("captcha reload error"))
  201. panic(fmt.Errorf("reload captcha: %v", err))
  202. }
  203. } else {
  204. if v, ok := cpt.store.Get(key).(string); ok {
  205. chars = v
  206. } else {
  207. ctx.Status(404)
  208. ctx.Write([]byte("captcha not found"))
  209. return
  210. }
  211. }
  212. if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight, cpt.ColorPalette).WriteTo(ctx.Resp); err != nil {
  213. panic(fmt.Errorf("write captcha: %v", err))
  214. }
  215. ctx.Status(200)
  216. return
  217. }
  218. ctx.Data["Captcha"] = cpt
  219. ctx.Map(cpt)
  220. }
  221. }