Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion config_example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion lib/crowdsec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 50 additions & 2 deletions lib/plugins/crowdsec/captcha.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand All @@ -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,
Expand Down Expand Up @@ -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