From 9f9796f2f37c03523dc0f336c3f91f3e46587ff3 Mon Sep 17 00:00:00 2001 From: WittF Date: Thu, 19 Jun 2025 11:31:17 +0800 Subject: [PATCH] Add Cap Captcha support (#2511) * Add Cap Captcha support - Add CaptchaCap type constant in types.go - Add Cap struct with InstanceURL, KeyID, and KeySecret fields - Add CapCaptcha method in provider.go to return Cap settings - Add default settings for Cap captcha in setting.go - Implement Cap captcha verification logic in middleware - Expose Cap captcha settings in site API This adds support for Cap captcha service as an alternative captcha option alongside existing reCAPTCHA, Turnstile and built-in captcha options. * update cap json tags --- inventory/setting.go | 3 +++ middleware/captcha.go | 58 +++++++++++++++++++++++++++++++++++++++++ pkg/setting/provider.go | 10 +++++++ pkg/setting/types.go | 7 +++++ service/basic/site.go | 5 ++++ 5 files changed, 83 insertions(+) diff --git a/inventory/setting.go b/inventory/setting.go index 8d48439..9bf9b2f 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -143,6 +143,9 @@ var DefaultSettings = map[string]string{ "captcha_ReCaptchaSecret": "defaultSecret", "captcha_turnstile_site_key": "", "captcha_turnstile_site_secret": "", + "captcha_cap_instance_url": "", + "captcha_cap_key_id": "", + "captcha_cap_key_secret": "", "thumb_width": "400", "thumb_height": "300", "thumb_entity_suffix": "._thumb", diff --git a/middleware/captcha.go b/middleware/captcha.go index b97c9ba..af72433 100644 --- a/middleware/captcha.go +++ b/middleware/captcha.go @@ -38,6 +38,9 @@ type ( turnstileResponse struct { Success bool `json:"success"` } + capResponse struct { + Success bool `json:"success"` + } ) // CaptchaRequired 验证请求签名 @@ -127,6 +130,61 @@ func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc { return } + break + case setting.CaptchaCap: + captchaSetting := settings.CapCaptcha(c) + if captchaSetting.InstanceURL == "" || captchaSetting.KeyID == "" || captchaSetting.KeySecret == "" { + l.Warning("Cap verification failed: missing configuration") + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil)) + c.Abort() + return + } + + r := dep.RequestClient( + request2.WithContext(c), + request2.WithLogger(logging.FromContext(c)), + request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}), + ) + + capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.KeyID + "/siteverify" + requestBody := map[string]string{ + "secret": captchaSetting.KeySecret, + "response": service.Ticket, + } + requestData, err := json.Marshal(requestBody) + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + res, err := r.Request("POST", capEndpoint, strings.NewReader(string(requestData))). + CheckHTTPResponse(http.StatusOK). + GetResponse() + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + var capRes capResponse + err = json.Unmarshal([]byte(res), &capRes) + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + if !capRes.Success { + l.Warning("Cap verification failed: validation returned false") + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", nil)) + c.Abort() + return + } + break } } diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 4cc4aa0..2b37922 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -38,6 +38,8 @@ type ( TcCaptcha(ctx context.Context) *TcCaptcha // TurnstileCaptcha returns the Cloudflare Turnstile settings. TurnstileCaptcha(ctx context.Context) *Turnstile + // CapCaptcha returns the Cap settings. + CapCaptcha(ctx context.Context) *Cap // EmailActivationEnabled returns true if email activation is required. EmailActivationEnabled(ctx context.Context) bool // DefaultGroup returns the default group ID for new users. @@ -638,6 +640,14 @@ func (s *settingProvider) TurnstileCaptcha(ctx context.Context) *Turnstile { } } +func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap { + return &Cap{ + InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""), + KeyID: s.getString(ctx, "captcha_cap_key_id", ""), + KeySecret: s.getString(ctx, "captcha_cap_key_secret", ""), + } +} + func (s *settingProvider) ReCaptcha(ctx context.Context) *ReCaptcha { return &ReCaptcha{ Secret: s.getString(ctx, "captcha_ReCaptchaSecret", ""), diff --git a/pkg/setting/types.go b/pkg/setting/types.go index b4c685d..d3e4d5e 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -28,6 +28,7 @@ const ( CaptchaReCaptcha = CaptchaType("recaptcha") CaptchaTcaptcha = CaptchaType("tcaptcha") CaptchaTurnstile = CaptchaType("turnstile") + CaptchaCap = CaptchaType("cap") ) type ReCaptcha struct { @@ -47,6 +48,12 @@ type Turnstile struct { Secret string } +type Cap struct { + InstanceURL string + KeyID string + KeySecret string +} + type SMTP struct { FromName string From string diff --git a/service/basic/site.go b/service/basic/site.go index 814cfea..d291a25 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -28,6 +28,8 @@ type SiteConfig struct { ReCaptchaKey string `json:"captcha_ReCaptchaKey,omitempty"` CaptchaType setting.CaptchaType `json:"captcha_type,omitempty"` TurnstileSiteID string `json:"turnstile_site_id,omitempty"` + CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"` + CapKeyID string `json:"captcha_cap_key_id,omitempty"` RegisterEnabled bool `json:"register_enabled,omitempty"` TosUrl string `json:"tos_url,omitempty"` PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` @@ -119,6 +121,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { userRes := user.BuildUser(u, dep.HashIDEncoder()) logo := settings.Logo(c) reCaptcha := settings.ReCaptcha(c) + capCaptcha := settings.CapCaptcha(c) appSetting := settings.AppSetting(c) return &SiteConfig{ @@ -132,6 +135,8 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { CaptchaType: settings.CaptchaType(c), TurnstileSiteID: settings.TurnstileCaptcha(c).Key, ReCaptchaKey: reCaptcha.Key, + CapInstanceURL: capCaptcha.InstanceURL, + CapKeyID: capCaptcha.KeyID, AppPromotion: appSetting.Promotion, }, nil }