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:
parent
cad85ae199
commit
902f055023
6 changed files with 228 additions and 286 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
Loading…
Add table
Reference in a new issue