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=
RET_CODE=
#those apply for "captcha" action
# ReCaptcha Secret Key
#valid providers are recaptcha, hcaptcha, turnstile
CAPTCHA_PROVIDER=
# Captcha Secret Key
SECRET_KEY=
# Recaptcha Site key
# Captcha Site key
SITE_KEY=
CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/templates/captcha.html
CAPTCHA_EXPIRATION=3600

View file

@ -4,7 +4,7 @@ local config = require "plugins.crowdsec.config"
local iputils = require "plugins.crowdsec.iputils"
local http = require "resty.http"
local cjson = require "cjson"
local recaptcha = require "plugins.crowdsec.recaptcha"
local captcha = require "plugins.crowdsec.captcha"
local utils = require "plugins.crowdsec.utils"
local ban = require "plugins.crowdsec.ban"
@ -43,9 +43,9 @@ function csmod.init(configFile, userAgent)
end
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
ngx.log(ngx.ERR, "error loading recaptcha plugin: " .. err)
ngx.log(ngx.ERR, "error loading captcha plugin: " .. err)
captcha_ok = false
end
local succ, err, forcible = runtime.cache:set("captcha_ok", captcha_ok)
@ -89,8 +89,8 @@ function csmod.init(configFile, userAgent)
end
function csmod.validateCaptcha(g_captcha_res, remote_ip)
return recaptcha.Validate(g_captcha_res, remote_ip)
function csmod.validateCaptcha(captcha_res, remote_ip)
return captcha.Validate(captcha_res, remote_ip)
end
@ -364,9 +364,12 @@ end
function csmod.GetCaptchaTemplate()
return recaptcha.GetTemplate()
return captcha.GetTemplate()
end
function csmod.GetCaptchaBackendKey()
return captcha.GetCaptchaBackendKey()
end
function csmod.SetupStream()
-- if it stream mode and startup start timer
@ -464,7 +467,7 @@ function csmod.Allow(ip)
local captcha_ok = runtime.cache:get("captcha_ok")
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
remediation = runtime.fallback
end
@ -478,17 +481,17 @@ function csmod.Allow(ip)
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
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()
local recaptcha_res = ngx.req.get_post_args()["g-recaptcha-response"] or 0
if recaptcha_res ~= 0 then
local valid, err = csmod.validateCaptcha(recaptcha_res, ngx.var.remote_addr)
local captcha_res = ngx.req.get_post_args()[csmod.GetCaptchaBackendKey()] or 0
if captcha_res ~= 0 then
local valid, err = csmod.validateCaptcha(captcha_res, ngx.var.remote_addr)
if err ~= nil then
ngx.log(ngx.ERR, "Error while validating captcha: " .. err)
end
if valid == true then
-- 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
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
end
@ -516,7 +519,7 @@ function csmod.Allow(ip)
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)
-- 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.say(csmod.GetCaptchaTemplate())
local uri = ngx.var.uri
@ -529,7 +532,7 @@ function csmod.Allow(ip)
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
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
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 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._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
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."
end
M.CaptchaProvider = captcha_provider
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)
M.Template = view
@ -70,6 +87,9 @@ function M.GetTemplate()
return M.Template
end
function M.GetCaptchaBackendKey()
return captcha_frontend_key[M.CaptchaProvider] .. "-response"
end
function table_to_encoded_url(args)
local params = {}
@ -77,17 +97,17 @@ function table_to_encoded_url(args)
return table.concat(params, "&")
end
function M.Validate(g_captcha_res, remote_ip)
function M.Validate(captcha_res, remote_ip)
local body = {
secret = M.SecretKey,
response = g_captcha_res,
response = captcha_res,
remoteip = remote_ip
}
local data = table_to_encoded_url(body)
local httpc = http.new()
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",
body = data,
headers = {
@ -114,4 +134,4 @@ function M.Validate(g_captcha_res, remote_ip)
end
return M
return M

View file

@ -40,7 +40,7 @@ function config.loadConfig(file)
return nil, "File ".. file .." doesn't exist"
end
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_bouncing_on_type_values = {'ban', 'captcha', 'all'}
local valid_truefalse_values = {'false', 'true'}
@ -53,7 +53,8 @@ function config.loadConfig(file)
['CAPTCHA_EXPIRATION'] = 3600,
['REDIRECT_LOCATION'] = "",
['EXCLUDE_LOCATION'] = {},
['RET_CODE'] = 0
['RET_CODE'] = 0,
['CAPTCHA_PROVIDER'] = "recaptcha"
}
for line in io.lines(file) do
local isOk = false
@ -130,4 +131,4 @@ function config.loadConfig(file)
end
return conf, nil
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