mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2025-11-08 15:15:05 +01:00
* ✨ Add wicketkeeper captcha * ✨ Anom config * 🍱 fix readme * 🍱 fix lint * 🍱 fix lint * 🍱 normalize * 🍱 fix lint * 🍱 fix lint * ✨ Add env for RemediationStatusCode (#250) * ✨ Add env for defaultStatusCode * 📝 doc * ✨change name of the parameter * 🔧 Add config check * fix lint * 📈 Report traffic dropped metrics to LAPI (#223) * Initial implementation * fix * fixes * Fixes * xx * progress * xx * xx * xx * fix linter * Progress * Fixes * xx * xx * Remove trace logger * Last fix * fix lint * fix lint * fix lint --------- Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com> * ✨ Anom config * 🍱 fix readme * 🍱 fix lint * 🍱 normalize * 🍱 fix lint * 📝 Add documentation * 📝 Fix example and makefile and doc for wicketkeeper * 🍱 fix last things * 🍱 add disclaimer to use maxlerebourg docker image * 🍱 Use official wicketpeeker image * 🍱 revert unnecessary code * 🍱 fix --------- Co-authored-by: David <deivid.garcia.garcia@gmail.com> Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com> Co-authored-by: mhx <mathieu@hanotaux.fr>
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
// Package captcha implements utility for captcha management.
|
|
package captcha
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
|
)
|
|
|
|
// Client Captcha client.
|
|
type Client struct {
|
|
Valid bool
|
|
siteKey string
|
|
secretKey string
|
|
remediationCustomHeader string
|
|
gracePeriodSeconds int64
|
|
captchaTemplate *template.Template
|
|
cacheClient *cache.Client
|
|
httpClient *http.Client
|
|
log *logger.Log
|
|
infoProvider *infoProvider
|
|
}
|
|
|
|
// Information for self-hosted provider.
|
|
type infoProvider struct {
|
|
js string
|
|
key string
|
|
response string
|
|
validate string
|
|
}
|
|
|
|
//nolint:gochecknoglobals
|
|
var infoProviders = map[string]*infoProvider{
|
|
configuration.HcaptchaProvider: {
|
|
js: "https://hcaptcha.com/1/api.js",
|
|
key: "h-captcha",
|
|
response: "h-captcha-response",
|
|
validate: "https://api.hcaptcha.com/siteverify",
|
|
},
|
|
configuration.RecaptchaProvider: {
|
|
js: "https://www.google.com/recaptcha/api.js",
|
|
key: "g-recaptcha",
|
|
response: "g-recaptcha-response",
|
|
validate: "https://www.google.com/recaptcha/api/siteverify",
|
|
},
|
|
configuration.TurnstileProvider: {
|
|
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
|
key: "cf-turnstile",
|
|
response: "cf-turnstile-response",
|
|
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
},
|
|
}
|
|
|
|
// New Initialize captcha client.
|
|
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, js, key, response, validate, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
|
|
c.Valid = provider != ""
|
|
if !c.Valid {
|
|
return nil
|
|
}
|
|
var info *infoProvider
|
|
if provider == configuration.CustomProvider {
|
|
info = &infoProvider{js: js, key: key, response: response, validate: validate}
|
|
} else {
|
|
info = infoProviders[provider]
|
|
}
|
|
c.infoProvider = info
|
|
c.siteKey = siteKey
|
|
c.secretKey = secretKey
|
|
c.remediationCustomHeader = remediationCustomHeader
|
|
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
|
|
c.captchaTemplate = html
|
|
c.gracePeriodSeconds = gracePeriodSeconds
|
|
c.log = log
|
|
c.httpClient = httpClient
|
|
c.cacheClient = cacheClient
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP Handle captcha html page or validation.
|
|
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
|
valid, err := c.Validate(r)
|
|
if err != nil {
|
|
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
|
|
rw.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if valid {
|
|
c.log.Debug("captcha:ServeHTTP captcha:valid")
|
|
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
|
|
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if c.remediationCustomHeader != "" {
|
|
rw.Header().Set(c.remediationCustomHeader, "captcha")
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
err = c.captchaTemplate.Execute(rw, map[string]string{
|
|
"SiteKey": c.siteKey,
|
|
"FrontendJS": c.infoProvider.js,
|
|
"FrontendKey": c.infoProvider.key,
|
|
})
|
|
if err != nil {
|
|
c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
|
|
}
|
|
}
|
|
|
|
// Check Verify if the captcha is already done.
|
|
func (c *Client) Check(remoteIP string) bool {
|
|
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
|
|
passed := value == cache.CaptchaDoneValue
|
|
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
|
return passed
|
|
}
|
|
|
|
type responseProvider struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
// Validate Verify the captcha from provider API.
|
|
func (c *Client) Validate(r *http.Request) (bool, error) {
|
|
if r.Method != http.MethodPost {
|
|
c.log.Debug("captcha:Validate invalid method: " + r.Method)
|
|
return false, nil
|
|
}
|
|
var response = r.FormValue(c.infoProvider.response)
|
|
if response == "" {
|
|
c.log.Debug("captcha:Validate no captcha response found in request")
|
|
return false, nil
|
|
}
|
|
var body = url.Values{}
|
|
body.Add("secret", c.secretKey)
|
|
body.Add("response", response)
|
|
res, err := c.httpClient.PostForm(c.infoProvider.validate, body)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() {
|
|
if err = res.Body.Close(); err != nil {
|
|
c.log.Error("captcha:Validate " + err.Error())
|
|
}
|
|
}()
|
|
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
|
|
c.log.Debug("captcha:Validate responseType:noJson")
|
|
return false, nil
|
|
}
|
|
var captchaResponse responseProvider
|
|
err = json.NewDecoder(res.Body).Decode(&captchaResponse)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
c.log.Debug(fmt.Sprintf("captcha:Validate success:%v", captchaResponse.Success))
|
|
return captchaResponse.Success, nil
|
|
}
|