Skip to content

Commit a6bfdc5

Browse files
committed
add new captcha: cloudflare turnstile
Signed-off-by: ByLCY <[email protected]>
1 parent f74293f commit a6bfdc5

File tree

9 files changed

+143
-5
lines changed

9 files changed

+143
-5
lines changed

custom/conf/app.example.ini

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ ROUTER = console
765765
;; Enable this to require captcha validation for login
766766
;REQUIRE_CAPTCHA_FOR_LOGIN = false
767767
;;
768-
;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha.
768+
;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile.
769769
;CAPTCHA_TYPE = image
770770
;;
771771
;; Change this to use recaptcha.net or other recaptcha service
@@ -787,6 +787,11 @@ ROUTER = console
787787
;MCAPTCHA_SECRET =
788788
;MCAPTCHA_SITEKEY =
789789
;;
790+
;; Go to https://dash.cloudflare.com/?to=/:account/turnstile to sign up for a key
791+
;CF_TURNSTILE_SITEKEY =
792+
;CF_TURNSTILE_SECRET =
793+
;CF_REVERSE_PROXY_HEADER =
794+
;;
790795
;; Default value for KeepEmailPrivate
791796
;; Each new user will get the value of this setting copied into their profile
792797
;DEFAULT_KEEP_EMAIL_PRIVATE = false

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
644644
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`.
645645
- `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation
646646
even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`.
647-
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\]
647+
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\]
648648
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
649649
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
650650
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net.
@@ -653,6 +653,9 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
653653
- `MCAPTCHA_SECRET`: **""**: Go to your mCaptcha instance to get a secret for mCaptcha.
654654
- `MCAPTCHA_SITEKEY`: **""**: Go to your mCaptcha instance to get a sitekey for mCaptcha.
655655
- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: Set the mCaptcha URL.
656+
- `CF_TURNSTILE_SECRET` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a secret for cloudflare turnstile.
657+
- `CF_TURNSTILE_SITEKEY` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a sitekey for cloudflare turnstile.
658+
- `CF_REVERSE_PROXY_HEADER` **""**: The http header where the user's real ip is located. Otherwise it should be `""`.
656659
- `DEFAULT_KEEP_EMAIL_PRIVATE`: **false**: By default set users to keep their email address private.
657660
- `DEFAULT_ALLOW_CREATE_ORGANIZATION`: **true**: Allow new users to create organizations by default.
658661
- `DEFAULT_USER_IS_RESTRICTED`: **false**: Give new users restricted permissions by default

docs/content/doc/advanced/config-cheat-sheet.zh-cn.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ menu:
147147
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。
148148
- `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。
149149
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`
150+
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\],人机验证类型,分别表示图片认证、 recaptcha 、 hcaptcha 、mcaptcha 、和 cloudlfare 的 turnstile。
151+
- `RECAPTCHA_SECRET`: **""**: recaptcha 服务的密钥,可在 https://www.google.com/recaptcha/admin 获取。
152+
- `RECAPTCHA_SITEKEY`: **""**: recaptcha 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。
153+
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: 设置 recaptcha 的 url 。
154+
- `HCAPTCHA_SECRET`: **""**: hcaptcha 服务的密钥,可在 https://www.hcaptcha.com/ 获取。
155+
- `HCAPTCHA_SITEKEY`: **""**: hcaptcha 服务的网站密钥,可在 https://www.hcaptcha.com/ 获取。
156+
- `MCAPTCHA_SECRET`: **""**: mCaptcha 服务的密钥。
157+
- `MCAPTCHA_SITEKEY`: **""**: mCaptcha 服务的网站密钥。
158+
- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: 设置 remCaptchacaptcha 的 url 。
159+
- `CF_TURNSTILE_SECRET` **""**: cloudlfare turnstile 服务的密钥,可在 https://dash.cloudflare.com/?to=/:account/turnstile 获取。
160+
- `CF_TURNSTILE_SITEKEY` **""**: cloudlfare turnstile 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。
161+
- `CF_REVERSE_PROXY_HEADER` **""**: http 的 header 字段,用于获取客户端的 ip 供 cloudflare turnstile 验证时使用。如果没有反向代理设置这里应设置为 `""`
150162

151163
### Service - Expore (`service.explore`)
152164

modules/context/captcha.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/mcaptcha"
1515
"code.gitea.io/gitea/modules/recaptcha"
1616
"code.gitea.io/gitea/modules/setting"
17+
"code.gitea.io/gitea/modules/turnstile"
1718

1819
"gitea.com/go-chi/captcha"
1920
)
@@ -47,12 +48,14 @@ func SetCaptchaData(ctx *Context) {
4748
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
4849
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
4950
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
51+
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
5052
}
5153

5254
const (
53-
gRecaptchaResponseField = "g-recaptcha-response"
54-
hCaptchaResponseField = "h-captcha-response"
55-
mCaptchaResponseField = "m-captcha-response"
55+
gRecaptchaResponseField = "g-recaptcha-response"
56+
hCaptchaResponseField = "h-captcha-response"
57+
mCaptchaResponseField = "m-captcha-response"
58+
cfTurnstileResponseField = "cf-turnstile-response"
5659
)
5760

5861
// VerifyCaptcha verifies Captcha data
@@ -73,6 +76,14 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) {
7376
valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField))
7477
case setting.MCaptcha:
7578
valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
79+
case setting.CfTurnstile:
80+
var ip string
81+
if setting.Service.CfReverseProxyHeader == "" {
82+
ip = ctx.RemoteAddr()
83+
} else {
84+
ip = ctx.Req.Header.Get(setting.Service.CfReverseProxyHeader)
85+
}
86+
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField), ip)
7687
default:
7788
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
7889
return

modules/setting/service.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ var Service = struct {
4646
RecaptchaSecret string
4747
RecaptchaSitekey string
4848
RecaptchaURL string
49+
CfTurnstileSecret string
50+
CfTurnstileSitekey string
51+
CfReverseProxyHeader string
4952
HcaptchaSecret string
5053
HcaptchaSitekey string
5154
McaptchaSecret string
@@ -137,6 +140,9 @@ func newService() {
137140
Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("")
138141
Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("")
139142
Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/")
143+
Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("")
144+
Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("")
145+
Service.CfReverseProxyHeader = sec.Key("CF_REVERSE_PROXY_HEADER").MustString("")
140146
Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("")
141147
Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("")
142148
Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/")

modules/setting/setting.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const (
6060
ReCaptcha = "recaptcha"
6161
HCaptcha = "hcaptcha"
6262
MCaptcha = "mcaptcha"
63+
CfTurnstile = "cfturnstile"
6364
)
6465

6566
// settings

modules/turnstile/turnstile.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package turnstile
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/url"
12+
"strings"
13+
14+
"code.gitea.io/gitea/modules/json"
15+
"code.gitea.io/gitea/modules/setting"
16+
)
17+
18+
// Response is the structure of JSON returned from API
19+
type Response struct {
20+
Success bool `json:"success"`
21+
ChallengeTS string `json:"challenge_ts"`
22+
Hostname string `json:"hostname"`
23+
ErrorCodes []ErrorCode `json:"error-codes"`
24+
Action string `json:"login"`
25+
Cdata string `json:"cdata"`
26+
}
27+
28+
// Verify calls Cloudflare Turnstile API to verify token
29+
func Verify(ctx context.Context, response, ip string) (bool, error) {
30+
// Cloudflare turnstile official access instruction address: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
31+
post := url.Values{
32+
"secret": {setting.Service.CfTurnstileSecret},
33+
"response": {response},
34+
"remoteip": {ip},
35+
}
36+
// Basically a copy of http.PostForm, but with a context
37+
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
38+
"https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode()))
39+
if err != nil {
40+
return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err)
41+
}
42+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
43+
44+
resp, err := http.DefaultClient.Do(req)
45+
if err != nil {
46+
return false, fmt.Errorf("Failed to send CAPTCHA response: %s", err)
47+
}
48+
defer resp.Body.Close()
49+
body, err := io.ReadAll(resp.Body)
50+
if err != nil {
51+
return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err)
52+
}
53+
54+
var jsonResponse Response
55+
err = json.Unmarshal(body, &jsonResponse)
56+
if err != nil {
57+
return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err)
58+
}
59+
var respErr error
60+
if len(jsonResponse.ErrorCodes) > 0 {
61+
respErr = jsonResponse.ErrorCodes[0]
62+
}
63+
return jsonResponse.Success, respErr
64+
}
65+
66+
// ErrorCode is a reCaptcha error
67+
type ErrorCode string
68+
69+
// String fulfills the Stringer interface
70+
func (e ErrorCode) String() string {
71+
switch e {
72+
case "missing-input-secret":
73+
return "The secret parameter was not passed."
74+
case "invalid-input-secret":
75+
return "The secret parameter was invalid or did not exist."
76+
case "missing-input-response":
77+
return "The response parameter was not passed."
78+
case "invalid-input-response":
79+
return "The response parameter is invalid or has expired."
80+
case "bad-request":
81+
return "The request was rejected because it was malformed."
82+
case "timeout-or-duplicate":
83+
return "The response parameter has already been validated before."
84+
case "internal-error":
85+
return "An internal error happened while validating the response. The request can be retried."
86+
}
87+
return string(e)
88+
}
89+
90+
// Error fulfills the error interface
91+
func (e ErrorCode) Error() string {
92+
return e.String()
93+
}

templates/base/footer.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
{{if eq .CaptchaType "hcaptcha"}}
2222
<script src='https://hcaptcha.com/1/api.js' async></script>
2323
{{end}}
24+
{{if eq .CaptchaType "cfturnstile"}}
25+
<script src='https://challenges.cloudflare.com/turnstile/v0/api.js' async defer></script>
26+
{{end}}
2427
{{end}}
2528
<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + ', please make sure the asset files can be accessed and the ROOT_URL setting in app.ini is correct.')"></script>
2629
{{template "custom/footer" .}}

templates/user/auth/captcha.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
<div class="border-secondary w-100-small" id="mcaptcha__widget-container" style="width: 50%; height: 5em"></div>
2222
<div class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
2323
</div>
24+
{{else if eq .CaptchaType "cfturnstile"}}
25+
<div class="inline field captcha-field" style="text-align: center">
26+
<div class="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
27+
</div>
2428
{{end}}{{end}}

0 commit comments

Comments
 (0)