diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 1689b00e95..1fc2c9ef0f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -585,12 +585,15 @@ ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false ENABLE_REVERSE_PROXY_EMAIL = false ; Enable captcha validation for registration ENABLE_CAPTCHA = false -; Type of captcha you want to use. Options: image, recaptcha +; Type of captcha you want to use. Options: image, recaptcha, hcaptcha CAPTCHA_TYPE = image ; Enable recaptcha to use Google's recaptcha service ; Go to https://www.google.com/recaptcha/admin to sign up for a key RECAPTCHA_SECRET = RECAPTCHA_SITEKEY = +; For hCaptcha, create an account at https://accounts.hcaptcha.com/login to get your keys +HCAPTCHA_SECRET = +HCAPTCHA_SITEKEY = ; Change this to use recaptcha.net or other recaptcha service RECAPTCHA_URL = https://www.google.com/recaptcha/ ; Default value for KeepEmailPrivate diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index a41791b1cf..fbf7affeaf 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -429,10 +429,12 @@ relation to port exhaustion. - `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration. - `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation even for External Accounts (i.e. GitHub, OpenID Connect, etc). You must `ENABLE_CAPTCHA` also. -- `CAPTCHA_TYPE`: **image**: \[image, recaptcha\] +- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha\] - `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha. - `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha. - `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net. +- `HCAPTCHA_SECRET`: **""**: Sign up at https://www.hcaptcha.com/ to get a secret for hcaptcha. +- `HCAPTCHA_SITEKEY`: **""**: Sign up at https://www.hcaptcha.com/ to get a sitekey for hcaptcha. - `DEFAULT_KEEP_EMAIL_PRIVATE`: **false**: By default set users to keep their email address private. - `DEFAULT_ALLOW_CREATE_ORGANIZATION`: **true**: Allow new users to create organizations by default. - `DEFAULT_ENABLE_DEPENDENCIES`: **true**: Enable this to have dependencies enabled by default. diff --git a/go.mod b/go.mod index ac417ac896..1e65e4b98a 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,7 @@ require ( github.com/yuin/goldmark v1.2.1 github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 + go.jolheiser.com/hcaptcha v0.0.4 go.jolheiser.com/pwn v0.0.3 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/net v0.0.0-20200904194848-62affa334b73 diff --git a/go.sum b/go.sum index 7a6fa8aef8..e0df98079a 100644 --- a/go.sum +++ b/go.sum @@ -933,6 +933,8 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.jolheiser.com/hcaptcha v0.0.4 h1:RrDERcr/Tz/kWyJenjVtI+V09RtLinXxlAemiwN5F+I= +go.jolheiser.com/hcaptcha v0.0.4/go.mod h1:aw32WQOxnQZ6E06C0LypCf+sxNxPACyOnq+ZGnrIYho= go.jolheiser.com/pwn v0.0.3 h1:MQowb3QvCL5r5NmHmCPxw93SdjfgJ0q6rAwYn4i1Hjg= go.jolheiser.com/pwn v0.0.3/go.mod h1:/j5Dl8ftNqqJ8Dlx3YTrJV1wIR2lWOTyrNU3Qe7rk6I= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -1085,7 +1087,6 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1136,7 +1137,6 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 999d4cd74d..e657f78e6d 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -83,6 +83,7 @@ type RegisterForm struct { Password string `binding:"MaxSize(255)"` Retype string GRecaptchaResponse string `form:"g-recaptcha-response"` + HcaptchaResponse string `form:"h-captcha-response"` } // Validate validates the fields diff --git a/modules/auth/user_form_auth_openid.go b/modules/auth/user_form_auth_openid.go index c1d19b9bb4..841dbd840a 100644 --- a/modules/auth/user_form_auth_openid.go +++ b/modules/auth/user_form_auth_openid.go @@ -25,6 +25,7 @@ type SignUpOpenIDForm struct { UserName string `binding:"Required;AlphaDashDot;MaxSize(40)"` Email string `binding:"Required;Email;MaxSize(254)"` GRecaptchaResponse string `form:"g-recaptcha-response"` + HcaptchaResponse string `form:"h-captcha-response"` } // Validate validates the fields diff --git a/modules/hcaptcha/hcaptcha.go b/modules/hcaptcha/hcaptcha.go new file mode 100644 index 0000000000..95fe2dd1c3 --- /dev/null +++ b/modules/hcaptcha/hcaptcha.go @@ -0,0 +1,34 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package hcaptcha + +import ( + "context" + + "code.gitea.io/gitea/modules/setting" + + "go.jolheiser.com/hcaptcha" +) + +// Verify calls hCaptcha API to verify token +func Verify(ctx context.Context, response string) (bool, error) { + client, err := hcaptcha.New(setting.Service.HcaptchaSecret, hcaptcha.WithContext(ctx)) + if err != nil { + return false, err + } + + resp, err := client.Verify(response, hcaptcha.PostOptions{ + Sitekey: setting.Service.HcaptchaSitekey, + }) + if err != nil { + return false, err + } + + var respErr error + if len(resp.ErrorCodes) > 0 { + respErr = resp.ErrorCodes[0] + } + return resp.Success, respErr +} diff --git a/modules/recaptcha/recaptcha.go b/modules/recaptcha/recaptcha.go index a9718f2fdd..54ea1dc0b3 100644 --- a/modules/recaptcha/recaptcha.go +++ b/modules/recaptcha/recaptcha.go @@ -5,12 +5,13 @@ package recaptcha import ( + "context" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" - "time" + "strings" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -18,18 +19,29 @@ import ( // Response is the structure of JSON returned from API type Response struct { - Success bool `json:"success"` - ChallengeTS time.Time `json:"challenge_ts"` - Hostname string `json:"hostname"` - ErrorCodes []string `json:"error-codes"` + Success bool `json:"success"` + ChallengeTS string `json:"challenge_ts"` + Hostname string `json:"hostname"` + ErrorCodes []ErrorCode `json:"error-codes"` } const apiURL = "api/siteverify" // Verify calls Google Recaptcha API to verify token -func Verify(response string) (bool, error) { - resp, err := http.PostForm(util.URLJoin(setting.Service.RecaptchaURL, apiURL), - url.Values{"secret": {setting.Service.RecaptchaSecret}, "response": {response}}) +func Verify(ctx context.Context, response string) (bool, error) { + post := url.Values{ + "secret": {setting.Service.RecaptchaSecret}, + "response": {response}, + } + // Basically a copy of http.PostForm, but with a context + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + util.URLJoin(setting.Service.RecaptchaURL, apiURL), strings.NewReader(post.Encode())) + if err != nil { + return false, fmt.Errorf("Failed to create CAPTCHA request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) if err != nil { return false, fmt.Errorf("Failed to send CAPTCHA response: %s", err) } @@ -43,6 +55,36 @@ func Verify(response string) (bool, error) { if err != nil { return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err) } - - return jsonResponse.Success, nil + var respErr error + if len(jsonResponse.ErrorCodes) > 0 { + respErr = jsonResponse.ErrorCodes[0] + } + return jsonResponse.Success, respErr +} + +// ErrorCode is a reCaptcha error +type ErrorCode string + +// String fulfills the Stringer interface +func (e ErrorCode) String() string { + switch e { + case "missing-input-secret": + return "The secret parameter is missing." + case "invalid-input-secret": + return "The secret parameter is invalid or malformed." + case "missing-input-response": + return "The response parameter is missing." + case "invalid-input-response": + return "The response parameter is invalid or malformed." + case "bad-request": + return "The request is invalid or malformed." + case "timeout-or-duplicate": + return "The response is no longer valid: either is too old or has been used previously." + } + return string(e) +} + +// Error fulfills the error interface +func (e ErrorCode) Error() string { + return e.String() } diff --git a/modules/setting/service.go b/modules/setting/service.go index c463b0a9d5..4d03df17a4 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -35,6 +35,8 @@ var Service struct { RecaptchaSecret string RecaptchaSitekey string RecaptchaURL string + HcaptchaSecret string + HcaptchaSitekey string DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool EnableTimetracking bool @@ -76,6 +78,8 @@ func newService() { Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("") Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("") Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/") + Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("") + Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("") Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool() Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true) Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index cdbe07f911..dc7697bca7 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -59,6 +59,7 @@ const ( const ( ImageCaptcha = "image" ReCaptcha = "recaptcha" + HCaptcha = "hcaptcha" ) // settings diff --git a/routers/user/auth.go b/routers/user/auth.go index 96a73c9dd4..32b031fc74 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/eventsource" + "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/recaptcha" @@ -896,15 +897,21 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha { var valid bool + var err error switch setting.Service.CaptchaType { case setting.ImageCaptcha: valid = cpt.VerifyReq(ctx.Req) case setting.ReCaptcha: - valid, _ = recaptcha.Verify(form.GRecaptchaResponse) + valid, err = recaptcha.Verify(ctx.Req.Context(), form.GRecaptchaResponse) + case setting.HCaptcha: + valid, err = hcaptcha.Verify(ctx.Req.Context(), form.HcaptchaResponse) default: ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) return } + if err != nil { + log.Debug("%s", err.Error()) + } if !valid { ctx.Data["Err_Captcha"] = true @@ -1040,6 +1047,7 @@ func SignUp(ctx *context.Context) { ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL ctx.Data["CaptchaType"] = setting.Service.CaptchaType ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey + ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["PageIsSignUp"] = true //Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true @@ -1058,6 +1066,7 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL ctx.Data["CaptchaType"] = setting.Service.CaptchaType ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey + ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["PageIsSignUp"] = true //Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true @@ -1073,15 +1082,21 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo if setting.Service.EnableCaptcha { var valid bool + var err error switch setting.Service.CaptchaType { case setting.ImageCaptcha: valid = cpt.VerifyReq(ctx.Req) case setting.ReCaptcha: - valid, _ = recaptcha.Verify(form.GRecaptchaResponse) + valid, err = recaptcha.Verify(ctx.Req.Context(), form.GRecaptchaResponse) + case setting.HCaptcha: + valid, err = hcaptcha.Verify(ctx.Req.Context(), form.HcaptchaResponse) default: ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) return } + if err != nil { + log.Debug("%s", err.Error()) + } if !valid { ctx.Data["Err_Captcha"] = true diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index ba2c8be8c2..39e75f202d 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/generate" + "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" @@ -330,6 +331,7 @@ func RegisterOpenID(ctx *context.Context) { ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha ctx.Data["CaptchaType"] = setting.Service.CaptchaType ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey + ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL ctx.Data["OpenID"] = oid userName, _ := ctx.Session.Get("openid_determined_username").(string) @@ -359,24 +361,34 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL ctx.Data["CaptchaType"] = setting.Service.CaptchaType ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey + ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["OpenID"] = oid if setting.Service.EnableCaptcha { var valid bool + var err error switch setting.Service.CaptchaType { case setting.ImageCaptcha: valid = cpt.VerifyReq(ctx.Req) case setting.ReCaptcha: - err := ctx.Req.ParseForm() - if err != nil { + if err := ctx.Req.ParseForm(); err != nil { ctx.ServerError("", err) return } - valid, _ = recaptcha.Verify(form.GRecaptchaResponse) + valid, err = recaptcha.Verify(ctx.Req.Context(), form.GRecaptchaResponse) + case setting.HCaptcha: + if err := ctx.Req.ParseForm(); err != nil { + ctx.ServerError("", err) + return + } + valid, err = hcaptcha.Verify(ctx.Req.Context(), form.HcaptchaResponse) default: ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) return } + if err != nil { + log.Debug("%s", err.Error()) + } if !valid { ctx.Data["Err_Captcha"] = true diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index d5d5970784..24bc44444f 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -28,6 +28,9 @@ {{if eq .CaptchaType "recaptcha"}} {{end}} + {{if eq .CaptchaType "hcaptcha"}} + + {{end}} {{end}} {{template "custom/footer" .}} diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 3d9ba17ad7..e52aa2e881 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -51,6 +51,11 @@
{{end}} + {{if and .EnableCaptcha (eq .CaptchaType "hcaptcha")}} +