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}) +}