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:
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">