From 0404662e99bbd7779f400108c490e619843ce569 Mon Sep 17 00:00:00 2001 From: Gusted Date: Tue, 27 Aug 2024 01:47:10 +0200 Subject: [PATCH] [CHORE] Move captcha library - This is a fork of https://github.com/dchest/captcha, as https://gitea.com/go-chi/captcha is a fork of github.com/go-macaron/captcha which is a fork (although not properly credited) of a older version of https://github.com/dchest/captcha. Hence why I've just forked the original. - The fork includes some QoL improvements (uses standard library for determistic RNG instead of rolling your own crypto), and removal of audio support (500KiB unused data that bloated the binary otherwise). Flips the image over the x-asis. https://code.forgejo.org/go-chi/captcha/compare/47270f2b55862b38f9f65f615b53c1e04e814ef0..main - This move is needed for the next commit, because gitea.com/go-chi/captcha included the gitea.com/go-chi/cache dependency. --- go.mod | 2 +- go.sum | 4 +-- routers/web/web.go | 4 +-- services/context/captcha.go | 45 +++++++++++++++++++++++++------- templates/user/auth/captcha.tmpl | 5 ++-- tests/integration/signup_test.go | 40 ++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index a60108e59d..187b72d3bf 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( code.forgejo.org/f3/gof3/v3 v3.7.0 code.forgejo.org/forgejo/reply v1.0.2 + code.forgejo.org/go-chi/captcha v0.0.0-20240827192619-ac88f17cdd8e code.forgejo.org/go-chi/session v0.0.0-20240825010209-bd25d509c8bf code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/gitea-vet v0.2.3 @@ -13,7 +14,6 @@ require ( connectrpc.com/connect v1.16.2 gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed gitea.com/go-chi/cache v0.2.0 - gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 diff --git a/go.sum b/go.sum index 19fab9a6e8..7c769f7078 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ code.forgejo.org/forgejo/archiver/v3 v3.5.1 h1:UmmbA7D5550uf71SQjarmrn6yKwOGxtEj code.forgejo.org/forgejo/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= code.forgejo.org/forgejo/reply v1.0.2 h1:dMhQCHV6/O3L5CLWNTol+dNzDAuyCK88z4J/lCdgFuQ= code.forgejo.org/forgejo/reply v1.0.2/go.mod h1:RyZUfzQLc+fuLIGjTSQWDAJWPiL4WtKXB/FifT5fM7U= +code.forgejo.org/go-chi/captcha v0.0.0-20240827192619-ac88f17cdd8e h1:8hqxBSf1M5JavIhz/Rgx3BP8kkwtCe2SP6AVTE6jjm8= +code.forgejo.org/go-chi/captcha v0.0.0-20240827192619-ac88f17cdd8e/go.mod h1:yxZHJ6up9d/mQUu8NHHoCtJj5VcyB+5ArkUY45Hp3hE= code.forgejo.org/go-chi/session v0.0.0-20240825010209-bd25d509c8bf h1:gJRuqEPd3/U0/1YM+uSgbC/fpR8qrcMdvT6E7eSetyM= code.forgejo.org/go-chi/session v0.0.0-20240825010209-bd25d509c8bf/go.mod h1:PcnIg89MAhO1yExkw1QXXNDiPssVdCsMmwUo67g7GD4= code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU= @@ -30,8 +32,6 @@ gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHc gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w= gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE= -gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= -gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= diff --git a/routers/web/web.go b/routers/web/web.go index dccb391270..a1499c7645 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -51,7 +51,7 @@ import ( _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters - "gitea.com/go-chi/captcha" + "code.forgejo.org/go-chi/captcha" chi_middleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/klauspost/compress/gzhttp" @@ -254,7 +254,7 @@ func Routes() *web.Route { if setting.Service.EnableCaptcha { // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url - routes.Methods("GET,HEAD", "/captcha/*", append(mid, captcha.Captchaer(context.GetImageCaptcha()))...) + routes.Methods("GET,HEAD", "/captcha/*", append(mid, captcha.Server(captcha.StdWidth, captcha.StdHeight).ServeHTTP)...) } if setting.Metrics.Enabled { diff --git a/services/context/captcha.go b/services/context/captcha.go index fa8d779f56..8d302dbf87 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -15,24 +15,47 @@ import ( "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/turnstile" + mc "gitea.com/go-chi/cache" - "gitea.com/go-chi/captcha" + "code.forgejo.org/go-chi/captcha" ) var ( imageCaptchaOnce sync.Once - cpt *captcha.Captcha + imageCachePrefix = "captcha:" ) -// GetImageCaptcha returns global image captcha -func GetImageCaptcha() *captcha.Captcha { +type imageCaptchaStore struct { + c mc.Cache +} + +func (c *imageCaptchaStore) Set(id string, digits []byte) { + if err := c.c.Put(imageCachePrefix+id, string(digits), int64(captcha.Expiration.Seconds())); err != nil { + log.Error("Couldn't store captcha cache for %q: %v", id, err) + } +} + +func (c *imageCaptchaStore) Get(id string, clear bool) (digits []byte) { + val, ok := c.c.Get(imageCachePrefix + id).(string) + if !ok { + return digits + } + + if clear { + if err := c.c.Delete(imageCachePrefix + id); err != nil { + log.Error("Couldn't delete captcha cache for %q: %v", id, err) + } + } + + return []byte(val) +} + +// GetImageCaptcha returns image captcha ID. +func GetImageCaptcha() string { imageCaptchaOnce.Do(func() { - cpt = captcha.NewCaptcha(captcha.Options{ - SubURL: setting.AppSubURL, - }) - cpt.Store = cache.GetCache() + captcha.SetCustomStore(&imageCaptchaStore{c: cache.GetCache()}) }) - return cpt + return captcha.New() } // SetCaptchaData sets common captcha data @@ -52,6 +75,8 @@ func SetCaptchaData(ctx *Context) { } const ( + imgCaptchaIDField = "img-captcha-id" + imgCaptchaResponseField = "img-captcha-response" gRecaptchaResponseField = "g-recaptcha-response" hCaptchaResponseField = "h-captcha-response" mCaptchaResponseField = "m-captcha-response" @@ -69,7 +94,7 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) { var err error switch setting.Service.CaptchaType { case setting.ImageCaptcha: - valid = GetImageCaptcha().VerifyReq(ctx.Req) + valid = captcha.VerifyString(ctx.Req.Form.Get(imgCaptchaIDField), ctx.Req.Form.Get(imgCaptchaResponseField)) case setting.ReCaptcha: valid, err = recaptcha.Verify(ctx, ctx.Req.Form.Get(gRecaptchaResponseField)) case setting.HCaptcha: diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl index 8dd4d1cc51..03e360703b 100644 --- a/templates/user/auth/captcha.tmpl +++ b/templates/user/auth/captcha.tmpl @@ -1,10 +1,11 @@ {{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
- {{.Captcha.CreateHTML}} + +
- +
{{else if eq .CaptchaType "recaptcha"}}
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index 582cf6aae1..d5df41fabf 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -167,3 +168,42 @@ func TestSignupEmailChangeForActiveUser(t *testing.T) { user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserY"}) assert.Equal(t, "wrong-email-2@example.com", user.Email) } + +func TestSignupImageCaptcha(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, false)() + defer test.MockVariableValue(&setting.Service.EnableCaptcha, true)() + defer test.MockVariableValue(&setting.Service.CaptchaType, "image")() + c := cache.GetCache() + + req := NewRequest(t, "GET", "/user/sign_up") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + idCaptcha, ok := htmlDoc.Find("input[name='img-captcha-id']").Attr("value") + assert.True(t, ok) + + digits, ok := c.Get("captcha:" + idCaptcha).(string) + assert.True(t, ok) + assert.Len(t, digits, 6) + + digitStr := "" + // Convert digits to ASCII digits. + for _, digit := range digits { + digitStr += string(digit + '0') + } + + req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": "captcha-test", + "email": "captcha-test@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + "img-captcha-id": idCaptcha, + "img-captcha-response": digitStr, + }) + MakeRequest(t, req, http.StatusSeeOther) + + loginUserWithPassword(t, "captcha-test", "examplePassword!1") + + unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "captcha-test", IsActive: true}) +}