Add other captcha providers / update templates (#39)

* Add option for another captcha providers (hcaptcha, turnstile)

* Fix wrong map usage

* Update templates to mobile first and light/dark mode options

* rename recaptcha to captcha more generic

* Just one more update to show pointer cursor on submit button

* height on mobile was out fixed

* Edit title

* Make button same color dark and light mode easily to see

* Add auto captcha submit on completion and increase height on ban template on mobile

* Fix typo

* Fix dark mode button on ban

* Add short timeout to make captcha smoother

* half timeout to make captcha smoother

* Fix the things I noticed when updating the haproxy bouncer

* Fix light mode on load within templates
This commit is contained in:
Laurence Jones 2023-03-29 10:07:30 +01:00 committed by GitHub
parent cad85ae199
commit 902f055023
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 228 additions and 286 deletions

View file

@ -17,9 +17,11 @@ BAN_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/ban.html
REDIRECT_LOCATION= REDIRECT_LOCATION=
RET_CODE= RET_CODE=
#those apply for "captcha" action #those apply for "captcha" action
# ReCaptcha Secret Key #valid providers are recaptcha, hcaptcha, turnstile
CAPTCHA_PROVIDER=
# Captcha Secret Key
SECRET_KEY= SECRET_KEY=
# Recaptcha Site key # Captcha Site key
SITE_KEY= SITE_KEY=
CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/captcha.html CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/captcha.html
CAPTCHA_EXPIRATION=3600 CAPTCHA_EXPIRATION=3600

View file

@ -4,7 +4,7 @@ local config = require "plugins.crowdsec.config"
local iputils = require "plugins.crowdsec.iputils" local iputils = require "plugins.crowdsec.iputils"
local http = require "resty.http" local http = require "resty.http"
local cjson = require "cjson" local cjson = require "cjson"
local recaptcha = require "plugins.crowdsec.recaptcha" local captcha = require "plugins.crowdsec.captcha"
local utils = require "plugins.crowdsec.utils" local utils = require "plugins.crowdsec.utils"
local ban = require "plugins.crowdsec.ban" local ban = require "plugins.crowdsec.ban"
@ -43,9 +43,9 @@ function csmod.init(configFile, userAgent)
end end
local captcha_ok = true local captcha_ok = true
local err = recaptcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"]) local err = captcha.New(runtime.conf["SITE_KEY"], runtime.conf["SECRET_KEY"], runtime.conf["CAPTCHA_TEMPLATE_PATH"], runtime.conf["CAPTCHA_PROVIDER"])
if err ~= nil then if err ~= nil then
ngx.log(ngx.ERR, "error loading recaptcha plugin: " .. err) ngx.log(ngx.ERR, "error loading captcha plugin: " .. err)
captcha_ok = false captcha_ok = false
end end
local succ, err, forcible = runtime.cache:set("captcha_ok", captcha_ok) local succ, err, forcible = runtime.cache:set("captcha_ok", captcha_ok)
@ -89,8 +89,8 @@ function csmod.init(configFile, userAgent)
end end
function csmod.validateCaptcha(g_captcha_res, remote_ip) function csmod.validateCaptcha(captcha_res, remote_ip)
return recaptcha.Validate(g_captcha_res, remote_ip) return captcha.Validate(captcha_res, remote_ip)
end end
@ -364,9 +364,12 @@ end
function csmod.GetCaptchaTemplate() function csmod.GetCaptchaTemplate()
return recaptcha.GetTemplate() return captcha.GetTemplate()
end end
function csmod.GetCaptchaBackendKey()
return captcha.GetCaptchaBackendKey()
end
function csmod.SetupStream() function csmod.SetupStream()
-- if it stream mode and startup start timer -- if it stream mode and startup start timer
@ -464,7 +467,7 @@ function csmod.Allow(ip)
local captcha_ok = runtime.cache:get("captcha_ok") local captcha_ok = runtime.cache:get("captcha_ok")
if runtime.fallback ~= "" then if runtime.fallback ~= "" then
-- if we can't use recaptcha, fallback -- if we can't use captcha, fallback
if remediation == "captcha" and captcha_ok == false then if remediation == "captcha" and captcha_ok == false then
remediation = runtime.fallback remediation = runtime.fallback
end end
@ -478,17 +481,17 @@ function csmod.Allow(ip)
if captcha_ok then -- if captcha can be use (configuration is valid) if captcha_ok then -- if captcha can be use (configuration is valid)
-- we check if the IP need to validate its captcha before checking it against crowdsec local API -- we check if the IP need to validate its captcha before checking it against crowdsec local API
local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr) local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
if previous_uri ~= nil and state_id == recaptcha.GetStateID(recaptcha._VERIFY_STATE) then if previous_uri ~= nil and state_id == captcha.GetStateID(captcha._VERIFY_STATE) then
ngx.req.read_body() ngx.req.read_body()
local recaptcha_res = ngx.req.get_post_args()["g-recaptcha-response"] or 0 local captcha_res = ngx.req.get_post_args()[csmod.GetCaptchaBackendKey()] or 0
if recaptcha_res ~= 0 then if captcha_res ~= 0 then
local valid, err = csmod.validateCaptcha(recaptcha_res, ngx.var.remote_addr) local valid, err = csmod.validateCaptcha(captcha_res, ngx.var.remote_addr)
if err ~= nil then if err ~= nil then
ngx.log(ngx.ERR, "Error while validating captcha: " .. err) ngx.log(ngx.ERR, "Error while validating captcha: " .. err)
end end
if valid == true then if valid == true then
-- captcha is valid, we redirect the IP to its previous URI but in GET method -- captcha is valid, we redirect the IP to its previous URI but in GET method
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, previous_uri, runtime.conf["CAPTCHA_EXPIRATION"], recaptcha.GetStateID(recaptcha._VALIDATED_STATE)) local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, previous_uri, runtime.conf["CAPTCHA_EXPIRATION"], captcha.GetStateID(captcha._VALIDATED_STATE))
if not succ then if not succ then
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err) ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
end end
@ -516,7 +519,7 @@ function csmod.Allow(ip)
if remediation == "captcha" and captcha_ok and ngx.var.uri ~= "/favicon.ico" then if remediation == "captcha" and captcha_ok and ngx.var.uri ~= "/favicon.ico" then
local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr) local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
-- we check if the IP is already in cache for captcha and not yet validated -- we check if the IP is already in cache for captcha and not yet validated
if previous_uri == nil or state_id ~= recaptcha.GetStateID(recaptcha._VALIDATED_STATE) then if previous_uri == nil or state_id ~= captcha.GetStateID(captcha._VALIDATED_STATE) then
ngx.header.content_type = "text/html" ngx.header.content_type = "text/html"
ngx.say(csmod.GetCaptchaTemplate()) ngx.say(csmod.GetCaptchaTemplate())
local uri = ngx.var.uri local uri = ngx.var.uri
@ -529,7 +532,7 @@ function csmod.Allow(ip)
end end
end end
end end
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, uri , 60, recaptcha.GetStateID(recaptcha._VERIFY_STATE)) local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, uri , 60, captcha.GetStateID(captcha._VERIFY_STATE))
if not succ then if not succ then
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err) ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
end end

View file

@ -6,7 +6,20 @@ local utils = require "plugins.crowdsec.utils"
local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'} local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'}
local recaptcha_verify_url = "https://www.google.com/recaptcha/api/siteverify" 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"
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"
local captcha_frontend_key = {}
captcha_frontend_key["recaptcha"] = "g-recaptcha"
captcha_frontend_key["hcaptcha"] = "h-captcha"
captcha_frontend_key["turnstile"] = "cf-turnstile"
M._VERIFY_STATE = "to_verify" M._VERIFY_STATE = "to_verify"
M._VALIDATED_STATE = "validated" M._VALIDATED_STATE = "validated"
@ -32,7 +45,7 @@ end
function M.New(siteKey, secretKey, TemplateFilePath) function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider)
if siteKey == nil or siteKey == "" then if siteKey == nil or siteKey == "" then
return "no recaptcha site key provided, can't use recaptcha" return "no recaptcha site key provided, can't use recaptcha"
@ -57,8 +70,12 @@ function M.New(siteKey, secretKey, TemplateFilePath)
return "Template file " .. TemplateFilePath .. "not found." return "Template file " .. TemplateFilePath .. "not found."
end end
M.CaptchaProvider = captcha_provider
local template_data = {} local template_data = {}
template_data["recaptcha_site_key"] = M.SiteKey template_data["captcha_site_key"] = M.SiteKey
template_data["captcha_frontend_js"] = captcha_frontend_js[M.CaptchaProvider]
template_data["captcha_frontend_key"] = captcha_frontend_key[M.CaptchaProvider]
local view = template.compile(captcha_template, template_data) local view = template.compile(captcha_template, template_data)
M.Template = view M.Template = view
@ -70,6 +87,9 @@ function M.GetTemplate()
return M.Template return M.Template
end end
function M.GetCaptchaBackendKey()
return captcha_frontend_key[M.CaptchaProvider] .. "-response"
end
function table_to_encoded_url(args) function table_to_encoded_url(args)
local params = {} local params = {}
@ -77,17 +97,17 @@ function table_to_encoded_url(args)
return table.concat(params, "&") return table.concat(params, "&")
end end
function M.Validate(g_captcha_res, remote_ip) function M.Validate(captcha_res, remote_ip)
local body = { local body = {
secret = M.SecretKey, secret = M.SecretKey,
response = g_captcha_res, response = captcha_res,
remoteip = remote_ip remoteip = remote_ip
} }
local data = table_to_encoded_url(body) local data = table_to_encoded_url(body)
local httpc = http.new() local httpc = http.new()
httpc:set_timeout(2000) httpc:set_timeout(2000)
local res, err = httpc:request_uri(recaptcha_verify_url, { local res, err = httpc:request_uri(captcha_backend_url[M.CaptchaProvider], {
method = "POST", method = "POST",
body = data, body = data,
headers = { headers = {
@ -114,4 +134,4 @@ function M.Validate(g_captcha_res, remote_ip)
end end
return M return M

View file

@ -40,7 +40,7 @@ function config.loadConfig(file)
return nil, "File ".. file .." doesn't exist" return nil, "File ".. file .." doesn't exist"
end end
local conf = {} local conf = {}
local valid_params = {'ENABLED','API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION'} local valid_params = {'ENABLED','API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER'}
local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION'} local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION'}
local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'} local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'}
local valid_truefalse_values = {'false', 'true'} local valid_truefalse_values = {'false', 'true'}
@ -53,7 +53,8 @@ function config.loadConfig(file)
['CAPTCHA_EXPIRATION'] = 3600, ['CAPTCHA_EXPIRATION'] = 3600,
['REDIRECT_LOCATION'] = "", ['REDIRECT_LOCATION'] = "",
['EXCLUDE_LOCATION'] = {}, ['EXCLUDE_LOCATION'] = {},
['RET_CODE'] = 0 ['RET_CODE'] = 0,
['CAPTCHA_PROVIDER'] = "recaptcha"
} }
for line in io.lines(file) do for line in io.lines(file) do
local isOk = false local isOk = false
@ -130,4 +131,4 @@ function config.loadConfig(file)
end end
return conf, nil return conf, nil
end end
return config return config

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long