mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 22:40:39 +00:00
refactor: move go-chi/captcha into Gitea (#39529)
The `gitea.com/go-chi/captcha` package only exists for Gitea, so it moves into `modules/imagecaptcha`. - Reloading an expired challenge shows a new image instead of a broken one - Every answer is consumed on its first check - OpenID registration stops after a failed captcha --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package imagecaptcha
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheKeyPrefix = "captcha_"
|
||||
ttlSeconds = 600
|
||||
codeLength = 6
|
||||
)
|
||||
|
||||
var primaryColors = []color.RGBA{ // readable on both light and dark backgrounds
|
||||
{R: 234, G: 67, B: 53, A: 255},
|
||||
{R: 66, G: 133, B: 244, A: 255},
|
||||
{R: 52, G: 168, B: 83, A: 255},
|
||||
{R: 251, G: 188, B: 5, A: 255},
|
||||
{R: 171, G: 71, B: 188, A: 255},
|
||||
}
|
||||
|
||||
type pngBufferPool struct{ sync.Pool }
|
||||
|
||||
func (p *pngBufferPool) Get() *png.EncoderBuffer {
|
||||
buf, _ := p.Pool.Get().(*png.EncoderBuffer)
|
||||
return buf
|
||||
}
|
||||
|
||||
func (p *pngBufferPool) Put(buf *png.EncoderBuffer) {
|
||||
p.Pool.Put(buf)
|
||||
}
|
||||
|
||||
func randomCode() string {
|
||||
s := "000000" + strconv.Itoa(util.FastCryptoRandomInt(1000000))
|
||||
return s[len(s)-6:]
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
IdLength int
|
||||
IdRegexp *regexp.Regexp
|
||||
NoiseKey []byte
|
||||
},
|
||||
) {
|
||||
ret.IdLength = 40
|
||||
ret.IdRegexp = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
||||
ret.NoiseKey = util.FastCryptoRandomBytes(32)
|
||||
return
|
||||
})
|
||||
|
||||
// noiseRand makes refetches of an image identical, so averaging them does not remove the noise
|
||||
func noiseRand(id, code string) *rand.Rand {
|
||||
mac := hmac.New(sha256.New, globalVars().NoiseKey)
|
||||
_, _ = mac.Write([]byte(id + "\x00" + code))
|
||||
return util.FastCryptoRand([32]byte(mac.Sum(nil)))
|
||||
}
|
||||
|
||||
func CreateNew() (string, error) {
|
||||
id := util.FastCryptoRandomHex(globalVars().IdLength)
|
||||
_, err := PrepareCode(id, true)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func PrepareCode(id string, generateNew bool) (code string, err error) {
|
||||
if !globalVars().IdRegexp.MatchString(id) {
|
||||
return "", nil
|
||||
}
|
||||
cacheKey := cacheKeyPrefix + id
|
||||
if generateNew {
|
||||
code = randomCode()
|
||||
if err = cache.GetCache().Put(cacheKey, code, ttlSeconds); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
code, _ = cache.GetCache().Get(cacheKey)
|
||||
}
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func Verify(id, answer string) bool {
|
||||
if !globalVars().IdRegexp.MatchString(id) {
|
||||
return false
|
||||
}
|
||||
key := cacheKeyPrefix + id
|
||||
code, ok := cache.GetCache().Get(key)
|
||||
_ = cache.GetCache().Delete(key)
|
||||
return ok && answer == code
|
||||
}
|
||||
|
||||
func ServeImage(resp http.ResponseWriter, req *http.Request) {
|
||||
urlQuery := req.URL.Query()
|
||||
id, reload := urlQuery.Get("id"), urlQuery.Get("reload") != ""
|
||||
code, err := PrepareCode(id, reload)
|
||||
if err != nil {
|
||||
log.Error("Failed to prepare captcha code for id %s: %v", id, err)
|
||||
http.Error(resp, "Failed to prepare captcha code", http.StatusInternalServerError)
|
||||
return
|
||||
} else if code == "" {
|
||||
http.NotFound(resp, req)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header().Set("Cache-Control", "no-store")
|
||||
resp.Header().Set("Content-Type", "image/png")
|
||||
if req.Method == http.MethodGet {
|
||||
pngEncoder := png.Encoder{BufferPool: &pngBufferPool{}}
|
||||
_ = pngEncoder.Encode(resp, drawImage(noiseRand(id, code), code))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user