Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new captcha: cloudflare turnstile #22369

Merged
merged 14 commits into from
Feb 5, 2023
Merged
6 changes: 5 additions & 1 deletion custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ ROUTER = console
;; Enable this to require captcha validation for login
;REQUIRE_CAPTCHA_FOR_LOGIN = false
;;
;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha.
;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile.
;CAPTCHA_TYPE = image
;;
;; Change this to use recaptcha.net or other recaptcha service
Expand All @@ -787,6 +787,10 @@ ROUTER = console
;MCAPTCHA_SECRET =
;MCAPTCHA_SITEKEY =
;;
;; Go to https://dash.cloudflare.com/?to=/:account/turnstile to sign up for a key
;CF_TURNSTILE_SITEKEY =
;CF_TURNSTILE_SECRET =
;;
;; Default value for KeepEmailPrivate
;; Each new user will get the value of this setting copied into their profile
;DEFAULT_KEEP_EMAIL_PRIVATE = false
Expand Down
4 changes: 3 additions & 1 deletion docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`.
- `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation
even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`.
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\]
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\]
- `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.
Expand All @@ -652,6 +652,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
- `MCAPTCHA_SECRET`: **""**: Go to your mCaptcha instance to get a secret for mCaptcha.
- `MCAPTCHA_SITEKEY`: **""**: Go to your mCaptcha instance to get a sitekey for mCaptcha.
- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: Set the mCaptcha URL.
- `CF_TURNSTILE_SECRET` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a secret for cloudflare turnstile.
- `CF_TURNSTILE_SITEKEY` **""**: Go to https://dash.cloudflare.com/?to=/:account/turnstile to get a sitekey for cloudflare turnstile.
- `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_USER_IS_RESTRICTED`: **false**: Give new users restricted permissions by default
Expand Down
11 changes: 11 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@ menu:
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。
- `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。
- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`。
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha, cfturnstile\],人机验证类型,分别表示图片认证、 recaptcha 、 hcaptcha 、mcaptcha 、和 cloudlfare 的 turnstile。
- `RECAPTCHA_SECRET`: **""**: recaptcha 服务的密钥,可在 https://www.google.com/recaptcha/admin 获取。
- `RECAPTCHA_SITEKEY`: **""**: recaptcha 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: 设置 recaptcha 的 url 。
- `HCAPTCHA_SECRET`: **""**: hcaptcha 服务的密钥,可在 https://www.hcaptcha.com/ 获取。
- `HCAPTCHA_SITEKEY`: **""**: hcaptcha 服务的网站密钥,可在 https://www.hcaptcha.com/ 获取。
- `MCAPTCHA_SECRET`: **""**: mCaptcha 服务的密钥。
- `MCAPTCHA_SITEKEY`: **""**: mCaptcha 服务的网站密钥。
- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: 设置 remCaptchacaptcha 的 url 。
- `CF_TURNSTILE_SECRET` **""**: cloudlfare turnstile 服务的密钥,可在 https://dash.cloudflare.com/?to=/:account/turnstile 获取。
- `CF_TURNSTILE_SITEKEY` **""**: cloudlfare turnstile 服务的网站密钥 ,可在 https://www.google.com/recaptcha/admin 获取。

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

Expand Down
11 changes: 8 additions & 3 deletions modules/context/captcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/mcaptcha"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/turnstile"

"gitea.com/go-chi/captcha"
)
Expand Down Expand Up @@ -47,12 +48,14 @@ func SetCaptchaData(ctx *Context) {
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
}

const (
gRecaptchaResponseField = "g-recaptcha-response"
hCaptchaResponseField = "h-captcha-response"
mCaptchaResponseField = "m-captcha-response"
gRecaptchaResponseField = "g-recaptcha-response"
hCaptchaResponseField = "h-captcha-response"
mCaptchaResponseField = "m-captcha-response"
cfTurnstileResponseField = "cf-turnstile-response"
)

// VerifyCaptcha verifies Captcha data
Expand All @@ -73,6 +76,8 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) {
valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField))
case setting.MCaptcha:
valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
case setting.CfTurnstile:
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
default:
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
return
Expand Down
4 changes: 4 additions & 0 deletions modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ var Service = struct {
RecaptchaSecret string
RecaptchaSitekey string
RecaptchaURL string
CfTurnstileSecret string
CfTurnstileSitekey string
HcaptchaSecret string
HcaptchaSitekey string
McaptchaSecret string
Expand Down Expand Up @@ -137,6 +139,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.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("")
Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("")
Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("")
Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("")
Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/")
Expand Down
1 change: 1 addition & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
ReCaptcha = "recaptcha"
HCaptcha = "hcaptcha"
MCaptcha = "mcaptcha"
CfTurnstile = "cfturnstile"
)

// settings
Expand Down
92 changes: 92 additions & 0 deletions modules/turnstile/turnstile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package turnstile

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
)

// Response is the structure of JSON returned from API
type Response struct {
Success bool `json:"success"`
ChallengeTS string `json:"challenge_ts"`
Hostname string `json:"hostname"`
ErrorCodes []ErrorCode `json:"error-codes"`
Action string `json:"login"`
Cdata string `json:"cdata"`
}

// Verify calls Cloudflare Turnstile API to verify token
func Verify(ctx context.Context, response string) (bool, error) {
// Cloudflare turnstile official access instruction address: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
post := url.Values{
"secret": {setting.Service.CfTurnstileSecret},
"response": {response},
}
// Basically a copy of http.PostForm, but with a context
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode()))
if err != nil {
return false, fmt.Errorf("Failed to create CAPTCHA request: %w", 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: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err)
}

var jsonResponse Response
if err := json.Unmarshal(body, &jsonResponse); err != nil {
return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err)
}

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 was not passed."
case "invalid-input-secret":
return "The secret parameter was invalid or did not exist."
case "missing-input-response":
return "The response parameter was not passed."
case "invalid-input-response":
return "The response parameter is invalid or has expired."
case "bad-request":
return "The request was rejected because it was malformed."
case "timeout-or-duplicate":
return "The response parameter has already been validated before."
case "internal-error":
return "An internal error happened while validating the response. The request can be retried."
}
return string(e)
}

// Error fulfills the error interface
func (e ErrorCode) Error() string {
return e.String()
}
7 changes: 5 additions & 2 deletions templates/base/footer.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
<!-- Third-party libraries -->
{{if .EnableCaptcha}}
{{if eq .CaptchaType "recaptcha"}}
<script src='{{URLJoin .RecaptchaURL "api.js"}}' async></script>
<script src='{{URLJoin .RecaptchaURL "api.js"}}'></script>
{{end}}
{{if eq .CaptchaType "hcaptcha"}}
<script src='https://hcaptcha.com/1/api.js' async></script>
<script src='https://hcaptcha.com/1/api.js'></script>
{{end}}
{{if eq .CaptchaType "cfturnstile"}}
<script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script>
{{end}}
{{end}}
<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>
Expand Down
10 changes: 7 additions & 3 deletions templates/user/auth/captcha.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
</div>
{{else if eq .CaptchaType "recaptcha"}}
<div class="inline field required">
<div class="g-recaptcha" data-sitekey="{{.RecaptchaSitekey}}"></div>
<div id="captcha" data-captcha-type="g-recaptcha" class="g-recaptcha-style" data-sitekey="{{.RecaptchaSitekey}}"></div>
</div>
{{else if eq .CaptchaType "hcaptcha"}}
<div class="inline field required">
<div class="h-captcha" data-sitekey="{{.HcaptchaSitekey}}"></div>
<div id="captcha" data-captcha-type="h-captcha" class="h-captcha-style" data-sitekey="{{.HcaptchaSitekey}}"></div>
</div>
{{else if eq .CaptchaType "mcaptcha"}}
<div class="inline field df ac db-small captcha-field">
<span>{{.locale.Tr "captcha"}}</span>
<div class="border-secondary w-100-small" id="mcaptcha__widget-container" style="width: 50%; height: 5em"></div>
<div class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
<div id="captcha" data-captcha-type="m-captcha" class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
</div>
{{else if eq .CaptchaType "cfturnstile"}}
<div class="inline field captcha-field tc">
<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
</div>
{{end}}{{end}}
51 changes: 51 additions & 0 deletions web_src/js/features/captcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {isDarkTheme} from '../utils.js';

export async function initCaptcha() {
const captchaEl = document.querySelector('#captcha');
if (!captchaEl) return;

const siteKey = captchaEl.getAttribute('data-sitekey');
const isDark = isDarkTheme();

const params = {
sitekey: siteKey,
theme: isDark ? 'dark' : 'light'
};

switch (captchaEl.getAttribute('data-captcha-type')) {
case 'g-recaptcha': {
if (window.grecaptcha) {
window.grecaptcha.ready(() => {
window.grecaptcha.render(captchaEl, params);
});
}
break;
}
case 'cf-turnstile': {
if (window.turnstile) {
window.turnstile.render(captchaEl, params);
}
break;
}
case 'h-captcha': {
if (window.hcaptcha) {
window.hcaptcha.render(captchaEl, params);
}
break;
}
case 'm-captcha': {
const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
mCaptcha.INPUT_NAME = 'm-captcha-response';
const instanceURL = captchaEl.getAttribute('data-instance-url');

mCaptcha.default({
siteKey: {
instanceUrl: new URL(instanceURL),
key: siteKey,
}
});
break;
}
default:
}
}
16 changes: 0 additions & 16 deletions web_src/js/features/mcaptcha.js

This file was deleted.

4 changes: 2 additions & 2 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ import {initCommonOrganization} from './features/common-organization.js';
import {initRepoWikiForm} from './features/repo-wiki.js';
import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
import {initFormattingReplacements} from './features/formatting.js';
import {initMcaptcha} from './features/mcaptcha.js';
import {initCopyContent} from './features/copycontent.js';
import {initCaptcha} from './features/captcha.js';
import {initRepositoryActionView} from './components/RepoActionView.vue';

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

initCommitStatuses();
initMcaptcha();
initCaptcha();

initUserAuthLinkAccountView();
initUserAuthOauth2();
Expand Down
14 changes: 10 additions & 4 deletions web_src/less/_form.less
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,24 @@ textarea:focus,
}

@media @mediaMdAndUp {
.g-recaptcha,
.h-captcha {
.g-recaptcha-style,
.h-captcha-style {
margin: 0 auto !important;
width: 304px;
padding-left: 30px;

iframe {
border-radius: 5px !important;
width: 302px !important;
height: 76px !important;
}
}
}

@media (max-height: 575px) {
#rc-imageselect,
.g-recaptcha,
.h-captcha {
.g-recaptcha-style,
.h-captcha-style {
transform: scale(.77);
transform-origin: 0 0;
}
Expand Down