-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
Changes from 4 commits
a6bfdc5
8c63433
59186ab
0821c09
034dc52
c5d9c49
4ba29b3
09b7707
a152ec7
09ff015
0d185a7
4b3b828
f178ef4
46aa739
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
wolfogre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
defer resp.Body.Close() | ||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err) | ||
wolfogre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
var jsonResponse Response | ||
if err := json.Unmarshal(body, &jsonResponse); err != nil { | ||
return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err) | ||
wolfogre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-standard attributes usually need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was fixed in this commit 4b3b828 |
||
case 'g-recaptcha': { | ||
// eslint-disable-next-line no-undef | ||
if (grecaptcha) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can avoid the eslint disables by using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm currently in the countryside for the Chinese New Year holiday, and I don't have the right equipment for me to write the code, I'll fix it tomorrow or the day after tomorrow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No hurry. Another method may be to replace the script tags with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have modified the code. The captcha's global variables will be fetched from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, looks good. If the providers support integration without relying on globals (by using url |
||
// 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: | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
remoteip
is optional. Does Gitea guarantee that the site admin always has the correct reverse-proxy config and Gitea can get the correct user remote IP by RemoteAddr? If no, what would happen? Just a question.Checked with hcaptcha code, hcaptcha also supports
remoteip
but it doesn't use it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My personal opinion is the same as this answer. Therefore, I think it may be a better choice to carry
remoteip
in the parameter.But the usage of
remoteip
above is just my personal guess, cloudflare’s official documentation does not explain how they useremoteip
.If you think that third parties should not be given so much information, then I will remove
remoteip
in a future commit.What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I prefer to keep the same behavior as hcaptcha (no
remoteip
)It might still be fine to have the
remoteip
for the cloudflare turnstile, then it could be better to have enough comments.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'm going to remove
remoteip
. If there is enough information in the future to prove that it would be better to carry this parameter, then we will discuss whether to add it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was removed in this commit 46aa739