package hcaptcha import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "tm/pkg/logger" ) const ( // DefaultVerifyURL is the default hCaptcha verification endpoint DefaultVerifyURL = "https://hcaptcha.com/siteverify" // DefaultTimeout is the default timeout for hCaptcha verification requests DefaultTimeout = 10 * time.Second ) // Verifier handles hCaptcha token verification type Verifier interface { Verify(token string, remoteIP string) (*VerificationResponse, error) } // Config holds configuration for hCaptcha verification type Config struct { SecretKey string `env:"HCAPTCHA_SECRET_KEY"` VerifyURL string `env:"HCAPTCHA_VERIFY_URL"` Timeout time.Duration `env:"HCAPTCHA_TIMEOUT"` } // VerificationResponse represents the response from hCaptcha verification API type VerificationResponse struct { Success bool `json:"success"` ChallengeTS string `json:"challenge_ts,omitempty"` Hostname string `json:"hostname,omitempty"` Credit bool `json:"credit,omitempty"` ErrorCodes []string `json:"error-codes,omitempty"` } // verifier implements the Verifier interface type verifier struct { secretKey string verifyURL string timeout time.Duration logger logger.Logger client *http.Client } // NewVerifier creates a new hCaptcha verifier func NewVerifier(config *Config, log logger.Logger) Verifier { verifyURL := config.VerifyURL if verifyURL == "" { verifyURL = DefaultVerifyURL } timeout := config.Timeout if timeout == 0 { timeout = DefaultTimeout } return &verifier{ secretKey: config.SecretKey, verifyURL: verifyURL, timeout: timeout, logger: log, client: &http.Client{ Timeout: timeout, }, } } // Verify verifies an hCaptcha token with the hCaptcha API func (v *verifier) Verify(token string, remoteIP string) (*VerificationResponse, error) { if token == "" { return &VerificationResponse{ Success: false, ErrorCodes: []string{"missing-input-response"}, }, fmt.Errorf("hCaptcha token is required") } // Prepare form data data := url.Values{} data.Set("secret", v.secretKey) data.Set("response", token) if remoteIP != "" { data.Set("remoteip", remoteIP) } // Create request with form data in body req, err := http.NewRequest("POST", v.verifyURL, strings.NewReader(data.Encode())) if err != nil { v.logger.Error("Failed to create hCaptcha verification request", map[string]interface{}{ "error": err.Error(), }) return nil, fmt.Errorf("failed to create verification request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") // Make request resp, err := v.client.Do(req) if err != nil { v.logger.Error("Failed to verify hCaptcha token", map[string]interface{}{ "error": err.Error(), }) return nil, fmt.Errorf("failed to verify token: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { v.logger.Error("Failed to read hCaptcha verification response", map[string]interface{}{ "error": err.Error(), }) return nil, fmt.Errorf("failed to read verification response: %w", err) } // Parse response var verificationResponse VerificationResponse if err := json.Unmarshal(body, &verificationResponse); err != nil { v.logger.Error("Failed to parse hCaptcha verification response", map[string]interface{}{ "error": err.Error(), "body": string(body), "status": resp.StatusCode, }) return nil, fmt.Errorf("failed to parse verification response: %w", err) } // Log verification result if verificationResponse.Success { v.logger.Debug("hCaptcha token verified successfully", map[string]interface{}{ "hostname": verificationResponse.Hostname, }) } else { v.logger.Warn("hCaptcha token verification failed", map[string]interface{}{ "error_codes": verificationResponse.ErrorCodes, }) } return &verificationResponse, nil }