captcha.go (7179B)
1 // Copyright 2011 Dmitry Chestnykh. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE file. 4 5 // Package captcha implements generation and verification of image and audio 6 // CAPTCHAs. 7 // 8 // A captcha solution is the sequence of digits 0-9 with the defined length. 9 // There are two captcha representations: image and audio. 10 // 11 // An image representation is a PNG-encoded image with the solution printed on 12 // it in such a way that makes it hard for computers to solve it using OCR. 13 // 14 // An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound with 15 // the spoken solution (currently in English, Russian, Chinese, and Japanese). 16 // To make it hard for computers to solve audio captcha, the voice that 17 // pronounces numbers has random speed and pitch, and there is a randomly 18 // generated background noise mixed into the sound. 19 // 20 // This package doesn't require external files or libraries to generate captcha 21 // representations; it is self-contained. 22 // 23 // To make captchas one-time, the package includes a memory storage that stores 24 // captcha ids, their solutions, and expiration time. Used captchas are removed 25 // from the store immediately after calling Verify or VerifyString, while 26 // unused captchas (user loaded a page with captcha, but didn't submit the 27 // form) are collected automatically after the predefined expiration time. 28 // Developers can also provide custom store (for example, which saves captcha 29 // ids and solutions in database) by implementing Store interface and 30 // registering the object with SetCustomStore. 31 // 32 // Captchas are created by calling New, which returns the captcha id. Their 33 // representations, though, are created on-the-fly by calling WriteImage or 34 // WriteAudio functions. Created representations are not stored anywhere, but 35 // subsequent calls to these functions with the same id will write the same 36 // captcha solution. Reload function will create a new different solution for 37 // the provided captcha, allowing users to "reload" captcha if they can't solve 38 // the displayed one without reloading the whole page. Verify and VerifyString 39 // are used to verify that the given solution is the right one for the given 40 // captcha id. 41 // 42 // Server provides an http.Handler which can serve image and audio 43 // representations of captchas automatically from the URL. It can also be used 44 // to reload captchas. Refer to Server function documentation for details, or 45 // take a look at the example in "capexample" subdirectory. 46 package captcha 47 48 import ( 49 "bytes" 50 "dkforest/pkg/config" 51 "encoding/base64" 52 "encoding/hex" 53 "errors" 54 "io" 55 "math/rand" 56 "strings" 57 "time" 58 ) 59 60 const ( 61 //alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 62 ALPHABET = "0123456789" 63 // The number of captchas created that triggers garbage collection used 64 // by default store. 65 CollectNum = 100 66 // Expiration time of captchas used by default store. 67 Expiration = 10 * time.Minute 68 ) 69 70 var ( 71 ErrNotFound = errors.New("captcha: id not found") 72 ErrInvalidCaptcha = errors.New("invalid captcha") 73 ErrCaptchaExpired = errors.New("captcha expired") 74 // globalStore is a shared storage for captchas, generated by New function. 75 globalStore = NewMemoryStore(CollectNum, Expiration) 76 ) 77 78 // SetCustomStore sets custom storage for captchas, replacing the default 79 // memory store. This function must be called before generating any captchas. 80 func SetCustomStore(s Store) { 81 globalStore = s 82 } 83 84 func randomId(rnd *rand.Rand) string { 85 b := make([]byte, 10) 86 _, _ = rnd.Read(b) 87 return hex.EncodeToString(b) 88 } 89 90 // randomAnswer returns a string of the given length containing 91 // pseudorandom characters in range A-Z2-7. The string can be used as a captcha 92 // solution. 93 func randomAnswer(length int, rnd *rand.Rand) string { 94 out := make([]rune, length) 95 alphabet := ALPHABET 96 for i := 0; i < length; i++ { 97 out[i] = rune(alphabet[rnd.Intn(len(alphabet))]) 98 } 99 return string(out) 100 } 101 102 type Params struct { 103 Store Store 104 Rnd *rand.Rand 105 } 106 107 // New creates a new captcha with the standard length, saves it in the internal 108 // storage and returns its id. 109 func New() (string, string) { 110 return newLen(Params{}, false) 111 } 112 113 func NewWithParams(params Params) (id, b64 string) { 114 return newLen(params, false) 115 } 116 117 func NewWithSolution(seed int64) (id string, answer string, captchaImg string, captchaHelpImg string) { 118 id, img := newLen(Params{Rnd: rand.New(rand.NewSource(seed))}, false) 119 _, solutionImg := newLen(Params{Rnd: rand.New(rand.NewSource(seed))}, true) 120 answer, _ = globalStore.Get(id, false) 121 return id, answer, img, solutionImg 122 } 123 124 func newLen(params Params, isHelpImg bool) (id, b64 string) { 125 r := rand.New(rand.NewSource(time.Now().UnixNano())) // rand.New is not thread-safe 126 s := globalStore 127 if params.Store != nil { 128 s = params.Store 129 } 130 if params.Rnd != nil { 131 r = params.Rnd 132 } 133 id = randomId(r) 134 answer := randomAnswer(6, r) 135 s.Set(id, answer) 136 137 var buf bytes.Buffer 138 _ = writeImage(s, &buf, id, r, isHelpImg) 139 captchaImg := base64.StdEncoding.EncodeToString(buf.Bytes()) 140 141 return id, captchaImg 142 } 143 144 // WriteImage writes PNG-encoded image representation of the captcha with the given id. 145 func WriteImage(w io.Writer, id string, rnd *rand.Rand) error { 146 return writeImage(globalStore, w, id, rnd, false) 147 } 148 149 func WriteImageWithStore(store Store, w io.Writer, id string, rnd *rand.Rand) error { 150 return writeImage(store, w, id, rnd, false) 151 } 152 153 func writeImage(store Store, w io.Writer, id string, rnd *rand.Rand, isHelpImg bool) error { 154 d, err := store.Get(id, false) 155 if err != nil { 156 return err 157 } 158 _, err = NewImage(d, config.CaptchaDifficulty.Load(), rnd, isHelpImg).WriteTo(w) 159 return err 160 } 161 162 // Verify returns true if the given digits are the ones that were used to 163 // create the given captcha id. 164 // 165 // The function deletes the captcha with the given id from the internal 166 // storage, so that the same captcha can't be verified anymore. 167 func Verify(id, answer string) error { 168 return verify(globalStore, id, answer, true) 169 } 170 171 func VerifyDangerous(store Store, id, answer string) error { 172 return verify(store, id, answer, false) 173 } 174 175 func reverse(s string) string { 176 rns := []rune(s) 177 for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 { 178 rns[i], rns[j] = rns[j], rns[i] 179 } 180 return string(rns) 181 } 182 183 func verify(store Store, id, answer string, clear bool) error { 184 if len(answer) == 0 { 185 return ErrInvalidCaptcha 186 } 187 answer = strings.ToUpper(answer) 188 realID, err := store.Get(id, clear) 189 if err != nil { 190 return err 191 } 192 if answer != realID { 193 answer = reverse(answer) 194 if answer != realID { 195 return ErrInvalidCaptcha 196 } 197 } 198 return nil 199 } 200 201 // VerifyString is like Verify, but accepts a string of digits. It removes 202 // spaces and commas from the string, but any other characters, apart from 203 // digits and listed above, will cause the function to return false. 204 func VerifyString(id, answer string) error { 205 return verifyString(globalStore, id, answer, true) 206 } 207 208 func VerifyStringDangerous(store Store, id, digits string) error { 209 return verifyString(store, id, digits, false) 210 } 211 212 func verifyString(store Store, id, answer string, clear bool) error { 213 return verify(store, id, answer, clear) 214 }