Add hcaptcha support besides Google ReCaptcha (#2834)

### Pull Request Checklist
This PR add support for hcaptcha.com as an alternative to Google
ReCaptcha. It also makes possible for user to customize ReCaptcha URL
when needed. (Such as use recaptcha.net instead of www.google.com)

This feature needs manual test cuz it involves 3rd party _captcha_.

Signed-off-by: `Simon Ding <dxl@plotbridge.com>`

Co-authored-by: dxl <dxl@plotbridge.com>
This commit is contained in:
X. Ding 2022-10-28 18:25:01 +08:00 committed by GitHub
parent f6035822e7
commit 0782011f54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 45 additions and 11 deletions

View file

@ -31,8 +31,7 @@ const recaptchaTemplate = `
<title>Authentication</title> <title>Authentication</title>
<meta name='viewport' content='width=device-width, initial-scale=1, <meta name='viewport' content='width=device-width, initial-scale=1,
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
<script src="https://www.google.com/recaptcha/api.js" <script src="{{.apiJsUrl}}" async defer></script>
async defer></script>
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script> <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
<script> <script>
function captchaDone() { function captchaDone() {
@ -51,8 +50,8 @@ function captchaDone() {
Please verify that you're not a robot. Please verify that you're not a robot.
</p> </p>
<input type="hidden" name="session" value="{{.session}}" /> <input type="hidden" name="session" value="{{.session}}" />
<div class="g-recaptcha" <div class="{{.sitekeyClass}}"
data-sitekey="{{.siteKey}}" data-sitekey="{{.sitekey}}"
data-callback="captchaDone"> data-callback="captchaDone">
</div> </div>
<noscript> <noscript>
@ -116,7 +115,10 @@ func AuthFallback(
data := map[string]string{ data := map[string]string{
"myUrl": req.URL.String(), "myUrl": req.URL.String(),
"session": sessionID, "session": sessionID,
"siteKey": cfg.RecaptchaPublicKey, "apiJsUrl": cfg.RecaptchaApiJsUrl,
"sitekey": cfg.RecaptchaPublicKey,
"sitekeyClass": cfg.RecaptchaSitekeyClass,
"formField": cfg.RecaptchaFormField,
} }
serveTemplate(w, recaptchaTemplate, data) serveTemplate(w, recaptchaTemplate, data)
} }
@ -155,7 +157,7 @@ func AuthFallback(
return &res return &res
} }
response := req.Form.Get("g-recaptcha-response") response := req.Form.Get(cfg.RecaptchaFormField)
if err := validateRecaptcha(cfg, response, clientIP); err != nil { if err := validateRecaptcha(cfg, response, clientIP); err != nil {
util.GetLogger(req.Context()).Error(err) util.GetLogger(req.Context()).Error(err)
return err return err

View file

@ -20,6 +20,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -336,6 +337,7 @@ func validateRecaptcha(
response string, response string,
clientip string, clientip string,
) *util.JSONResponse { ) *util.JSONResponse {
ip, _, _ := net.SplitHostPort(clientip)
if !cfg.RecaptchaEnabled { if !cfg.RecaptchaEnabled {
return &util.JSONResponse{ return &util.JSONResponse{
Code: http.StatusConflict, Code: http.StatusConflict,
@ -355,7 +357,7 @@ func validateRecaptcha(
url.Values{ url.Values{
"secret": {cfg.RecaptchaPrivateKey}, "secret": {cfg.RecaptchaPrivateKey},
"response": {response}, "response": {response},
"remoteip": {clientip}, "remoteip": {ip},
}, },
) )

View file

@ -179,7 +179,13 @@ client_api:
recaptcha_public_key: "" recaptcha_public_key: ""
recaptcha_private_key: "" recaptcha_private_key: ""
recaptcha_bypass_secret: "" recaptcha_bypass_secret: ""
recaptcha_siteverify_api: ""
# To use hcaptcha.com instead of ReCAPTCHA, set the following parameters, otherwise just keep them empty.
# recaptcha_siteverify_api: "https://hcaptcha.com/siteverify"
# recaptcha_api_js_url: "https://js.hcaptcha.com/1/api.js"
# recaptcha_form_field: "h-captcha-response"
# recaptcha_sitekey_class: "h-captcha"
# TURN server information that this homeserver should send to clients. # TURN server information that this homeserver should send to clients.
turn: turn:

View file

@ -175,7 +175,13 @@ client_api:
recaptcha_public_key: "" recaptcha_public_key: ""
recaptcha_private_key: "" recaptcha_private_key: ""
recaptcha_bypass_secret: "" recaptcha_bypass_secret: ""
recaptcha_siteverify_api: ""
# To use hcaptcha.com instead of ReCAPTCHA, set the following parameters, otherwise just keep them empty.
# recaptcha_siteverify_api: "https://hcaptcha.com/siteverify"
# recaptcha_api_js_url: "https://js.hcaptcha.com/1/api.js"
# recaptcha_form_field: "h-captcha-response"
# recaptcha_sitekey_class: "h-captcha"
# TURN server information that this homeserver should send to clients. # TURN server information that this homeserver should send to clients.
turn: turn:

View file

@ -32,6 +32,12 @@ type ClientAPI struct {
// Boolean stating whether catpcha registration is enabled // Boolean stating whether catpcha registration is enabled
// and required // and required
RecaptchaEnabled bool `yaml:"enable_registration_captcha"` RecaptchaEnabled bool `yaml:"enable_registration_captcha"`
// Recaptcha api.js Url, for compatible with hcaptcha.com, etc.
RecaptchaApiJsUrl string `yaml:"recaptcha_api_js_url"`
// Recaptcha div class for sitekey, for compatible with hcaptcha.com, etc.
RecaptchaSitekeyClass string `yaml:"recaptcha_sitekey_class"`
// Recaptcha form field, for compatible with hcaptcha.com, etc.
RecaptchaFormField string `yaml:"recaptcha_form_field"`
// This Home Server's ReCAPTCHA public key. // This Home Server's ReCAPTCHA public key.
RecaptchaPublicKey string `yaml:"recaptcha_public_key"` RecaptchaPublicKey string `yaml:"recaptcha_public_key"`
// This Home Server's ReCAPTCHA private key. // This Home Server's ReCAPTCHA private key.
@ -75,6 +81,18 @@ func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) {
checkNotEmpty(configErrs, "client_api.recaptcha_public_key", c.RecaptchaPublicKey) checkNotEmpty(configErrs, "client_api.recaptcha_public_key", c.RecaptchaPublicKey)
checkNotEmpty(configErrs, "client_api.recaptcha_private_key", c.RecaptchaPrivateKey) checkNotEmpty(configErrs, "client_api.recaptcha_private_key", c.RecaptchaPrivateKey)
checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", c.RecaptchaSiteVerifyAPI) checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", c.RecaptchaSiteVerifyAPI)
if c.RecaptchaSiteVerifyAPI == "" {
c.RecaptchaSiteVerifyAPI = "https://www.google.com/recaptcha/api/siteverify"
}
if c.RecaptchaApiJsUrl == "" {
c.RecaptchaApiJsUrl = "https://www.google.com/recaptcha/api.js"
}
if c.RecaptchaFormField == "" {
c.RecaptchaFormField = "g-recaptcha"
}
if c.RecaptchaSitekeyClass == "" {
c.RecaptchaSitekeyClass = "g-recaptcha-response"
}
} }
// Ensure there is any spam counter measure when enabling registration // Ensure there is any spam counter measure when enabling registration
if !c.RegistrationDisabled && !c.OpenRegistrationWithoutVerificationEnabled { if !c.RegistrationDisabled && !c.OpenRegistrationWithoutVerificationEnabled {