146 lines
3.8 KiB
Go
146 lines
3.8 KiB
Go
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
|
|
}
|