dkforest

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

commit da3c0d97c212cbe1b3e61d472df611e63a7b27a7
parent e59b23c8400f806b71fd436e959fe443cac804b1
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Sat, 25 Mar 2023 13:06:15 -0700

upgrade captcha to use characters and numbers

Diffstat:
Mpkg/captcha/captcha.go | 93++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpkg/captcha/image.go | 21+++++++++++----------
Mpkg/captcha/store.go | 18+++++++++---------
Mpkg/web/handlers/data.go | 2++
Mpkg/web/handlers/handlers.go | 7++++---
Mpkg/web/handlers/utils/utils.go | 6+++---
Mpkg/web/public/views/pages/captcha-help.gohtml | 1+
Mpkg/web/public/views/pages/captcha.gohtml | 3+++
8 files changed, 80 insertions(+), 71 deletions(-)

diff --git a/pkg/captcha/captcha.go b/pkg/captcha/captcha.go @@ -53,6 +53,7 @@ import ( "errors" "io" "math/rand" + "strings" "time" ) @@ -86,14 +87,16 @@ func randomId(rnd *rand.Rand) string { return hex.EncodeToString(b) } -// randomDigits returns a byte slice of the given length containing -// pseudorandom numbers in range 0-9. The slice can be used as a captcha +// randomAnswer returns a string of the given length containing +// pseudorandom characters in range A-Z2-7. The string can be used as a captcha // solution. -func randomDigits(length int, rnd *rand.Rand) (out []byte) { +func randomAnswer(length int, rnd *rand.Rand) string { + out := make([]rune, length) + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" for i := 0; i < length; i++ { - out = append(out, byte(rnd.Intn(10))) + out[i] = rune(alphabet[rnd.Intn(32)]) } - return + return string(out) } type Params struct { @@ -104,14 +107,21 @@ type Params struct { // New creates a new captcha with the standard length, saves it in the internal // storage and returns its id. func New() (string, string) { - return newLen(Params{}) + return newLen(Params{}, false) } func NewWithParams(params Params) (id, b64 string) { - return newLen(params) + return newLen(params, false) } -func newLen(params Params) (id, b64 string) { +func NewWithSolution(seed int64) (id string, answer string, captchaImg string, captchaHelpImg string) { + id, img := newLen(Params{Rnd: rand.New(rand.NewSource(seed))}, false) + _, solutionImg := newLen(Params{Rnd: rand.New(rand.NewSource(seed))}, true) + answer, _ = globalStore.Get(id, false) + return id, answer, img, solutionImg +} + +func newLen(params Params, isHelpImg bool) (id, b64 string) { r := rnd s := globalStore if params.Store != nil { @@ -121,11 +131,11 @@ func newLen(params Params) (id, b64 string) { r = params.Rnd } id = randomId(r) - digits := randomDigits(6, r) - s.Set(id, digits) + answer := randomAnswer(6, r) + s.Set(id, answer) var buf bytes.Buffer - _ = writeImage(s, &buf, id, r) + _ = writeImage(s, &buf, id, r, isHelpImg) captchaImg := base64.StdEncoding.EncodeToString(buf.Bytes()) return id, captchaImg @@ -133,19 +143,19 @@ func newLen(params Params) (id, b64 string) { // WriteImage writes PNG-encoded image representation of the captcha with the given id. func WriteImage(w io.Writer, id string, rnd *rand.Rand) error { - return writeImage(globalStore, w, id, rnd) + return writeImage(globalStore, w, id, rnd, false) } func WriteImageWithStore(store Store, w io.Writer, id string, rnd *rand.Rand) error { - return writeImage(store, w, id, rnd) + return writeImage(store, w, id, rnd, false) } -func writeImage(store Store, w io.Writer, id string, rnd *rand.Rand) error { +func writeImage(store Store, w io.Writer, id string, rnd *rand.Rand, isHelpImg bool) error { d, err := store.Get(id, false) if err != nil { return err } - _, err = NewImage(d, config.CaptchaDifficulty.Load(), rnd).WriteTo(w) + _, err = NewImage(d, config.CaptchaDifficulty.Load(), rnd, isHelpImg).WriteTo(w) return err } @@ -154,28 +164,34 @@ func writeImage(store Store, w io.Writer, id string, rnd *rand.Rand) error { // // The function deletes the captcha with the given id from the internal // storage, so that the same captcha can't be verified anymore. -func Verify(id string, digits []byte) error { - return verify(globalStore, id, digits, true) +func Verify(id, answer string) error { + return verify(globalStore, id, answer, true) +} + +func VerifyDangerous(store Store, id, answer string) error { + return verify(store, id, answer, false) } -func VerifyDangerous(store Store, id string, digits []byte) error { - return verify(store, id, digits, false) +func reverse(s string) string { + rns := []rune(s) + for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 { + rns[i], rns[j] = rns[j], rns[i] + } + return string(rns) } -func verify(store Store, id string, digits []byte, clear bool) error { - if digits == nil || len(digits) == 0 { +func verify(store Store, id, answer string, clear bool) error { + if len(answer) == 0 { return ErrInvalidCaptcha } + answer = strings.ToUpper(answer) realID, err := store.Get(id, clear) if err != nil { return err } - if !bytes.Equal(digits, realID) { - // reverse digits - for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 { - digits[i], digits[j] = digits[j], digits[i] - } - if !bytes.Equal(digits, realID) { + if answer != realID { + answer = reverse(answer) + if answer != realID { return ErrInvalidCaptcha } } @@ -185,29 +201,14 @@ func verify(store Store, id string, digits []byte, clear bool) error { // VerifyString is like Verify, but accepts a string of digits. It removes // spaces and commas from the string, but any other characters, apart from // digits and listed above, will cause the function to return false. -func VerifyString(id, digits string) error { - return verifyString(globalStore, id, digits, true) +func VerifyString(id, answer string) error { + return verifyString(globalStore, id, answer, true) } func VerifyStringDangerous(store Store, id, digits string) error { return verifyString(store, id, digits, false) } -func verifyString(store Store, id, digits string, clear bool) error { - if digits == "" { - return ErrInvalidCaptcha - } - ns := make([]byte, len(digits)) - for i := range ns { - d := digits[i] - switch { - case '0' <= d && d <= '9': - ns[i] = d - '0' - case d == ' ' || d == ',': - // ignore - default: - return ErrInvalidCaptcha - } - } - return verify(store, id, ns, clear) +func verifyString(store Store, id, answer string, clear bool) error { + return verify(store, id, answer, clear) } diff --git a/pkg/captcha/image.go b/pkg/captcha/image.go @@ -13,7 +13,6 @@ import ( "io" "math" "math/rand" - "strconv" "sync" ) @@ -44,7 +43,7 @@ type Image struct { } type Number struct { - Num int + Num uint8 Angle float64 FaceIdx int Face font1.Face @@ -338,7 +337,7 @@ func (m *Image) RandInt(min, max int) int { if max < min { min, max = max, min } - return int(m.rnd.Int63n(int64(max-min+1))) + min + return m.rnd.Intn(max-min+1) + min } func (m *Image) RandFloat(min, max float64) float64 { @@ -354,13 +353,13 @@ func (m *Image) RandFloat(min, max float64) float64 { var signatureFace = loadSignatureFace() var faces = loadFontFaces() -func NewImage(digits []byte, difficulty int64, rnd *rand.Rand) *Image { +func NewImage(answer string, difficulty int64, rnd *rand.Rand, isHelpImg bool) *Image { //start := time.Now() //defer func() { fmt.Println("took:", time.Since(start)) }() m := new(Image) m.rnd = rnd - m.renderHelpImg = false + m.renderHelpImg = isHelpImg m.displayDebug = false m.imageWidth = 240 m.imageHeight = 120 @@ -396,19 +395,21 @@ func NewImage(digits []byte, difficulty int64, rnd *rand.Rand) *Image { m.thirdPath = m.generateSemiValidPath(usedCoordsMap) updateUsedCoordsMap(m.thirdPath, usedCoordsMap) + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + // Generate all numbers m.numbersMatrix = make([][]Number, 0) for row := 0; row < m.nbNumRows; row++ { numbers := make([]Number, 0) for col := 0; col < m.nbNumCols; col++ { - d := m.RandInt(0, 9) + d := m.RandInt(0, 31) opacity := uint8(255) if m.renderHelpImg { opacity = 100 } faceIdx := m.RandInt(0, len(faces)-1) numbers = append(numbers, Number{ - Num: d, + Num: alphabet[d], Angle: m.RandFloat(-40, 40), FaceIdx: faceIdx, Face: faces[faceIdx], @@ -419,7 +420,7 @@ func NewImage(digits []byte, difficulty int64, rnd *rand.Rand) *Image { // Replace numbers by the answer digits for i, c := range m.mainPath.points { - d := int(digits[i]) + d := answer[i] // 7 with negative angle looks like 1 if d == 7 { m.numbersMatrix[c.Y][c.X].Angle = m.RandFloat(0, 40) @@ -451,7 +452,7 @@ func loadFontFaces() (out []font1.Face) { loadFF("font/Lato-Regular.ttf", 25) loadFF("font/JessicaGroovyBabyFINAL2.ttf", 25) loadFF("font/PineappleDelight.ttf", 25) - loadFF("font/df66c.ttf", 20) + //loadFF("font/df66c.ttf", 20) loadFF("font/agengsans.ttf", 30) return } @@ -895,7 +896,7 @@ func (m *Image) renderDigits(idx int64, clr color.RGBA) { m.c.Translate(float64(x+m.numWidth/2), float64(y+m.numHeight/2)) m.c.Rotate(gg.Radians(num.Angle)) m.c.SetFontFace(num.Face) - m.c.DrawString(strconv.Itoa(num.Num), float64(-m.numWidth/2), float64(m.numHeight/2)) + m.c.DrawString(string(num.Num), float64(-m.numWidth/2), float64(m.numHeight/2)) //m.withState(func() { // m.c.SetColor(color.RGBA{255, 0, 0, 255}) // m.c.DrawCircle(0, 0, 2) diff --git a/pkg/captcha/store.go b/pkg/captcha/store.go @@ -18,11 +18,11 @@ import ( // method after the certain amount of captchas has been stored.) type Store interface { // Set sets the digits for the captcha id. - Set(id string, digits []byte) + Set(id string, answer string) // Get returns stored digits for the captcha id. Clear indicates // whether the captcha must be deleted from the store. - Get(id string, clear bool) (digits []byte, err error) + Get(id string, clear bool) (answer string, err error) } // expValue stores timestamp and id of captchas. It is used in the list inside @@ -31,7 +31,7 @@ type Store interface { type idByTimeValue struct { timestamp time.Time id string - digits []byte + answer string } // memoryStore is an internal store for captcha ids and their values. @@ -57,9 +57,9 @@ func NewMemoryStore(collectNum int, expiration time.Duration) Store { return s } -func (s *memoryStore) Set(id string, digits []byte) { +func (s *memoryStore) Set(id string, answer string) { s.Lock() - s.digitsById[id] = idByTimeValue{time.Now(), id, digits} + s.digitsById[id] = idByTimeValue{time.Now(), id, answer} s.numStored++ if s.numStored <= s.collectNum { s.Unlock() @@ -69,7 +69,7 @@ func (s *memoryStore) Set(id string, digits []byte) { go s.collect() } -func (s *memoryStore) Get(id string, clear bool) ([]byte, error) { +func (s *memoryStore) Get(id string, clear bool) (string, error) { if !clear { // When we don't need to clear captcha, acquire read lock. s.RLock() @@ -80,15 +80,15 @@ func (s *memoryStore) Get(id string, clear bool) ([]byte, error) { } el, ok := s.digitsById[id] if el.timestamp.Add(s.expiration).Before(time.Now()) { - return nil, ErrCaptchaExpired + return "", ErrCaptchaExpired } if !ok { - return nil, ErrNotFound + return "", ErrNotFound } if clear { delete(s.digitsById, id) } - return el.digits, nil + return el.answer, nil } func (s *memoryStore) collect() { diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go @@ -418,6 +418,8 @@ type captchaData struct { Frames []string CaptchaImg string CaptchaID string + ShowAnswer bool + Answer string Captcha string Success string Error string diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -26,7 +26,6 @@ import ( "io" "io/ioutil" "math" - "math/rand" "net/http" "net/url" "os" @@ -3876,9 +3875,11 @@ func CaptchaRequiredHandler(c echo.Context) error { func CaptchaHandler(c echo.Context) error { var data captchaData + if c.QueryParam("a") != "" { + data.ShowAnswer = true + } setCaptcha := func(seed int64) { - rnd := rand.New(rand.NewSource(seed)) - data.CaptchaID, data.CaptchaImg = captcha.NewWithParams(captcha.Params{Rnd: rnd}) + data.CaptchaID, data.Answer, data.CaptchaImg, _ = captcha.NewWithSolution(seed) } data.Seed = time.Now().UnixNano() data.Ts = time.Now().UnixMilli() diff --git a/pkg/web/handlers/utils/utils.go b/pkg/web/handlers/utils/utils.go @@ -152,12 +152,12 @@ func CreateAprilFoolCookie(c echo.Context, v int) { } // CaptchaVerifyString ensure that all captcha across the website makes HB life miserable. -func CaptchaVerifyString(c echo.Context, id, digits string) error { +func CaptchaVerifyString(c echo.Context, id, answer string) error { // Can bypass captcha in dev mode - if config.Development.IsTrue() && digits == "000000" { + if config.Development.IsTrue() && answer == "000000" { return nil } - if err := captcha.VerifyString(id, digits); err != nil { + if err := captcha.VerifyString(id, answer); err != nil { return errors.New("invalid answer") } // HB has 50% chance of having the captcha fails for no reason diff --git a/pkg/web/public/views/pages/captcha-help.gohtml b/pkg/web/public/views/pages/captcha-help.gohtml @@ -14,6 +14,7 @@ You can start by finding a long line that doesn't have dashes in it, follow that line until you find the edge.<br /> Then go back and follow the line to the other edge and note the 6 numbers that the line cross path with.<br /> The direction is not important, both combination will work.<br /> + The possible characters are all capital letters A-Z and numbers from 2 to 7.<br /> </p> <img src="/public/img/captcha_example1.png" alt="" /> <img src="/public/img/captcha_example2.png" alt="" /> diff --git a/pkg/web/public/views/pages/captcha.gohtml b/pkg/web/public/views/pages/captcha.gohtml @@ -36,6 +36,9 @@ </div> <div class="form-group"> <button class="btn btn-primary btn-lg btn-block">{{ t "Test captcha" . }}</button> + {{ if .Data.ShowAnswer }} + {{ .Data.Answer }} + {{ end }} </div> </form> <form method="get" class="d-inline mr-2">