dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

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 }