From 8acc81166fdb2810b4f9e21a9bbdac24eb36fa68 Mon Sep 17 00:00:00 2001 From: Benjamin MALYNOVYTCH Date: Fri, 13 Feb 2026 13:24:35 +0100 Subject: [PATCH] PoC: add provider Cap.js --- config_example.conf | 7 ++++- lib/crowdsec.lua | 2 +- lib/plugins/crowdsec/captcha.lua | 52 ++++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/config_example.conf b/config_example.conf index c9d698eb..28c8b6b4 100644 --- a/config_example.conf +++ b/config_example.conf @@ -24,7 +24,7 @@ BAN_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/ban.html REDIRECT_LOCATION= RET_CODE= #those apply for "captcha" action -#valid providers are recaptcha, hcaptcha, turnstile +#valid providers are recaptcha, hcaptcha, turnstile, cap CAPTCHA_PROVIDER= # Captcha Secret Key SECRET_KEY= @@ -33,6 +33,11 @@ SITE_KEY= CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/captcha.html CAPTCHA_EXPIRATION=3600 +# Cap provider options (only used when CAPTCHA_PROVIDER=cap) +# Cap is a self-hosted, privacy-friendly proof-of-work captcha: https://capjs.js.org +CAPTCHA_VERIFY_URL= +CAPTCHA_JS_URL= + APPSEC_URL= APPSEC_FAILURE_ACTION=passthrough APPSEC_CONNECT_TIMEOUT= diff --git a/lib/crowdsec.lua b/lib/crowdsec.lua index 71fa6c30..8c43419c 100644 --- a/lib/crowdsec.lua +++ b/lib/crowdsec.lua @@ -114,7 +114,7 @@ function csmod.init(configFile, userAgent) end local captcha_ok = true - local err = captcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"], runtime.conf["CAPTCHA_PROVIDER"], runtime.conf["CAPTCHA_RET_CODE"]) + local err = captcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"], runtime.conf["CAPTCHA_PROVIDER"], runtime.conf["CAPTCHA_RET_CODE"], runtime.conf["CAPTCHA_VERIFY_URL"], runtime.conf["CAPTCHA_JS_URL"]) if err ~= nil then ngx.log(ngx.ERR, "error loading captcha plugin: " .. err) captcha_ok = false diff --git a/lib/plugins/crowdsec/captcha.lua b/lib/plugins/crowdsec/captcha.lua index 81d54346..3f574a58 100644 --- a/lib/plugins/crowdsec/captcha.lua +++ b/lib/plugins/crowdsec/captcha.lua @@ -9,23 +9,29 @@ local captcha_backend_url = {} captcha_backend_url["recaptcha"] = "https://www.recaptcha.net/recaptcha/api/siteverify" captcha_backend_url["hcaptcha"] = "https://hcaptcha.com/siteverify" captcha_backend_url["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/siteverify" +-- cap: self-hosted proof-of-work captcha (https://capjs.js.org) +-- The verify URL is configurable via CAPTCHA_VERIFY_URL (defaults to http://localhost:3000/api/validate) +captcha_backend_url["cap"] = nil -- set dynamically in New() from CAPTCHA_VERIFY_URL local captcha_frontend_js = {} captcha_frontend_js["recaptcha"] = "https://www.recaptcha.net/recaptcha/api.js" captcha_frontend_js["hcaptcha"] = "https://js.hcaptcha.com/1/api.js" captcha_frontend_js["turnstile"] = "https://challenges.cloudflare.com/turnstile/v0/api.js" +-- cap: widget JS URL is configurable via CAPTCHA_JS_URL (defaults to http://localhost:3000/cap.js) +captcha_frontend_js["cap"] = nil -- set dynamically in New() from CAPTCHA_JS_URL local captcha_frontend_key = {} captcha_frontend_key["recaptcha"] = "g-recaptcha" captcha_frontend_key["hcaptcha"] = "h-captcha" captcha_frontend_key["turnstile"] = "cf-turnstile" +captcha_frontend_key["cap"] = "cap" M.SecretKey = "" M.SiteKey = "" M.Template = "" M.ret_code = ngx.HTTP_OK -function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider, ret_code) +function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider, ret_code, captcha_verify_url, captcha_js_url) if siteKey == nil or siteKey == "" then return "no recaptcha site key provided, can't use recaptcha" @@ -66,6 +72,14 @@ function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider, ret_code) end end + -- For the cap provider, the backend URL and frontend JS URL are configurable + -- because Cap is self-hosted. We read them from the runtime config passed via + -- the extra parameters, falling back to the canonical defaults. + if captcha_provider == "cap" then + captcha_backend_url["cap"] = (captcha_verify_url ~= nil and captcha_verify_url ~= "") and captcha_verify_url or "http://localhost:3000/api/validate" + captcha_frontend_js["cap"] = (captcha_js_url ~= nil and captcha_js_url ~= "") and captcha_js_url or "http://localhost:3000/cap.js" + end + local template_data = {} template_data["captcha_site_key"] = M.SiteKey template_data["captcha_frontend_js"] = captcha_frontend_js[M.CaptchaProvider] @@ -88,13 +102,19 @@ function M.GetCaptchaBackendKey() return captcha_frontend_key[M.CaptchaProvider] .. "-response" end -function table_to_encoded_url(args) +local function table_to_encoded_url(args) local params = {} for k, v in pairs(args) do table.insert(params, k .. '=' .. v) end return table.concat(params, "&") end function M.Validate(captcha_res, remote_ip) + -- Cap uses a JSON POST to a self-hosted server instead of form-encoded POST + -- to a third-party API, so it needs its own request path. + if M.CaptchaProvider == "cap" then + return M.ValidateCap(captcha_res, remote_ip) + end + local body = { secret = M.SecretKey, response = captcha_res, @@ -130,5 +150,33 @@ function M.Validate(captcha_res, remote_ip) return result.success, nil end +-- Cap validation: POST JSON { token, secret } to the self-hosted Cap server. +-- The Cap server responds with { "success": true } or { "success": false }. +function M.ValidateCap(captcha_res, remote_ip) + local payload = cjson.encode({ + token = captcha_res, + secret = M.SecretKey, + remoteip = remote_ip, + }) + + local httpc = http.new() + httpc:set_timeout(2000) + local res, err = httpc:request_uri(captcha_backend_url["cap"], { + method = "POST", + body = payload, + headers = { + ["Content-Type"] = "application/json", + }, + }) + httpc:close() + + if err ~= nil then + return true, err + end + + local result = cjson.decode(res.body) + return result.success, nil +end + return M