package services import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "go.uber.org/zap" ) // CaptchaService verifies CAPTCHA tokens. // F027/F029: ORIGIN_FEATURES_REGISTRY.md — CAPTCHA anti-bot. // Supports Cloudflare Turnstile (preferred over reCAPTCHA per ORIGIN spec). type CaptchaService struct { secretKey string verifyURL string enabled bool httpClient *http.Client logger *zap.Logger } // CaptchaConfig holds configuration for the CAPTCHA service. type CaptchaConfig struct { Enabled bool SecretKey string // Provider: "turnstile" (default) or "hcaptcha" Provider string } // turnstileResponse is the response from Cloudflare Turnstile verification API. type turnstileResponse struct { Success bool `json:"success"` ErrorCodes []string `json:"error-codes"` } // NewCaptchaService creates a new CAPTCHA verification service. func NewCaptchaService(config CaptchaConfig, logger *zap.Logger) *CaptchaService { verifyURL := "https://challenges.cloudflare.com/turnstile/v0/siteverify" if config.Provider == "hcaptcha" { verifyURL = "https://hcaptcha.com/siteverify" } return &CaptchaService{ secretKey: config.SecretKey, verifyURL: verifyURL, enabled: config.Enabled, httpClient: &http.Client{ Timeout: 10 * time.Second, }, logger: logger, } } // Verify validates a CAPTCHA response token. // Returns nil if valid, error if invalid or verification failed. func (s *CaptchaService) Verify(ctx context.Context, token, remoteIP string) error { if !s.enabled { return nil } if token == "" { return fmt.Errorf("captcha token required") } form := url.Values{ "secret": {s.secretKey}, "response": {token}, } if remoteIP != "" { form.Set("remoteip", remoteIP) } resp, err := s.httpClient.PostForm(s.verifyURL, form) if err != nil { s.logger.Warn("captcha verification request failed", zap.Error(err)) // Fail open: if CAPTCHA service is down, allow the request return nil } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { s.logger.Warn("failed to read captcha response", zap.Error(err)) return nil } var result turnstileResponse if err := json.Unmarshal(body, &result); err != nil { s.logger.Warn("failed to parse captcha response", zap.Error(err)) return nil } if !result.Success { return fmt.Errorf("captcha verification failed: %v", result.ErrorCodes) } return nil } // IsEnabled returns whether CAPTCHA verification is active. func (s *CaptchaService) IsEnabled() bool { return s.enabled }