Skip to content

Commit 7baeb9c

Browse files
ByLCYlunnywolfogre
authored
Add new captcha: cloudflare turnstile (#22369)
Added a new captcha(cloudflare turnstile) and its corresponding document. Cloudflare turnstile official instructions are here: https://developers.cloudflare.com/turnstile Signed-off-by: ByLCY <[email protected]> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: Jason Song <[email protected]>
1 parent e35f8e1 commit 7baeb9c

File tree

13 files changed

+199
-32
lines changed

13 files changed

+199
-32
lines changed

custom/conf/app.example.ini

+5-1
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,10 @@ 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+
;;
790794
;; Default value for KeepEmailPrivate
791795
;; Each new user will get the value of this setting copied into their profile
792796
;DEFAULT_KEEP_EMAIL_PRIVATE = false

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
643643
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`.
644644
- `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation
645645
even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`.
646-
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\]
646+
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\]
647647
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
648648
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
649649
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net.
@@ -652,6 +652,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
652652
- `MCAPTCHA_SECRET`: **""**: Go to your mCaptcha instance to get a secret for mCaptcha.
653653
- `MCAPTCHA_SITEKEY`: **""**: Go to your mCaptcha instance to get a sitekey for mCaptcha.
654654
- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: Set the mCaptcha URL.
655+
- `CF_TURNSTILE_SECRET` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a secret for cloudflare turnstile.
656+
- `CF_TURNSTILE_SITEKEY` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a sitekey for cloudflare turnstile.
655657
- `DEFAULT_KEEP_EMAIL_PRIVATE`: **false**: By default set users to keep their email address private.
656658
- `DEFAULT_ALLOW_CREATE_ORGANIZATION`: **true**: Allow new users to create organizations by default.
657659
- `DEFAULT_USER_IS_RESTRICTED`: **false**: Give new users restricted permissions by default

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

+11
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ 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 获取。
150161

151162
### Service - Expore (`service.explore`)
152163

modules/context/captcha.go

+8-3
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,8 @@ 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+
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
7681
default:
7782
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
7883
return

modules/setting/service.go

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ var Service = struct {
4646
RecaptchaSecret string
4747
RecaptchaSitekey string
4848
RecaptchaURL string
49+
CfTurnstileSecret string
50+
CfTurnstileSitekey string
4951
HcaptchaSecret string
5052
HcaptchaSitekey string
5153
McaptchaSecret string
@@ -137,6 +139,8 @@ func newService() {
137139
Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("")
138140
Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("")
139141
Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/")
142+
Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("")
143+
Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("")
140144
Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("")
141145
Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("")
142146
Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/")

modules/setting/setting.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
ReCaptcha = "recaptcha"
6262
HCaptcha = "hcaptcha"
6363
MCaptcha = "mcaptcha"
64+
CfTurnstile = "cfturnstile"
6465
)
6566

6667
// settings

modules/turnstile/turnstile.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 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+
}
35+
// Basically a copy of http.PostForm, but with a context
36+
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
37+
"https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode()))
38+
if err != nil {
39+
return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err)
40+
}
41+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
42+
43+
resp, err := http.DefaultClient.Do(req)
44+
if err != nil {
45+
return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err)
46+
}
47+
defer resp.Body.Close()
48+
body, err := io.ReadAll(resp.Body)
49+
if err != nil {
50+
return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err)
51+
}
52+
53+
var jsonResponse Response
54+
if err := json.Unmarshal(body, &jsonResponse); err != nil {
55+
return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err)
56+
}
57+
58+
var respErr error
59+
if len(jsonResponse.ErrorCodes) > 0 {
60+
respErr = jsonResponse.ErrorCodes[0]
61+
}
62+
return jsonResponse.Success, respErr
63+
}
64+
65+
// ErrorCode is a reCaptcha error
66+
type ErrorCode string
67+
68+
// String fulfills the Stringer interface
69+
func (e ErrorCode) String() string {
70+
switch e {
71+
case "missing-input-secret":
72+
return "The secret parameter was not passed."
73+
case "invalid-input-secret":
74+
return "The secret parameter was invalid or did not exist."
75+
case "missing-input-response":
76+
return "The response parameter was not passed."
77+
case "invalid-input-response":
78+
return "The response parameter is invalid or has expired."
79+
case "bad-request":
80+
return "The request was rejected because it was malformed."
81+
case "timeout-or-duplicate":
82+
return "The response parameter has already been validated before."
83+
case "internal-error":
84+
return "An internal error happened while validating the response. The request can be retried."
85+
}
86+
return string(e)
87+
}
88+
89+
// Error fulfills the error interface
90+
func (e ErrorCode) Error() string {
91+
return e.String()
92+
}

templates/base/footer.tmpl

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
<!-- Third-party libraries -->
1717
{{if .EnableCaptcha}}
1818
{{if eq .CaptchaType "recaptcha"}}
19-
<script src='{{URLJoin .RecaptchaURL "api.js"}}' async></script>
19+
<script src='{{URLJoin .RecaptchaURL "api.js"}}'></script>
2020
{{end}}
2121
{{if eq .CaptchaType "hcaptcha"}}
22-
<script src='https://hcaptcha.com/1/api.js' async></script>
22+
<script src='https://hcaptcha.com/1/api.js'></script>
23+
{{end}}
24+
{{if eq .CaptchaType "cfturnstile"}}
25+
<script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
2326
{{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.')"></script>

templates/user/auth/captcha.tmpl

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@
99
</div>
1010
{{else if eq .CaptchaType "recaptcha"}}
1111
<div class="inline field required">
12-
<div class="g-recaptcha" data-sitekey="{{.RecaptchaSitekey}}"></div>
12+
<div id="captcha" data-captcha-type="g-recaptcha" class="g-recaptcha-style" data-sitekey="{{.RecaptchaSitekey}}"></div>
1313
</div>
1414
{{else if eq .CaptchaType "hcaptcha"}}
1515
<div class="inline field required">
16-
<div class="h-captcha" data-sitekey="{{.HcaptchaSitekey}}"></div>
16+
<div id="captcha" data-captcha-type="h-captcha" class="h-captcha-style" data-sitekey="{{.HcaptchaSitekey}}"></div>
1717
</div>
1818
{{else if eq .CaptchaType "mcaptcha"}}
1919
<div class="inline field df ac db-small captcha-field">
2020
<span>{{.locale.Tr "captcha"}}</span>
2121
<div class="border-secondary w-100-small" id="mcaptcha__widget-container" style="width: 50%; height: 5em"></div>
22-
<div class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
22+
<div id="captcha" data-captcha-type="m-captcha" class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
23+
</div>
24+
{{else if eq .CaptchaType "cfturnstile"}}
25+
<div class="inline field captcha-field tc">
26+
<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
2327
</div>
2428
{{end}}{{end}}

web_src/js/features/captcha.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {isDarkTheme} from '../utils.js';
2+
3+
export async function initCaptcha() {
4+
const captchaEl = document.querySelector('#captcha');
5+
if (!captchaEl) return;
6+
7+
const siteKey = captchaEl.getAttribute('data-sitekey');
8+
const isDark = isDarkTheme();
9+
10+
const params = {
11+
sitekey: siteKey,
12+
theme: isDark ? 'dark' : 'light'
13+
};
14+
15+
switch (captchaEl.getAttribute('data-captcha-type')) {
16+
case 'g-recaptcha': {
17+
if (window.grecaptcha) {
18+
window.grecaptcha.ready(() => {
19+
window.grecaptcha.render(captchaEl, params);
20+
});
21+
}
22+
break;
23+
}
24+
case 'cf-turnstile': {
25+
if (window.turnstile) {
26+
window.turnstile.render(captchaEl, params);
27+
}
28+
break;
29+
}
30+
case 'h-captcha': {
31+
if (window.hcaptcha) {
32+
window.hcaptcha.render(captchaEl, params);
33+
}
34+
break;
35+
}
36+
case 'm-captcha': {
37+
const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
38+
mCaptcha.INPUT_NAME = 'm-captcha-response';
39+
const instanceURL = captchaEl.getAttribute('data-instance-url');
40+
41+
mCaptcha.default({
42+
siteKey: {
43+
instanceUrl: new URL(instanceURL),
44+
key: siteKey,
45+
}
46+
});
47+
break;
48+
}
49+
default:
50+
}
51+
}

web_src/js/features/mcaptcha.js

-16
This file was deleted.

web_src/js/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ import {initCommonOrganization} from './features/common-organization.js';
8888
import {initRepoWikiForm} from './features/repo-wiki.js';
8989
import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
9090
import {initFormattingReplacements} from './features/formatting.js';
91-
import {initMcaptcha} from './features/mcaptcha.js';
9291
import {initCopyContent} from './features/copycontent.js';
92+
import {initCaptcha} from './features/captcha.js';
9393
import {initRepositoryActionView} from './components/RepoActionView.vue';
9494

9595
// Run time-critical code as soon as possible. This is safe to do because this
@@ -191,7 +191,7 @@ $(document).ready(() => {
191191
initRepositoryActionView();
192192

193193
initCommitStatuses();
194-
initMcaptcha();
194+
initCaptcha();
195195

196196
initUserAuthLinkAccountView();
197197
initUserAuthOauth2();

web_src/less/_form.less

+10-4
Original file line numberDiff line numberDiff line change
@@ -220,18 +220,24 @@ textarea:focus,
220220
}
221221

222222
@media @mediaMdAndUp {
223-
.g-recaptcha,
224-
.h-captcha {
223+
.g-recaptcha-style,
224+
.h-captcha-style {
225225
margin: 0 auto !important;
226226
width: 304px;
227227
padding-left: 30px;
228+
229+
iframe {
230+
border-radius: 5px !important;
231+
width: 302px !important;
232+
height: 76px !important;
233+
}
228234
}
229235
}
230236

231237
@media (max-height: 575px) {
232238
#rc-imageselect,
233-
.g-recaptcha,
234-
.h-captcha {
239+
.g-recaptcha-style,
240+
.h-captcha-style {
235241
transform: scale(.77);
236242
transform-origin: 0 0;
237243
}

0 commit comments

Comments
 (0)