From 7c4f5163e9d6d947dfab66347de4ba2e38e82db3 Mon Sep 17 00:00:00 2001 From: maxlerebourg Date: Mon, 1 Sep 2025 19:41:45 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20custom=20selfhosted=20captcha?= =?UTF-8?q?=20(#259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add wicketkeeper captcha * :sparkles: Anom config * :bento: fix readme * :bento: fix lint * :bento: fix lint * :bento: normalize * :bento: fix lint * :bento: 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 * :sparkles: Anom config * :bento: fix readme * :bento: fix lint * :bento: normalize * :bento: fix lint * :memo: Add documentation * πŸ“ Fix example and makefile and doc for wicketkeeper * :bento: fix last things * :bento: add disclaimer to use maxlerebourg docker image * :bento: Use official wicketpeeker image * :bento: revert unnecessary code * :bento: fix --------- Co-authored-by: David Co-authored-by: max.lerebourg Co-authored-by: mhx --- Makefile | 12 +- README.md | 17 +- bouncer.go | 5 + .../docker-compose.appsec-enabled.yml | 8 +- examples/captcha/README.md | 2 +- examples/captcha/captcha.html | 2 +- examples/custom-captcha/README.md | 76 ++++ examples/custom-captcha/acquis.yaml | 4 + examples/custom-captcha/captcha.html | 338 ++++++++++++++++++ examples/custom-captcha/docker-compose.yml | 111 ++++++ examples/custom-captcha/profiles.yaml | 17 + pkg/captcha/captcha.go | 63 ++-- pkg/configuration/configuration.go | 40 ++- 13 files changed, 649 insertions(+), 46 deletions(-) create mode 100644 examples/custom-captcha/README.md create mode 100644 examples/custom-captcha/acquis.yaml create mode 100644 examples/custom-captcha/captcha.html create mode 100644 examples/custom-captcha/docker-compose.yml create mode 100644 examples/custom-captcha/profiles.yaml diff --git a/Makefile b/Makefile index f266a6e..5894fde 100644 --- a/Makefile +++ b/Makefile @@ -41,13 +41,16 @@ run_tlsauth: docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans run_appsec: - docker compose -f examples/appsec-enabled/docker-compose.yml up -d + docker compose -f examples/appsec-enabled/docker-compose.yml up -d --remove-orphans + +run_custom_captcha: + docker compose -f examples/custom-captcha/docker-compose.yml up -d --remove-orphans run_captcha: - docker compose -f examples/captcha/docker-compose.yml up -d + docker compose -f examples/captcha/docker-compose.yml up -d --remove-orphans run_custom_ban_page: - docker compose -f examples/custom-ban-page/docker-compose.yml up -d + docker compose -f examples/custom-ban-page/docker-compose.yml up -d --remove-orphans run: docker compose -f docker-compose.yml up -d --remove-orphans @@ -96,8 +99,9 @@ clean_all_docker: docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans - docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans + docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans docker compose -f examples/captcha/docker-compose.yml down --remove-orphans + docker compose -f examples/custom-captcha/docker-compose.yml down --remove-orphans docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans docker compose -f docker-compose.local.yml down --remove-orphans docker compose -f docker-compose.yml down --remove-orphans diff --git a/README.md b/README.md index da74f1a..ce09359 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The following captcha providers are supported now: - [hcaptcha](https://www.hcaptcha.com/) - [recaptcha](https://www.google.com/recaptcha/about/) - [turnstile](https://www.cloudflare.com/products/turnstile/) +- [custom/wicketkeeper](https://github.com/a-ve/wicketkeeper) There are 5 operating modes (CrowdsecMode) for this plugin: @@ -465,7 +466,19 @@ make run - Used only in `alone` mode, scenarios for Crowdsec CAPI - CaptchaProvider - string - - Provider to validate the captcha, expected values are: `hcaptcha`, `recaptcha`, `turnstile` + - Provider to validate the captcha, expected values are: `hcaptcha`, `recaptcha`, `turnstile` or `custom` +- CaptchaCustomJsURL + - string + - If CaptchaProvider is `custom`, URL used to load the challenge in the HTML (in case of hcaptcha: `https://hcaptcha.com/1/api.js`) +- CaptchaCustomValidateURL + - string + - If CaptchaProvider is `custom`, URL used to validate the challenge (in case of hcaptcha: `https://api.hcaptcha.com/siteverify`) +- CaptchaCustomKey + - string + - If CaptchaProvider is `custom`, used to set class name of the div used by captcha provider (in case of hcaptcha: `h-captcha`) +- CaptchaCustomResponse + - string + - If CaptchaProvider is `custom`, used to set the field in the POST body from the captcha.html to Traefik (in case of hcaptcha: `h-captcha-response`) - CaptchaSiteKey - string - Site key for the captcha provider @@ -690,6 +703,8 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10 -t captcha #### 10. Using Traefik with Custom Ban HTML Page [examples/custom-ban-page/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-ban-page/README.md) +#### 11. Using Traefik with Custom Captcha Whiketkeeper[examples/custom-captcha/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-captcha/README.md) + ### Local Mode Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub. diff --git a/bouncer.go b/bouncer.go index 1fc560e..aaeaeb0 100644 --- a/bouncer.go +++ b/bouncer.go @@ -240,6 +240,10 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second, }, config.CaptchaProvider, + config.CaptchaCustomJsURL, + config.CaptchaCustomKey, + config.CaptchaCustomResponse, + config.CaptchaCustomValidateURL, config.CaptchaSiteKey, config.CaptchaSecretKey, config.RemediationHeadersCustomName, @@ -247,6 +251,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam config.CaptchaGracePeriodSeconds, ) if err != nil { + log.Error("CaptchaClient not valid " + err.Error()) return nil, err } diff --git a/examples/appsec-enabled/docker-compose.appsec-enabled.yml b/examples/appsec-enabled/docker-compose.appsec-enabled.yml index b2cc9ca..6ee6372 100644 --- a/examples/appsec-enabled/docker-compose.appsec-enabled.yml +++ b/examples/appsec-enabled/docker-compose.appsec-enabled.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: traefik: image: "traefik:v3.0.0" @@ -36,7 +34,7 @@ services: # Definition of the router - "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)" - "traefik.http.routers.router-foo.entrypoints=web" - - "traefik.http.routers.router-foo.middlewares=crowdsec@docker" + - "traefik.http.routers.router-foo.middlewares=crowdsec@docker" # Definition of the service - "traefik.http.services.service-foo.loadbalancer.server.port=80" # Definition of the middleware @@ -48,8 +46,6 @@ services: # Define AppSec host and port informations - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422" - - crowdsec: image: crowdsecurity/crowdsec:v1.6.1-2 container_name: "crowdsec" @@ -65,7 +61,7 @@ services: - crowdsec-config-appsec-enabled:/etc/crowdsec/ labels: - "traefik.enable=false" - + volumes: logs-appsec-enabled: crowdsec-db-appsec-enabled: diff --git a/examples/captcha/README.md b/examples/captcha/README.md index 211e4d7..a370f84 100644 --- a/examples/captcha/README.md +++ b/examples/captcha/README.md @@ -20,7 +20,7 @@ For now 3 captcha providers are supported: - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME" # Define captcha secret key - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME" - # Define captcha grade period seconds + # Define captcha grace period seconds - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800" # Define captcha HTML file path - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html" diff --git a/examples/captcha/captcha.html b/examples/captcha/captcha.html index 0fd982b..bf7a8af 100644 --- a/examples/captcha/captcha.html +++ b/examples/captcha/captcha.html @@ -293,7 +293,7 @@

CrowdSec Captcha

-
+
diff --git a/examples/custom-captcha/README.md b/examples/custom-captcha/README.md new file mode 100644 index 0000000..9bc1e2e --- /dev/null +++ b/examples/custom-captcha/README.md @@ -0,0 +1,76 @@ +# Example + +Read the example captcha before this, to better understand what is done here. + +### Traefik configuration + +The minimal configuration is defined below to implement custom captcha. +This documentation use https://github.com/a-ve/wicketpeeker, a self-hosted captcha provider that have a similar API than big providers. + +Minimal API requirement: + +- the JS file URL to load the captcha on the served `captcha.html` +- the HTML className to tell to the JS where to display the challenge +- the verify URL endpoint to send the field `response` from the captcha with `content-type: application/x-www-form-urlencoded` +- the name of the field when you POST the resolved captcha to Traefik + +- the JS file need to respect the `data-callback` on the div that contains the captcha if you use our template, but you can customize it by your side + +```yaml + traefik: + ... + labels: + # Choose captcha provider + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=custom" + # Define captcha grace period seconds + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaCustomJsURL=http://captcha.localhost:8000/fast.js" + # Inside Traefik container the plugin must be able to reach wicketkeeper service so we can go through a Traefik localhost + # domain which would resolve traefik itself and the port for the dashboard + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomValidateURL=http://wicketkeeper:8080/v0/siteverify" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomKey=wicketkeeper" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomResponse=wicketkeeper_solution" + # Define captcha HTML file path + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html" +``` + +```yaml +wicketkeeper: + image: ghcr.io/a-ve/wicketkeeper:latest + user: root + ports: + - "8080:8080" + environment: + - ROOT_URL=http://localhost:8080 + - LISTEN_PORT=8080 + - REDIS_ADDR=redis:6379 + - DIFFICULTY=4 + - ALLOWED_ORIGINS=* + - PRIVATE_KEY_PATH=/data/wicketkeeper.key + volumes: + - ./data:/data + depends_on: + - redis +redis: + image: redis/redis-stack-server:latest +``` + +## Exemple navigation + +We can try to query normally the whoami server: + +```bash +curl http://localhost:8000/foo +``` + +We can try to ban ourself and retry. + +```bash +docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 10m --type captcha +``` + +To play the demo environment run: + +```bash +make run_custom_captcha +``` diff --git a/examples/custom-captcha/acquis.yaml b/examples/custom-captcha/acquis.yaml new file mode 100644 index 0000000..5d52554 --- /dev/null +++ b/examples/custom-captcha/acquis.yaml @@ -0,0 +1,4 @@ +filenames: + - /var/log/traefik/access.log +labels: + type: traefik diff --git a/examples/custom-captcha/captcha.html b/examples/custom-captcha/captcha.html new file mode 100644 index 0000000..bf7a8af --- /dev/null +++ b/examples/custom-captcha/captcha.html @@ -0,0 +1,338 @@ + + + + + CrowdSec Captcha + + + + + + + +
+
+
+ +

CrowdSec Captcha

+
+
+
+
+
+
+

This security check has been powered by

+ + + + + + + + + + + + + + + + + + + + + CrowdSec + +
+
+
+ + + diff --git a/examples/custom-captcha/docker-compose.yml b/examples/custom-captcha/docker-compose.yml new file mode 100644 index 0000000..e72e170 --- /dev/null +++ b/examples/custom-captcha/docker-compose.yml @@ -0,0 +1,111 @@ +services: + traefik: + image: "traefik:v3.5.0" + container_name: "traefik" + restart: unless-stopped + command: + # - "--log.level=DEBUG" + - "--accesslog" + - "--accesslog.filepath=/var/log/traefik/access.log" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.web.forwardedheaders.trustedips=172.18.0.0/24" + + - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + - "--experimental.plugins.bouncer.version=v1.4.5" + # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - logs-custom-captcha-enabled:/var/log/traefik + - "./captcha.html:/captcha.html" + # - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + ports: + - 8000:80 + - 8080:8080 + depends_on: + - crowdsec + + whoami-foo: + image: traefik/whoami + container_name: "simple-service-custom-captcha-foo" + restart: unless-stopped + labels: + - "traefik.enable=true" + # Definition of the router + - "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.router-foo.entrypoints=web" + - "traefik.http.routers.router-foo.middlewares=crowdsec@docker" + # Definition of the service + - "traefik.http.services.service-foo.loadbalancer.server.port=80" + # Definition of the middleware + - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=none" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + + # Choose captcha provider + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=custom" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaCustomJsURL=http://captcha.localhost:8000/fast.js" + # Inside Traefik container the plugin must be able to reach wicketkeeper service so we can go through a Traefik localhost + # domain which would resolve traefik itself and the port for the dashboard + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomValidateURL=http://wicketkeeper:8080/v0/siteverify" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomKey=wicketkeeper" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomResponse=wicketkeeper_solution" + # Define captcha grade period seconds + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=20" + # Define captcha HTML file path + - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html" + + crowdsec: + image: crowdsecurity/crowdsec:v1.6.1-2 + container_name: "crowdsec" + restart: unless-stopped + environment: + COLLECTIONS: crowdsecurity/traefik + CUSTOM_HOSTNAME: crowdsec + BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5 + volumes: + # For captcha and ban mixed decision + - "./profiles.yaml:/etc/crowdsec/profiles.yaml:ro" + # For captcha only remediation + # - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro' + - "./acquis.yaml:/etc/crowdsec/acquis.yaml:ro" + - logs-custom-captcha-enabled:/var/log/traefik:ro + - crowdsec-db-custom-captcha-enabled:/var/lib/crowdsec/data/ + - crowdsec-config-custom-captcha-enabled:/etc/crowdsec/ + labels: + - "traefik.enable=false" + + wicketkeeper: + image: ghcr.io/a-ve/wicketkeeper:latest + container_name: "wicketkeeper" + environment: + - ROOT_URL=http://captcha.localhost:8000 + - LISTEN_PORT=8080 + - REDIS_ADDR=redis:6379 + - DIFFICULTY=4 + - ALLOWED_ORIGINS=* + - PRIVATE_KEY_PATH=/data/wicketkeeper.key # To override in production environment + volumes: + - wicketkeeper-custom-captcha-enabled:/data + user: root + labels: + - "traefik.enable=true" + # Definition of the router + - "traefik.http.routers.router-wicketpeeker.rule=Host(`captcha.localhost`)" + - "traefik.http.routers.router-wicketpeeker.entrypoints=web" + # Definition of the service + - "traefik.http.services.service-whitekeeper.loadbalancer.server.port=8080" + depends_on: + - redis + + redis: + image: redis/redis-stack-server:latest + +volumes: + logs-custom-captcha-enabled: + wicketkeeper-custom-captcha-enabled: + crowdsec-db-custom-captcha-enabled: + crowdsec-config-custom-captcha-enabled: diff --git a/examples/custom-captcha/profiles.yaml b/examples/custom-captcha/profiles.yaml new file mode 100644 index 0000000..b396c7c --- /dev/null +++ b/examples/custom-captcha/profiles.yaml @@ -0,0 +1,17 @@ +name: captcha_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" && GetDecisionsSinceCount(Alert.GetValue(), "24h") <= 3 +## Same as above but only 3 captcha decision per 24 hours before ban +decisions: + - type: captcha + duration: 4h +on_success: break +--- +name: default_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" +decisions: + - type: ban + duration: 4h +#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)" +on_success: break \ No newline at end of file diff --git a/pkg/captcha/captcha.go b/pkg/captcha/captcha.go index 3698340..9170e1e 100644 --- a/pkg/captcha/captcha.go +++ b/pkg/captcha/captcha.go @@ -17,7 +17,6 @@ import ( // Client Captcha client. type Client struct { Valid bool - provider string siteKey string secretKey string remediationCustomHeader string @@ -26,44 +25,54 @@ type Client struct { 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 } -var ( - //nolint:gochecknoglobals - captcha = map[string]infoProvider{ - configuration.HcaptchaProvider: { - js: "https://hcaptcha.com/1/api.js", - key: "h-captcha", - validate: "https://api.hcaptcha.com/siteverify", - }, - configuration.RecaptchaProvider: { - js: "https://www.google.com/recaptcha/api.js", - key: "g-recaptcha", - validate: "https://www.google.com/recaptcha/api/siteverify", - }, - configuration.TurnstileProvider: { - js: "https://challenges.cloudflare.com/turnstile/v0/api.js", - key: "cf-turnstile", - validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify", - }, - } -) +//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, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error { +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.provider = provider c.remediationCustomHeader = remediationCustomHeader html, _ := configuration.GetHTMLTemplate(captchaTemplatePath) c.captchaTemplate = html @@ -95,8 +104,8 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str rw.WriteHeader(http.StatusOK) err = c.captchaTemplate.Execute(rw, map[string]string{ "SiteKey": c.siteKey, - "FrontendJS": captcha[c.provider].js, - "FrontendKey": captcha[c.provider].key, + "FrontendJS": c.infoProvider.js, + "FrontendKey": c.infoProvider.key, }) if err != nil { c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error()) @@ -121,7 +130,7 @@ func (c *Client) Validate(r *http.Request) (bool, error) { c.log.Debug("captcha:Validate invalid method: " + r.Method) return false, nil } - var response = r.FormValue(captcha[c.provider].key + "-response") + var response = r.FormValue(c.infoProvider.response) if response == "" { c.log.Debug("captcha:Validate no captcha response found in request") return false, nil @@ -129,7 +138,7 @@ func (c *Client) Validate(r *http.Request) (bool, error) { var body = url.Values{} body.Add("secret", c.secretKey) body.Add("response", response) - res, err := c.httpClient.PostForm(captcha[c.provider].validate, body) + res, err := c.httpClient.PostForm(c.infoProvider.validate, body) if err != nil { return false, err } diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index 09d72ff..0ad4432 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -34,6 +34,7 @@ const ( HcaptchaProvider = "hcaptcha" RecaptchaProvider = "recaptcha" TurnstileProvider = "turnstile" + CustomProvider = "custom" ) // Config the plugin configuration. @@ -84,6 +85,10 @@ type Config struct { BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"` CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"` CaptchaProvider string `json:"captchaProvider,omitempty"` + CaptchaCustomJsURL string `json:"captchaCustomJsUrl,omitempty"` + CaptchaCustomValidateURL string `json:"captchaCustomValidateUrl,omitempty"` + CaptchaCustomKey string `json:"captchaCustomKey,omitempty"` + CaptchaCustomResponse string `json:"captchaCustomResponse,omitempty"` CaptchaSiteKey string `json:"captchaSiteKey,omitempty"` CaptchaSiteKeyFile string `json:"captchaSiteKeyFile,omitempty"` CaptchaSecretKey string `json:"captchaSecretKey,omitempty"` @@ -125,6 +130,10 @@ func New() *Config { RemediationStatusCode: http.StatusForbidden, HTTPTimeoutSeconds: 10, CaptchaProvider: "", + CaptchaCustomJsURL: "", + CaptchaCustomValidateURL: "", + CaptchaCustomKey: "", + CaptchaCustomResponse: "", CaptchaSiteKey: "", CaptchaSecretKey: "", CaptchaGracePeriodSeconds: 1800, @@ -196,6 +205,10 @@ func ValidateParams(config *Config) error { return err } + if err := validateCaptcha(config); err != nil { + return err + } + if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil { return err } @@ -331,6 +344,24 @@ func validateParamsIPs(listIP []string, key string) error { return nil } +func validateCaptcha(config *Config) error { + if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider, CustomProvider}, config.CaptchaProvider) { + return fmt.Errorf("CaptchaProvider: must be one of '%s', '%s', '%s' or '%s'", HcaptchaProvider, RecaptchaProvider, TurnstileProvider, CustomProvider) + } + if config.CaptchaProvider == CustomProvider { + if config.CaptchaCustomKey == "" || config.CaptchaCustomResponse == "" || config.CaptchaCustomValidateURL == "" || config.CaptchaCustomJsURL == "" { + return fmt.Errorf( + "CaptchaProvider: provider is custom, captchaCustom variables must be filled: CaptchaCustomKey:%s, CaptchaCustomResponse:%s, CaptchaCustomValidateURL:%s, CaptchaCustomJsURL:%s", + config.CaptchaCustomKey, + config.CaptchaCustomResponse, + config.CaptchaCustomValidateURL, + config.CaptchaCustomJsURL, + ) + } + } + return nil +} + func validateParamsRequired(config *Config) error { requiredStrings := map[string]string{ "CrowdsecLapiScheme": config.CrowdsecLapiScheme, @@ -339,7 +370,7 @@ func validateParamsRequired(config *Config) error { } for key, val := range requiredStrings { if len(val) == 0 { - return fmt.Errorf("%v: cannot be empty", key) + return errors.New(key + ": cannot be empty") } } requiredInt0 := map[string]int64{ @@ -348,7 +379,7 @@ func validateParamsRequired(config *Config) error { } for key, val := range requiredInt0 { if val < 0 { - return fmt.Errorf("%v: cannot be less than 0", key) + return errors.New(key + ": cannot be less than 0") } } requiredInt1 := map[string]int64{ @@ -359,7 +390,7 @@ func validateParamsRequired(config *Config) error { } for key, val := range requiredInt1 { if val < 1 { - return fmt.Errorf("%v: cannot be less than 1", key) + return errors.New(key + ": cannot be less than 1") } } if config.UpdateMaxFailure < -1 { @@ -378,9 +409,6 @@ func validateParamsRequired(config *Config) error { if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) { return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'") } - if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) { - return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'") - } return nil }