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=
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
Loading…
Add table
Reference in a new issue