From a6bfdc516ed494c275e25fbca2a2c1feefff3cd0 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Sun, 8 Jan 2023 20:38:38 +0800
Subject: [PATCH 01/11] add new captcha: cloudflare turnstile

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 custom/conf/app.example.ini                   |  7 +-
 .../doc/advanced/config-cheat-sheet.en-us.md  |  5 +-
 .../doc/advanced/config-cheat-sheet.zh-cn.md  | 12 +++
 modules/context/captcha.go                    | 17 +++-
 modules/setting/service.go                    |  6 ++
 modules/setting/setting.go                    |  1 +
 modules/turnstile/turnstile.go                | 93 +++++++++++++++++++
 templates/base/footer.tmpl                    |  3 +
 templates/user/auth/captcha.tmpl              |  4 +
 9 files changed, 143 insertions(+), 5 deletions(-)
 create mode 100644 modules/turnstile/turnstile.go

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index cec5e8cf03821..8a0d0437b1251 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -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
@@ -787,6 +787,11 @@ 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 =
+;CF_REVERSE_PROXY_HEADER =
+;;
 ;; Default value for KeepEmailPrivate
 ;; Each new user will get the value of this setting copied into their profile
 ;DEFAULT_KEEP_EMAIL_PRIVATE = false
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 3b2ff4cbbf1f0..16e5be3190b80 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -644,7 +644,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.
@@ -653,6 +653,9 @@ 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.
+- `CF_REVERSE_PROXY_HEADER` **""**: The http header where the user's real ip is located. Otherwise it should be `""`.
 - `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
diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
index f10b6258c87a2..fd0828c24e79f 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
@@ -147,6 +147,18 @@ 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 获取。
+- `CF_REVERSE_PROXY_HEADER` **""**: http 的 header 字段,用于获取客户端的 ip 供 cloudflare turnstile 验证时使用。如果没有反向代理设置这里应设置为 `""` 。
 
 ### Service - Expore (`service.explore`)
 
diff --git a/modules/context/captcha.go b/modules/context/captcha.go
index 735613504caee..dd4b246a7a382 100644
--- a/modules/context/captcha.go
+++ b/modules/context/captcha.go
@@ -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"
 )
@@ -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
@@ -73,6 +76,14 @@ 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:
+		var ip string
+		if setting.Service.CfReverseProxyHeader == "" {
+			ip = ctx.RemoteAddr()
+		} else {
+			ip = ctx.Req.Header.Get(setting.Service.CfReverseProxyHeader)
+		}
+		valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField), ip)
 	default:
 		ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
 		return
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 7b4bfc5c7b6b2..0992a0c629cdd 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -46,6 +46,9 @@ var Service = struct {
 	RecaptchaSecret                         string
 	RecaptchaSitekey                        string
 	RecaptchaURL                            string
+	CfTurnstileSecret                       string
+	CfTurnstileSitekey                      string
+	CfReverseProxyHeader                    string
 	HcaptchaSecret                          string
 	HcaptchaSitekey                         string
 	McaptchaSecret                          string
@@ -137,6 +140,9 @@ 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.CfReverseProxyHeader = sec.Key("CF_REVERSE_PROXY_HEADER").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/")
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 07290fbfeb9f3..5f6efa52e01a2 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -60,6 +60,7 @@ const (
 	ReCaptcha    = "recaptcha"
 	HCaptcha     = "hcaptcha"
 	MCaptcha     = "mcaptcha"
+	CfTurnstile  = "cfturnstile"
 )
 
 // settings
diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
new file mode 100644
index 0000000000000..8b49c01ddd364
--- /dev/null
+++ b/modules/turnstile/turnstile.go
@@ -0,0 +1,93 @@
+// 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, ip 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},
+		"remoteip": {ip},
+	}
+	// 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: %s", err)
+	}
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err)
+	}
+
+	var jsonResponse Response
+	err = json.Unmarshal(body, &jsonResponse)
+	if err != nil {
+		return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", 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()
+}
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index 3fa0f8e7aca8f..5f3ef74bdcb4f 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -21,6 +21,9 @@
 	{{if eq .CaptchaType "hcaptcha"}}
 		<script src='https://hcaptcha.com/1/api.js' async></script>
 	{{end}}
+	{{if eq .CaptchaType "cfturnstile"}}
+		<script src='https://challenges.cloudflare.com/turnstile/v0/api.js' async defer></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 and the ROOT_URL setting in app.ini is correct.')"></script>
 {{template "custom/footer" .}}
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index 87b22a0720eda..d96203624eb17 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -21,4 +21,8 @@
 		<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>
+{{else if eq .CaptchaType "cfturnstile"}}
+	<div class="inline field captcha-field" style="text-align: center">
+		<div class="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
+	</div>
 {{end}}{{end}}

From 8c634337822b44709610e4045cf2c78d4e3238da Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Wed, 18 Jan 2023 09:42:31 +0800
Subject: [PATCH 02/11] Change from inline css to using css class

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 templates/user/auth/captcha.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index d96203624eb17..2e915fdb01806 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -22,7 +22,7 @@
 		<div class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
 	</div>
 {{else if eq .CaptchaType "cfturnstile"}}
-	<div class="inline field captcha-field" style="text-align: center">
+	<div class="inline field captcha-field tc">
 		<div class="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
 	</div>
 {{end}}{{end}}

From 59186ab2eb9124b3c2b2a55bcf90fdb0dfcbe273 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Thu, 19 Jan 2023 16:53:59 +0800
Subject: [PATCH 03/11] Added dark mode for hcaptcha, recaptcha and cloudflare
 turnstile

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 templates/base/footer.tmpl       |  6 ++---
 templates/user/auth/captcha.tmpl |  8 +++---
 web_src/js/features/captcha.js   | 42 ++++++++++++++++++++++++++++++++
 web_src/js/index.js              |  2 ++
 web_src/less/_form.less          | 14 ++++++++---
 5 files changed, 61 insertions(+), 11 deletions(-)
 create mode 100644 web_src/js/features/captcha.js

diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index 5f3ef74bdcb4f..abcfc95630f51 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -16,13 +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' async defer></script>
+		<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 and the ROOT_URL setting in app.ini is correct.')"></script>
diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index 2e915fdb01806..4cf28b414f911 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -9,20 +9,20 @@
 	</div>
 {{else if eq .CaptchaType "recaptcha"}}
 	<div class="inline field required">
-		<div class="g-recaptcha" data-sitekey="{{.RecaptchaSitekey}}"></div>
+		<div id="captcha" 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" 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" 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 class="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
+		<div id="captcha" captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
 	</div>
 {{end}}{{end}}
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
new file mode 100644
index 0000000000000..aa19ddc737162
--- /dev/null
+++ b/web_src/js/features/captcha.js
@@ -0,0 +1,42 @@
+import {isDarkTheme} from '../utils.js';
+
+export 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('captcha-type')) {
+    case 'g-recaptcha': {
+      // eslint-disable-next-line no-undef
+      if (grecaptcha) {
+        // eslint-disable-next-line no-undef
+        grecaptcha.ready(() => {
+          // eslint-disable-next-line no-undef
+          grecaptcha.render(captchaEl, params);
+        });
+      }
+      break;
+    }
+    case 'cf-turnstile': {
+      // eslint-disable-next-line no-undef
+      if (turnstile) {
+        // eslint-disable-next-line no-undef
+        turnstile.render(captchaEl, params);
+      }
+      break;
+    }
+    case 'h-captcha': {
+      // eslint-disable-next-line no-undef
+      hcaptcha.render(captchaEl, params);
+      break;
+    }
+    default:
+  }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index a866184203fd6..199d77945239d 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -90,6 +90,7 @@ 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';
 
 // Run time-critical code as soon as possible. This is safe to do because this
 // script appears at the end of <body> and rendered HTML is accessible at that point.
@@ -190,6 +191,7 @@ $(document).ready(() => {
 
   initCommitStatuses();
   initMcaptcha();
+  initCaptcha();
 
   initUserAuthLinkAccountView();
   initUserAuthOauth2();
diff --git a/web_src/less/_form.less b/web_src/less/_form.less
index 3d2ec9fb8a9ee..1a1c1678f8b3a 100644
--- a/web_src/less/_form.less
+++ b/web_src/less/_form.less
@@ -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;
   }

From 0821c09d4be89eb5e702e191c37028fbec15378d Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Thu, 19 Jan 2023 18:42:24 +0800
Subject: [PATCH 04/11] Use ctx.RemoteAddr() to get the real ip instead of
 getting it from the http header

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 custom/conf/app.example.ini                           | 1 -
 docs/content/doc/advanced/config-cheat-sheet.en-us.md | 1 -
 docs/content/doc/advanced/config-cheat-sheet.zh-cn.md | 1 -
 modules/context/captcha.go                            | 8 ++++----
 modules/setting/service.go                            | 2 --
 modules/turnstile/turnstile.go                        | 4 ++--
 6 files changed, 6 insertions(+), 11 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 8a0d0437b1251..d915588e5571f 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -790,7 +790,6 @@ ROUTER = console
 ;; Go to https://dash.cloudflare.com/?to=/:account/turnstile to sign up for a key
 ;CF_TURNSTILE_SITEKEY =
 ;CF_TURNSTILE_SECRET =
-;CF_REVERSE_PROXY_HEADER =
 ;;
 ;; Default value for KeepEmailPrivate
 ;; Each new user will get the value of this setting copied into their profile
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 16e5be3190b80..4ef10f1a2fbb9 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -655,7 +655,6 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 - `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.
-- `CF_REVERSE_PROXY_HEADER` **""**: The http header where the user's real ip is located. Otherwise it should be `""`.
 - `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
diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
index fd0828c24e79f..2598f16a14963 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
@@ -158,7 +158,6 @@ menu:
 - `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 获取。
-- `CF_REVERSE_PROXY_HEADER` **""**: http 的 header 字段,用于获取客户端的 ip 供 cloudflare turnstile 验证时使用。如果没有反向代理设置这里应设置为 `""` 。
 
 ### Service - Expore (`service.explore`)
 
diff --git a/modules/context/captcha.go b/modules/context/captcha.go
index dd4b246a7a382..b6c0ebddabdcc 100644
--- a/modules/context/captcha.go
+++ b/modules/context/captcha.go
@@ -5,6 +5,7 @@ package context
 
 import (
 	"fmt"
+	"net"
 	"sync"
 
 	"code.gitea.io/gitea/modules/base"
@@ -78,10 +79,9 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) {
 		valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
 	case setting.CfTurnstile:
 		var ip string
-		if setting.Service.CfReverseProxyHeader == "" {
-			ip = ctx.RemoteAddr()
-		} else {
-			ip = ctx.Req.Header.Get(setting.Service.CfReverseProxyHeader)
+		ip, _, err = net.SplitHostPort(ctx.RemoteAddr())
+		if err != nil {
+			break
 		}
 		valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField), ip)
 	default:
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 0992a0c629cdd..1d33ac6bce88f 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -48,7 +48,6 @@ var Service = struct {
 	RecaptchaURL                            string
 	CfTurnstileSecret                       string
 	CfTurnstileSitekey                      string
-	CfReverseProxyHeader                    string
 	HcaptchaSecret                          string
 	HcaptchaSitekey                         string
 	McaptchaSecret                          string
@@ -142,7 +141,6 @@ func newService() {
 	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.CfReverseProxyHeader = sec.Key("CF_REVERSE_PROXY_HEADER").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/")
diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
index 8b49c01ddd364..1f9d1963e45ac 100644
--- a/modules/turnstile/turnstile.go
+++ b/modules/turnstile/turnstile.go
@@ -52,10 +52,10 @@ func Verify(ctx context.Context, response, ip string) (bool, error) {
 	}
 
 	var jsonResponse Response
-	err = json.Unmarshal(body, &jsonResponse)
-	if err != nil {
+	if err := json.Unmarshal(body, &jsonResponse); err != nil {
 		return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err)
 	}
+
 	var respErr error
 	if len(jsonResponse.ErrorCodes) > 0 {
 		respErr = jsonResponse.ErrorCodes[0]

From c5d9c49af8466dd6d737660bc8cd7212e08e99f3 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Thu, 26 Jan 2023 16:51:46 +0800
Subject: [PATCH 05/11] fix linter errors

Get global variables of various captchas from `window`

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 web_src/js/features/captcha.js | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
index aa19ddc737162..ff7d598007953 100644
--- a/web_src/js/features/captcha.js
+++ b/web_src/js/features/captcha.js
@@ -14,27 +14,23 @@ export function initCaptcha() {
 
   switch (captchaEl.getAttribute('captcha-type')) {
     case 'g-recaptcha': {
-      // eslint-disable-next-line no-undef
-      if (grecaptcha) {
-        // eslint-disable-next-line no-undef
-        grecaptcha.ready(() => {
-          // eslint-disable-next-line no-undef
-          grecaptcha.render(captchaEl, params);
+      if (window.grecaptcha) {
+        window.grecaptcha.ready(() => {
+          window.grecaptcha.render(captchaEl, params);
         });
       }
       break;
     }
     case 'cf-turnstile': {
-      // eslint-disable-next-line no-undef
-      if (turnstile) {
-        // eslint-disable-next-line no-undef
-        turnstile.render(captchaEl, params);
+      if (window.turnstile) {
+        window.turnstile.render(captchaEl, params);
       }
       break;
     }
     case 'h-captcha': {
-      // eslint-disable-next-line no-undef
-      hcaptcha.render(captchaEl, params);
+      if (window.hcaptcha) {
+        window.hcaptcha.render(captchaEl, params);
+      }
       break;
     }
     default:

From 4ba29b3a28011dd635a61ee78e029d0d29cf1756 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Thu, 26 Jan 2023 17:44:06 +0800
Subject: [PATCH 06/11] Move code from mcaptcha.js to captcha.js

Signed-off-by: ByLCY <bylcy@bylcy.dev>
---
 templates/user/auth/captcha.tmpl |  2 +-
 web_src/js/features/captcha.js   | 15 ++++++++++++++-
 web_src/js/features/mcaptcha.js  | 16 ----------------
 web_src/js/index.js              |  2 --
 4 files changed, 15 insertions(+), 20 deletions(-)
 delete mode 100644 web_src/js/features/mcaptcha.js

diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index 4cf28b414f911..da909cf258f49 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -19,7 +19,7 @@
 	<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 id="captcha" class="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div>
+		<div id="captcha" 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">
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
index ff7d598007953..29da93f3e2085 100644
--- a/web_src/js/features/captcha.js
+++ b/web_src/js/features/captcha.js
@@ -1,6 +1,6 @@
 import {isDarkTheme} from '../utils.js';
 
-export function initCaptcha() {
+export async function initCaptcha() {
   const captchaEl = document.querySelector('#captcha');
   if (!captchaEl) return;
 
@@ -33,6 +33,19 @@ export function initCaptcha() {
       }
       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:
   }
 }
diff --git a/web_src/js/features/mcaptcha.js b/web_src/js/features/mcaptcha.js
deleted file mode 100644
index 725e2e28acf11..0000000000000
--- a/web_src/js/features/mcaptcha.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export async function initMcaptcha() {
-  const mCaptchaEl = document.querySelector('.m-captcha');
-  if (!mCaptchaEl) return;
-
-  const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
-  mCaptcha.INPUT_NAME = 'm-captcha-response';
-  const siteKey = mCaptchaEl.getAttribute('data-sitekey');
-  const instanceURL = mCaptchaEl.getAttribute('data-instance-url');
-
-  mCaptcha.default({
-    siteKey: {
-      instanceUrl: new URL(instanceURL),
-      key: siteKey,
-    }
-  });
-}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 199d77945239d..2d96e792d2756 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -88,7 +88,6 @@ 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';
 
@@ -190,7 +189,6 @@ $(document).ready(() => {
   initRepository();
 
   initCommitStatuses();
-  initMcaptcha();
   initCaptcha();
 
   initUserAuthLinkAccountView();

From a152ec7c8d746683d18a9a9bed8fc13938a34c7e Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Sat, 4 Feb 2023 23:27:23 +0800
Subject: [PATCH 07/11] Update modules/turnstile/turnstile.go

---
 modules/turnstile/turnstile.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
index 1f9d1963e45ac..f19169ba0e45d 100644
--- a/modules/turnstile/turnstile.go
+++ b/modules/turnstile/turnstile.go
@@ -43,7 +43,7 @@ func Verify(ctx context.Context, response, ip string) (bool, error) {
 
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
-		return false, fmt.Errorf("Failed to send CAPTCHA response: %s", err)
+		return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err)
 	}
 	defer resp.Body.Close()
 	body, err := io.ReadAll(resp.Body)

From 09ff01526beedd36bdf33e5062574198482fc507 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Sat, 4 Feb 2023 23:27:32 +0800
Subject: [PATCH 08/11] Update modules/turnstile/turnstile.go

---
 modules/turnstile/turnstile.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
index f19169ba0e45d..36ae278522421 100644
--- a/modules/turnstile/turnstile.go
+++ b/modules/turnstile/turnstile.go
@@ -48,7 +48,7 @@ func Verify(ctx context.Context, response, ip string) (bool, error) {
 	defer resp.Body.Close()
 	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err)
+		return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err)
 	}
 
 	var jsonResponse Response

From 0d185a78ca15ab8b9d87c6f06fb7055ed078e685 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Sat, 4 Feb 2023 23:27:40 +0800
Subject: [PATCH 09/11] Update modules/turnstile/turnstile.go

---
 modules/turnstile/turnstile.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
index 36ae278522421..361f897d2894f 100644
--- a/modules/turnstile/turnstile.go
+++ b/modules/turnstile/turnstile.go
@@ -53,7 +53,7 @@ func Verify(ctx context.Context, response, ip string) (bool, error) {
 
 	var jsonResponse Response
 	if err := json.Unmarshal(body, &jsonResponse); err != nil {
-		return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err)
+		return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err)
 	}
 
 	var respErr error

From 4b3b828c8d308a558b0fe5cfb35c8276c9e13214 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Sun, 5 Feb 2023 12:48:13 +0800
Subject: [PATCH 10/11] Modify custom attribute

Custom attribute changed from `captcha-type` to `data-captcha-type`
---
 templates/user/auth/captcha.tmpl | 8 ++++----
 web_src/js/features/captcha.js   | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl
index da909cf258f49..a794c8f543ea8 100644
--- a/templates/user/auth/captcha.tmpl
+++ b/templates/user/auth/captcha.tmpl
@@ -9,20 +9,20 @@
 	</div>
 {{else if eq .CaptchaType "recaptcha"}}
 	<div class="inline field required">
-		<div id="captcha" captcha-type="g-recaptcha" class="g-recaptcha-style" 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 id="captcha" captcha-type="h-captcha" class="h-captcha-style" 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 id="captcha" captcha-type="m-captcha" 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" captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
+		<div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div>
 	</div>
 {{end}}{{end}}
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
index 29da93f3e2085..3da5dbda41854 100644
--- a/web_src/js/features/captcha.js
+++ b/web_src/js/features/captcha.js
@@ -12,7 +12,7 @@ export async function initCaptcha() {
     theme: isDark ? 'dark' : 'light'
   };
 
-  switch (captchaEl.getAttribute('captcha-type')) {
+  switch (captchaEl.getAttribute('data-captcha-type')) {
     case 'g-recaptcha': {
       if (window.grecaptcha) {
         window.grecaptcha.ready(() => {

From 46aa739879cd5a199a3d74be43b1a66c6e954a05 Mon Sep 17 00:00:00 2001
From: ByLCY <bylcy@bylcy.dev>
Date: Sun, 5 Feb 2023 13:11:56 +0800
Subject: [PATCH 11/11] Remove the remoteip parameter for Turnstile

---
 modules/context/captcha.go     | 8 +-------
 modules/turnstile/turnstile.go | 3 +--
 2 files changed, 2 insertions(+), 9 deletions(-)

diff --git a/modules/context/captcha.go b/modules/context/captcha.go
index b6c0ebddabdcc..07232e9390663 100644
--- a/modules/context/captcha.go
+++ b/modules/context/captcha.go
@@ -5,7 +5,6 @@ package context
 
 import (
 	"fmt"
-	"net"
 	"sync"
 
 	"code.gitea.io/gitea/modules/base"
@@ -78,12 +77,7 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) {
 	case setting.MCaptcha:
 		valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
 	case setting.CfTurnstile:
-		var ip string
-		ip, _, err = net.SplitHostPort(ctx.RemoteAddr())
-		if err != nil {
-			break
-		}
-		valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField), ip)
+		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
diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go
index 361f897d2894f..38d0233446608 100644
--- a/modules/turnstile/turnstile.go
+++ b/modules/turnstile/turnstile.go
@@ -26,12 +26,11 @@ type Response struct {
 }
 
 // Verify calls Cloudflare Turnstile API to verify token
-func Verify(ctx context.Context, response, ip string) (bool, error) {
+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},
-		"remoteip": {ip},
 	}
 	// Basically a copy of http.PostForm, but with a context
 	req, err := http.NewRequestWithContext(ctx, http.MethodPost,