refactor - improve clusterstore interface and automatically retrieve variables for plugins

This commit is contained in:
florian 2023-04-12 23:45:16 +02:00
parent 29c57915cb
commit 03ec271e21
No known key found for this signature in database
GPG Key ID: 3D80806F12602A7C
6 changed files with 232 additions and 174 deletions

View File

@ -1,13 +1,10 @@
local M = {}
local redis = require "resty.redis"
local utils = require "bunkerweb.utils"
local class = require "middleclass"
local utils = require "bunkerweb.utils"
local redis = require "resty.redis"
function M:connect()
-- Instantiate object
local redis_client, err = redis:new()
if redis_client == nil then
return false, err
end
local clusterstore = class("clusterstore")
function clusterstore:new()
-- Get variables
local variables = {
["REDIS_HOST"] = "",
@ -18,22 +15,35 @@ function M:connect()
["REDIS_KEEPALIVE_IDLE"] = "",
["REDIS_KEEPALIVE_POOL"] = ""
}
-- Set them for later user
self.variables = {}
for k, v in pairs(variables) do
local value, err = utils.get_variable(k, false)
if value == nil then
return false, err
end
variables[k] = value
self.variables[k] = value
end
-- Don't instantiate a redis object for now
self.redis_client = nil
return true, "success"
end
function clusterstore:connect()
-- Instantiate object
local redis_client, err = redis:new()
if redis_client == nil then
return false, err
end
-- Set timeouts
redis_client:set_timeouts(tonumber(variables["REDIS_TIMEOUT"]), tonumber(variables["REDIS_TIMEOUT"]), tonumber(variables["REDIS_TIMEOUT"]))
redis_client:set_timeouts(tonumber(self.variables["REDIS_TIMEOUT"]), tonumber(self.variables["REDIS_TIMEOUT"]), tonumber(self.variables["REDIS_TIMEOUT"]))
-- Connect
local options = {
ssl = variables["REDIS_SSL"] == "yes",
ssl = self.variables["REDIS_SSL"] == "yes",
pool = "bw",
pool_size = tonumber(variables["REDIS_KEEPALIVE_POOL"])
pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"])
}
local ok, err = redis_client:connect(variables["REDIS_HOST"], tonumber(variables["REDIS_PORT"]), options)
local ok, err = redis_client:connect(self.variables["REDIS_HOST"], tonumber(self.variables["REDIS_PORT"]), options)
if not ok then
return false, err
end
@ -48,24 +58,55 @@ function M:connect()
return false, err
end
end
return redis_client
self.redis_client = redis_client
return return true, "success"
end
function M:close(redis_client)
-- Get variables
local variables = {
["REDIS_KEEPALIVE_IDLE"] = "",
["REDIS_KEEPALIVE_POOL"] = ""
}
for k, v in pairs(variables) do
local value, err = utils.get_variable(k, false)
if value == nil then
return false, err
end
variables[k] = value
function clusterstore:close()
if self.redis_client then
-- Equivalent to close but keep a pool of connections
return self.redis_client:set_keepalive(tonumber(self.variables["REDIS_KEEPALIVE_IDLE"]), tonumber(self.variables["REDIS_KEEPALIVE_POOL"]))
end
-- Equivalent to close but keep a pool of connections
return redis_client:set_keepalive(tonumber(variables["REDIS_KEEPALIVE_IDLE"]), tonumber(variables["REDIS_KEEPALIVE_POOL"]))
return false, "not connected"
end
return M
function clusterstore:call(method, ...)
-- Check if we are connected
if not self.redis_client then
return false, "not connected"
end
-- Call method
return self.redis_client[method](self.redis_client, ...)
end
function clusterstore:multi(calls)
-- Check if we are connected
if not self.redis_client then
return false, "not connected"
end
-- Start transaction
local ok, err = self.redis_client:multi()
if not ok then
return false, "multi() failed : " .. err
end
-- Loop on calls
for i, call in ipairs(calls) do
local method = call[1]
local args = table.unpack(call[2])
local ok, err = self.redis_client[method](self.redis_client, args)
if not ok then
return false, method + "() failed : " .. err
end
end
-- Exec transaction
local exec, err = self.redis_client:exec()
if not exec then
return false, "exec() failed : " .. err
end
if type(exec) ~= "table" then
return false, "exec() result is not a table"
end
return true, "success", exec
end
return clusterstore

View File

@ -1,10 +1,32 @@
local class = require "middleclass"
local datastore = require "bunkerweb.datastore"
local datastore = require "bunkerweb.utils"
local cjson = require "cjson"
local plugin = class("plugin")
function plugin:new(id)
-- Store default values
self.id = id
self.variables = {}
-- Instantiate logger
self.logger = require "bunkerweb.logger"
self.logger:new(id)
-- Get metadata
local encoded_metadata, err = datastore:get("plugin_" .. id)
if not encoded_metadata then
return false, err
end
-- Store variables
local metadata = cjson.decode(encoded_metadata)
for k, v in pairs(metadata.settings) do
local value, err = utils.get_variable(k, v.context == "multisite")
if value == nil then
return false, "can't get " .. k .. " variable : " .. err
end
self.variables[k] = value
end
return true, "success"
end
function plugin:get_id()

View File

@ -120,11 +120,16 @@ for i, plugin_path in ipairs(plugin_paths) do
if ok then
logger:log(ngx.ERR, err)
else
table.insert(plugins, plugin)
table.sort(plugins, function (a, b)
return a.order < b.order
end)
logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
local ok, err = datastore:set("plugin_" .. plugin.id, cjson.encode(plugin))
if not ok then
logger:log(ngx.ERR, "can't save " .. plugin.id .. " into datastore : " .. err)
else
table.insert(plugins, plugin)
table.sort(plugins, function (a, b)
return a.order < b.order
end)
logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
end
end
end
end

View File

@ -13,51 +13,48 @@ local http = require "resty.http"
local antibot = class("antibot", plugin)
function antibot:new()
plugin.new(self, "antibot")
-- Call parent new
local ok, err = plugin.new(self, "antibot")
if not ok then
return false, err
end
-- Check if init is needed
if ngx.get_phase() == "init" then
local init_needed, err = utils.has_not_variable("USE_ANTIBOT", "no")
if init_needed == nil then
return false, err
end
self.init_needed = init_needed
end
return true, "success"
end
function antibot:init()
-- Check if init is needed
local init_needed, err = utils.has_not_variable("USE_ANTIBOT", "no")
if init_needed == nil then
return self:ret(false, err)
end
if not init_needed then
return self:ret(true, "no service uses Antibot, skipping init")
end
-- Load templates
local templates = {}
for i, template in ipairs({ "javascript", "captcha", "recaptcha", "hcaptcha" }) do
local f, err = io.open("/usr/share/bunkerweb/core/antibot/files/" .. template .. ".html")
if not f then
return self:ret(false, "error while loading " .. template .. ".html : " .. err)
if self.init_needed then
-- Load templates
local templates = {}
for i, template in ipairs({ "javascript", "captcha", "recaptcha", "hcaptcha" }) do
local f, err = io.open("/usr/share/bunkerweb/core/antibot/files/" .. template .. ".html")
if not f then
return self:ret(false, "error while loading " .. template .. ".html : " .. err)
end
templates[template] = f:read("*all")
f:close()
end
local ok, err = datastore:set("plugin_antibot_templates", cjson.encode(templates))
if not ok then
return self:ret(false, "can't save templates to datastore : " .. err)
end
templates[template] = f:read("*all")
f:close()
end
local ok, err = datastore:set("plugin_antibot_templates", cjson.encode(templates))
if not ok then
return self:ret(false, "can't save templates to datastore : " .. err)
end
return self:ret(true, "success")
end
function antibot:access()
-- Check if access is needed
local antibot, err = utils.get_variable("USE_ANTIBOT")
if antibot == nil then
return self:ret(false, err)
end
if antibot == "no" then
if self.variables["USE_ANTIBOT"] == "no" then
return self:ret(true, "antibot not activated")
end
-- Get challenge URI
local challenge_uri, err = utils.get_variable("ANTIBOT_URI")
if not challenge_uri then
return self:ret(false, "can't get antibot URI from datastore : " .. err)
end
-- Prepare challenge
local ok, err = self:prepare_challenge(antibot, challenge_uri)
if not ok then
@ -120,12 +117,12 @@ function antibot:content()
-- Display content
local ok, err = self:display_challenge(antibot)
if not ok then
return self:ret(false, "display challenge error : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "display challenge error : " .. err)
end
return self:ret(true, "content displayed")
end
function _M:challenge_resolved(antibot)
function antibot:challenge_resolved()
local session, err, exists = utils.get_session()
if err then
return false, "session error : " .. err
@ -135,41 +132,56 @@ function _M:challenge_resolved(antibot)
return false, "session is set but no antibot data", nil
end
local data = cjson.decode(raw_data)
if data.resolved and antibot == data.antibot then
if data.resolved and self.variables["USE_ANTIBOT"] == data.antibot then
return true, "challenge resolved", data.original_uri
end
return false, "challenge not resolved", data.original_uri
return false, "no session", nil
end
function _M:prepare_challenge(antibot, challenge_uri)
function antibot:prepare_challenge()
local session, err, exists = utils.get_session()
if err then
return false, "session error : " .. err
end
local current_data = nil
local set_needed = false
local data = nil
if exists then
local raw_data = get_session("antibot")
if raw_data then
current_data = cjson.decode(raw_data)
data = cjson.decode(raw_data)
end
end
if not current_data or current_data.antibot ~= antibot then
local data = {
type = antibot,
resolved = antibot == "cookie",
if not data or current_data.antibot ~= self.variables["USE_ANTIBOT"] then
data = {
type = self.variables["USE_ANTIBOT"],
resolved = self.variables["USE_ANTIBOT"] == "cookie",
original_uri = ngx.var.request_uri
}
if ngx.var.original_uri == challenge_uri then
data.original_uri = "/"
end
utils.set_session("antibot", cjson.encode(data))
return true, "prepared"
set_needed = true
end
return true, "already prepared"
if not data.resolved then
if self.variables["USE_ANTIBOT"] == "javascript" then
data.random = utils.rand(20)
set_needed = true
elseif self.variables["USE_ANTIBOT"] == "captcha" then
local chall_captcha = captcha.new()
chall_captcha:font("/usr/share/bunkerweb/core/antibot/files/font.ttf")
chall_captcha:generate()
data.image = base64.encode(chall_captcha:jpegStr(70))
data.text = chall_captcha:getStr()
set_needed = true
end
end
if set_needed then
utils.set_session("antibot", cjson.encode(data))
end
return true, "prepared"
end
function _M:display_challenge(antibot, challenge_uri)
function antibot:display_challenge(challenge_uri)
-- Open session
local session, err, exists = utils.get_session()
if err then
@ -184,21 +196,10 @@ function _M:display_challenge(antibot, challenge_uri)
local data = cjson.decode(raw_data)
-- Check if session type is equal to antibot type
if antibot ~= data.type then
if self.variables["USE_ANTIBOT"] ~= data.type then
return false, "session type is different from antibot type"
end
-- Compute challenges
if antibot == "javascript" then
data.random = utils.rand(20)
elseif antibot == "captcha" then
local chall_captcha = captcha.new()
chall_captcha:font("/usr/share/bunkerweb/core/antibot/files/font.ttf")
chall_captcha:generate()
data.image = base64.encode(chall_captcha:jpegStr(70))
data.text = chall_captcha:getStr()
end
-- Load HTML templates
local str_templates, err = datastore:get("plugin_antibot_templates")
if not str_templates then
@ -209,46 +210,33 @@ function _M:display_challenge(antibot, challenge_uri)
local html = ""
-- Javascript case
if antibot == "javascript" then
html = templates.javascript:format(challenge_uri, data.random)
if self.variables["USE_ANTIBOT"] == "javascript" then
html = templates.javascript:format(self.variables["ANTIBOT_URI"], data.random)
end
-- Captcha case
if antibot == "captcha" then
html = templates.captcha:format(challenge_uri, data.image)
if self.variables["USE_ANTIBOT"] == "captcha" then
html = templates.captcha:format(self.variables["ANTIBOT_URI"], data.image)
end
-- reCAPTCHA case
if antibot == "recaptcha" then
local recaptcha_sitekey, err = utils.get_variable("ANTIBOT_RECAPTCHA_SITEKEY")
if not recaptcha_sitekey then
return false, "can't get reCAPTCHA sitekey variable : " .. err
end
html = templates.recaptcha:format(recaptcha_sitekey, challenge_uri, recaptcha_sitekey)
if self.variables["USE_ANTIBOT"] == "recaptcha" then
html = templates.recaptcha:format(self.variables["ANTIBOT_RECAPTCHA_SITEKEY"], self.variables["ANTIBOT_URI"], self.variables["ANTIBOT_RECAPTCHA_SITEKEY"])
end
-- hCaptcha case
if antibot == "hcaptcha" then
local hcaptcha_sitekey, err = utils.get_variable("ANTIBOT_HCAPTCHA_SITEKEY")
if not hcaptcha_sitekey then
return false, "can't get hCaptcha sitekey variable : " .. err
end
html = templates.hcaptcha:format(challenge_uri, hcaptcha_sitekey)
if self.variables["USE_ANTIBOT"] == "hcaptcha" then
html = templates.hcaptcha:format(self.variables["ANTIBOT_URI"], self.variables["ANTIBOT_HCAPTCHA_SITEKEY"])
end
-- Set new data
utils.set_session("antibot", cjson.encode(data))
local ok, err = utils.save_session()
if not ok then
return false, "can't save session : " .. err
end
-- Send content
ngx.header["Content-Type"] = "text/html"
ngx.say(html)
return true, "displayed challenge"
end
function _M:check_challenge(antibot)
function antibot:check_challenge()
-- Open session
local session, err, exists = utils.get_session()
if err then
@ -263,7 +251,7 @@ function _M:check_challenge(antibot)
local data = cjson.decode(raw_data)
-- Check if session type is equal to antibot type
if antibot ~= data.type then
if elf.variables["USE_ANTIBOT"] ~= data.type then
return nil, "session type is different from antibot type", nil
end
@ -272,7 +260,7 @@ function _M:check_challenge(antibot)
local redirect = nil
-- Javascript case
if antibot == "javascript" then
if self.variables["USE_ANTIBOT"] == "javascript" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
if err == "truncated" or not args or not args["challenge"] then
@ -291,7 +279,7 @@ function _M:check_challenge(antibot)
end
-- Captcha case
if antibot == "captcha" then
if self.variables["USE_ANTIBOT"] == "captcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
if err == "truncated" or not args or not args["captcha"] then
@ -306,23 +294,19 @@ function _M:check_challenge(antibot)
end
-- reCAPTCHA case
if antibot == "recaptcha" then
if self.variables["USE_ANTIBOT"] == "recaptcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
if err == "truncated" or not args or not args["token"] then
return false, "missing challenge arg", nil
end
local recaptcha_secret, err = utils.get_variable("ANTIBOT_RECAPTCHA_SECRET")
if not recaptcha_secret then
return nil, "can't get reCAPTCHA secret variable : " .. err, nil
end
local httpc, err = http.new()
if not httpc then
return false, "can't instantiate http object : " .. err, nil, nil
end
local res, err = httpc:request_uri("https://www.google.com/recaptcha/api/siteverify", {
method = "POST",
body = "secret=" .. recaptcha_secret .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
body = "secret=" .. self.variables["ANTIBOT_RECAPTCHA_SECRET"] .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}
@ -335,11 +319,7 @@ function _M:check_challenge(antibot)
if not ok then
return nil, "error while decoding JSON from reCAPTCHA API : " .. rdata, nil
end
local recaptcha_score, err = utils.get_variable("ANTIBOT_RECAPTCHA_SCORE")
if not recaptcha_score then
return nil, "can't get reCAPTCHA score variable : " .. err, nil
end
if not rdata.success or rdata.score < tonumber(recaptcha_score) then
if not rdata.success or rdata.score < tonumber(self.variables["ANTIBOT_RECAPTCHA_SCORE"]) then
return false, "client failed challenge with score " .. tostring(rdata.score), nil
end
data.resolved = true
@ -348,23 +328,19 @@ function _M:check_challenge(antibot)
end
-- hCaptcha case
if antibot == "hcaptcha" then
if self.variables["USE_ANTIBOT"] == "hcaptcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
if err == "truncated" or not args or not args["token"] then
return false, "missing challenge arg", nil
end
local hcaptcha_secret, err = utils.get_variable("ANTIBOT_HCAPTCHA_SECRET")
if not hcaptcha_secret then
return nil, "can't get hCaptcha secret variable : " .. err, nil
end
local httpc, err = http.new()
if not httpc then
return false, "can't instantiate http object : " .. err, nil, nil
end
local res, err = httpc:request_uri("https://hcaptcha.com/siteverify", {
method = "POST",
body = "secret=" .. hcaptcha_secret .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
body = "secret=" .. self.variables["ANTIBOT_HCAPTCHA_SECRET"] .. "&response=" .. args["token"] .. "&remoteip=" .. ngx.var.remote_addr,
headers = {
["Content-Type"] = "application/x-www-form-urlencoded"
}

View File

@ -0,0 +1,16 @@
{% if USE_ANTIBOT == "yes" +%}
location /{{ ANTIBOT_URI }} {
content_by_lua_block {
local antibot = require "antibot.antibot"
local logger = require "bunkerweb.logger"
antibot:new()
logger:new("ANTIBOT")
local ok, err = antibot:content()
if not ok then
logger:log(ngx.ERR, "antibot:content() failed : " .. err)
else
logger:log(ngx.INFO, "antibot:content() success : " .. err)
end
}
}
{% endif %}

View File

@ -1,43 +1,41 @@
local _M = {}
_M.__index = _M
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local logger = require "bunkerweb.logger"
local utils = require "bunkerweb.utils"
local clusterstore = require "bunkerweb.clusterstore"
local utils = require "utils"
local datastore = require "datastore"
local logger = require "logger"
local cjson = require "cjson"
local resolver = require "resty.dns.resolver"
local clusterstore = require "clusterstore"
local redis = class("redis", plugin)
function _M.new()
local self = setmetatable({}, _M)
return self, nil
end
function _M:init()
-- Check if init is needed
function redis:new()
plugin.new(self, "redis")
-- Store variable for later use
local use_redis, err = utils.get_variable("USE_REDIS", false)
if use_redis == nil then
return false, "can't check USE_REDIS variable : " .. err
return self:ret(false, "can't check USE_REDIS variable : " .. err)
end
if use_redis ~= "yes" then
return true, "redis not used"
end
-- Check redis connection
local redis_client, err = clusterstore:connect()
if not redis_client then
return false, "can't connect to redis server"
end
local ok, err = redis_client:ping()
if err then
clusterstore:close(redis_client)
return false, "error while sending ping command : " .. err
end
if not ok then
clusterstore:close(redis_client)
return false, "ping command failed"
end
clusterstore:close(redis_client)
return true, "redis ping successful"
self.use_redis = use_redis == "yes"
return self:ret(true, "success")
end
return _M
function redis:init()
-- Check if init is needed
if not self.use_redis then
return self:ret(true, "redis not used")
end
-- Check redis connection
local ok, err = clusterstore:connect()
if not ok then
return self:ret(false, "redis connect error : " .. err)
end
local ok, err = clusterstore:call("ping")
clusterstore:close()
if err then
return self:ret(false, "error while sending ping command : " .. err)
end
if not ok then
return self:ret(false, "ping command failed")
end
return self:ret(true, "redis ping successful")
end
return redis