package antispam import ( "encoding/json" "fmt" "html/template" "log" "log/slog" "net/http" "net/url" "strings" ) type HCaptcha struct { formField template.HTML secretKey string siteverifyEndpoint string } func (hcaptcha *HCaptcha) Script() template.HTML { return `` } func (hcaptcha *HCaptcha) FormField() template.HTML { return hcaptcha.formField } func (hcaptcha *HCaptcha) CheckRequest(r *http.Request) (bool, error) { err := r.ParseForm() if err != nil { return false, fmt.Errorf("error parsing form: %w", err) } token := r.PostForm.Get("x-captcha-response") if token == "" { slog.Debug("No hCaptcha response token in request") return false, nil } form := url.Values{} form.Set("secret", hcaptcha.secretKey) form.Set("response", token) res, err := http.PostForm(hcaptcha.siteverifyEndpoint, form) if err != nil { return false, fmt.Errorf("hCaptcha site verify error: %w", err) } var data struct { Success bool `json:"success"` Hostname string `json:"hostname"` ErrorCodes []string `json:"error-codes"` } err = json.NewDecoder(res.Body).Decode(&data) if err != nil { return false, fmt.Errorf("hCaptcha site verify: JSON decode error: %w", err) } if res.StatusCode != http.StatusOK { return false, fmt.Errorf("hCaptcha site verify: error codes %s, status %d", strings.Join(data.ErrorCodes, ", "), res.StatusCode) } result := data.Success && data.Hostname == r.URL.Hostname() return result, nil } // NewHcaptcha returns an antispam implementation using the hCaptcha service. func NewHcaptcha(siteKey string, secretKey string) *HCaptcha { return newHcaptcha(siteKey, secretKey, "https://hcaptcha.com") } func newHcaptcha(siteKey string, secretKey string, endpoint string) *HCaptcha { formField := `
` siteverifyEndpoint, err := url.JoinPath(endpoint, "siteverify") if err != nil { log.Panicf("error constructing siteverify endpoint: %v", err) } return &HCaptcha{ formField: template.HTML(formField), secretKey: secretKey, siteverifyEndpoint: siteverifyEndpoint, } }