added hcaptcha
This commit is contained in:
@@ -89,6 +89,12 @@ type OllamaConfig struct {
|
||||
EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"`
|
||||
}
|
||||
|
||||
type HCaptchaConfig struct {
|
||||
SecretKey string `env:"HCAPTCHA_SECRET_KEY"`
|
||||
VerifyURL string `env:"HCAPTCHA_VERIFY_URL"`
|
||||
Timeout time.Duration `env:"HCAPTCHA_TIMEOUT"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration with priority: OS environment variables > .env file
|
||||
// OS environment variables take precedence over .env file values
|
||||
func LoadConfig[T any](path string, config T) (T, error) {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user