Repository: hanguofeng/gocaptcha Branch: master Commit: d395745388d1 Files: 37 Total size: 47.9 KB Directory structure: gitextract_al2ckzk5/ ├── .github/ │ └── workflows/ │ └── go.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README-en.md ├── README.md ├── VERSION ├── captcha.go ├── captcha_test.go ├── captchainfo.go ├── cimage.go ├── confighelper.go ├── configtypes.go ├── cstore.go ├── cstore_test.go ├── data/ │ ├── cn_char │ ├── cn_phrases │ ├── en_char │ └── en_phrases ├── fontmanager.go ├── go.mod ├── go.sum ├── imagefilter.go ├── imagefiltermanager.go ├── imagefiltermanager_test.go ├── imagefilternoiseline.go ├── imagefilternoisepoint.go ├── imagefilterstrike.go ├── mcstore.go ├── mcstore_test.go ├── randhelper.go ├── redisstore.go ├── redisstore_test.go ├── storeinterface.go ├── wordmanager.go └── wordmanager_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Install memcache/redis tool run: | sudo apt-get update sudo apt-get install memcached redis-server memcached -d - name: Build run: go build -v ./... - name: Test run: go test -v ./... ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test _test_files # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe ================================================ FILE: .travis.yml ================================================ language: go go: - 1.3 - 1.4 - tip services: - memcached - redis-server ================================================ FILE: Dockerfile ================================================ FROM centos RUN yum -y update RUN yum install -y golang git mercurial memcached RUN mkdir -p /home/work/gopath ENV GOPATH /home/work/gopath; RUN go get github.com/hanguofeng/gocaptcha/samples/gocaptcha-server; WORKDIR $GOPATH/src/github.com/hanguofeng/gocaptcha/samples/gocaptcha-server RUN ["go","build"] EXPOSE 80 ENTRYPOINT ["./gocaptcha-server"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2013 hanguofeng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-en.md ================================================ *** View in [[English](README-en.md)][[中文](README.md)] *** # gocaptcha Captcha server writen in golang [![Build Status](https://travis-ci.org/hanguofeng/gocaptcha.png?branch=master)](https://travis-ci.org/hanguofeng/gocaptcha) [![Build Status](https://drone.io/github.com/hanguofeng/gocaptcha/status.png)](https://drone.io/github.com/hanguofeng/gocaptcha/latest) [![Coverage Status](https://coveralls.io/repos/hanguofeng/gocaptcha/badge.png)](https://coveralls.io/r/hanguofeng/gocaptcha) Feature ------- * supports captcha char in Chinese * supports self-define word/char dictionary * supports filter plugin * filters: * noise point * noise line * other type of noise * plugin * supports extensible store engine * build-in store engine * memcache * redis (from https://github.com/dtynn/gocaptcha) * implement your own by implement the StoreInterface Useage ------ **Install** go get github.com/hanguofeng/gocaptcha **Quick Start** See [captcha_test.go](captcha_test.go) See [samples/gocaptcha-server](samples/gocaptcha-server) [Demo](http://hanguofeng-gocaptcha.daoapp.io/) **Document** [[captcha.go Wiki](https://github.com/hanguofeng/gocaptcha/wiki)] TODO ---- * ops tools LICENCE ------- gocaptcha use [[MIT LICENSE](LICENSE)] Thanks: * https://github.com/dchest/captcha * https://github.com/golang/freetype * https://github.com/bradfitz/gomemcache * https://code.google.com/p/zpix/ ================================================ FILE: README.md ================================================ *** View in [[English](README-en.md)][[中文](README.md)] *** # gocaptcha go语言验证码服务器 Feature ------- * 支持中文验证码 * 支持自定义词库、字库 * 支持自定义滤镜机制,通过滤镜来增加干扰,加大识别难度 * 当前的滤镜包括: * 支持干扰点 * 支持干扰线 * 支持其他模式的干扰 * 更多模式,可实现imagefilter接口来扩展 * 支持自定义存储引擎,存储引擎可扩展 * 目前支持的存储引擎包括: * 内置(buildin) * memcache * redis (from https://github.com/dtynn/gocaptcha) * 如需扩展存储引擎,可实现StoreInterface接口 Useage ------ **安装** go get github.com/hanguofeng/gocaptcha **Quick Start** 参考 [captcha_test.go](captcha_test.go) 参考 [samples/gocaptcha-server](samples/gocaptcha-server) [Demo](http://hanguofeng-gocaptcha.daoapp.io/) **文档** [[captcha.go Wiki](https://github.com/hanguofeng/gocaptcha/wiki)] TODO ---- * 运维管理工具 LICENCE ------- gocaptcha使用[[MIT许可协议](LICENSE)] 使用的开源软件列表,表示感谢 * https://github.com/dchest/captcha * https://github.com/golang/freetype * https://github.com/bradfitz/gomemcache * https://code.google.com/p/zpix/ ================================================ FILE: VERSION ================================================ ================================================ FILE: captcha.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "errors" "image" "strings" "time" ) //Captcha is the core captcha struct type Captcha struct { store StoreInterface wordManager *WordManager filterManager *ImageFilterManager captchaConfig *CaptchaConfig imageConfig *ImageConfig filterConfig *FilterConfig storeConfig *StoreConfig } //CreateCaptcha is a method to create new Captcha struct func CreateCaptcha(wordManager *WordManager, captchaConfig *CaptchaConfig, imageConfig *ImageConfig, filterConfig *FilterConfig, storeConfig *StoreConfig) (*Captcha, error) { var retErr error captcha := new(Captcha) store, err := createStore(storeConfig) if nil == err { captcha.store = store } else { retErr = err } if nil != wordManager { captcha.wordManager = wordManager } else { retErr = errors.New("CreateCaptcha fail:invalid wordManager") } captcha.captchaConfig = captchaConfig captcha.imageConfig = imageConfig captcha.filterConfig = filterConfig captcha.filterManager = CreateImageFilterManagerByConfig(filterConfig) return captcha, retErr } //CreateCaptchaFromConfigFile is a method to create new Captcha struct func CreateCaptchaFromConfigFile(configFile string) (*Captcha, error) { var captcha *Captcha var retErr error err, wordDict, captchaConfig, imageConfig, filterConfig, storeConfig := loadConfigFromFile(configFile) if nil == err { wordmgr, err := CreateWordManagerFromDataFile(wordDict) if nil == err { captcha, retErr = CreateCaptcha(wordmgr, captchaConfig, imageConfig, filterConfig, storeConfig) } else { retErr = err } } else { retErr = err } return captcha, retErr } //GetKey will generate a key with required length func (captcha *Captcha) GetKey(length int) (string, error) { var retErr error var rst string text, err := captcha.wordManager.Get(length) if nil != err { retErr = err } else { info := new(CaptchaInfo) info.Text = text info.CreateTime = time.Now() info.ShownTimes = 0 rst = captcha.store.Add(info) } return rst, retErr } //Verify will verify the user's input and the server stored captcha text func (captcha *Captcha) Verify(key, textToVerify string) (bool, string) { info := captcha.store.Get(key) if nil == info { return false, "captcha info not found" } if info.CreateTime.Add(captcha.captchaConfig.LifeTime).Before(time.Now()) { return false, "captcha expires" } verified := false if captcha.captchaConfig.CaseSensitive { verified = info.Text == textToVerify } else { verified = strings.ToLower(info.Text) == strings.ToLower(textToVerify) } if !verified { return false, "captcha text not match" } captcha.store.Del(key) return true, "" } //GetImage will generate the binary image data func (captcha *Captcha) GetImage(key string) (image.Image, error) { info := captcha.store.Get(key) if nil == info { return nil, errors.New("captcha info not found") } if info.CreateTime.Add(captcha.captchaConfig.LifeTime).Before(time.Now()) { return nil, errors.New("captcha expires") } if captcha.captchaConfig.ChangeTextOnRefresh { if info.ShownTimes > 0 { text, err := captcha.wordManager.Get(len(info.Text)) if nil != err { return nil, err } else { info.Text = text } } info.ShownTimes++ captcha.store.Update(key, info) } cimg := captcha.genImage(info.Text) return cimg, nil } func createStore(config *StoreConfig) (StoreInterface, error) { var store StoreInterface var err error switch config.Engine { case STORE_ENGINE_BUILDIN: store = CreateCStore(config.LifeTime, config.GcProbability, config.GcDivisor) break case STORE_ENGINE_MEMCACHE: store = CreateMCStore(config.LifeTime, config.Servers) break default: creator, has := storeCreators[config.Engine] if !has { err = errors.New("Not supported engine:'" + config.Engine + "'") break } store, err = creator(config) } return store, err } func (captcha *Captcha) genImage(text string) *CImage { cimg := CreateCImage(captcha.imageConfig) cimg.drawString(text) for _, filter := range captcha.filterManager.GetFilters() { filter.Proc(cimg) } return cimg } ================================================ FILE: captcha_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "os" "testing" "time" ) func TestCaptcha(t *testing.T) { captcha, err := getCaptcha() if nil != err { t.Fatalf("getCaptcha Error:%s", err.Error()) } key, err := captcha.GetKey(4) if nil != err { t.Fatalf("GetKey Error:%s", err.Error()) } captcha.GetImage(key) captcha.Verify(key, "test") } func BenchmarkCaptcha(t *testing.B) { captcha, err := getCaptcha() if nil != err { t.Fatalf("getCaptcha Error:%s", err.Error()) } for i := 0; i < t.N; i++ { s, _ := captcha.GetKey(4) captcha.GetImage(s) captcha.Verify(s, "ssss") } } func BenchmarkCaptchaInternalAPI(t *testing.B) { captcha, err := getCaptcha() if nil != err { t.Fatalf("getCaptcha Error:%s", err.Error()) } for i := 0; i < t.N; i++ { s, _ := captcha.GetKey(4) captcha.Verify(s, "ssss") } } func BenchmarkCaptchaDrawImage(t *testing.B) { captcha, err := getCaptcha() if nil != err { t.Fatalf("getCaptcha Error:%s", err.Error()) } for i := 0; i < t.N; i++ { s, _ := captcha.GetKey(4) captcha.GetImage(s) } } func getCaptcha() (*Captcha, error) { wordDict, captchaConfig, imageConfig, filterConfig, storeConfig := loadConfig() wordmgr, err := CreateWordManagerFromDataFile(wordDict) captcha, err := CreateCaptcha(wordmgr, captchaConfig, imageConfig, filterConfig, storeConfig) return captcha, err } func loadConfig() (string, *CaptchaConfig, *ImageConfig, *FilterConfig, *StoreConfig) { pwd, _ := os.Getwd() data_path := pwd + "/data/" wordDict := data_path + "en_phrases" captchaConfig := new(CaptchaConfig) captchaConfig.LifeTime = 10 * time.Second imageConfig := new(ImageConfig) imageConfig.FontFiles = []string{data_path + "zpix.ttf"} imageConfig.FontSize = 26 imageConfig.Height = 40 imageConfig.Width = 120 filterConfig := new(FilterConfig) filterConfig.Init() filterConfig.Filters = []string{"ImageFilterNoiseLine", "ImageFilterNoisePoint", "ImageFilterStrike"} var filterConfigGroup *FilterConfigGroup filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "5") filterConfig.SetGroup("ImageFilterNoiseLine", filterConfigGroup) filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "10") filterConfig.SetGroup("ImageFilterNoisePoint", filterConfigGroup) filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "3") filterConfig.SetGroup("ImageFilterStrike", filterConfigGroup) storeConfig := new(StoreConfig) storeConfig.Engine = STORE_ENGINE_BUILDIN storeConfig.GcDivisor = 100 storeConfig.GcProbability = 1 return wordDict, captchaConfig, imageConfig, filterConfig, storeConfig } ================================================ FILE: captchainfo.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "time" ) // CaptchaInfo is the entity of a captcha // text:the content text,for the image display and user to recognize // createTime:the time when the captcha is created type CaptchaInfo struct { Text string CreateTime time.Time ShownTimes int } ================================================ FILE: cimage.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "image" "image/color" "image/draw" "github.com/golang/freetype" ) const ( colorCount = 20 ) //CImage is a image process tool type CImage struct { *image.Paletted config *ImageConfig } func (m *CImage) drawHorizLine(fromX, toX, y int, colorIdx uint8) *CImage { if 0 >= colorIdx || colorIdx > colorCount { colorIdx = uint8(rnd(0, colorCount)) } for x := fromX; x <= toX; x++ { m.SetColorIndex(x, y, colorIdx) } return m } func (m *CImage) drawCircle(x, y, radius int, colorIdx uint8) { f := 1 - radius dfx := 1 dfy := -2 * radius xo := 0 yo := radius m.SetColorIndex(x, y+radius, colorIdx) m.SetColorIndex(x, y-radius, colorIdx) m.drawHorizLine(x-radius, x+radius, y, colorIdx) for xo < yo { if f >= 0 { yo-- dfy += 2 f += dfy } xo++ dfx += 2 f += dfx m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx) m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx) m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx) m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx) } } func (m *CImage) drawString(text string) *CImage { fg, bg := image.Black, &image.Uniform{color.RGBA{255, 255, 255, 255}} draw.Draw(m, m.Bounds(), bg, image.ZP, draw.Src) c := freetype.NewContext() c.SetFontSize(m.config.FontSize) c.SetClip(m.Bounds()) c.SetDst(m) c.SetSrc(fg) i := 0 for _, s := range text { c.SetFont(m.config.fontManager.GetRandomFont()) charX := (int(c.PointToFixed(m.config.FontSize) >> 8)) * i charY := int(c.PointToFixed(m.config.FontSize) >> 8) charPt := freetype.Pt(charX, charY) c.DrawString(string(s), charPt) i = i + 1 } return m } //CreateCImage will create a CImage struct with config func CreateCImage(config *ImageConfig) *CImage { r := new(CImage) r.Paletted = image.NewPaletted(image.Rect(0, 0, config.Width, config.Height), randomPalette()) r.config = config if nil == r.config.fontManager { fm := CreateFontManager() for _, fontFile := range config.FontFiles { fm.AddFont(fontFile) } r.config.fontManager = fm } return r } func randomPalette() color.Palette { p := make([]color.Color, colorCount+1) // Transparent color. p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00} // Primary color. prim := color.RGBA{ uint8(rnd(0, 255)), uint8(rnd(0, 255)), uint8(rnd(0, 255)), 0xFF, } p[1] = prim // Circle colors. for i := 2; i <= colorCount; i++ { p[i] = randomBrightness(prim, 255) } return p } func randomBrightness(c color.RGBA, max uint8) color.RGBA { minc := min3(c.R, c.G, c.B) maxc := max3(c.R, c.G, c.B) if maxc > max { return c } n := rnd(0, int(max-maxc)) - int(minc) return color.RGBA{ uint8(int(c.R) + n), uint8(int(c.G) + n), uint8(int(c.B) + n), uint8(c.A), } } func min3(x, y, z uint8) (m uint8) { m = x if y < m { m = y } if z < m { m = z } return } func max3(x, y, z uint8) (m uint8) { m = x if y > m { m = y } if z > m { m = z } return } ================================================ FILE: confighelper.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "errors" "strings" "time" "github.com/hanguofeng/config" ) const ( DEFAULT_LIFE_TIME = time.Minute * 5 DEFAULT_FONT_SIZE = 26 DEFAULT_WIDTH = 120 DEFAULT_HEIGHT = 40 DEFAULT_GC_PROBABILITY = 1 DEFAULT_GC_DIVISOR = 100 ) func loadConfigFromFile(configFile string) (error, string, *CaptchaConfig, *ImageConfig, *FilterConfig, *StoreConfig) { var retErr error c, err := config.ReadDefault(configFile) //wordDict wordDict, err := c.String("captcha", "word_dict") if nil != err { retErr = errors.New("loadConfigFromFile Fail,Get word_dict options failed:" + err.Error()) } //captchaConfig captchaConfig := new(CaptchaConfig) var lifeTime time.Duration cfgLifeTime, err := c.String("captcha", "life_time") if nil == err { lifeTime, err = time.ParseDuration(cfgLifeTime) if nil != err { lifeTime = DEFAULT_LIFE_TIME } } else { lifeTime = DEFAULT_LIFE_TIME } captchaConfig.LifeTime = lifeTime cfgChangeTextOnRefresh, err := c.Bool("captcha", "change_text_on_refresh") if nil != err { cfgChangeTextOnRefresh = false } captchaConfig.ChangeTextOnRefresh = cfgChangeTextOnRefresh //imageConfig imageConfig := new(ImageConfig) var fontFiles []string cfgFontFiles, err := c.StringMuti("image", "font_files") if nil != err { retErr = errors.New("loadConfigFromFile Fail,font_files options failed:" + err.Error()) } else { fontFiles = cfgFontFiles } imageConfig.FontFiles = fontFiles var fontSize float64 cfgFontSize, err := c.Int("image", "font_size") if nil != err { fontSize = DEFAULT_FONT_SIZE } else { fontSize = float64(cfgFontSize) } imageConfig.FontSize = fontSize var width int cfgWidth, err := c.Int("image", "width") if nil != err { width = DEFAULT_WIDTH } else { width = int(cfgWidth) } imageConfig.Width = width var height int cfgHeight, err := c.Int("image", "height") if nil != err { height = DEFAULT_HEIGHT } else { height = int(cfgHeight) } imageConfig.Height = height //filterConfig filterConfig := new(FilterConfig) filterConfig.Init() cfgOpenFilter, err := c.StringMuti("filter", "open_filter") if nil == err { filterConfig.Filters = cfgOpenFilter } else { filterConfig.Filters = []string{} } for _, section := range c.Sections() { if strings.HasPrefix(section, "ImageFilter") { filterConfigGroup := new(FilterConfigGroup) filterConfigGroup.Init() options, err := c.Options(section) if nil == err { for _, option := range options { v, err := c.String(section, option) if nil == err { filterConfigGroup.SetItem(option, v) } } } filterConfig.SetGroup(section, filterConfigGroup) } } //storeConfig storeConfig := new(StoreConfig) storeConfig.CaptchaConfig = *captchaConfig engine, err := c.String("store", "engine") if nil != err { retErr = errors.New("loadConfigFromFile Fail,engine options failed" + err.Error()) } else { storeConfig.Engine = engine } servers, err := c.StringMuti("store", "servers") if nil != err { storeConfig.Servers = []string{} } else { storeConfig.Servers = servers } gcProbability, err := c.Int("store", "gc_probability") if nil != err { storeConfig.GcProbability = gcProbability } else { storeConfig.GcProbability = DEFAULT_GC_PROBABILITY } gcDivisor, err := c.Int("store", "gc_divisor") if nil != err { storeConfig.GcDivisor = gcDivisor } else { storeConfig.GcDivisor = DEFAULT_GC_DIVISOR } if nil != err && nil == retErr { retErr = err } return retErr, wordDict, captchaConfig, imageConfig, filterConfig, storeConfig } ================================================ FILE: configtypes.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "time" ) //CaptchaConfig ,the captcha config type CaptchaConfig struct { LifeTime time.Duration CaseSensitive bool ChangeTextOnRefresh bool } //FilterConfigGroup type FilterConfigGroup struct { data map[string]string } func (this *FilterConfigGroup) Init() { this.data = map[string]string{} } func (this *FilterConfigGroup) GetItem(key string) (string, bool) { val, ok := this.data[key] return val, ok } func (this *FilterConfigGroup) SetItem(key string, val string) { this.data[key] = val } type FilterConfig struct { Filters []string data map[string]*FilterConfigGroup } func (this *FilterConfig) Init() { this.Filters = []string{} this.data = map[string]*FilterConfigGroup{} } func (this *FilterConfig) GetGroup(key string) (FilterConfigGroup, bool) { val, ok := this.data[key] if !ok { val = new(FilterConfigGroup) } return *val, ok } func (this *FilterConfig) SetGroup(key string, group *FilterConfigGroup) { this.data[key] = group } //ImageConfig ,the image config type ImageConfig struct { Width int Height int FontSize float64 FontFiles []string fontManager *FontManager } const STORE_ENGINE_BUILDIN = "buildin" const STORE_ENGINE_MEMCACHE = "memcache" //StoreConfig ,the store engine config type StoreConfig struct { CaptchaConfig Engine string Servers []string GcProbability int GcDivisor int } ================================================ FILE: cstore.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "crypto/md5" enc "encoding/gob" "encoding/hex" "fmt" "os" "sync" "time" ) //CStore is the Captcha info store service type CStore struct { engine string mu sync.RWMutex data map[string]*CaptchaInfo expiresTime time.Duration gcProbability int gcDivisor int } //CreateCStore will create a new CStore func CreateCStore(expiresTime time.Duration, gcProbability int, gcDivisor int) *CStore { store := new(CStore) store.engine = STORE_ENGINE_BUILDIN store.data = make(map[string]*CaptchaInfo) store.expiresTime = expiresTime store.gcProbability = gcProbability store.gcDivisor = gcDivisor return store } //Get captcha info by key func (store *CStore) Get(key string) *CaptchaInfo { defer store.gcWrapper() //run gc after get store.mu.Lock() defer store.mu.Unlock() ret := store.data[key] return ret } //Add captcha info and get the auto generated key func (store *CStore) Add(captcha *CaptchaInfo) string { store.mu.Lock() defer store.mu.Unlock() key := fmt.Sprintf("%s%s%x", captcha.Text, randStr(20), time.Now().UnixNano()) key = hex.EncodeToString(md5.New().Sum([]byte(key))) key = key[:32] store.data[key] = captcha return key } //Update captcha info func (store *CStore) Update(key string, captcha *CaptchaInfo) bool { store.mu.Lock() defer store.mu.Unlock() store.data[key] = captcha return true } //Del captcha info by key func (store *CStore) Del(key string) { store.mu.Lock() defer store.mu.Unlock() delete(store.data, key) } //Destroy the whole store func (store *CStore) Destroy() { for key := range store.data { store.Del(key) } } //OnConstruct load data func (store *CStore) OnConstruct() { } //OnDestruct dump data func (store *CStore) OnDestruct() { } //Dump the whole store func (store *CStore) Dump(file string) error { store.mu.Lock() defer store.mu.Unlock() pwd, err := os.Getwd() if (nil == err) && ("" != pwd) { f, err := os.Create(pwd + "/" + file) if nil == err { encoder := enc.NewEncoder(f) err := encoder.Encode(store.data) f.Close() if nil == err { return err } else { return nil } } else { return err } } else { return err } return nil } //LoadDumped file to store func (store *CStore) LoadDumped(file string) error { data := &map[string]*CaptchaInfo{} pwd, err := os.Getwd() if (nil == err) && ("" != pwd) { f, err := os.Open(pwd + "/" + file) if nil == err { decoder := enc.NewDecoder(f) err := decoder.Decode(data) f.Close() if nil == err { store.data = *data return nil } else { return err } } else { return err } } else { return err } return err } func (store *CStore) gcWrapper() { //run PROBABILITY if rnd(0, store.gcDivisor) == store.gcProbability { go store.gc() } } func (store *CStore) gc() { for key, val := range store.data { if val.CreateTime.Add(store.expiresTime).Before(time.Now()) { store.Del(key) } } } ================================================ FILE: cstore_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "os" "testing" "time" ) func TestStore(t *testing.T) { store := CreateCStore(10*time.Second, 1, 100) //10 s captcha := new(CaptchaInfo) captcha.Text = "hello" captcha.CreateTime = time.Now() //test add and get key := store.Add(captcha) retV := store.Get(key) if retV != captcha { t.Errorf("not equal") } retV.Text = "world" store.Update(key, retV) retV = store.Get(key) if retV.Text != "world" { t.Errorf("update not equal") } //test dump,destroy and loaddumped store.Dump("data/data.dat") store.Destroy() retV = store.Get(key) if nil != retV { t.Errorf("Destroy error") } store.LoadDumped("data/data.dat") retV = store.Get(key) if captcha.Text != retV.Text { t.Errorf("LoadDumped error") } os.Remove("data/data.dat") //test del store.Del(key) retV = store.Get(key) if nil != retV { t.Errorf("not del") } } ================================================ FILE: data/cn_char ================================================ 你 好 啊 哈 哦 浏 览 器 不 要 执 行 与 事 件 关 联 的 默 认 动 作 ================================================ FILE: data/cn_phrases ================================================ 高枕无忧 各自为政 华而不实 好好先生 狐假虎威 汗流浃背 后来居上 合浦珠还 后起之秀 后生可畏 火树银花 坚壁清野 间不容发 井底之蛙 鸡口牛后 江郎才尽 见猎心喜 鸡鸣狗盗 九牛一毛 胶漆相投 举一反三 开诚布公 空洞无物 沆瀣—气 网油鱼卷 燕窝四字 抓炒鱼片 三鲜瑶柱 芙蓉大虾 龙井竹荪 桂花干贝 金钱吐丝 凤凰展翅 炸鸡葫芦 桃仁鸡丁 鸭丝掐菜 肉末烧饼 龙凤柔情 鸡沾口蘑 咖喱菜花 凤凰趴窝 宫保兔肉 熊猫品竹 御扇豆黄 炝玉龙片 糖醋鱼卷 金鱼鸭掌 琉璃珠玑 金糕 栗子糕 芝麻卷 仿膳饽饽 酥卷佛手 油焖鲜蘑 莲子膳粥 麻辣牛肉 金丝烧麦 凤尾群翅 太极发财燕 雪月羊肉 炸春卷 如意卷 金糕卷 兰花豆干 萝卜桂鱼 凤穿金衣 云河段霄 翠玉豆糕 四喜饺 绣球全鱼 秋菊傲霜 龙衔海棠 滑溜鹌鹑 侉炖羊肉 玉掌献寿 炒榛子酱 发菜黄花 熊猫蟹肉 清炸鹌鹑 双色豆糕 烤羊腿 龙井金鱼 琥珀鸽蛋 二龙戏珠 炒黄瓜酱 雨后春笋 如意竹荪 檀扇鸭掌 鲤跃龙门 炝黄瓜衣 菊花里脊 八宝膳粥 母子相会 豌豆黄 小豆糕 枣泥糕 红烧鱼唇 金鱼角 凤凰鱼肚 宫廷排翅 玉兔白菜 冰花雪莲 松鹤延年 香露苹果 荷花酥 金钱鱼肚 明珠豆腐 芙蓉鱼骨 百子冬瓜 翡翠玉扇 仙鹤鲍鱼 薏米膳继 莲子糕 日月生辉 荷包蟹肉 姜汁扁豆 佛手金卷 翠柳凤丝 燕影金蔬 白银如意 黄袍加身 葵花麻鱼 五丝菜卷 佛手广肚 白梨凤脯 参婆千子 杏仁豆腐 御龙火锅 糖醋荷藕 罗汉大虾 黑米膳粥 金屋藏娇 翡翠银耳 抓炒大虾 喜鹊登枝 油攒大虾 茸鸡待哺 随滑飞龙 鸡油香菇 怪味鸡片 鹦鹉莴笋 芝麻锅炸 芜爆鲜贝 双色马蹄糕 口蘑鹿肉 琵琶大虾 长春羹 干煸牛肉丝 蝴蝶海参 鸳鸯酥盒 芜爆山鸡 燕尾桃花 麦穗虾卷 三鲜鸭舌 金银鸽肉 红烧鹿筋 芙蓉鹿尾 百寿桃 莲花卷 三色糕 玉板翠带 珍珠雪耳 清炒鳝丝 万年长青 金鸡独立 豢蝶大虾 凤戏牡丹 荷塘莲香 一品官燕 芸豆金鱼 豆沙糕 酥页层层 金蟾拜月 奶油菠萝冻 花篮白菜 桂花海蜇 鲍王闹府 凤凰出世 绣球蛋糕 云片鸽蛋 烧瓤菜花 龙凤双锤 鸡丝汤面 红烧鱼骨 凤脯珍珠 干烧冬笋 鸳鸯哺乳 百鸟还巢 卧龙戏珠 如意乌龙 金狮绣球 豆沙凉糕 凤眼秋波 红豆膳粥 棠花吐蕊 群虾戏荷 炒豌豆酱 金镶玉板 梅竹山石 芙蓉鱼角 怀胎桂鱼 雪里藏珍 抓炒里脊 叉烧猪肉 松树猴头 象眼鸽蛋 清蒸鹌鹑 龙凤呈祥 金钱香菇 香爆螺盏 鸳鸯鱼枣 桔子盏 藕丝羹 沙舟踏翠 芥末鸭膀 翡翠鱼丁 桂花鱼条 玉盏龙眼 棒渣膳粥 五丝洋粉 三丝驼峰 晶玉海棠 蜜汁山药 拌银耳 蛋挞 宫保鹌鹑 芙蓉糕 火炼金身 ================================================ FILE: data/en_char ================================================ a b c d e f g h i j k l m n o p q r s t u v w x y z ================================================ FILE: data/en_phrases ================================================ hello foo bar quick fox ================================================ FILE: fontmanager.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "github.com/golang/freetype/truetype" "io/ioutil" "math/rand" "time" ) //FontManager is a Font Manager the manage font list type FontManager struct { fontFiles []string fontObjects map[string]*truetype.Font randObject *rand.Rand } //CreateFontManager will create a new Font Manager func CreateFontManager() *FontManager { fm := new(FontManager) fm.fontFiles = []string{} fm.fontObjects = make(map[string]*truetype.Font) fm.randObject = rand.New(rand.NewSource(time.Now().UnixNano())) return fm } //AddFont will add a new font file to the list func (fm *FontManager) AddFont(pathToFontFile string) error { fontBytes, err := ioutil.ReadFile(pathToFontFile) if err != nil { return err } font, err := truetype.Parse(fontBytes) if err != nil { return err } fm.fontFiles = append(fm.fontFiles, pathToFontFile) fm.fontObjects[pathToFontFile] = font return nil } //GetFont will return a Font struct by path func (fm *FontManager) GetFont(pathToFontFile string) *truetype.Font { return fm.fontObjects[pathToFontFile] } //GetRandomFont will return a random Font struct func (fm *FontManager) GetRandomFont() *truetype.Font { randomIndex := fm.randObject.Intn(len(fm.fontFiles)) fontFile := fm.fontFiles[randomIndex] rst := fm.GetFont(fontFile) return rst } ================================================ FILE: go.mod ================================================ module github.com/hanguofeng/gocaptcha go 1.17 require ( github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/hanguofeng/config v1.0.0 gopkg.in/redis.v2 v2.3.2 ) require ( golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) ================================================ FILE: go.sum ================================================ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hanguofeng/config v1.0.0 h1:AIpXdv7iEzK4v4Qyov7jeZWIwepiwBfELzfpd2r0dH0= github.com/hanguofeng/config v1.0.0/go.mod h1:YX//r7JfVHJCHW/+HP8WqQ2FVDaUGCo0j5z3ddMnrk4= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU= gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/redis.v2 v2.3.2 h1:GPVIIB/JnL1wvfULefy3qXmPu1nfNu2d0yA09FHgwfs= gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU= ================================================ FILE: imagefilter.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import () var imageFilterCreators = map[string]func(FilterConfigGroup) ImageFilter{} func RegisterImageFilter(id string, f func(FilterConfigGroup) ImageFilter) bool { if _, has := imageFilterCreators[id]; has { return false } imageFilterCreators[id] = f return true } //ImageFilter is the interface of image filter type ImageFilter interface { Proc(cimage *CImage) GetId() string SetConfig(FilterConfigGroup) GetConfig() FilterConfigGroup } //ImageFilter is the base class of image filter type ImageFilterBase struct { config FilterConfigGroup } func (filter *ImageFilterBase) Proc(cimage *CImage) { panic("not impl") } func (filter *ImageFilterBase) GetId() string { panic("not impl") } func (filter *ImageFilterBase) SetConfig(config FilterConfigGroup) { filter.config = config } func (filter *ImageFilterBase) GetConfig() FilterConfigGroup { return filter.config } ================================================ FILE: imagefiltermanager.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import () //ImageFilterManager type ImageFilterManager struct { filters []ImageFilter } func init() { RegisterImageFilter(IMAGE_FILTER_NOISE_LINE, imageFilterNoiseLineCreator) RegisterImageFilter(IMAGE_FILTER_NOISE_POINT, imageFilterNoisePointCreator) RegisterImageFilter(IMAGE_FILTER_STRIKE, imageFilterStrikeCreator) } func CreateImageFilterManager() *ImageFilterManager { ret := new(ImageFilterManager) ret.filters = []ImageFilter{} return ret } func (manager *ImageFilterManager) AddFilter(filter ImageFilter) { manager.filters = append(manager.filters, filter) } func (manager *ImageFilterManager) GetFilters() []ImageFilter { return manager.filters } func CreateImageFilterManagerByConfig(config *FilterConfig) *ImageFilterManager { mgr := new(ImageFilterManager) mgr.filters = []ImageFilter{} for _, cfgId := range config.Filters { creator, has := imageFilterCreators[cfgId] if !has { continue } cfgGroup, has := config.GetGroup(cfgId) if !has { continue } mgr.AddFilter(creator(cfgGroup)) } return mgr } ================================================ FILE: imagefiltermanager_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "testing" ) func TestImageFilterManager(t *testing.T) { manager := CreateImageFilterManager() manager.AddFilter(&ImageFilterBase{}) if 1 != len(manager.GetFilters()) { t.Error("add or get failed") } } func TestImageFilterManagerConfig(t *testing.T) { filterConfig := new(FilterConfig) filterConfig.Init() filterConfig.Filters = []string{"ImageFilterNoiseLine", "ImageFilterNoisePoint", "ImageFilterStrike"} var filterConfigGroup *FilterConfigGroup filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "5") filterConfig.SetGroup("ImageFilterNoiseLine", filterConfigGroup) filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "10") filterConfig.SetGroup("ImageFilterNoisePoint", filterConfigGroup) filterConfigGroup = new(FilterConfigGroup) filterConfigGroup.Init() filterConfigGroup.SetItem("Num", "3") filterConfig.SetGroup("ImageFilterStrike", filterConfigGroup) manager := CreateImageFilterManagerByConfig(filterConfig) filters := manager.GetFilters() if 3 != len(filters) { t.Error("create by config failed") } for _, filter := range filters { config := filter.GetConfig() var num string switch filter.GetId() { case "ImageFilterNoiseLine": if num, _ = config.GetItem("Num"); "5" != num { t.Error("ImageFilterNoiseLine ERROR") } break case "ImageFilterNoisePoint": if num, _ = config.GetItem("Num"); "10" != num { t.Error("ImageFilterNoisePoint ERROR") } break case "ImageFilterStrike": if num, _ = config.GetItem("Num"); "3" != num { t.Error("ImageFilterStrike ERROR") } break } } } ================================================ FILE: imagefilternoiseline.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "strconv" ) //ImageFilter is the interface of image filter const IMAGE_FILTER_NOISE_LINE = "ImageFilterNoiseLine" type ImageFilterNoiseLine struct { ImageFilterBase } func imageFilterNoiseLineCreator(config FilterConfigGroup) ImageFilter { filter := ImageFilterNoiseLine{} filter.SetConfig(config) return &filter } //Proc the image func (filter *ImageFilterNoiseLine) Proc(cimage *CImage) { var num int var err error v, ok := filter.config.GetItem("Num") if ok { num, err = strconv.Atoi(v) if nil != err { num = 3 } } else { num = 3 } for i := 0; i < num; i++ { x := rnd(0, cimage.Bounds().Max.X) cimage.drawHorizLine(int(float32(x)/1.5), x, rnd(0, cimage.Bounds().Max.Y), uint8(rnd(1, colorCount))) } } func (filter *ImageFilterNoiseLine) GetId() string { return IMAGE_FILTER_NOISE_LINE } ================================================ FILE: imagefilternoisepoint.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "strconv" ) const IMAGE_FILTER_NOISE_POINT = "ImageFilterNoisePoint" //ImageFilter is the interface of image filter type ImageFilterNoisePoint struct { ImageFilterBase } func imageFilterNoisePointCreator(config FilterConfigGroup) ImageFilter { filter := ImageFilterNoisePoint{} filter.SetConfig(config) return &filter } //Proc the image func (filter *ImageFilterNoisePoint) Proc(cimage *CImage) { var num int var err error v, ok := filter.config.GetItem("Num") if ok { num, err = strconv.Atoi(v) if nil != err { num = 3 } } else { num = 3 } for i := 0; i < num; i++ { cimage.drawCircle(rnd(0, cimage.Bounds().Max.X), rnd(0, cimage.Bounds().Max.Y), rnd(0, 2), uint8(rnd(1, colorCount))) } } func (filter *ImageFilterNoisePoint) GetId() string { return IMAGE_FILTER_NOISE_POINT } ================================================ FILE: imagefilterstrike.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "math" "strconv" ) const IMAGE_FILTER_STRIKE = "ImageFilterStrike" //ImageFilter is the interface of image filter type ImageFilterStrike struct { ImageFilterBase } func imageFilterStrikeCreator(config FilterConfigGroup) ImageFilter { filter := ImageFilterStrike{} filter.SetConfig(config) return &filter } //Proc the image func (filter *ImageFilterStrike) Proc(cimage *CImage) { var num int var err error v, ok := filter.config.GetItem("Num") if ok { num, err = strconv.Atoi(v) if nil != err { num = 1 } } else { num = 1 } maxx := cimage.Bounds().Max.X maxy := cimage.Bounds().Max.Y y := rnd(maxy/2, maxy-maxy/2) amplitude := rndf(10, 15) period := rndf(80, 100) dx := 2.0 * math.Pi / period for x := 0; x < maxx; x++ { xo := amplitude * math.Cos(float64(y)*dx) yo := amplitude * math.Sin(float64(x)*dx) for yn := 0; yn < num; yn++ { r := rnd(0, 2) cimage.drawCircle(x+int(xo), y+int(yo)+(yn*(num+1)), r/2, 1) } } } func (filter *ImageFilterStrike) GetId() string { return IMAGE_FILTER_STRIKE } ================================================ FILE: mcstore.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "bytes" "crypto/md5" "encoding/hex" "fmt" "log" "time" enc "encoding/gob" "github.com/bradfitz/gomemcache/memcache" ) const ( MC_KEY_PREFIX = "gocaptcha-" ) //MCStore is the Captcha info store service type MCStore struct { engine string mc *memcache.Client } //CreateCStore will create a new CStore func CreateMCStore(expiresTime time.Duration, servers []string) *MCStore { store := new(MCStore) store.mc = memcache.New(servers...) store.mc.Timeout = expiresTime return store } //Get captcha info by key func (store *MCStore) Get(key string) *CaptchaInfo { item, err := store.mc.Get(MC_KEY_PREFIX + key) if nil != err { return nil } ret := store.decodeValue(item.Value) return ret } //Add captcha info and get the auto generated key func (store *MCStore) Add(captcha *CaptchaInfo) string { key := fmt.Sprintf("%s%s%x", captcha.Text, randStr(20), time.Now().UnixNano()) key = hex.EncodeToString(md5.New().Sum([]byte(key))) key = key[:32] item := new(memcache.Item) item.Key = MC_KEY_PREFIX + key item.Value = store.encodeValue(captcha) err := store.mc.Add(item) if nil != err { log.Printf("add key in memcache err:%s", err) } return key } //Update captcha info func (store *MCStore) Update(key string, captcha *CaptchaInfo) bool { item := new(memcache.Item) item.Key = MC_KEY_PREFIX + key item.Value = store.encodeValue(captcha) err := store.mc.Set(item) if nil != err { log.Printf("update key in memcache err:%s", err) return false } else { return true } } //Del captcha info by key func (store *MCStore) Del(key string) { store.mc.Delete(MC_KEY_PREFIX + key) } //Destroy the whole store func (store *MCStore) Destroy() { } //OnConstruct load data func (store *MCStore) OnConstruct() { } //OnDestruct dump data func (store *MCStore) OnDestruct() { } func (store *MCStore) encodeValue(val *CaptchaInfo) []byte { buf := bytes.NewBufferString("") encoder := enc.NewEncoder(buf) err := encoder.Encode(val) if nil != err { return nil } return buf.Bytes() } func (store *MCStore) decodeValue(value []byte) *CaptchaInfo { data := &CaptchaInfo{} buf := bytes.NewBuffer(value) decoder := enc.NewDecoder(buf) decoder.Decode(data) return data } ================================================ FILE: mcstore_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "testing" "time" ) func TestMCStore(t *testing.T) { memcacheServers := []string{"127.0.0.1:11211"} store := CreateMCStore(100*time.Second, memcacheServers) //100 s captcha := new(CaptchaInfo) captcha.Text = "hello" captcha.CreateTime = time.Now() //test add and get key := store.Add(captcha) retV := store.Get(key) if retV.Text != captcha.Text { t.Errorf("not equal,retV:%#v,captcha:%#v", retV, captcha) } retV.Text = "world" store.Update(key, retV) retV = store.Get(key) if retV.Text != "world" { t.Errorf("update not equal,retV:%#v,captcha:%#v", retV, captcha) } t.Logf("TestMCStore:get from mc:%#v", retV) //test del store.Del(key) retV = store.Get(key) if nil != retV { t.Errorf("not del") } } ================================================ FILE: randhelper.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "math/rand" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } // rnd returns a non-crypto pseudorandom int in range [from, to]. func rnd(from, to int) int { return rand.Intn(to+1-from) + from } // rndf returns a non-crypto pseudorandom float64 in range [from, to]. func rndf(from, to float64) float64 { return (to-from)*rand.Float64() + from } func randStr(length int) string { rst := "" charset := "abcdefghijklmnopqrstuvwxyz1234567890" for i := 0; i < length; i++ { rst = rst + string(charset[rnd(0, len(charset)-1)]) } return rst } ================================================ FILE: redisstore.go ================================================ package gocaptcha import ( "bytes" "crypto/md5" "encoding/gob" "encoding/hex" "fmt" "log" "strconv" "strings" "time" "gopkg.in/redis.v2" ) func init() { RegisterStore(storeName, CreateCaptchaRedisStore) } const ( captchaKeyFormat = "captcha:text:%s;rand:%s;time:%x;" storeName = "redis" ) type CaptchaRedisStore struct { lifeTime time.Duration stg *redis.Client } func CreateCaptchaRedisStore(config *StoreConfig) (StoreInterface, error) { lifeTime := config.LifeTime if config.Servers == nil || len(config.Servers) == 0 { return nil, fmt.Errorf("servers must not be empty") } fullAddr := strings.TrimPrefix(config.Servers[0], "redis://") pieces := strings.SplitN(fullAddr, "/", 2) db := 0 addr := pieces[0] if len(pieces) == 2 { db, _ = strconv.Atoi(pieces[1]) } opt := redis.Options{} opt.Addr = addr opt.DB = int64(db) opt.PoolSize = 0 stg := redis.NewTCPClient(&opt) return &CaptchaRedisStore{lifeTime, stg}, nil } func (this *CaptchaRedisStore) Get(key string) *CaptchaInfo { s, err := this.stg.Get(key).Result() if err != nil { log.Printf("get key in redis error:%s", err) return nil } captcha := CaptchaInfo{} this.decodeCaptachInfo([]byte(s), &captcha) return &captcha } func (this *CaptchaRedisStore) Add(captcha *CaptchaInfo) string { key := fmt.Sprintf(captchaKeyFormat, captcha.Text, randStr(20), captcha.CreateTime.Unix()) key = hex.EncodeToString(md5.New().Sum([]byte(key))) key = key[:32] val, err := this.encodeCaptchaInfo(captcha) if err == nil { if seterr := this.stg.SetEx(key, this.lifeTime, string(val)); seterr != nil { log.Printf("add key in redis error:%s", seterr) } } return key } func (this *CaptchaRedisStore) Update(key string, captcha *CaptchaInfo) bool { val, err := this.encodeCaptchaInfo(captcha) if err == nil { if seterr := this.stg.Set(key, string(val)); seterr != nil { log.Printf("set key in redis error:%s", seterr) return false } else { return true } } else { return false } } func (this *CaptchaRedisStore) Del(key string) { this.stg.Del(key) } func (this *CaptchaRedisStore) Destroy() { } func (this *CaptchaRedisStore) OnConstruct() { } func (this *CaptchaRedisStore) OnDestruct() { } func (this *CaptchaRedisStore) encodeCaptchaInfo(captcha *CaptchaInfo) ([]byte, error) { buf := new(bytes.Buffer) encoder := gob.NewEncoder(buf) err := encoder.Encode(captcha) if err != nil { log.Printf("encode captcha info error:%s", err) return nil, err } return buf.Bytes(), nil } func (this *CaptchaRedisStore) decodeCaptachInfo(b []byte, ret *CaptchaInfo) { buf := bytes.NewBuffer(b) decoder := gob.NewDecoder(buf) if err := decoder.Decode(ret); err != nil { log.Printf("decode captcha info error:%s", err) } return } ================================================ FILE: redisstore_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "testing" "time" ) func TestRedisStore(t *testing.T) { storeConfig := &StoreConfig{} storeConfig.CaptchaConfig.LifeTime = time.Second * 100 storeConfig.Engine = "redis" storeConfig.Servers = []string{"127.0.0.1:6379"} store, _ := CreateCaptchaRedisStore(storeConfig) captcha := new(CaptchaInfo) captcha.Text = "hello" captcha.CreateTime = time.Now() //test add and get key := store.Add(captcha) retV := store.Get(key) if retV.Text != captcha.Text { t.Errorf("not equal,retV:%#v,captcha:%#v", retV, captcha) } retV.Text = "world" store.Update(key, retV) retV = store.Get(key) if retV.Text != "world" { t.Errorf("update not equal,retV:%#v,captcha:%#v", retV, captcha) } t.Logf("TestMCStore:get from redis:%#v", retV) //test del store.Del(key) retV = store.Get(key) if nil != retV { t.Errorf("not del") } } ================================================ FILE: storeinterface.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import () var storeCreators = map[string]func(*StoreConfig) (StoreInterface, error){} //StoreInterface is the interface of store type StoreInterface interface { Get(key string) *CaptchaInfo Add(captcha *CaptchaInfo) string Update(key string, captcha *CaptchaInfo) bool Del(key string) Destroy() OnConstruct() OnDestruct() } func RegisterStore(name string, f func(*StoreConfig) (StoreInterface, error)) bool { if _, has := storeCreators[name]; has { return false } storeCreators[name] = f return true } ================================================ FILE: wordmanager.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "encoding/csv" "errors" "io" "os" "strings" "unicode/utf8" ) //WordManager is a captcha word manage tool type WordManager struct { words []string isDataSingleChar bool isValid bool } //CreateWordManagerFromDataFile will create a entity from a dictionary file func CreateWordManagerFromDataFile(filename string) (*WordManager, error) { mgr := &WordManager{} mgr.words = []string{} mgr.isValid = false f, err := os.Open(filename) if nil != err { return mgr, err } defer f.Close() reader := csv.NewReader(f) mgr.isDataSingleChar = true for { record, err := reader.Read() if err == io.EOF { break } else if nil != err { return mgr, err } if 1 < len([]rune(record[0])) { mgr.isDataSingleChar = false } mgr.words = append(mgr.words, strings.TrimSpace(record[0])) } mgr.isValid = true return mgr, nil } //Get a specifical length word func (mgr *WordManager) Get(length int) (string, error) { var retErr error rst := "" if mgr.isValid { if true == mgr.isDataSingleChar { if len(mgr.words) < length { return "", errors.New("dict words count is less than your length") } for { line := mgr.getLine() if false == strings.ContainsRune(rst, []rune(line)[0]) { rst = rst + line } if utf8.RuneCountInString(rst) >= length { break } } rstRune := []rune(rst) rst = string(rstRune[0:length]) } else { rst = mgr.getLine() } } else { retErr = errors.New("WordManager is invalid") } return rst, retErr } func (mgr *WordManager) SetWords(words []string) { mgr.words = words mgr.isValid = len(words) > 0 mgr.isDataSingleChar = true for _, s := range words { if len([]rune(s)) > 1 { mgr.isDataSingleChar = false } } } func (mgr *WordManager) getLine() string { maxIndex := len(mgr.words) - 1 rstIndex := rnd(0, maxIndex) rst := mgr.words[rstIndex] return rst } ================================================ FILE: wordmanager_test.go ================================================ // Copyright 2013 hanguofeng. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gocaptcha import ( "os" "testing" ) func TestWordManager(t *testing.T) { pwd, err := os.Getwd() if (nil != err) || "" == pwd { return } path := pwd + "/data/" singleCharDict := []string{"cn_char", "en_char"} phrasesDict := []string{"cn_phrases", "en_phrases"} length := 6 for _, f := range singleCharDict { mgr, err := CreateWordManagerFromDataFile(path + f) s, err := mgr.Get(length) if nil != err { t.Errorf(err.Error()) } else if length != len([]rune(s)) { t.Errorf("get no equals length:" + f) } } for _, f := range phrasesDict { mgr, err := CreateWordManagerFromDataFile(path + f) s, err := mgr.Get(length) if nil != err { t.Errorf(err.Error()) } else if 0 >= len([]rune(s)) { t.Errorf("not get:" + f) } } }