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 = {

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long