Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
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.
248 lines
6.4 KiB
248 lines
6.4 KiB
// Copyright 2013 Beego Authors |
|
// Copyright 2014 The Macaron Authors |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may |
|
// not use this file except in compliance with the License. You may obtain |
|
// a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
// License for the specific language governing permissions and limitations |
|
// under the License. |
|
|
|
// Package captcha a middleware that provides captcha service for Macaron. |
|
package captcha |
|
|
|
import ( |
|
"fmt" |
|
"html/template" |
|
"image/color" |
|
"net/http" |
|
"path" |
|
"strings" |
|
|
|
"gitea.com/go-chi/cache" |
|
"github.com/unknwon/com" |
|
) |
|
|
|
const _VERSION = "0.1.0" |
|
|
|
func Version() string { |
|
return _VERSION |
|
} |
|
|
|
var ( |
|
defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
|
) |
|
|
|
// Captcha represents a captcha service. |
|
type Captcha struct { |
|
Store cache.Cache |
|
SubURL string |
|
URLPrefix string |
|
FieldIdName string |
|
FieldCaptchaName string |
|
StdWidth int |
|
StdHeight int |
|
ChallengeNums int |
|
Expiration int64 |
|
CachePrefix string |
|
ColorPalette color.Palette |
|
} |
|
|
|
// generate key string |
|
func (c *Captcha) key(id string) string { |
|
return c.CachePrefix + id |
|
} |
|
|
|
// generate rand chars with default chars |
|
func (c *Captcha) genRandChars() string { |
|
return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...)) |
|
} |
|
|
|
// CreateHTML outputs HTML for display and fetch new captcha images. |
|
func (c *Captcha) CreateHTML() template.HTML { |
|
value, err := c.CreateCaptcha() |
|
if err != nil { |
|
panic(fmt.Errorf("fail to create captcha: %v", err)) |
|
} |
|
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s"> |
|
<a class="captcha" href="javascript:" tabindex="-1"> |
|
<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"> |
|
</a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix)) |
|
} |
|
|
|
// create a new captcha id |
|
func (c *Captcha) CreateCaptcha() (string, error) { |
|
id := string(com.RandomCreateBytes(15)) |
|
if err := c.Store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil { |
|
return "", err |
|
} |
|
return id, nil |
|
} |
|
|
|
// verify from a request |
|
func (c *Captcha) VerifyReq(req *http.Request) bool { |
|
req.ParseForm() |
|
return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName)) |
|
} |
|
|
|
// direct verify id and challenge string |
|
func (c *Captcha) Verify(id string, challenge string) bool { |
|
if len(challenge) == 0 || len(id) == 0 { |
|
return false |
|
} |
|
|
|
var chars string |
|
|
|
key := c.key(id) |
|
|
|
if v, ok := c.Store.Get(key).(string); ok { |
|
chars = v |
|
} else { |
|
return false |
|
} |
|
|
|
defer c.Store.Delete(key) |
|
|
|
if len(chars) != len(challenge) { |
|
return false |
|
} |
|
|
|
// verify challenge |
|
for i, c := range []byte(chars) { |
|
if c != challenge[i]-48 { |
|
return false |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
type Options struct { |
|
// Suburl path. Default is empty. |
|
SubURL string |
|
// URL prefix of getting captcha pictures. Default is "/captcha/". |
|
URLPrefix string |
|
// Hidden input element ID. Default is "captcha_id". |
|
FieldIdName string |
|
// User input value element name in request form. Default is "captcha". |
|
FieldCaptchaName string |
|
// Challenge number. Default is 6. |
|
ChallengeNums int |
|
// Captcha image width. Default is 240. |
|
Width int |
|
// Captcha image height. Default is 80. |
|
Height int |
|
// Captcha expiration time in seconds. Default is 600. |
|
Expiration int64 |
|
// Cache key prefix captcha characters. Default is "captcha_". |
|
CachePrefix string |
|
// ColorPalette holds a collection of primary colors used for |
|
// the captcha's text. If not defined, a random color will be generated. |
|
ColorPalette color.Palette |
|
} |
|
|
|
func prepareOptions(options []Options) Options { |
|
var opt Options |
|
if len(options) > 0 { |
|
opt = options[0] |
|
} |
|
|
|
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/") |
|
|
|
// Defaults. |
|
if len(opt.URLPrefix) == 0 { |
|
opt.URLPrefix = "/captcha/" |
|
} else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' { |
|
opt.URLPrefix += "/" |
|
} |
|
if len(opt.FieldIdName) == 0 { |
|
opt.FieldIdName = "captcha_id" |
|
} |
|
if len(opt.FieldCaptchaName) == 0 { |
|
opt.FieldCaptchaName = "captcha" |
|
} |
|
if opt.ChallengeNums == 0 { |
|
opt.ChallengeNums = 6 |
|
} |
|
if opt.Width == 0 { |
|
opt.Width = stdWidth |
|
} |
|
if opt.Height == 0 { |
|
opt.Height = stdHeight |
|
} |
|
if opt.Expiration == 0 { |
|
opt.Expiration = 600 |
|
} |
|
if len(opt.CachePrefix) == 0 { |
|
opt.CachePrefix = "captcha_" |
|
} |
|
|
|
return opt |
|
} |
|
|
|
// NewCaptcha initializes and returns a captcha with given options. |
|
func NewCaptcha(opts ...Options) *Captcha { |
|
opt := prepareOptions(opts) |
|
return &Captcha{ |
|
SubURL: opt.SubURL, |
|
URLPrefix: opt.URLPrefix, |
|
FieldIdName: opt.FieldIdName, |
|
FieldCaptchaName: opt.FieldCaptchaName, |
|
StdWidth: opt.Width, |
|
StdHeight: opt.Height, |
|
ChallengeNums: opt.ChallengeNums, |
|
Expiration: opt.Expiration, |
|
CachePrefix: opt.CachePrefix, |
|
ColorPalette: opt.ColorPalette, |
|
} |
|
} |
|
|
|
// Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain. |
|
// An single variadic captcha.Options struct can be optionally provided to configure. |
|
// This should be register after cache.Cacher. |
|
func Captchaer(cpt *Captcha) func(http.Handler) http.Handler { |
|
return func(next http.Handler) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
|
if strings.HasPrefix(req.URL.Path, cpt.URLPrefix) { |
|
var chars string |
|
id := path.Base(req.URL.Path) |
|
if i := strings.Index(id, "."); i > -1 { |
|
id = id[:i] |
|
} |
|
key := cpt.key(id) |
|
|
|
reloads := req.URL.Query()["reload"] |
|
// Reload captcha. |
|
if len(reloads) > 0 && len(reloads[0]) > 0 { |
|
chars = cpt.genRandChars() |
|
if err := cpt.Store.Put(key, chars, cpt.Expiration); err != nil { |
|
w.WriteHeader(500) |
|
w.Write([]byte("captcha reload error")) |
|
panic(fmt.Errorf("reload captcha: %v", err)) |
|
} |
|
} else { |
|
if v, ok := cpt.Store.Get(key).(string); ok { |
|
chars = v |
|
} else { |
|
w.WriteHeader(404) |
|
w.Write([]byte("captcha not found")) |
|
return |
|
} |
|
} |
|
|
|
w.WriteHeader(200) |
|
if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight, cpt.ColorPalette).WriteTo(w); err != nil { |
|
panic(fmt.Errorf("write captcha: %v", err)) |
|
} |
|
return |
|
} |
|
|
|
next.ServeHTTP(w, req) |
|
}) |
|
} |
|
}
|
|
|