github.com/geniusesgroup/libgo@v0.0.0-20220713101832-828057a9d3d4/captcha/phrase.go (about) 1 /* For license and copyright information please see LEGAL file in repository */ 2 3 package captcha 4 5 import ( 6 "bytes" 7 "container/list" 8 "image" 9 "image/draw" 10 "image/jpeg" 11 "image/png" 12 "math/rand" 13 "strconv" 14 "time" 15 "unsafe" 16 17 "github.com/golang/freetype" 18 "github.com/golang/freetype/truetype" 19 "golang.org/x/image/font/gofont/gobolditalic" 20 21 etime "../earth-time" 22 "../log" 23 "../uuid" 24 ) 25 26 /* 27 Usage: 28 var phraseCaptchas = captcha.NewDefaultPhraseCaptchas() 29 var pc *captcha.PhraseCaptcha = phraseCaptchas.NewImage(req.Language, req.ImageFormat) 30 31 */ 32 33 // PhraseCaptchas store 34 type PhraseCaptchas struct { 35 Len uint8 // Number of captcha solution. can't be more than 16! 36 Difficulty uint8 // 0:very-easy, 1:easy, 2:medium, 3:hard, 4:very-hard, 5:extreme-hard 37 Type uint8 // 0:Number(625896), 1:Word(A19Cat), 2:Math(+ - * /), 38 Duration int64 // The number of seconds indicate expiration time of captchas 39 ImageSize image.Point // Standard width & height of a captcha image. 40 Pool map[[16]byte]*PhraseCaptcha 41 idByTime *list.List 42 } 43 44 // PhraseCaptcha store 45 type PhraseCaptcha struct { 46 ID [16]byte 47 Answer string 48 ExpireIn int64 49 State state 50 Image []byte // In requested lang & format 51 Audio []byte // In requested lang & format 52 } 53 54 // NewDefaultPhraseCaptchas use to make new captchas with defaults values! 55 func NewDefaultPhraseCaptchas() (pcs *PhraseCaptchas) { 56 pcs = &PhraseCaptchas{ 57 Len: 6, 58 Difficulty: 2, 59 Type: 0, 60 Duration: 2 * 60, // 2 Minute 61 ImageSize: image.Point{128, 64}, 62 Pool: make(map[[16]byte]*PhraseCaptcha, 1024), 63 idByTime: list.New(), 64 } 65 // cleaner for expired captchas! 66 go pcs.expirationProcessing() 67 return 68 } 69 70 // NewImage make, store and return new captcha! 71 func (pcs *PhraseCaptchas) NewImage(lang Language, imageformat ImageFormat) (pc *PhraseCaptcha) { 72 pc = &PhraseCaptcha{ 73 ID: uuid.NewV4(), 74 ExpireIn: etime.Now() + pcs.Duration, 75 State: StateCreated, 76 } 77 switch pcs.Type { 78 case 0: 79 pc.Answer = pcs.randomDigits() 80 case 1: 81 pc.Answer = pcs.randomWord(lang) 82 case 2: 83 pc.Answer = pcs.randomMath() 84 } 85 pc.Image = pcs.createImage(pc.Answer, imageformat) 86 87 pcs.Pool[pc.ID] = pc 88 return 89 } 90 91 // GetAudio return exiting captcha with audio generated if exits otherwise returns nil! 92 func (pcs *PhraseCaptchas) GetAudio(captchaID [16]byte, lang Language, audioFormat AudioFormat) (pc *PhraseCaptcha) { 93 pc = pcs.Pool[captchaID] 94 if pc == nil { 95 return nil 96 } 97 pc.Audio = pcs.createAudio(pc.Answer, audioFormat) 98 return 99 } 100 101 // Get return exiting captcha if exits otherwise returns nil! 102 func (pcs *PhraseCaptchas) Get(captchaID [16]byte) (pc *PhraseCaptcha) { 103 pc = pcs.Pool[captchaID] 104 if pc != nil && pc.ExpireIn < etime.Now() { 105 delete(pcs.Pool, captchaID) 106 return nil 107 } 108 return 109 } 110 111 // Solve check answer and return captcha state! 112 func (pcs *PhraseCaptchas) Solve(captchaID [16]byte, answer string) error { 113 var pc *PhraseCaptcha 114 pc = pcs.Pool[captchaID] 115 if pc == nil { 116 return ErrCaptchaNotFound 117 } 118 if pc.ExpireIn < etime.Now() { 119 delete(pcs.Pool, captchaID) 120 return ErrCaptchaExpired 121 } 122 if pc.Answer != answer { 123 pc.State = StateLastAnswerNotValid 124 return ErrCaptchaAnswerNotValid 125 } 126 // Give more time to user to complete any proccess need captcha! 127 pc.ExpireIn += pcs.Duration 128 pc.State = StateSolved 129 return nil 130 } 131 132 // Check return true if captcha exits and solved otherwise returns false! 133 func (pcs *PhraseCaptchas) Check(captchaID [16]byte) error { 134 var pc *PhraseCaptcha 135 pc = pcs.Pool[captchaID] 136 if pc == nil { 137 return ErrCaptchaNotFound 138 } 139 if pc.ExpireIn < etime.Now() { 140 delete(pcs.Pool, captchaID) 141 return ErrCaptchaExpired 142 } 143 if pc.State != StateSolved { 144 return ErrCaptchaNotSolved 145 } 146 return nil 147 } 148 149 func (pcs *PhraseCaptchas) randomDigits() string { 150 var low, hi int64 151 switch pcs.Len { 152 case 6: 153 low, hi = 100000, 999999 154 case 7: 155 low, hi = 1000000, 9999999 156 case 8: 157 low, hi = 10000000, 99999999 158 case 9: 159 low, hi = 100000000, 999999999 160 case 10: 161 low, hi = 1000000000, 9999999999 162 default: 163 low, hi = 1000000000, 9999999999 164 } 165 var rand = low + rand.Int63n(hi-low) 166 return strconv.FormatInt(rand, 10) 167 } 168 169 var src = rand.NewSource(time.Now().UnixNano()) 170 171 const ( 172 letterIdxBits = 6 // 6 bits to represent a letter index 173 letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits 174 ) 175 const englishLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789-" 176 177 func (pcs *PhraseCaptchas) randomWord(lang Language) string { 178 var b = make([]byte, pcs.Len) 179 var i uint8 180 for i = 0; i < pcs.Len; i++ { 181 var random = src.Int63() 182 if idx := int(random & letterIdxMask); idx < len(englishLetters) { 183 b[i] = englishLetters[idx] 184 } 185 random >>= letterIdxBits 186 } 187 return *(*string)(unsafe.Pointer(&b)) 188 } 189 190 func (pcs *PhraseCaptchas) randomMath() string { 191 // TODO::: 192 return "" 193 } 194 195 var goBoldItalic *truetype.Font 196 197 func init() { 198 var err error 199 goBoldItalic, err = freetype.ParseFont(gobolditalic.TTF) 200 if err != nil { 201 // Almost never occur! 202 log.Fatal(err) 203 } 204 } 205 206 func (pcs *PhraseCaptchas) createImage(answer string, imageFormat ImageFormat) []byte { 207 var img = image.NewRGBA(image.Rect(0, 0, pcs.ImageSize.X, pcs.ImageSize.Y)) 208 draw.Draw(img, img.Bounds(), image.White, image.ZP, draw.Src) 209 210 // TODO::: Difficulty??!! 211 var c = freetype.NewContext() 212 var point = freetype.Pt(10, 10+int(c.PointToFixed(24)>>6)) 213 c.SetDst(img) 214 c.SetSrc(image.Black) //(image.NewUniform(color.RGBA{200, 100, 0, 255})) 215 c.SetFont(goBoldItalic) 216 c.SetFontSize(24) 217 c.SetDPI(72) 218 c.SetClip(img.Bounds()) 219 c.DrawString(answer, point) 220 221 var buf bytes.Buffer 222 switch imageFormat { 223 case ImageFormatPNG: 224 png.Encode(&buf, img) 225 case ImageFormatJPEG: 226 jpeg.Encode(&buf, img, &jpeg.Options{Quality: jpeg.DefaultQuality}) 227 } 228 return buf.Bytes() 229 } 230 231 func (pcs *PhraseCaptchas) createAudio(answer string, audioFormat AudioFormat) []byte { 232 return []byte{} 233 } 234 235 func (pcs *PhraseCaptchas) expirationProcessing() { 236 var timer = time.NewTimer(time.Duration(pcs.Duration) * time.Second) 237 for { 238 select { 239 // case shutdownFeedback := <-pcs.shutdownSignal: 240 // timer.Stop() 241 // shutdownFeedback <- struct{}{} 242 // return 243 case <-timer.C: 244 timer.Reset(time.Duration(pcs.Duration) * time.Second) 245 246 if len(pcs.Pool) == 0 { 247 continue 248 } 249 250 // Usually this proccess is less than one second, so get time once for compare! 251 var timeNow = etime.Now() 252 for _, captcha := range pcs.Pool { 253 if captcha.ExpireIn < timeNow { 254 delete(pcs.Pool, captcha.ID) 255 } 256 } 257 } 258 } 259 } 260 261 // https://github.com/search?l=Go&q=captcha&type=Repositories 262 // https://github.com/dchest/captcha 263 // https://github.com/afocus/captcha 264 // https://github.com/steambap/captcha 265 // https://github.com/lifei6671/gocaptcha 266 // https://www.hcaptcha.com/