Merge remote-tracking branch 'origin/dev' into ui

This commit is contained in:
Jordan Blasenhauer 2024-01-02 14:40:00 +01:00
commit 66fa2df6ce
76 changed files with 5503 additions and 949 deletions

View file

@ -72,28 +72,21 @@ jobs:
echo "127.0.0.1 www.example.com" | sudo tee -a /etc/hosts
echo "127.0.0.1 app1.example.com" | sudo tee -a /etc/hosts
# BunkerWeb
echo "SERVER_NAME=www.example.com" | sudo tee /etc/bunkerweb/variables.env
echo "SERVER_NAME=" | sudo tee /etc/bunkerweb/variables.env
echo "HTTP_PORT=80" | sudo tee -a /etc/bunkerweb/variables.env
echo "HTTPS_PORT=443" | sudo tee -a /etc/bunkerweb/variables.env
echo 'DNS_RESOLVERS=9.9.9.9 8.8.8.8 8.8.4.4' | sudo tee -a /etc/bunkerweb/variables.env
echo 'API_LISTEN_IP=127.0.0.1' | sudo tee -a /etc/bunkerweb/variables.env
echo "MULTISITE=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "LOG_LEVEL=info" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_BUNKERNET=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_BLACKLIST=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "SEND_ANONYMOUS_REPORT=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "DISABLE_DEFAULT_SERVER=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_CLIENT_CACHE=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_GZIP=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "DATASTORE_MEMORY_SIZE=384m" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_USE_UI=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_SERVE_FILES=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_USE_REVERSE_PROXY=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_REVERSE_PROXY_URL=/admin" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_REVERSE_PROXY_HOST=http://127.0.0.1:7000" | sudo tee -a /etc/bunkerweb/variables.env
echo "www.example.com_INTERCEPTED_ERROR_CODES=400 405 413 429 500 501 502 503 504" | sudo tee -a /etc/bunkerweb/variables.env
echo "ADMIN_USERNAME=admin" | sudo tee /etc/bunkerweb/ui.env
echo "ADMIN_PASSWORD=S\$cr3tP@ssw0rd" | sudo tee -a /etc/bunkerweb/ui.env
echo "UI_HOST=http://127.0.0.1:7000" | sudo tee -a /etc/bunkerweb/variables.env
sudo touch /etc/bunkerweb/ui.env
sudo chown nginx:nginx /etc/bunkerweb/variables.env /etc/bunkerweb/ui.env
sudo chmod 777 /etc/bunkerweb/variables.env /etc/bunkerweb/ui.env

View file

@ -8,7 +8,10 @@
- [FEATURE] Add support for fallback Referrer-Policies
- [FEATURE] Add profile page to web ui and the possibility to activate the 2FA
- [FEATURE] Add setting REVERSE_PROXY_INCLUDES to manually add "include" directives in the reverse proxies
- [FEATURE] Add support for Redis Sentinel
- [FEATURE] Add support for tls in Ingress definition
- [MISC] Fallback to default HTTPS certificate to prevent errors
- [MISC] Various internal improvements in LUA code
- [MISC] Updated Python Docker image to 3.12.1-alpine3.18 in Dockerfiles
- [DEPS] Updated ModSecurity to v3.0.11

View file

@ -205,11 +205,13 @@ STREAM support :white_check_mark:
Choose custom certificate for HTTPS.
| Setting |Default| Context |Multiple| Description |
|-----------------|-------|---------|--------|--------------------------------------------------------------------------------|
|`USE_CUSTOM_SSL` |`no` |multisite|no |Use custom HTTPS certificate. |
|`CUSTOM_SSL_CERT`| |multisite|no |Full path of the certificate or bundle file (must be readable by the scheduler).|
|`CUSTOM_SSL_KEY` | |multisite|no |Full path of the key file (must be readable by the scheduler). |
| Setting |Default| Context |Multiple| Description |
|----------------------|-------|---------|--------|--------------------------------------------------------------------------------|
|`USE_CUSTOM_SSL` |`no` |multisite|no |Use custom HTTPS certificate. |
|`CUSTOM_SSL_CERT` | |multisite|no |Full path of the certificate or bundle file (must be readable by the scheduler).|
|`CUSTOM_SSL_KEY` | |multisite|no |Full path of the key file (must be readable by the scheduler). |
|`CUSTOM_SSL_CERT_DATA`| |multisite|no |Certificate data encoded in base64. |
|`CUSTOM_SSL_KEY_DATA` | |multisite|no |Key data encoded in base64. |
### DB
@ -425,16 +427,22 @@ STREAM support :white_check_mark:
Redis server configuration when using BunkerWeb in cluster mode.
| Setting |Default|Context|Multiple| Description |
|----------------------|-------|-------|--------|------------------------------------------------------------------|
|`USE_REDIS` |`no` |global |no |Activate Redis. |
|`REDIS_HOST` | |global |no |Redis server IP or hostname. |
|`REDIS_PORT` |`6379` |global |no |Redis server port. |
|`REDIS_DATABASE` |`0` |global |no |Redis database number. |
|`REDIS_SSL` |`no` |global |no |Use SSL/TLS connection with Redis server. |
|`REDIS_TIMEOUT` |`1000` |global |no |Redis server timeout (in ms) for connect, read and write. |
|`REDIS_KEEPALIVE_IDLE`|`30000`|global |no |Max idle time (in ms) before closing redis connection in the pool.|
|`REDIS_KEEPALIVE_POOL`|`10` |global |no |Max number of redis connection(s) kept in the pool. |
| Setting |Default|Context|Multiple| Description |
|-------------------------|-------|-------|--------|-------------------------------------------------------------------|
|`USE_REDIS` |`no` |global |no |Activate Redis. |
|`REDIS_HOST` | |global |no |Redis server IP or hostname. |
|`REDIS_PORT` |`6379` |global |no |Redis server port. |
|`REDIS_DATABASE` |`0` |global |no |Redis database number. |
|`REDIS_SSL` |`no` |global |no |Use SSL/TLS connection with Redis server. |
|`REDIS_TIMEOUT` |`1000` |global |no |Redis server timeout (in ms) for connect, read and write. |
|`REDIS_KEEPALIVE_IDLE` |`30000`|global |no |Max idle time (in ms) before closing redis connection in the pool. |
|`REDIS_KEEPALIVE_POOL` |`10` |global |no |Max number of redis connection(s) kept in the pool. |
|`REDIS_USERNAME` | |global |no |Redis username used in AUTH command. |
|`REDIS_PASSWORD` | |global |no |Redis password used in AUTH command. |
|`REDIS_SENTINEL_HOSTS` | |global |no |Redis sentinel hosts with format host:[port] separated with spaces.|
|`REDIS_SENTINEL_USERNAME`| |global |no |Redis sentinel username. |
|`REDIS_SENTINEL_PASSWORD`| |global |no |Redis sentinel password. |
|`REDIS_SENTINEL_MASTER` | |global |no |Redis sentinel master name. |
### Reverse proxy
@ -469,6 +477,7 @@ Manage reverse proxy configurations.
|`REVERSE_PROXY_CONNECT_TIMEOUT` |`60s` |multisite|yes |Timeout when connecting to the proxied resource. |
|`REVERSE_PROXY_READ_TIMEOUT` |`60s` |multisite|yes |Timeout when reading from the proxied resource. |
|`REVERSE_PROXY_SEND_TIMEOUT` |`60s` |multisite|yes |Timeout when sending to the proxied resource. |
|`REVERSE_PROXY_INCLUDES` | |multisite|yes |Additional configuration to include in the location block, separated with spaces. |
### Reverse scan
@ -541,3 +550,4 @@ Allow access based on internal and external IP/network/rDNS/ASN whitelists.
|`WHITELIST_USER_AGENT_URLS`| |global |no |List of URLs, separated with spaces, containing good User-Agent to whitelist. |
|`WHITELIST_URI` | |multisite|no |List of URI (PCRE regex), separated with spaces, to whitelist. |
|`WHITELIST_URI_URLS` | |global |no |List of URLs, separated with spaces, containing bad URI to whitelist. |

View file

@ -1,24 +1,52 @@
local ngx = ngx
local ngx_req = ngx.req
local cjson = require "cjson"
local class = require "middleclass"
local datastore = require "bunkerweb.datastore"
local logger = require "bunkerweb.logger"
local cdatastore = require "bunkerweb.datastore"
local clogger = require "bunkerweb.logger"
local process = require "ngx.process"
local rsignal = require "resty.signal"
local upload = require "resty.upload"
local utils = require "bunkerweb.utils"
local helpers = require "bunkerweb.helpers"
local api = class("api")
local datastore = cdatastore:new()
local logger = clogger:new("API")
local get_variable = utils.get_variable
local is_ip_in_networks = utils.is_ip_in_networks
-- local run = shell.run
local NOTICE = ngx.NOTICE
local ERR = ngx.ERR
local HTTP_OK = ngx.HTTP_OK
local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR
local HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST
local HTTP_NOT_FOUND = ngx.HTTP_NOT_FOUND
local kill = rsignal.kill
local get_master_pid = process.get_master_pid
local execute = os.execute
local open = io.open
local read_body = ngx_req.read_body
local get_body_data = ngx_req.get_body_data
local get_body_file = ngx_req.get_body_file
local decode = cjson.decode
local encode = cjson.encode
local floor = math.floor
local match = string.match
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
api.global = { GET = {}, POST = {}, PUT = {}, DELETE = {} }
function api:initialize()
self.datastore = datastore:new()
self.logger = logger:new("API")
self.ctx = ngx.ctx
local data, err = utils.get_variable("API_WHITELIST_IP", false)
function api:initialize(ctx)
self.ctx = ctx
local data, err = get_variable("API_WHITELIST_IP", false)
self.ips = {}
if not data then
self.logger.log(ngx.ERR, "can't get API_WHITELIST_IP variable : " .. err)
logger:log(ERR, "can't get API_WHITELIST_IP variable : " .. err)
else
for ip in data:gmatch("%S+") do
table.insert(self.ips, ip)
@ -28,23 +56,23 @@ end
-- luacheck: ignore 212
function api:log_cmd(cmd, status, stdout, stderr)
local level = ngx.NOTICE
local level = NOTICE
local prefix = "success"
if status ~= 0 then
level = ngx.ERR
level = ERR
prefix = "error"
end
self.logger:log(level, prefix .. " while running command " .. cmd)
self.logger:log(level, "stdout = " .. stdout)
self.logger:log(level, "stdout = " .. stderr)
logger:log(level, prefix .. " while running command " .. cmd)
logger:log(level, "stdout = " .. stdout)
logger:log(level, "stdout = " .. stderr)
end
-- TODO : use this if we switch to OpenResty
function api:cmd(cmd)
-- Non-blocking command
-- luacheck: ignore 113
local ok, stdout, stderr, reason, status = shell.run(cmd, nil, 10000)
self.logger:log_cmd(cmd, status, stdout, stderr)
local ok, stdout, stderr, reason, status = run(cmd, nil, 10000)
self:log_cmd(cmd, status, stdout, stderr)
-- Timeout
if ok == nil then
return nil, reason
@ -62,25 +90,30 @@ function api:response(http_status, api_status, msg)
end
api.global.GET["^/ping$"] = function(self)
return self:response(ngx.HTTP_OK, "success", "pong")
return self:response(HTTP_OK, "success", "pong")
end
api.global.POST["^/reload$"] = function(self)
-- Send HUP signal to master process
local ok, err = rsignal.kill(process.get_master_pid(), "HUP")
if not ok then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err)
-- Check config
local status = execute("nginx -t")
if status ~= 0 then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "config check failed")
end
return self:response(ngx.HTTP_OK, "success", "reload successful")
-- Send HUP signal to master process
local ok, err = kill(get_master_pid(), "HUP")
if not ok then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err)
end
return self:response(HTTP_OK, "success", "reload successful")
end
api.global.POST["^/stop$"] = function(self)
-- Send QUIT signal to master process
local ok, err = rsignal.kill(process.get_master_pid(), "QUIT")
local ok, err = kill(get_master_pid(), "QUIT")
if not ok then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err)
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err)
end
return self:response(ngx.HTTP_OK, "success", "stop successful")
return self:response(HTTP_OK, "success", "stop successful")
end
api.global.POST["^/confs$"] = function(self)
@ -99,16 +132,19 @@ api.global.POST["^/confs$"] = function(self)
end
local form, err = upload:new(4096)
if not form then
return self:response(ngx.HTTP_BAD_REQUEST, "error", err)
return self:response(HTTP_BAD_REQUEST, "error", err)
end
form:set_timeout(1000)
local file = io.open(tmp, "w+")
local file, err = open(tmp, "w+")
if not file then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", err)
end
while true do
-- luacheck: ignore 421
local typ, res, err = form:read()
if not typ then
file:close()
return self:response(ngx.HTTP_BAD_REQUEST, "error", err)
return self:response(HTTP_BAD_REQUEST, "error", err)
end
if typ == "eof" then
break
@ -124,12 +160,12 @@ api.global.POST["^/confs$"] = function(self)
"tar xzf " .. tmp .. " -C " .. destination,
}
for _, cmd in ipairs(cmds) do
local status = os.execute(cmd)
local status = execute(cmd)
if status ~= 0 then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
end
end
return self:response(ngx.HTTP_OK, "success", "saved data at " .. destination)
return self:response(HTTP_OK, "success", "saved data at " .. destination)
end
api.global.POST["^/data$"] = api.global.POST["^/confs$"]
@ -141,80 +177,86 @@ api.global.POST["^/custom_configs$"] = api.global.POST["^/confs$"]
api.global.POST["^/plugins$"] = api.global.POST["^/confs$"]
api.global.POST["^/unban$"] = function(self)
ngx.req.read_body()
local data = ngx.req.get_body_data()
read_body()
local data = get_body_data()
if not data then
local data_file = ngx.req.get_body_file()
local data_file = get_body_file()
if data_file then
local file = io.open(data_file)
local file, err = open(data_file)
if not file then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", err)
end
data = file:read("*a")
file:close()
end
end
local ok, ip = pcall(cjson.decode, data)
local ok, ip = pcall(decode, data)
if not ok then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
end
self.datastore:delete("bans_ip_" .. ip["ip"])
return self:response(ngx.HTTP_OK, "success", "ip " .. ip["ip"] .. " unbanned")
datastore:delete("bans_ip_" .. ip["ip"])
return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " unbanned")
end
api.global.POST["^/ban$"] = function(self)
ngx.req.read_body()
local data = ngx.req.get_body_data()
read_body()
local data = get_body_data()
if not data then
local data_file = ngx.req.get_body_file()
local data_file = get_body_file()
if data_file then
local file = io.open(data_file)
local file, err = io.open(data_file)
if not file then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", err)
end
data = file:read("*a")
file:close()
end
end
local ok, ip = pcall(cjson.decode, data)
local ok, ip = pcall(decode, data)
if not ok then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
end
self.datastore:set("bans_ip_" .. ip["ip"], "manual", ip["exp"])
return self:response(ngx.HTTP_OK, "success", "ip " .. ip["ip"] .. " banned")
datastore:set("bans_ip_" .. ip["ip"], "manual", ip["exp"])
return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " banned")
end
api.global.GET["^/bans$"] = function(self)
local data = {}
for _, k in ipairs(self.datastore:keys()) do
for _, k in ipairs(datastore:keys()) do
if k:find("^bans_ip_") then
local reason, err = self.datastore:get(k)
local reason, err = datastore:get(k)
if err then
return self:response(
ngx.HTTP_INTERNAL_SERVER_ERROR,
HTTP_INTERNAL_SERVER_ERROR,
"error",
"can't access " .. k .. " from datastore : " .. reason
)
end
local ok, ttl = self.datastore:ttl(k)
local ok, ttl = datastore:ttl(k)
if not ok then
return self:response(
ngx.HTTP_INTERNAL_SERVER_ERROR,
HTTP_INTERNAL_SERVER_ERROR,
"error",
"can't access ttl " .. k .. " from datastore : " .. ttl
)
end
local ban = { ip = k:sub(9, #k), reason = reason, exp = math.floor(ttl) }
local ban = { ip = k:sub(9, #k), reason = reason, exp = floor(ttl) }
table.insert(data, ban)
end
end
return self:response(ngx.HTTP_OK, "success", data)
return self:response(HTTP_OK, "success", data)
end
api.global.GET["^/variables$"] = function(self)
local variables, err = datastore:get("variables", true)
if not variables then
return self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't access variables from datastore : " .. err)
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't access variables from datastore : " .. err)
end
return self:response(ngx.HTTP_OK, "success", variables)
return self:response(HTTP_OK, "success", variables)
end
function api:is_allowed_ip()
if utils.is_ip_in_networks(self.ctx.bw.remote_addr, self.ips) then
if is_ip_in_networks(self.ctx.bw.remote_addr, self.ips) then
return true, "ok"
end
return false, "IP is not in API_WHITELIST_IP"
@ -223,10 +265,10 @@ end
function api:do_api_call()
if self.global[self.ctx.bw.request_method] ~= nil then
for uri, api_fun in pairs(self.global[self.ctx.bw.request_method]) do
if string.match(self.ctx.bw.uri, uri) then
if match(self.ctx.bw.uri, uri) then
local status, resp = api_fun(self)
local ret = true
if status ~= ngx.HTTP_OK then
if status ~= HTTP_OK then
ret = false
end
if #resp["msg"] == 0 then
@ -235,26 +277,36 @@ function api:do_api_call()
resp["data"] = resp["msg"]
resp["msg"] = resp["status"]
end
return ret, resp["msg"], status, cjson.encode(resp)
return ret, resp["msg"], status, encode(resp)
end
end
end
local list, err = self.datastore:get("plugins", true)
local list, err = datastore:get("plugins", true)
if not list then
local _, resp = self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't list loaded plugins : " .. err)
return false, resp["msg"], ngx.HTTP_INTERNAL_SERVER_ERROR, cjson.encode(resp)
local _, resp = self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't list loaded plugins : " .. err)
return false, resp["msg"], HTTP_INTERNAL_SERVER_ERROR, encode(resp)
end
for _, plugin in ipairs(list) do
if pcall(require, plugin.id .. "/" .. plugin.id) then
local plugin_lua = require(plugin.id .. "/" .. plugin.id)
if plugin_lua.api ~= nil then
local matched, status, resp = plugin_lua:api(self.ctx)
if matched then
local ret = true
if status ~= ngx.HTTP_OK then
ret = false
local plugin_lua, err = require_plugin(plugin.id)
if plugin_lua and plugin_lua.api ~= nil then
local ok, plugin_obj = new_plugin(plugin_lua, self.ctx)
if not ok then
logger:log(ERR, "can't instantiate " .. plugin.id .. " : " .. plugin_obj)
else
local ok, ret = call_plugin(plugin_obj, "api")
if not ok then
logger:log(ERR, "error while executing " .. plugin.id .. ":api() : " .. ret)
else
if ret.ret then
local resp = {}
if ret.status == HTTP_OK then
resp["status"] = "success"
else
resp["status"] = "error"
end
resp["msg"] = ret.msg
return ret.status == HTTP_OK, resp["status"], ret.status, encode(resp)
end
return ret, resp["msg"], status, cjson.encode(resp)
end
end
end
@ -262,7 +314,7 @@ function api:do_api_call()
local resp = {}
resp["status"] = "error"
resp["msg"] = "not found"
return false, "error", ngx.HTTP_NOT_FOUND, cjson.encode(resp)
return false, "error", HTTP_NOT_FOUND, encode(resp)
end
return api

View file

@ -1,17 +1,27 @@
local ngx = ngx
local class = require "middleclass"
local clusterstore = require "bunkerweb.clusterstore"
local logger = require "bunkerweb.logger"
local clogger = require "bunkerweb.logger"
local mlcache = require "resty.mlcache"
local utils = require "bunkerweb.utils"
local cachestore = class("cachestore")
local logger = clogger:new("CACHESTORE")
local subsystem = ngx.config.subsystem
local ERR = ngx.ERR
local INFO = ngx.INFO
local null = ngx.null
local get_ctx_obj = utils.get_ctx_obj
local is_cosocket_available = utils.is_cosocket_available
-- Instantiate mlcache object at module level (which will be cached when running init phase)
-- TODO : custom settings
local shm = "cachestore"
local ipc_shm = "cachestore_ipc"
local shm_miss = "cachestore_miss"
local shm_locks = "cachestore_locks"
if not ngx.shared.cachestore then
if subsystem == "stream" then
shm = "cachestore_stream"
ipc_shm = "cachestore_ipc_stream"
shm_miss = "cachestore_miss_stream"
@ -33,22 +43,18 @@ local cache, err = mlcache.new("cachestore", shm, {
},
ipc_shm = ipc_shm,
})
local module_logger = logger:new("CACHESTORE")
if not cache then
module_logger:log(ngx.ERR, "can't instantiate mlcache : " .. err)
logger:log(ERR, "can't instantiate mlcache : " .. err)
end
function cachestore:initialize(use_redis, new_cs, ctx)
self.ctx = ctx
self.cache = cache
function cachestore:initialize(use_redis, ctx, pool)
self.use_redis = use_redis or false
self.logger = module_logger
if new_cs then
self.clusterstore = clusterstore:new(false)
self.shared_cs = false
else
self.clusterstore = utils.get_ctx_obj("clusterstore", self.ctx)
self.shared_cs = true
if self.use_redis then
if ctx then
self.clusterstore = get_ctx_obj("clusterstore", ctx)
else
self.clusterstore = clusterstore:new(pool)
end
end
end
@ -57,8 +63,7 @@ function cachestore:get(key)
local callback = function(key, cs)
-- Connect to redis
-- luacheck: ignore 431
local clusterstore = cs or require "bunkerweb.clusterstore":new(false)
local ok, err, _ = clusterstore:connect()
local ok, err, _ = cs:connect()
if not ok then
return nil, "can't connect to redis : " .. err, nil
end
@ -76,14 +81,14 @@ function cachestore:get(key)
end
return {ret_get, ret_ttl}
]]
local ret, err = clusterstore:call("eval", redis_script, 1, key)
local ret, err = cs:call("eval", redis_script, 1, key)
if not ret then
clusterstore:close()
cs:close()
return nil, err, nil
end
-- Extract values
clusterstore:close()
if ret[1] == ngx.null then
cs:close()
if ret[1] == null then
ret[1] = nil
ret[2] = -1
elseif ret[2] < 0 then
@ -96,35 +101,31 @@ function cachestore:get(key)
end
-- luacheck: ignore 431
local value, err, hit_level
if self.use_redis and utils.is_cosocket_available() then
local cs = nil
if self.shared_cs then
cs = self.clusterstore
end
value, err, hit_level = self.cache:get(key, nil, callback, key, cs)
if self.use_redis and is_cosocket_available() then
value, err, hit_level = cache:get(key, nil, callback, key, self.clusterstore)
else
value, err, hit_level = self.cache:get(key, nil, callback_no_miss)
value, err, hit_level = cache:get(key, nil, callback_no_miss)
end
if value == nil and err ~= nil then
return false, err
end
self.logger:log(ngx.INFO, "hit level for " .. key .. " = " .. tostring(hit_level))
logger:log(INFO, "hit level for " .. key .. " = " .. tostring(hit_level))
return true, value
end
function cachestore:set(key, value, ex)
-- luacheck: ignore 431
local ok, err
if self.use_redis and utils.is_cosocket_available() then
if self.use_redis and is_cosocket_available() then
ok, err = self:set_redis(key, value, ex)
if not ok then
self.logger:log(ngx.ERR, err)
logger:log(ERR, err)
end
end
if ex then
ok, err = self.cache:set(key, { ttl = ex }, value)
ok, err = cache:set(key, { ttl = ex }, value)
else
ok, err = self.cache:set(key, nil, value)
ok, err = cache:set(key, nil, value)
end
if not ok then
return false, err
@ -153,13 +154,13 @@ end
function cachestore:delete(key)
-- luacheck: ignore 431
local ok, err
if self.use_redis and utils.is_cosocket_available() then
if self.use_redis and is_cosocket_available() then
ok, err = self:del_redis(key)
if not ok then
self.logger:log(ngx.ERR, err)
logger:log(ERR, err)
end
end
ok, err = self.cache:delete(key)
ok, err = cache:delete(key)
if not ok then
return false, err
end
@ -184,7 +185,11 @@ function cachestore:del_redis(key)
end
function cachestore:purge()
return self.cache:purge(true)
return cache:purge(true)
end
function cachestore:update()
return cache:update()
end
return cachestore

View file

@ -1,15 +1,24 @@
local ngx = ngx
local class = require "middleclass"
local logger = require "bunkerweb.logger"
local redis = require "resty.redis"
local clogger = require "bunkerweb.logger"
local rc = require "resty.redis.connector"
local rs = require("resty.redis.sentinel")
local utils = require "bunkerweb.utils"
local clusterstore = class("clusterstore")
local logger = clogger:new("CLUSTERSTORE")
local get_variable = utils.get_variable
local is_cosocket_available = utils.is_cosocket_available
local ERR = ngx.ERR
local tonumber = tonumber
local random = math.random
function clusterstore:initialize(pool)
-- Instantiate logger
self.logger = logger:new("CLUSTERSTORE")
-- Get variables
local variables = {
["USE_REDIS"] = "",
["REDIS_HOST"] = "",
["REDIS_PORT"] = "",
["REDIS_DATABASE"] = "",
@ -17,98 +26,150 @@ function clusterstore:initialize(pool)
["REDIS_TIMEOUT"] = "",
["REDIS_KEEPALIVE_IDLE"] = "",
["REDIS_KEEPALIVE_POOL"] = "",
["REDIS_USERNAME"] = "",
["REDIS_PASSWORD"] = "",
["REDIS_SENTINEL_HOSTS"] = "",
["REDIS_SENTINEL_USERNAME"] = "",
["REDIS_SENTINEL_PASSWORD"] = "",
["REDIS_SENTINEL_MASTER"] = ""
}
-- Set them for later user
-- Set them for later use
self.variables = {}
for k, _ in pairs(variables) do
local value, err = utils.get_variable(k, false)
local value, err = get_variable(k, false)
if value == nil then
self.logger:log(ngx.ERR, err)
logger:log(ERR, err)
end
self.variables[k] = value
end
-- Don't instantiate a redis object for now
self.redis_client = nil
self.pool = pool == nil or pool
end
function clusterstore:connect()
-- Check if we are already connected
if self.redis_client then
return true, "already connected", self.redis_client:get_reused_times()
-- Don't go further if redis is not used
if self.variables["USE_REDIS"] ~= "yes" then
return
end
-- Instantiate object
local redis_client, err = redis:new()
if redis_client == nil then
return false, err
end
-- Set timeouts
redis_client:set_timeout(tonumber(self.variables["REDIS_TIMEOUT"]))
-- Connect
-- Compute options
local options = {
ssl = self.variables["REDIS_SSL"] == "yes",
connect_timeout = tonumber(self.variables["REDIS_TIMEOUT"]),
read_timeout = tonumber(self.variables["REDIS_TIMEOUT"]),
send_timeout = tonumber(self.variables["REDIS_TIMEOUT"]),
keepalive_timeout = tonumber(self.variables["REDIS_KEEPALIVE_IDLE"]),
keepalive_poolsize = tonumber(self.variables["REDIS_KEEPALIVE_POOL"]),
connection_options = {
ssl = self.variables["REDIS_SSL"] == "yes",
},
host = self.variables["REDIS_HOST"],
port = tonumber(self.variables["REDIS_PORT"]),
db = tonumber(self.variables["REDIS_DATABASE"]),
username = self.variables["REDIS_USERNAME"],
password = self.variables["REDIS_PASSWORD"],
sentinel_username = self.variables["REDIS_SENTINEL_USERNAME"],
sentinel_password = self.variables["REDIS_SENTINEL_PASSWORD"],
master_name = self.variables["REDIS_SENTINEL_MASTER"],
role = "master",
sentinels = {}
}
if self.pool then
options.pool = "bw-redis"
options.pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"])
if pool == nil or pool then
options.connection_options.pool = "bw-redis"
options.connection_options.pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"])
end
local ok, err = redis_client:connect(self.variables["REDIS_HOST"], tonumber(self.variables["REDIS_PORT"]), options)
if not ok then
return false, err
end
self.redis_client = redis_client
-- Select database if needed
local times, err = self.redis_client:get_reused_times()
if err then
self:close()
return false, err
end
if times == 0 then
-- luacheck: ignore 421
local _, err = self.redis_client:select(tonumber(self.variables["REDIS_DATABASE"]))
if err then
self:close()
return false, err
if self.variables["REDIS_SENTINEL_HOSTS"] ~= "" then
for sentinel_host in self.variables["REDIS_SENTINEL_HOSTS"]:gmatch("%S+") do
local shost, sport = sentinel_host:match("([^:]+):?(%d*)")
if sport == "" then
sport = 26379
else
sport = tonumber(sport)
end
table.insert(options.sentinel, {host = shost, port = sport})
end
end
return true, "success", times
self.options = options
-- Instantiate object
if is_cosocket_available() then
local redis_connector, err = rc.new(self.options)
self.redis_connector = redis_connector
if self.redis_connector == nil then
logger:log(ERR, "can't instantiate redis object : " .. err)
return
end
end
end
function clusterstore:connect(readonly)
-- Check if connector is created
if not self.redis_connector then
return false, "connector is not instantiated"
end
-- Disconnect if needed
if self.redis_client then
self:close()
end
-- Connect to sentinels if needed
local redis_client, err
if #self.options.sentinels > 0 then
local redis_sentinel, err = self.redis_connector:connect()
if not redis_sentinel then
return false, "error while connecting to sentinels : " .. err
end
if readonly then
redis_clients, err = rs.get_slaves(redis_sentinel, self.options.master_name)
if redis_clients then
redis_client = redis_clients[random(#redis_clients)]
else
redis_client = nil
end
else
redis_client, err = rs.get_master(redis_sentinel, self.options.master_name)
end
-- Classic connection
else
redis_client, err = self.redis_connector:connect()
end
self.redis_client = redis_client
if not self.redis_client then
return false, "error while getting redis client : " .. err
end
-- Everything went well
return true, "success", self.redis_client:get_reused_times()
end
function clusterstore:close()
if self.redis_client then
-- Equivalent to close but keep a pool of connections
if self.pool then
local ok, err = self.redis_client:set_keepalive(
tonumber(self.variables["REDIS_KEEPALIVE_IDLE"]),
tonumber(self.variables["REDIS_KEEPALIVE_POOL"])
)
self.redis_client = nil
if not ok then
require("bunkerweb.logger"):new("clusterstore-close"):log(ngx.ERR, err)
end
return ok, err
end
-- Close
local ok, err = self.redis_client:close()
self.redis_client.redis_client = nil
return ok, err
-- Check if connected is created
if not self.redis_connector then
return false, "connector is not instantiated"
end
return false, "not connected"
-- Check if client is created
if not self.redis_client then
return false, "client is not instantiated"
end
-- Pool case
local ok, err
if self.pool then
ok, err = self.redis_connector:set_keepalive(self.redis_client)
-- No pool
else
ok, err = self.redis_client:close()
end
self.redis_client = nil
if err then
logger:log(ERR, "error while closing redis_client : " .. err)
end
return ok ~= nil, err
end
function clusterstore:call(method, ...)
-- Check if we are connected
-- Check if client is created
if not self.redis_client then
return false, "not connected"
return false, "client is not instantiated"
end
-- Call method
return self.redis_client[method](self.redis_client, ...)
end
function clusterstore:multi(calls)
-- Check if we are connected
-- Check if client is created
if not self.redis_client then
return false, "not connected"
return false, "client is not instantiated"
end
-- Start transaction
local ok, err = self.redis_client:multi()
@ -121,7 +182,7 @@ function clusterstore:multi(calls)
local args = unpack(call[2])
ok, err = self.redis_client[method](self.redis_client, args)
if not ok then
return false, method + "() failed : " .. err
return false, method .. "() failed : " .. err
end
end
-- Exec transaction

View file

@ -1,18 +1,25 @@
local ngx = ngx
local class = require "middleclass"
local clogger = require "bunkerweb.logger"
local lrucache = require "resty.lrucache"
local datastore = class("datastore")
local lru, err = lrucache.new(100000)
local logger = clogger:new("DATASTORE")
local ERR = ngx.ERR
local subsystem = ngx.config.subsystem
local shared = ngx.shared
local lru, err_lru = lrucache.new(100000)
if not lru then
require "bunkerweb.logger"
:new("DATASTORE")
:log(ngx.ERR, "failed to instantiate LRU cache : " .. (err or "unknown error"))
logger:log(ERR, "failed to instantiate LRU cache : " .. err_lru)
end
function datastore:initialize()
self.dict = ngx.shared.datastore
if not self.dict then
self.dict = ngx.shared.datastore_stream
if subsystem == "http" then
self.dict = shared.datastore
else
self.dict = shared.datastore_stream
end
end
@ -20,6 +27,9 @@ function datastore:get(key, worker)
-- luacheck: ignore 431
local value, err
if worker then
if not lru then
return nil, "lru is not instantiated"
end
value, err = lru:get(key)
return value, err or "not found"
end
@ -32,6 +42,9 @@ end
function datastore:set(key, value, exptime, worker)
if worker then
if not lru then
return false, "lru is not instantiated"
end
lru:set(key, value, exptime)
return true, "success"
end
@ -41,6 +54,9 @@ end
function datastore:delete(key, worker)
if worker then
if not lru then
return false, "lru is not instantiated"
end
lru:delete(key)
return true, "success"
end
@ -50,6 +66,9 @@ end
function datastore:keys(worker)
if worker then
if not lru then
return false, "lru is not instantiated"
end
return lru:keys(0)
end
return self.dict:get_keys(0)
@ -70,6 +89,9 @@ end
function datastore:delete_all(pattern, worker)
local keys
if worker then
if not lru then
return false, "lru is not instantiated"
end
keys = lru:keys(0)
else
keys = self.dict:get_keys(0)
@ -84,6 +106,9 @@ end
-- luacheck: ignore 212
function datastore:flush_lru()
if not lru then
return false, "lru is not instantiated"
end
lru:flush_all()
end

View file

@ -1,18 +1,35 @@
local ngx = ngx
local cjson = require "cjson"
local utils = require "bunkerweb.utils"
local bwctx = require "bunkerweb.ctx"
local base = require "resty.core.base"
local open = io.open
local decode = cjson.decode
local encode = cjson.encode
local tostring = tostring
local get_phases = utils.get_phases
local get_request = base.get_request
local apply_ref = bwctx.apply_ref
local stash_ref = bwctx.stash_ref
local subsystem = ngx.config.subsystem
local var = ngx.var
local req = ngx.req
local ip_is_global = utils.ip_is_global
local is_ipv4 = utils.is_ipv4
local is_ipv6 = utils.is_ipv6
local get_variable = utils.get_variable
local helpers = {}
helpers.load_plugin = function(json)
-- Open file
local file, err, nb = io.open(json, "r")
local file, err, nb = open(json, "r")
if not file then
return false, "can't load JSON at " .. json .. " : " .. err .. " (nb = " .. tostring(nb) .. ")"
end
-- Decode JSON
local ok, plugin = pcall(cjson.decode, file:read("*a"))
local ok, plugin = pcall(decode, file:read("*a"))
file:close()
if not ok then
return false, "invalid JSON at " .. json .. " : " .. err
@ -26,7 +43,7 @@ helpers.load_plugin = function(json)
end
end
if #missing_fields > 0 then
return false, "missing field(s) " .. cjson.encode(missing_fields) .. " for JSON at " .. json
return false, "missing field(s) " .. encode(missing_fields) .. " for JSON at " .. json
end
-- Try require
local plugin_lua, err = helpers.require_plugin(plugin.id)
@ -34,7 +51,7 @@ helpers.load_plugin = function(json)
return false, err
end
-- Fill phases
local phases = utils.get_phases()
local phases = get_phases()
plugin.phases = {}
if plugin_lua then
for _, phase in ipairs(phases) do
@ -49,11 +66,11 @@ end
helpers.order_plugins = function(plugins)
-- Extract orders
local file, err, nb = io.open("/usr/share/bunkerweb/core/order.json", "r")
local file, err, nb = open("/usr/share/bunkerweb/core/order.json", "r")
if not file then
return false, err .. " (nb = " .. tostring(nb) .. ")"
end
local ok, orders = pcall(cjson.decode, file:read("*a"))
local ok, orders = pcall(decode, file:read("*a"))
file:close()
if not ok then
return false, "invalid order.json : " .. err
@ -68,7 +85,7 @@ helpers.order_plugins = function(plugins)
end
-- Order result
local result_orders = {}
for _, phase in ipairs(utils.get_phases()) do
for _, phase in ipairs(get_phases()) do
result_orders[phase] = {}
end
-- Fill order first
@ -82,7 +99,7 @@ helpers.order_plugins = function(plugins)
end
end
-- Then append missing plugins to the end
for _, phase in ipairs(utils.get_phases()) do
for _, phase in ipairs(get_phases()) do
for id, plugin in pairs(plugins_phases) do
if plugin[phase] then
table.insert(result_orders[phase], id)
@ -141,7 +158,7 @@ helpers.call_plugin = function(plugin, method)
end
end
if #missing_values > 0 then
return false, "missing required return value(s) : " .. cjson.encode(missing_values)
return false, "missing required return value(s) : " .. encode(missing_values)
end
-- Return
return true, ret
@ -151,64 +168,63 @@ helpers.fill_ctx = function()
-- Return errors as table
local errors = {}
-- Try to load saved ctx
if base.get_request() then
bwctx.apply_ref()
local request = get_request()
if request then
apply_ref()
end
local ctx = ngx.ctx
-- Check if ctx is already filled
if not ctx.bw then
-- Instantiate bw table
local data = {}
-- Common vars
data.kind = "http"
if ngx.shared.datastore_stream then
data.kind = "stream"
if request then
-- Common vars
data.kind = "http"
if subsystem == "stream" then
data.kind = "stream"
end
data.remote_addr = var.remote_addr
data.server_name = var.server_name
if data.kind == "http" then
data.uri = var.uri
data.request_uri = var.request_uri
data.request_method = var.request_method
data.http_user_agent = var.http_user_agent
data.http_host = var.http_host
data.http_content_type = var.http_content_type
data.http_content_length = var.http_content_length
data.http_origin = var.http_origin
data.http_version = req.http_version()
data.scheme = var.scheme
end
-- IP data : global
local ip_global, err = ip_is_global(data.remote_addr)
if ip_global == nil then
table.insert(errors, "can't check if IP is global : " .. err)
else
data.ip_is_global = ip_global
end
-- IP data : v4 / v6
data.ip_is_ipv4 = is_ipv4(data.ip)
data.ip_is_ipv6 = is_ipv6(data.ip)
end
data.remote_addr = ngx.var.remote_addr
data.server_name = ngx.var.server_name
if data.kind == "http" then
data.uri = ngx.var.uri
data.request_uri = ngx.var.request_uri
data.request_method = ngx.var.request_method
data.http_user_agent = ngx.var.http_user_agent
data.http_host = ngx.var.http_host
data.server_name = ngx.var.server_name
data.http_content_type = ngx.var.http_content_type
data.http_content_length = ngx.var.http_content_length
data.http_origin = ngx.var.http_origin
data.http_version = ngx.req.http_version()
data.scheme = ngx.var.scheme
end
-- IP data : global
local ip_is_global, err = utils.ip_is_global(data.remote_addr)
if ip_is_global == nil then
table.insert(errors, "can't check if IP is global : " .. err)
else
data.ip_is_global = ip_is_global
end
-- IP data : v4 / v6
data.ip_is_ipv4 = utils.is_ipv4(data.ip)
data.ip_is_ipv6 = utils.is_ipv6(data.ip)
-- Misc info
data.integration = utils.get_integration()
data.version = utils.get_version()
-- Fill ctx
ctx.bw = data
end
-- Always create new objects for current phases in case of cosockets
local use_redis, err = utils.get_variable("USE_REDIS", false)
local use_redis, err = get_variable("USE_REDIS", false)
if not use_redis then
table.insert(errors, "can't get variable from datastore : " .. err)
end
ctx.bw.datastore = require "bunkerweb.datastore":new()
ctx.bw.clusterstore = require "bunkerweb.clusterstore":new()
ctx.bw.cachestore = require "bunkerweb.cachestore":new(use_redis == "yes")
ctx.bw.cachestore = require "bunkerweb.cachestore":new(use_redis == "yes", ctx)
return true, "ctx filled", errors, ctx
end
helpers.save_ctx = function(ctx)
if base.get_request() then
bwctx.stash_ref(ctx)
if get_request() then
stash_ref(ctx)
end
end
@ -222,11 +238,11 @@ function helpers.load_variables(all_variables, plugins)
end
end
end
local file = io.open("/usr/share/bunkerweb/settings.json")
local file = open("/usr/share/bunkerweb/settings.json")
if not file then
return false, "can't open settings.json"
end
local ok, settings = pcall(cjson.decode, file:read("*a"))
local ok, settings = pcall(decode, file:read("*a"))
file:close()
if not ok then
return false, "invalid settings.json : " .. settings

View file

@ -2,12 +2,15 @@ local class = require "middleclass"
local errlog = require "ngx.errlog"
local logger = class("logger")
local upper = string.upper
local raw_log = errlog.raw_log
function logger:initialize(prefix)
self.prefix = string.upper(prefix)
self.prefix = upper(prefix)
end
function logger:log(level, msg)
errlog.raw_log(level, "[" .. self.prefix .. "] " .. msg)
raw_log(level, "[" .. self.prefix .. "] " .. msg)
end
return logger

View file

@ -1,3 +1,4 @@
local ngx = ngx
local cachestore = require "bunkerweb.cachestore"
local class = require "middleclass"
local clusterstore = require "bunkerweb.clusterstore"
@ -6,50 +7,55 @@ local logger = require "bunkerweb.logger"
local utils = require "bunkerweb.utils"
local plugin = class("plugin")
local ERR = ngx.ERR
local get_phase = ngx.get_phase
local get_variable = utils.get_variable
local get_ctx_obj = utils.get_ctx_obj
local subsystem = ngx.config.subsystem
function plugin:initialize(id, ctx)
-- Store common, values
self.id = id
local multisite = false
local current_phase = ngx.get_phase()
local is_request = false
local current_phase = get_phase()
for _, check_phase in ipairs {
"set",
"rewrite",
"access",
"content",
"header_filter",
"body_filter",
"log",
"preread",
"log_stream",
"log_default",
"preread"
} do
if current_phase == check_phase then
multisite = true
is_request = true
break
end
end
self.is_request = multisite
self.is_request = is_request
-- Store common objects
self.logger = logger:new(self.id)
local use_redis, err = utils.get_variable("USE_REDIS", false)
local use_redis, err = get_variable("USE_REDIS", false)
if not use_redis then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
end
self.use_redis = use_redis == "yes"
if self.is_request then
-- Store ctx
self.ctx = ctx or ngx.ctx
self.datastore = utils.get_ctx_obj("datastore", self.ctx) or datastore:new()
self.cachestore = utils.get_ctx_obj("cachestore", self.ctx)
or cachestore:new(use_redis == "yes", true, self.ctx)
self.clusterstore = utils.get_ctx_obj("clusterstore", self.ctx) or clusterstore:new(false)
self.datastore = get_ctx_obj("datastore", self.ctx) or datastore:new()
self.cachestore = get_ctx_obj("cachestore", self.ctx)
or cachestore:new(use_redis == "yes", self.ctx)
self.clusterstore = get_ctx_obj("clusterstore", self.ctx) or clusterstore:new()
else
self.datastore = datastore:new()
self.cachestore = cachestore:new(use_redis == "yes", true)
self.cachestore = cachestore:new(use_redis == "yes")
self.clusterstore = clusterstore:new(false)
end
-- Get metadata
local metadata, err = self.datastore:get("plugin_" .. id, true)
if not metadata then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
return
end
-- Store variables
@ -57,21 +63,22 @@ function plugin:initialize(id, ctx)
self.multiples = {}
local value
for k, v in pairs(metadata.settings) do
value, err = utils.get_variable(k, v.context == "multisite" and multisite)
value, err = get_variable(k, v.context == "multisite" and self.is_request)
if value == nil then
self.logger:log(ngx.ERR, "can't get " .. k .. " variable : " .. err)
self.logger:log(ERR, "can't get " .. k .. " variable : " .. err)
end
self.variables[k] = value
end
-- Is loading
local is_loading, err = utils.get_variable("IS_LOADING", false)
local is_loading, err = get_variable("IS_LOADING", false)
if is_loading == nil then
self.logger:log(ngx.ERR, "can't get IS_LOADING variable : " .. err)
self.logger:log(ERR, "can't get IS_LOADING variable : " .. err)
end
self.is_loading = is_loading == "yes"
-- Kind of server
self.kind = "http"
if ngx.shared.datastore_stream then
if subsystem == "http" then
self.kind = "http"
else
self.kind = "stream"
end
end

View file

@ -1,3 +1,4 @@
local ngx = ngx
local cdatastore = require "bunkerweb.datastore"
local clogger = require "bunkerweb.logger"
local mmdb = require "bunkerweb.mmdb"
@ -10,11 +11,32 @@ local session = require "resty.session"
local logger = clogger:new("UTILS")
local datastore = cdatastore:new()
local var = ngx.var
local ERR = ngx.ERR
local INFO = ngx.INFO
local WARN = ngx.WARN
local null = ngx.null
local re_match = ngx.re.match
local subsystem = ngx.config.subsystem
local get_phase = ngx.get_phase
local kill = ngx.thread.kill
local ipmatcher_new = ipmatcher.new
local parse_ipv4 = ipmatcher.parse_ipv4
local parse_ipv6 = ipmatcher.parse_ipv6
local open = io.open
local encode = cjson.encode
local decode = cjson.decode
local char = string.char
local random = math.random
local session_start = session.start
local session_open = session.open
local tonumber = tonumber
local utils = {}
math.randomseed(os.time())
utils.get_variable = function(var, site_search)
utils.get_variable = function(variable, site_search, ctx)
-- Default site search to true
if site_search == nil then
site_search = true
@ -24,20 +46,27 @@ utils.get_variable = function(var, site_search)
if not variables then
return nil, "can't access variables from datastore : " .. err
end
local value = variables["global"][var]
local value = variables["global"][variable]
-- Site search case
local multisite = site_search and variables["global"]["MULTISITE"] == "yes" and ngx.var.server_name ~= "_"
if multisite then
value = variables[ngx.var.server_name][var]
if site_search and variables["global"]["MULTISITE"] == "yes" then
local server_name
if ctx and ctx.bw then
server_name = ctx.bw.server_name
else
server_name = var.server_name
end
if variables[server_name] then
value = variables[server_name][variable]
end
end
return value, "success"
end
utils.has_variable = function(var, value)
utils.has_variable = function(variable, value)
-- Get global variable
local variables, err = datastore:get("variables", true)
if not variables then
return nil, "can't access variables " .. var .. " from datastore : " .. err
return nil, "can't access variables " .. variable .. " from datastore : " .. err
end
-- Multisite case
local multisite = variables["global"]["MULTISITE"] == "yes"
@ -45,7 +74,7 @@ utils.has_variable = function(var, value)
local servers = variables["global"]["SERVER_NAME"]
-- Check each server
for server in servers:gmatch("%S+") do
if variables[server][var] == value then
if variables[server][variable] == value then
return true, "success"
end
end
@ -53,14 +82,14 @@ utils.has_variable = function(var, value)
return false, "success"
end
end
return variables["global"][var] == value, "success"
return variables["global"][variable] == value, "success"
end
utils.has_not_variable = function(var, value)
utils.has_not_variable = function(variable, value)
-- Get global variable
local variables, err = datastore:get("variables", true)
if not variables then
return nil, "can't access variables " .. var .. " from datastore : " .. err
return nil, "can't access variables " .. variable .. " from datastore : " .. err
end
-- Multisite case
local multisite = variables["global"]["MULTISITE"] == "yes"
@ -68,7 +97,7 @@ utils.has_not_variable = function(var, value)
local servers = variables["global"]["SERVER_NAME"]
-- Check each server
for server in servers:gmatch("%S+") do
if variables[server][var] ~= "value" then
if variables[server][variable] ~= "value" then
return true, "success"
end
end
@ -76,7 +105,7 @@ utils.has_not_variable = function(var, value)
return false, "success"
end
end
return variables["global"][var] ~= value, "success"
return variables["global"][variable] ~= value, "success"
end
utils.get_multiple_variables = function(vars)
@ -90,8 +119,8 @@ utils.get_multiple_variables = function(vars)
result[scope] = {}
-- Loop on vars
for variable, value in pairs(scoped_vars) do
for _, var in ipairs(vars) do
if variable:find("^" .. var .. "_?[0-9]*$") then
for _, tvar in ipairs(vars) do
if variable:find("^" .. tvar .. "_?[0-9]*$") then
result[scope][variable] = value
end
end
@ -102,7 +131,7 @@ end
utils.is_ip_in_networks = function(ip, networks)
-- Instantiate ipmatcher
local ipm, err = ipmatcher.new(networks)
local ipm, err = ipmatcher_new(networks)
if not ipm then
return nil, "can't instantiate ipmatcher : " .. err
end
@ -115,11 +144,11 @@ utils.is_ip_in_networks = function(ip, networks)
end
utils.is_ipv4 = function(ip)
return ipmatcher.parse_ipv4(ip)
return parse_ipv4(ip)
end
utils.is_ipv6 = function(ip)
return ipmatcher.parse_ipv6(ip)
return parse_ipv6(ip)
end
utils.ip_is_global = function(ip)
@ -157,7 +186,7 @@ utils.ip_is_global = function(ip)
"ff00::/8",
}
-- Instantiate ipmatcher
local ipm, err = ipmatcher.new(reserved_ips)
local ipm, err = ipmatcher_new(reserved_ips)
if not ipm then
return nil, "can't instantiate ipmatcher : " .. err
end
@ -169,7 +198,11 @@ utils.ip_is_global = function(ip)
return not matched, "success"
end
utils.get_integration = function()
utils.get_integration = function(ctx)
-- Check if already in ctx
if ctx and ctx.bw.integration then
return ctx.bw.integration
end
-- Check if already in datastore
local integration, _ = datastore:get("misc_integration", true)
if integration then
@ -177,7 +210,7 @@ utils.get_integration = function()
end
local variables, err = datastore:get("variables", true)
if not variables then
logger:log(ngx.ERR, "can't get variables from datastore : " .. err)
logger:log(ERR, "can't get variables from datastore : " .. err)
return "unknown"
end
-- Swarm
@ -193,12 +226,12 @@ utils.get_integration = function()
integration = "autoconf"
else
-- Already present (e.g. : linux)
local f, _ = io.open("/usr/share/bunkerweb/INTEGRATION", "r")
local f, _ = open("/usr/share/bunkerweb/INTEGRATION", "r")
if f then
integration = f:read("*a"):gsub("[\n\r]", "")
f:close()
else
f, _ = io.open("/etc/os-release", "r")
f, _ = open("/etc/os-release", "r")
if f then
local data = f:read("*a")
f:close()
@ -217,58 +250,86 @@ utils.get_integration = function()
-- Save integration
local ok, err = datastore:set("misc_integration", integration, nil, true)
if not ok then
logger:log(ngx.ERR, "can't cache integration to datastore : " .. err)
logger:log(ERR, "can't cache integration to datastore : " .. err)
end
if ctx then
ctx.bw.integration = integration
end
return integration
end
utils.get_version = function()
utils.get_version = function(ctx)
-- Check if already in ctx
if ctx and ctx.bw.version then
return ctx.bw.version
end
-- Check if already in datastore
local version, _ = datastore:get("misc_version", true)
if version then
return version
end
-- Read VERSION file
local f, err = io.open("/usr/share/bunkerweb/VERSION", "r")
local f, err = open("/usr/share/bunkerweb/VERSION", "r")
if not f then
logger:log(ngx.ERR, "can't read VERSION file : " .. err)
logger:log(ERR, "can't read VERSION file : " .. err)
return nil
end
version = f:read("*a"):gsub("[\n\r]", "")
f:close()
-- Save it to datastore
-- Save version
local ok, err = datastore:set("misc_version", version, nil, true)
if not ok then
logger:log(ngx.ERR, "can't cache version to datastore : " .. err)
logger:log(ERR, "can't cache version to datastore : " .. err)
end
if ctx then
ctx.bw.version = version
end
return version
end
utils.get_reason = function(ctx)
-- ngx.ctx
if ctx.bw.reason then
if ctx and ctx.bw and ctx.bw.reason then
return ctx.bw.reason
end
-- ngx.var
if ngx.var.reason and ngx.var.reason ~= "" then
return ngx.var.reason
if var.reason and var.reason ~= "" then
return var.reason
end
-- os.getenv
if os.getenv("REASON") == "modsecurity" then
return "modsecurity"
end
-- datastore ban
local banned, _ = datastore:get("bans_ip_" .. ngx.var.remote_addr)
local ip
if ctx and ctx.bw then
ip = ctx.bw.remote_addr
else
ip = var.remote_addr
end
local banned, _ = datastore:get("bans_ip_" .. ip)
if banned then
return banned
end
-- unknown
if ngx.status == utils.get_deny_status(ctx) then
if ngx.status == utils.get_deny_status() then
return "unknown"
end
return nil
end
utils.is_whitelisted = function(ctx)
-- ngx.ctx
if ctx and ctx.bw and ctx.bw.is_whitelisted then
return ctx.bw.is_whitelisted == "yes"
end
-- ngx.var
if var.is_whitelisted and var.is_whitelisted == "yes" then
return true
end
return false
end
utils.get_resolvers = function()
-- Get resolvers from datastore if existing
local resolvers, _ = datastore:get("misc_resolvers", true)
@ -278,7 +339,7 @@ utils.get_resolvers = function()
-- Otherwise extract DNS_RESOLVERS variable
local variables, err = datastore:get("variables", true)
if not variables then
logger:log(ngx.ERR, "can't get variables from datastore : " .. err)
logger:log(ERR, "can't get variables from datastore : " .. err)
return "unknown"
end
-- Make table for resolver1 resolver2 ... string
@ -289,19 +350,19 @@ utils.get_resolvers = function()
-- Add it to the datastore
local ok, err = datastore:set("misc_resolvers", resolvers, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save misc_resolvers to datastore : " .. err)
logger:log(ERR, "can't save misc_resolvers to datastore : " .. err)
end
return resolvers
end
utils.get_rdns = function(ip)
utils.get_rdns = function(ip, ctx, pool)
-- Check cache
local cachestore = utils.new_cachestore()
local cachestore = utils.new_cachestore(ctx, pool)
local ok, value = cachestore:get("rdns_" .. ip)
if not ok then
logger:log(ngx.ERR, "can't get rdns from cachestore : " .. value)
logger:log(ERR, "can't get rdns from cachestore : " .. value)
elseif value then
return cjson.decode(value), "success"
return decode(value), "success"
end
-- Get resolvers
local resolvers, err = utils.get_resolvers()
@ -323,7 +384,7 @@ utils.get_rdns = function(ip)
-- Do rDNS query
local answers, err = rdns:reverse_query(ip)
if not answers then
logger:log(ngx.ERR, "error while doing reverse DNS query for " .. ip .. " : " .. err)
logger:log(ERR, "error while doing reverse DNS query for " .. ip .. " : " .. err)
ret_err = err
else
if answers.errcode then
@ -337,21 +398,21 @@ utils.get_rdns = function(ip)
end
end
-- Save to cache
ok, err = cachestore:set("rdns_" .. ip, cjson.encode(ptrs), 3600)
ok, err = cachestore:set("rdns_" .. ip, encode(ptrs), 3600)
if not ok then
logger:log(ngx.ERR, "can't set rdns into cachestore : " .. err)
logger:log(ERR, "can't set rdns into cachestore : " .. err)
end
return ptrs, ret_err
end
utils.get_ips = function(fqdn, ipv6)
utils.get_ips = function(fqdn, ipv6, ctx, pool)
-- Check cache
local cachestore = utils.new_cachestore()
local cachestore = utils.new_cachestore(ctx, pool)
local ok, value = cachestore:get("dns_" .. fqdn)
if not ok then
logger:log(ngx.ERR, "can't get dns from cachestore : " .. value)
logger:log(ERR, "can't get dns from cachestore : " .. value)
elseif value then
return cjson.decode(value), "success"
return decode(value), "success"
end
-- By default perform ipv6 lookups (only if USE_IPV6=yes)
if ipv6 == nil then
@ -377,7 +438,7 @@ utils.get_ips = function(fqdn, ipv6)
-- luacheck: ignore 421
local use_ipv6, err = utils.get_variable("USE_IPV6", false)
if not use_ipv6 then
logger:log(ngx.ERR, "can't get USE_IPV6 variable " .. err)
logger:log(ERR, "can't get USE_IPV6 variable " .. err)
elseif use_ipv6 == "yes" then
table.insert(qtypes, res.TYPE_AAAA)
end
@ -401,7 +462,7 @@ utils.get_ips = function(fqdn, ipv6)
end
end
for qtype, error in pairs(res_errors) do
logger:log(ngx.ERR, "error while doing " .. qtype .. " DNS query for " .. fqdn .. " : " .. error)
logger:log(ERR, "error while doing " .. qtype .. " DNS query for " .. fqdn .. " : " .. error)
end
-- Extract all IPs
local ips = {}
@ -414,11 +475,11 @@ utils.get_ips = function(fqdn, ipv6)
end
end
-- Save to cache
ok, err = cachestore:set("dns_" .. fqdn, cjson.encode(ips), 3600)
ok, err = cachestore:set("dns_" .. fqdn, encode(ips), 3600)
if not ok then
logger:log(ngx.ERR, "can't set dns into cachestore : " .. err)
logger:log(ERR, "can't set dns into cachestore : " .. err)
end
return ips, cjson.encode(res_errors) .. " " .. cjson.encode(ans_errors)
return ips, encode(res_errors) .. " " .. encode(ans_errors)
end
utils.get_country = function(ip)
@ -458,38 +519,36 @@ utils.rand = function(nb, no_numbers)
-- lowers, uppers and numbers
if not no_numbers then
for i = 48, 57 do
table.insert(charset, string.char(i))
table.insert(charset, char(i))
end
end
for i = 65, 90 do
table.insert(charset, string.char(i))
table.insert(charset, char(i))
end
for i = 97, 122 do
table.insert(charset, string.char(i))
table.insert(charset, char(i))
end
local result = ""
for _ = 1, nb do
result = result .. charset[math.random(1, #charset)]
result = result .. charset[random(1, #charset)]
end
return result
end
utils.get_deny_status = function(ctx)
-- Stream case
if ctx.bw and ctx.bw.kind == "stream" then
return 444
utils.get_deny_status = function()
if subsystem == "http" then
local variables, err = datastore:get("variables", true)
if not variables then
logger:log(ERR, "can't get variables from datastore : " .. err)
return 403
end
return tonumber(variables["global"]["DENY_HTTP_STATUS"])
end
-- http case
local variables, err = datastore:get("variables", true)
if not variables then
logger:log(ngx.ERR, "can't get variables from datastore : " .. err)
return 403
end
return tonumber(variables["global"]["DENY_HTTP_STATUS"])
return 444
end
utils.check_session = function(ctx)
local _session, _, exists, _ = session.start({ audience = "metadata" })
local _session, _, exists, _ = session_start({ audience = "metadata" })
if exists then
for _, check in ipairs(ctx.bw.sessions_checks) do
local key = check[1]
@ -500,7 +559,7 @@ utils.check_session = function(ctx)
if not ok then
return false, "session:destroy() error : " .. err
end
logger:log(ngx.WARN, "session check " .. key .. " failed, destroying session")
logger:log(WARN, "session check " .. key .. " failed, destroying session")
return utils.check_session(ctx)
end
end
@ -527,9 +586,9 @@ utils.get_session = function(audience, ctx)
end
end
-- Open session with specific audience
local _session, err, _ = session.open({ audience = audience })
local _session, err, _ = session_open({ audience = audience })
if err then
logger:log(ngx.INFO, "session:open() error : " .. err)
logger:log(INFO, "session:open() error : " .. err)
end
return _session
end
@ -607,7 +666,7 @@ utils.is_banned = function(ip)
elseif data.err then
clusterstore:close()
return nil, "redis script error : " .. data.err
elseif data[1] ~= ngx.null then
elseif data[1] ~= null then
clusterstore:close()
-- Update local cache
ok, err = datastore:set("bans_ip_" .. ip, data[1], data[2])
@ -649,16 +708,17 @@ utils.add_ban = function(ip, reason, ttl)
return true, "success"
end
utils.new_cachestore = function()
utils.new_cachestore = function(ctx, pool)
-- Check if redis is used
local use_redis, err = utils.get_variable("USE_REDIS", false)
if not use_redis then
logger:log(ngx.ERR, "can't get USE_REDIS variable : " .. err)
logger:log(ERR, "can't get USE_REDIS variable : " .. err)
use_redis = false
else
use_redis = use_redis == "yes"
end
-- Instantiate
return require "bunkerweb.cachestore":new(use_redis, true)
return require "bunkerweb.cachestore":new(use_redis, ctx, pool == nil or pool)
end
utils.regex_match = function(str, regex, options)
@ -666,9 +726,9 @@ utils.regex_match = function(str, regex, options)
if options then
all_options = all_options .. options
end
local match, err = ngx.re.match(str, regex, all_options)
local match, err = re_match(str, regex, all_options)
if err then
logger:log(ngx.ERR, "error while matching regex " .. regex .. "with string " .. str)
logger:log(ERR, "error while matching regex " .. regex .. "with string " .. str)
return nil
end
return match
@ -679,24 +739,28 @@ utils.get_phases = function()
"init",
"init_worker",
"set",
"rewrite",
"access",
"content",
"ssl_certificate",
"header",
"log",
"preread",
"log_stream",
"log_default",
"log_default"
}
end
utils.is_cosocket_available = function()
local phases = {
"timer",
"rewrite",
"access",
"content",
"ssl_certificate",
"preread",
"preread"
}
local current_phase = ngx.get_phase()
local current_phase = get_phase()
for _, phase in ipairs(phases) do
if current_phase == phase then
return true
@ -707,16 +771,17 @@ end
utils.kill_all_threads = function(threads)
for _, thread in ipairs(threads) do
local ok, err = ngx.thread.kill(thread)
local ok, err = kill(thread)
if not ok then
logger:log(ngx.ERR, "error while killing thread : " .. err)
logger:log(ERR, "error while killing thread : " .. err)
end
end
end
utils.get_ctx_obj = function(obj)
if ngx.ctx and ngx.ctx.bw then
return ngx.ctx.bw[obj]
utils.get_ctx_obj = function(obj, ctx)
local vctx = ctx or ngx.ctx
if vctx and vctx.bw then
return vctx.bw[obj]
end
return nil
end

Binary file not shown.

Binary file not shown.

View file

@ -17,55 +17,63 @@ server {
access_by_lua_block {
-- Instantiate objects and import required modules
local logger = require "bunkerweb.logger":new("API")
local api = require "bunkerweb.api":new()
local helpers = require "bunkerweb.helpers"
local ngx = ngx
local INFO = ngx.INFO
local ERR = ngx.ERR
local WARN = ngx.WARN
local NOTICE = ngx.NOTICE
local HTTP_CLOSE = ngx.HTTP_CLOSE
local exit = ngx.exit
local say = ngx.say
local fill_ctx = helpers.fill_ctx
local tostring = tostring
-- Start API handler
logger:log(ngx.INFO, "API handler started")
logger:log(INFO, "API handler started")
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Check host header
if not ctx.bw.http_host or ctx.bw.http_host ~= "{{ API_SERVER_NAME }}" then
logger:log(ngx.WARN, "wrong Host header from IP " .. ctx.bw.remote_addr)
return ngx.exit(ngx.HTTP_CLOSE)
logger:log(WARN, "wrong Host header from IP " .. ctx.bw.remote_addr)
return exit(HTTP_CLOSE)
end
-- Check IP
local api = require "bunkerweb.api":new(ctx)
local ok, err = api:is_allowed_ip()
if not ok then
logger:log(ngx.WARN, "can't validate access from IP " .. ctx.bw.remote_addr .. " : " .. err)
return ngx.exit(ngx.HTTP_CLOSE)
logger:log(WARN, "can't validate access from IP " .. ctx.bw.remote_addr .. " : " .. err)
return exit(HTTP_CLOSE)
end
logger:log(ngx.NOTICE, "validated access from IP " .. ctx.bw.remote_addr)
logger:log(NOTICE, "validated access from IP " .. ctx.bw.remote_addr)
-- Do API call
local ok, err, status, resp = api:do_api_call()
if not ok then
logger:log(ngx.WARN, "call from " .. ctx.bw.remote_addr .. " on " .. ctx.bw.uri .. " failed : " .. err)
logger:log(WARN, "call from " .. ctx.bw.remote_addr .. " on " .. ctx.bw.uri .. " failed : " .. err)
else
logger:log(ngx.NOTICE, "successful call from " .. ctx.bw.remote_addr .. " on " .. ctx.bw.uri .. " : " .. err)
logger:log(NOTICE, "successful call from " .. ctx.bw.remote_addr .. " on " .. ctx.bw.uri .. " : " .. err)
end
-- Start API handler
logger:log(ngx.INFO, "API handler ended")
-- Save ctx
ngx.ctx = ctx
-- Stop API handler
logger:log(INFO, "API handler ended")
-- Send response
ngx.status = status
ngx.say(resp)
return ngx.exit(status)
say(resp)
return exit(status)
}
}

View file

@ -45,70 +45,78 @@ server {
local helpers = require "bunkerweb.helpers"
local cjson = require "cjson"
local ngx = ngx
local INFO = ngx.INFO
local ERR = ngx.ERR
local fill_ctx = helpers.fill_ctx
local tostring = tostring
local get_reason = utils.get_reason
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
-- Start log phase
local logger = clogger:new("LOG-DEFAULT")
local datastore = cdatastore:new()
logger:log(ngx.INFO, "log_default phase started")
logger:log(INFO, "log_default phase started")
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
-- Call log_default() methods
logger:log(ngx.INFO, "calling log_default() methods of plugins ...")
logger:log(INFO, "calling log_default() methods of plugins ...")
for i, plugin_id in ipairs(order.log_default) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has log method
if plugin_lua.log_default ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua)
local ok, plugin_obj = new_plugin(plugin_lua, ctx)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "log_default")
local ok, ret = call_plugin(plugin_obj, "log_default")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
else
logger:log(ngx.INFO, plugin_id .. ":log_default() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":log_default() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method log_default() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method log_default() is not defined")
end
end
end
logger:log(ngx.INFO, "called log_default() methods of plugins")
logger:log(INFO, "called log_default() methods of plugins")
-- Display reason at info level
if ctx.reason then
logger:log(ngx.INFO, "client was denied with reason : " .. reason)
local reason = get_reason(ctx)
if reason then
logger:log(INFO, "client was denied with reason : " .. reason)
end
-- Save ctx
ngx.ctx = ctx
logger:log(ngx.INFO, "log_default phase ended")
logger:log(INFO, "log_default phase ended")
}

View file

@ -5,57 +5,71 @@ init_by_lua_block {
local cdatastore = require "bunkerweb.datastore"
local cjson = require "cjson"
local ngx = ngx
local INFO = ngx.INFO
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local popen = io.popen
local open = io.open
local load_plugin = helpers.load_plugin
local load_variables = helpers.load_variables
local order_plugins = helpers.order_plugins
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local encode = cjson.encode
-- Start init phase
local logger = clogger:new("INIT")
local datastore = cdatastore:new()
logger:log(ngx.NOTICE, "init phase started")
logger:log(NOTICE, "init phase started")
-- Remove previous data from the datastore
logger:log(ngx.NOTICE, "deleting old keys from datastore ...")
logger:log(NOTICE, "deleting old keys from datastore ...")
datastore:flush_lru()
local data_keys = { "^plugin", "^misc_" }
for i, key in pairs(data_keys) do
local ok, err = datastore:delete_all(key)
if not ok then
logger:log(ngx.ERR, "can't delete " .. key .. " from datastore : " .. err)
logger:log(ERR, "can't delete " .. key .. " from datastore : " .. err)
return false
end
logger:log(ngx.INFO, "deleted " .. key .. " from datastore")
logger:log(INFO, "deleted " .. key .. " from datastore")
end
logger:log(ngx.NOTICE, "deleted old keys from datastore")
logger:log(NOTICE, "deleted old keys from datastore")
-- Load plugins into the datastore
logger:log(ngx.NOTICE, "saving plugins into datastore ...")
logger:log(NOTICE, "saving plugins into datastore ...")
local plugins = {}
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins" }
for i, plugin_path in ipairs(plugin_paths) do
local paths = io.popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
local paths = popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
for path in paths:lines() do
local ok, plugin = helpers.load_plugin(path .. "/plugin.json")
local ok, plugin = load_plugin(path .. "/plugin.json")
if not ok then
logger:log(ngx.ERR, plugin)
logger:log(ERR, plugin)
else
local ok, err = datastore:set("plugin_" .. plugin.id, plugin, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save " .. plugin.id .. " into datastore : " .. err)
logger:log(ERR, "can't save " .. plugin.id .. " into datastore : " .. err)
else
table.insert(plugins, plugin)
logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
logger:log(NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
end
end
end
end
local ok, err = datastore:set("plugins", plugins, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins into datastore : " .. err)
logger:log(ERR, "can't save plugins into datastore : " .. err)
return false
end
-- Load variables into the datastore
logger:log(ngx.NOTICE, "saving variables into datastore ...")
local file = io.open("/etc/nginx/variables.env")
logger:log(NOTICE, "saving variables into datastore ...")
local file = open("/etc/nginx/variables.env")
if not file then
logger:log(ngx.ERR, "can't open /etc/nginx/variables.env file")
logger:log(ERR, "can't open /etc/nginx/variables.env file")
return false
end
file:close()
@ -64,73 +78,73 @@ init_by_lua_block {
local variable, value = line:match("^([^=]+)=(.*)$")
all_variables[variable] = value
end
local ok, variables = helpers.load_variables(all_variables, plugins)
local ok, variables = load_variables(all_variables, plugins)
if not ok then
logger:log(ngx.ERR, "error while loading variables : " .. variables)
logger:log(ERR, "error while loading variables : " .. variables)
return false
end
local ok, err = datastore:set("variables", variables, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins into datastore : " .. err)
logger:log(ERR, "can't save plugins into datastore : " .. err)
return false
end
logger:log(ngx.NOTICE, "saved variables into datastore")
logger:log(NOTICE, "saved variables into datastore")
-- Purge cache
local cachestore = require "bunkerweb.cachestore":new(false, true)
local cachestore = require "bunkerweb.cachestore":new(false)
local ok, err = cachestore:purge()
if not ok then
logger:log(ngx.ERR, "can't purge cachestore : " .. err)
logger:log(ERR, "can't purge cachestore : " .. err)
end
logger:log(ngx.NOTICE, "saving plugins order into datastore ...")
local ok, order = helpers.order_plugins(plugins)
logger:log(NOTICE, "saving plugins order into datastore ...")
local ok, order = order_plugins(plugins)
if not ok then
logger:log(ngx.ERR, "can't compute plugins order : " .. err)
logger:log(ERR, "can't compute plugins order : " .. err)
return false
end
for phase, id_list in pairs(order) do
logger:log(ngx.NOTICE, "plugins order for phase " .. phase .. " : " .. cjson.encode(id_list))
logger:log(NOTICE, "plugins order for phase " .. phase .. " : " .. encode(id_list))
end
local ok, err = datastore:set("plugins_order", order, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins order into datastore : " .. err)
logger:log(ERR, "can't save plugins order into datastore : " .. err)
return false
end
logger:log(ngx.NOTICE, "saved plugins order into datastore")
logger:log(NOTICE, "saved plugins order into datastore")
-- Call init() method
logger:log(ngx.NOTICE, "calling init() methods of plugins ...")
logger:log(NOTICE, "calling init() methods of plugins ...")
for i, plugin_id in ipairs(order["init"]) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.NOTICE, err)
logger:log(NOTICE, err)
else
-- Check if plugin has init method
if plugin_lua.init ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua)
local ok, plugin_obj = new_plugin(plugin_lua)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "init")
local ok, ret = call_plugin(plugin_obj, "init")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":init() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":init() call failed : " .. ret.msg)
else
logger:log(ngx.NOTICE, plugin_id .. ":init() call successful : " .. ret.msg)
logger:log(NOTICE, plugin_id .. ":init() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined")
logger:log(NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined")
end
end
end
logger:log(ngx.NOTICE, "called init() methods of plugins")
logger:log(NOTICE, "called init() methods of plugins")
logger:log(ngx.NOTICE, "init phase ended")
logger:log(NOTICE, "init phase ended")
}

View file

@ -5,57 +5,71 @@ init_by_lua_block {
local cdatastore = require "bunkerweb.datastore"
local cjson = require "cjson"
local ngx = ngx
local INFO = ngx.INFO
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local popen = io.popen
local open = io.open
local load_plugin = helpers.load_plugin
local load_variables = helpers.load_variables
local order_plugins = helpers.order_plugins
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local encode = cjson.encode
-- Start init phase
local logger = clogger:new("INIT")
local datastore = cdatastore:new()
logger:log(ngx.NOTICE, "init-stream phase started")
logger:log(NOTICE, "init-stream phase started")
-- Remove previous data from the datastore
logger:log(ngx.NOTICE, "deleting old keys from datastore ...")
logger:log(NOTICE, "deleting old keys from datastore ...")
datastore:flush_lru()
local data_keys = { "^plugin", "^misc_" }
for i, key in pairs(data_keys) do
local ok, err = datastore:delete_all(key)
if not ok then
logger:log(ngx.ERR, "can't delete " .. key .. " from datastore : " .. err)
logger:log(ERR, "can't delete " .. key .. " from datastore : " .. err)
return false
end
logger:log(ngx.INFO, "deleted " .. key .. " from datastore")
logger:log(INFO, "deleted " .. key .. " from datastore")
end
logger:log(ngx.NOTICE, "deleted old keys from datastore")
logger:log(NOTICE, "deleted old keys from datastore")
-- Load plugins into the datastore
logger:log(ngx.NOTICE, "saving plugins into datastore ...")
logger:log(NOTICE, "saving plugins into datastore ...")
local plugins = {}
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins" }
for i, plugin_path in ipairs(plugin_paths) do
local paths = io.popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
local paths = popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
for path in paths:lines() do
local ok, plugin = helpers.load_plugin(path .. "/plugin.json")
local ok, plugin = load_plugin(path .. "/plugin.json")
if not ok then
logger:log(ngx.ERR, plugin)
logger:log(ERR, plugin)
else
local ok, err = datastore:set("plugin_" .. plugin.id, plugin, true)
if not ok then
logger:log(ngx.ERR, "can't save " .. plugin.id .. " into datastore : " .. err)
logger:log(ERR, "can't save " .. plugin.id .. " into datastore : " .. err)
else
table.insert(plugins, plugin)
logger:log(ngx.NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
logger:log(NOTICE, "loaded plugin " .. plugin.id .. " v" .. plugin.version)
end
end
end
end
local ok, err = datastore:set("plugins", plugins, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins into datastore : " .. err)
logger:log(ERR, "can't save plugins into datastore : " .. err)
return false
end
-- Load variables into the datastore
logger:log(ngx.NOTICE, "saving variables into datastore ...")
local file = io.open("/etc/nginx/variables.env")
local file = open("/etc/nginx/variables.env")
if not file then
logger:log(ngx.ERR, "can't open /etc/nginx/variables.env file")
logger:log(ERR, "can't open /etc/nginx/variables.env file")
return false
end
file:close()
@ -64,73 +78,73 @@ init_by_lua_block {
local variable, value = line:match("^([^=]+)=(.*)$")
all_variables[variable] = value
end
local ok, variables = helpers.load_variables(all_variables, plugins)
local ok, variables = load_variables(all_variables, plugins)
if not ok then
logger:log(ngx.ERR, "error while loading variables : " .. variables)
logger:log(ERR, "error while loading variables : " .. variables)
return false
end
local ok, err = datastore:set("variables", variables, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins into datastore : " .. err)
logger:log(ERR, "can't save plugins into datastore : " .. err)
return false
end
logger:log(ngx.NOTICE, "saved variables into datastore")
logger:log(NOTICE, "saved variables into datastore")
-- Purge cache
local cachestore = require "bunkerweb.cachestore":new(false, true)
local cachestore = require "bunkerweb.cachestore":new(false)
local ok, err = cachestore:purge()
if not ok then
logger:log(ngx.ERR, "can't purge cachestore : " .. err)
logger:log(ERR, "can't purge cachestore : " .. err)
end
logger:log(ngx.NOTICE, "saving plugins order into datastore ...")
local ok, order = helpers.order_plugins(plugins)
logger:log(NOTICE, "saving plugins order into datastore ...")
local ok, order = order_plugins(plugins)
if not ok then
logger:log(ngx.ERR, "can't compute plugins order : " .. err)
logger:log(ERR, "can't compute plugins order : " .. err)
return false
end
for phase, id_list in pairs(order) do
logger:log(ngx.NOTICE, "plugins order for phase " .. phase .. " : " .. cjson.encode(id_list))
logger:log(NOTICE, "plugins order for phase " .. phase .. " : " .. encode(id_list))
end
local ok, err = datastore:set("plugins_order", order, nil, true)
if not ok then
logger:log(ngx.ERR, "can't save plugins order into datastore : " .. err)
logger:log(ERR, "can't save plugins order into datastore : " .. err)
return false
end
logger:log(ngx.NOTICE, "saved plugins order into datastore")
logger:log(NOTICE, "saved plugins order into datastore")
-- Call init() method
logger:log(ngx.NOTICE, "calling init() methods of plugins ...")
logger:log(NOTICE, "calling init() methods of plugins ...")
for i, plugin_id in ipairs(order["init"]) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.NOTICE, err)
logger:log(NOTICE, err)
else
-- Check if plugin has init method
if plugin_lua.init ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua)
local ok, plugin_obj = new_plugin(plugin_lua)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "init")
local ok, ret = call_plugin(plugin_obj, "init")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":init() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":init() call failed : " .. ret.msg)
else
logger:log(ngx.NOTICE, plugin_id .. ":init() call successful : " .. ret.msg)
logger:log(NOTICE, plugin_id .. ":init() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined")
logger:log(NOTICE, "skipped execution of " .. plugin.id .. " because method init() is not defined")
end
end
end
logger:log(ngx.NOTICE, "called init() methods of plugins")
logger:log(NOTICE, "called init() methods of plugins")
logger:log(ngx.NOTICE, "init-stream phase ended")
logger:log(NOTICE, "init-stream phase ended")
}

View file

@ -5,16 +5,23 @@ init_worker_by_lua_block {
local ready_work = function(premature)
-- Libs
local helpers = require "bunkerweb.helpers"
local cjson = require "cjson"
-- Instantiate objects
local logger = require "bunkerweb.logger":new("INIT-WORKER")
local datastore = require "bunkerweb.datastore":new()
local ngx = ngx
local INFO = ngx.INFO
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
-- Don't go further we are in loading state
local is_loading, err = require "bunkerweb.utils".get_variable("IS_LOADING", false)
if not is_loading then
logger:log(ngx.ERR, "utils.get_variable() failed : " .. err)
logger:log(ERR, "utils.get_variable() failed : " .. err)
return
elseif is_loading == "yes" then
return
@ -23,92 +30,92 @@ init_worker_by_lua_block {
-- Instantiate lock
local lock = require "resty.lock":new("worker_lock", { timeout = 10 })
if not lock then
logger:log(ngx.ERR, "lock:new() failed : " .. err)
logger:log(ERR, "lock:new() failed : " .. err)
return
end
-- Acquire lock
local elapsed, err = lock:lock("ready")
if elapsed == nil then
logger:log(ngx.ERR, "lock:lock() failed : " .. err)
logger:log(ERR, "lock:lock() failed : " .. err)
return
end
-- Check if work is done
local ok, err = datastore:get("misc_ready")
if not ok and err ~= "not found" then
logger:log(ngx.ERR, "datastore:get() failed : " .. err)
logger:log(ERR, "datastore:get() failed : " .. err)
local ok, err = lock:unlock()
if not ok then
logger:log(ngx.ERR, "lock:unlock() failed : " .. err)
logger:log(ERR, "lock:unlock() failed : " .. err)
end
return
end
if ok then
local ok, err = lock:unlock()
if not ok then
logger:log(ngx.ERR, "lock:unlock() failed : " .. err)
logger:log(ERR, "lock:unlock() failed : " .. err)
end
return
end
logger:log(ngx.INFO, "init_worker phase started")
logger:log(INFO, "init_worker phase started")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
local ok, err = lock:unlock()
if not ok then
logger:log(ngx.ERR, "lock:unlock() failed : " .. err)
logger:log(ERR, "lock:unlock() failed : " .. err)
end
return
end
-- Call init_worker() methods
logger:log(ngx.INFO, "calling init_worker() methods of plugins ...")
logger:log(INFO, "calling init_worker() methods of plugins ...")
for i, plugin_id in ipairs(order.init_worker) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has init_worker method
if plugin_lua.init_worker ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua)
local ok, plugin_obj = new_plugin(plugin_lua)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "init_worker")
local ok, ret = call_plugin(plugin_obj, "init_worker")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":init_worker() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":init_worker() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":init_worker() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":init_worker() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method init_worker() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method init_worker() is not defined")
end
end
end
logger:log(ngx.INFO, "called init_worker() methods of plugins")
logger:log(INFO, "called init_worker() methods of plugins")
-- End
local ok, err = datastore:set("misc_ready", "ok")
if not ok then
logger:log(ngx.ERR, "datastore:set() failed : " .. err)
logger:log(ERR, "datastore:set() failed : " .. err)
end
local ok, err = lock:unlock()
if not ok then
logger:log(ngx.ERR, "lock:unlock() failed : " .. err)
logger:log(ERR, "lock:unlock() failed : " .. err)
end
logger:log(ngx.INFO, "init phase ended")
logger:log(ngx.NOTICE, "BunkerWeb is ready to fool hackers ! 🚀")
logger:log(INFO, "init phase ended")
logger:log(NOTICE, "BunkerWeb is ready to fool hackers ! 🚀")
end
-- Start timer

View file

@ -7,114 +7,134 @@ access_by_lua_block {
local cclusterstore = require "bunkerweb.clusterstore"
local cjson = require "cjson"
local ngx = ngx
local ngx_req = ngx.req
local is_internal = ngx_req.is_internal
local exit = ngx.exit
local ngx_redirect = ngx.redirect
local ERR = ngx.ERR
local INFO = ngx.INFO
local WARN = ngx.WARN
local NOTICE = ngx.NOTICE
local fill_ctx = helpers.fill_ctx
local save_ctx = helpers.save_ctx
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local is_whitelisted = utils.is_whitelisted
local is_banned = utils.is_banned
local get_deny_status = utils.get_deny_status
local tostring = tostring
-- Don't process internal requests
local logger = clogger:new("ACCESS")
if ngx.req.is_internal() then
logger:log(ngx.INFO, "skipped access phase because request is internal")
if is_internal() then
logger:log(INFO, "skipped access phase because request is internal")
return true
end
-- Start access phase
local datastore = cdatastore:new()
logger:log(ngx.INFO, "access phase started")
logger:log(INFO, "access phase started")
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Process bans as soon as possible
if ctx.bw.is_whitelisted ~= "yes" then
local banned, reason, ttl = utils.is_banned(ctx.bw.remote_addr)
if not is_whitelisted(ctx) then
local banned, reason, ttl = is_banned(ctx.bw.remote_addr)
if banned == nil then
logger:log(ngx.ERR, "can't check if IP " .. ctx.bw.remote_addr .. " is banned : " .. reason)
logger:log(ERR, "can't check if IP " .. ctx.bw.remote_addr .. " is banned : " .. reason)
elseif banned then
ctx.bw.is_banned = true
helpers.save_ctx(ctx)
logger:log(ngx.WARN,
ctx.bw.reason = reason
save_ctx(ctx)
logger:log(WARN,
"IP " .. ctx.bw.remote_addr .. " is banned with reason " .. reason .. " (" .. tostring(ttl) .. "s remaining)")
return ngx.exit(utils.get_deny_status(ctx))
return exit(get_deny_status())
else
logger:log(ngx.INFO, "IP " .. ctx.bw.remote_addr .. " is not banned")
logger:log(INFO, "IP " .. ctx.bw.remote_addr .. " is not banned")
end
end
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
-- Call access() methods
logger:log(ngx.INFO, "calling access() methods of plugins ...")
logger:log(INFO, "calling access() methods of plugins ...")
local status = nil
local redirect = nil
for i, plugin_id in ipairs(order.access) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has access method
if plugin_lua.access ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua, ctx)
local ok, plugin_obj = new_plugin(plugin_lua, ctx)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "access")
local ok, ret = call_plugin(plugin_obj, "access")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":access() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":access() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":access() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":access() call successful : " .. ret.msg)
end
if ret.status then
if ret.status == utils.get_deny_status(ctx) then
if ret.status == get_deny_status() then
ctx.bw.reason = plugin_id
logger:log(ngx.WARN, "denied access from " .. plugin_id .. " : " .. ret.msg)
logger:log(WARN, "denied access from " .. plugin_id .. " : " .. ret.msg)
else
logger:log(ngx.NOTICE, plugin_id .. " returned status " .. tostring(ret.status) .. " : " .. ret.msg)
logger:log(NOTICE, plugin_id .. " returned status " .. tostring(ret.status) .. " : " .. ret.msg)
end
status = ret.status
break
elseif ret.redirect then
logger:log(ngx.NOTICE, plugin_id .. " redirect to " .. ret.redirect .. " : " .. ret.msg)
logger:log(NOTICE, plugin_id .. " redirect to " .. ret.redirect .. " : " .. ret.msg)
redirect = ret.redirect
break
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method access() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method access() is not defined")
end
end
end
logger:log(ngx.INFO, "called access() methods of plugins")
logger:log(INFO, "called access() methods of plugins")
-- Save ctx
helpers.save_ctx(ctx)
save_ctx(ctx)
logger:log(ngx.INFO, "access phase ended")
logger:log(INFO, "access phase ended")
-- Return status if needed
if status then
return ngx.exit(status)
return exit(status)
end
-- Redirect if needed
if redirect then
return ngx.redirect(redirect)
return ngx_redirect(redirect)
end
return true

View file

@ -1,69 +1,77 @@
header_filter_by_lua_block {
local class = require "middleclass"
local clogger = require "bunkerweb.logger"
local helpers = require "bunkerweb.helpers"
local cdatastore = require "bunkerweb.datastore"
local cjson = require "cjson"
local ngx = ngx
local ERR = ngx.ERR
local INFO = ngx.INFO
local fill_ctx = helpers.fill_ctx
local save_ctx = helpers.save_ctx
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local tostring = tostring
-- Start set phase
local logger = clogger:new("HEADER")
local datastore = cdatastore:new()
logger:log(ngx.INFO, "header phase started")
logger:log(INFO, "header phase started")
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
-- Call header() methods
logger:log(ngx.INFO, "calling header() methods of plugins ...")
logger:log(INFO, "calling header() methods of plugins ...")
for i, plugin_id in ipairs(order.header) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has header method
if plugin_lua.header ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua, ctx)
local ok, plugin_obj = new_plugin(plugin_lua, ctx)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "header")
local ok, ret = call_plugin(plugin_obj, "header")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":header() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":header() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":header() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":header() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method header() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method header() is not defined")
end
end
end
logger:log(ngx.INFO, "called header() methods of plugins")
logger:log(INFO, "called header() methods of plugins")
-- Save ctx
helpers.save_ctx(ctx)
save_ctx(ctx)
return true
}

View file

@ -1,71 +1,81 @@
log_by_lua_block {
local class = require "middleclass"
local clogger = require "bunkerweb.logger"
local helpers = require "bunkerweb.helpers"
local cdatastore = require "bunkerweb.datastore"
local cjson = require "cjson"
local utils = require "bunkerweb.utils"
local ngx = ngx
local ERR = ngx.ERR
local INFO = ngx.INFO
local fill_ctx = helpers.fill_ctx
local get_reason = utils.get_reason
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local tostring = tostring
-- Start log phase
local logger = clogger:new("LOG")
local datastore = cdatastore:new()
logger:log(ngx.INFO, "log phase started")
logger:log(INFO, "log phase started")
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
-- Call log() methods
logger:log(ngx.INFO, "calling log() methods of plugins ...")
logger:log(INFO, "calling log() methods of plugins ...")
for i, plugin_id in ipairs(order.log) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has log method
if plugin_lua.log ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua, ctx)
local ok, plugin_obj = new_plugin(plugin_lua, ctx)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "log")
local ok, ret = call_plugin(plugin_obj, "log")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":log() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":log() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":log() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":log() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method log() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method log() is not defined")
end
end
end
logger:log(ngx.INFO, "called log() methods of plugins")
logger:log(INFO, "called log() methods of plugins")
-- Display reason at info level
if ctx.bw.reason then
logger:log(ngx.INFO, "client was denied with reason : " .. ctx.bw.reason)
local reason = get_reason(ctx)
if reason then
logger:log(INFO, "client was denied with reason : " .. reason)
end
logger:log(ngx.INFO, "log phase ended")
logger:log(INFO, "log phase ended")
}

View file

@ -1,46 +1,56 @@
set $dummy_set "";
set_by_lua_block $dummy_set {
local class = require "middleclass"
local clogger = require "bunkerweb.logger"
local helpers = require "bunkerweb.helpers"
local cdatastore = require "bunkerweb.datastore"
local ccachestore = require "bunkerweb.cachestore"
local cjson = require "cjson"
local ngx = ngx
local ngx_req = ngx.req
local is_internal = ngx_req.is_internal
local ERR = ngx.ERR
local INFO = ngx.INFO
local fill_ctx = helpers.fill_ctx
local save_ctx = helpers.save_ctx
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local tostring = tostring
-- Don't process internal requests
local logger = clogger:new("SET")
if ngx.req.is_internal() then
logger:log(ngx.INFO, "skipped set phase because request is internal")
if is_internal() then
logger:log(INFO, "skipped set phase because request is internal")
return true
end
-- Start set phase
local datastore = cdatastore:new()
logger:log(ngx.INFO, "set phase started")
logger:log(INFO, "set phase started")
-- Update cachestore only once and before any other code
local cachestore = ccachestore:new()
local ok, err = cachestore.cache:update()
local cachestore = ccachestore:new(false)
local ok, err = cachestore:update()
if not ok then
logger:log(ngx.ERR, "can't update cachestore : " .. err)
logger:log(ERR, "can't update cachestore : " .. err)
end
-- Fill ctx
logger:log(ngx.INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = helpers.fill_ctx()
logger:log(INFO, "filling ngx.ctx ...")
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")")
logger:log(INFO, "ngx.ctx filled (ret = " .. ret .. ")")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
@ -48,37 +58,37 @@ set_by_lua_block $dummy_set {
logger:log(ngx.INFO, "calling set() methods of plugins ...")
for i, plugin_id in ipairs(order.set) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has set method
if plugin_lua.set ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua, ctx)
local ok, plugin_obj = new_plugin(plugin_lua, ctx)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "set")
local ok, ret = call_plugin(plugin_obj, "set")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":set() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":set() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":set() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":set() call successful : " .. ret.msg)
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method set() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method set() is not defined")
end
end
end
logger:log(ngx.INFO, "called set() methods of plugins")
logger:log(INFO, "called set() methods of plugins")
-- Save ctx
helpers.save_ctx(ctx)
save_ctx(ctx)
return true
}

View file

@ -27,51 +27,63 @@ ssl_certificate_by_lua_block {
local cjson = require "cjson"
local ssl = require "ngx.ssl"
local ngx = ngx
local ngx_req = ngx.req
local is_internal = ngx_req.is_internal
local ERR = ngx.ERR
local INFO = ngx.INFO
local set_cert = ssl.set_cert
local set_priv_key = ssl.set_priv_key
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local tostring = tostring
-- Start ssl_certificate phase
local logger = clogger:new("SSL-CERTIFICATE")
local datastore = cdatastore:new()
logger:log(ngx.INFO, "ssl_certificate phase started")
logger:log(INFO, "ssl_certificate phase started")
-- Get plugins order
local order, err = datastore:get("plugins_order", true)
if not order then
logger:log(ngx.ERR, "can't get plugins order from datastore : " .. err)
logger:log(ERR, "can't get plugins order from datastore : " .. err)
return
end
-- Call ssl_certificate() methods
logger:log(ngx.INFO, "calling ssl_certificate() methods of plugins ...")
logger:log(INFO, "calling ssl_certificate() methods of plugins ...")
for i, plugin_id in ipairs(order.ssl_certificate) do
-- Require call
local plugin_lua, err = helpers.require_plugin(plugin_id)
local plugin_lua, err = require_plugin(plugin_id)
if plugin_lua == false then
logger:log(ngx.ERR, err)
logger:log(ERR, err)
elseif plugin_lua == nil then
logger:log(ngx.INFO, err)
logger:log(INFO, err)
else
-- Check if plugin has ssl_certificate method
if plugin_lua.ssl_certificate ~= nil then
-- New call
local ok, plugin_obj = helpers.new_plugin(plugin_lua)
local ok, plugin_obj = new_plugin(plugin_lua)
if not ok then
logger:log(ngx.ERR, plugin_obj)
logger:log(ERR, plugin_obj)
else
local ok, ret = helpers.call_plugin(plugin_obj, "ssl_certificate")
local ok, ret = call_plugin(plugin_obj, "ssl_certificate")
if not ok then
logger:log(ngx.ERR, ret)
logger:log(ERR, ret)
elseif not ret.ret then
logger:log(ngx.ERR, plugin_id .. ":ssl_certificate() call failed : " .. ret.msg)
logger:log(ERR, plugin_id .. ":ssl_certificate() call failed : " .. ret.msg)
else
logger:log(ngx.INFO, plugin_id .. ":ssl_certificate() call successful : " .. ret.msg)
logger:log(INFO, plugin_id .. ":ssl_certificate() call successful : " .. ret.msg)
if ret.status then
logger:log(ngx.INFO, plugin_id .. " is setting certificate/key : " .. ret.msg)
local ok, err = ssl.set_cert(ret.status[1])
logger:log(INFO, plugin_id .. " is setting certificate/key : " .. ret.msg)
local ok, err = set_cert(ret.status[1])
if not ok then
logger:log(ngx.ERR, "error while setting certificate : " .. err)
logger:log(ERR, "error while setting certificate : " .. err)
else
local ok, err = ssl.set_priv_key(ret.status[2])
local ok, err = set_priv_key(ret.status[2])
if not ok then
logger:log(ngx.ERR, "error while setting private key : " .. err)
logger:log(ERR, "error while setting private key : " .. err)
else
return true
end
@ -80,13 +92,13 @@ ssl_certificate_by_lua_block {
end
end
else
logger:log(ngx.INFO, "skipped execution of " .. plugin_id .. " because method ssl_certificate() is not defined")
logger:log(INFO, "skipped execution of " .. plugin_id .. " because method ssl_certificate() is not defined")
end
end
end
logger:log(ngx.INFO, "called ssl_certificate() methods of plugins")
logger:log(INFO, "called ssl_certificate() methods of plugins")
logger:log(ngx.INFO, "ssl_certificate phase ended")
logger:log(INFO, "ssl_certificate phase ended")
return true
}

View file

@ -7,9 +7,30 @@ local plugin = require "bunkerweb.plugin"
local sha256 = require "resty.sha256"
local str = require "resty.string"
local utils = require "bunkerweb.utils"
local ngx = ngx
local subsystem = ngx.config.subsystem
local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR
local OK = ngx.OK
local tonumber = tonumber
local tostring = tostring
local get_session = utils.get_session
local get_session_data = utils.get_session_data
local set_session_data = utils.set_session_data
local get_deny_status = utils.get_deny_status
local rand = utils.rand
local now = ngx.now
local captcha_new = captcha.new
local base64_encode = base64.encode
local to_hex = str.to_hex
local http_new = http.new
local decode = cjson.decode
local template = nil
if ngx.shared.datastore then
local render = nil
if subsystem == "http" then
template = require "resty.template"
render = template.render
end
local antibot = class("antibot", plugin)
@ -30,12 +51,12 @@ function antibot:header()
end
-- Get session data
local session, err = utils.get_session("antibot", self.ctx)
local session, err = get_session("antibot", self.ctx)
if not session then
return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
self.session = session
self.session_data = utils.get_session_data(self.session, true, self.ctx)
self.session_data = get_session_data(self.session, true, self.ctx)
-- Check if session is valid
self:check_session()
@ -48,7 +69,7 @@ function antibot:header()
end
if self.ctx.bw.uri ~= self.variables["ANTIBOT_URI"] then
return self:ret(true, "Not antibot uri")
return self:ret(true, "not antibot uri")
end
local header = "Content-Security-Policy"
@ -97,12 +118,12 @@ function antibot:access()
end
-- Get session data
local session, err = utils.get_session("antibot", self.ctx)
local session, err = get_session("antibot", self.ctx)
if not session then
return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
self.session = session
self.session_data = utils.get_session_data(self.session, true, self.ctx)
self.session_data = get_session_data(self.session, true, self.ctx)
-- Check if session is valid
self:check_session()
@ -118,7 +139,7 @@ function antibot:access()
self:prepare_challenge()
local ok, err = self:set_session_data()
if not ok then
return self:ret(false, "can't save session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't save session : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
-- Redirect to challenge page
@ -143,10 +164,10 @@ function antibot:access()
local ok, err, redirect = self:check_challenge()
local set_ok, set_err = self:set_session_data()
if not set_ok then
return self:ret(false, "can't save session : " .. set_err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't save session : " .. set_err, HTTP_INTERNAL_SERVER_ERROR)
end
if ok == nil then
return self:ret(false, "check challenge error : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "check challenge error : " .. err, HTTP_INTERNAL_SERVER_ERROR)
elseif not ok then
self.logger:log(ngx.WARN, "client failed challenge : " .. err)
end
@ -156,14 +177,14 @@ function antibot:access()
self:prepare_challenge()
ok, err = self:set_session_data()
if not ok then
return self:ret(false, "can't save session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't save session : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
self.ctx.bw.antibot_display_content = true
return self:ret(true, "displaying challenge to client", ngx.OK)
return self:ret(true, "displaying challenge to client", OK)
end
-- Method is suspicious, let's deny the request
return self:ret(true, "unsupported HTTP method for antibot", utils.get_deny_status(self.ctx))
return self:ret(true, "unsupported HTTP method for antibot", get_deny_status())
end
function antibot:content()
@ -178,12 +199,12 @@ function antibot:content()
end
-- Get session data
local session, err = utils.get_session("antibot", self.ctx)
local session, err = get_session("antibot", self.ctx)
if not session then
return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
self.session = session
self.session_data = utils.get_session_data(self.session, true, self.ctx)
self.session_data = get_session_data(self.session, true, self.ctx)
-- Direct access without session
if not self.session_data.prepared then
@ -228,7 +249,7 @@ end
function antibot:set_session_data()
if self.session_updated then
local ok, err = utils.set_session_data(self.session, self.session_data, true, self.ctx)
local ok, err = set_session_data(self.session, self.session_data, true, self.ctx)
if not ok then
return false, err
end
@ -246,18 +267,18 @@ function antibot:prepare_challenge()
self.session_data.type = self.variables["USE_ANTIBOT"]
self.session_data.resolved = false
self.session_data.original_uri = self.ctx.bw.request_uri
self.session_data.nonce_script = utils.rand(16)
self.session_data.nonce_style = utils.rand(16)
self.session_data.nonce_script = rand(16)
self.session_data.nonce_style = rand(16)
if self.ctx.bw.uri == self.variables["ANTIBOT_URI"] then
self.session_data.original_uri = "/"
end
if self.session_data.type == "cookie" then
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
elseif self.session_data.type == "javascript" then
self.session_data.random = utils.rand(20)
self.session_data.random = rand(20)
elseif self.session_data.type == "captcha" then
self.session_data.captcha = utils.rand(6, true)
self.session_data.captcha = rand(6, true)
end
end
end
@ -282,11 +303,11 @@ function antibot:display_challenge()
-- Captcha case
if self.session_data.type == "captcha" then
local chall_captcha = captcha.new()
local chall_captcha = captcha_new()
chall_captcha:font("/usr/share/bunkerweb/core/antibot/files/font.ttf")
chall_captcha:string(self.session_data.captcha)
chall_captcha:generate()
template_vars.captcha = base64.encode(chall_captcha:jpegStr(70))
template_vars.captcha = base64_encode(chall_captcha:jpegStr(70))
end
-- reCAPTCHA case
@ -305,7 +326,7 @@ function antibot:display_challenge()
end
-- Render content
template.render(self.session_data.type .. ".html", template_vars)
render(self.session_data.type .. ".html", template_vars)
return true, "displayed challenge"
end
@ -317,33 +338,35 @@ function antibot:check_challenge()
end
local resolved
local ngx_req = ngx.req
local read_body = ngx_req.read_body
local get_post_args = ngx_req.get_post_args
self.session_data.prepared = false
self.session_updated = true
-- Javascript case
if self.session_data.type == "javascript" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
read_body()
local args, err = get_post_args(1)
if err == "truncated" or not args or not args["challenge"] then
return nil, "missing challenge arg"
end
local hash = sha256:new()
hash:update(self.session_data.random .. args["challenge"])
local digest = hash:final()
resolved = str.to_hex(digest):find("^0000") ~= nil
resolved = to_hex(digest):find("^0000") ~= nil
if not resolved then
return false, "wrong value"
end
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
return true, "resolved", self.session_data.original_uri
end
-- Captcha case
if self.session_data.type == "captcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
read_body()
local args, err = get_post_args(1)
if err == "truncated" or not args or not args["captcha"] then
return nil, "missing challenge arg", nil
end
@ -351,18 +374,18 @@ function antibot:check_challenge()
return false, "wrong value", nil
end
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
return true, "resolved", self.session_data.original_uri
end
-- reCAPTCHA case
if self.session_data.type == "recaptcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
read_body()
local args, err = get_post_args(1)
if err == "truncated" or not args or not args["token"] then
return nil, "missing challenge arg", nil
end
local httpc, err = http.new()
local httpc, err = http_new()
if not httpc then
return nil, "can't instantiate http object : " .. err, nil, nil
end
@ -382,7 +405,7 @@ function antibot:check_challenge()
if not res then
return nil, "can't send request to reCAPTCHA API : " .. err, nil
end
local ok, rdata = pcall(cjson.decode, res.body)
local ok, rdata = pcall(decode, res.body)
if not ok then
return nil, "error while decoding JSON from reCAPTCHA API : " .. rdata, nil
end
@ -390,18 +413,18 @@ function antibot:check_challenge()
return false, "client failed challenge with score " .. tostring(rdata.score), nil
end
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
return true, "resolved", self.session_data.original_uri
end
-- hCaptcha case
if self.session_data.type == "hcaptcha" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
read_body()
local args, err = get_post_args(1)
if err == "truncated" or not args or not args["token"] then
return nil, "missing challenge arg", nil
end
local httpc, err = http.new()
local httpc, err = http_new()
if not httpc then
return nil, "can't instantiate http object : " .. err, nil, nil
end
@ -421,7 +444,7 @@ function antibot:check_challenge()
if not res then
return nil, "can't send request to hCaptcha API : " .. err, nil
end
local ok, hdata = pcall(cjson.decode, res.body)
local ok, hdata = pcall(decode, res.body)
if not ok then
return nil, "error while decoding JSON from hCaptcha API : " .. hdata, nil
end
@ -429,18 +452,18 @@ function antibot:check_challenge()
return false, "client failed challenge", nil
end
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
return true, "resolved", self.session_data.original_uri
end
-- Turnstile case
if self.session_data.type == "turnstile" then
ngx.req.read_body()
local args, err = ngx.req.get_post_args(1)
read_body()
local args, err = get_post_args(1)
if err == "truncated" or not args or not args["token"] then
return nil, "missing challenge arg", nil
end
local httpc, err = http.new()
local httpc, err = http_new()
if not httpc then
return nil, "can't instantiate http object : " .. err, nil, nil
end
@ -460,7 +483,7 @@ function antibot:check_challenge()
if not res then
return nil, "can't send request to Turnstile API : " .. err, nil
end
local ok, tdata = pcall(cjson.decode, res.body)
local ok, tdata = pcall(decode, res.body)
if not ok then
return nil, "error while decoding JSON from Turnstile API : " .. tdata, nil
end
@ -468,7 +491,7 @@ function antibot:check_challenge()
return false, "client failed challenge", nil
end
self.session_data.resolved = true
self.session_data.time_valid = ngx.now()
self.session_data.time_valid = now()
return true, "resolved", self.session_data.original_uri
end

View file

@ -5,22 +5,30 @@ location {{ ANTIBOT_URI }} {
content_by_lua_block {
local logger = require "bunkerweb.logger":new("ANTIBOT")
local helpers = require "bunkerweb.helpers"
local ok, ret, errors, ctx = helpers.fill_ctx()
local ngx = ngx
local ERR = ngx.ERR
local INFO = ngx.INFO
local fill_ctx = helpers.fill_ctx
local save_ctx = helpers.save_ctx
local tostring = tostring
local ok, ret, errors, ctx = fill_ctx()
if not ok then
logger:log(ngx.ERR, "fill_ctx() failed : " .. ret)
logger:log(ERR, "fill_ctx() failed : " .. ret)
elseif errors then
for i, error in ipairs(errors) do
logger:log(ngx.ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
logger:log(ERR, "fill_ctx() error " .. tostring(i) .. " : " .. error)
end
end
local antibot = require "antibot.antibot":new(ctx)
local ret = antibot:content()
if not ret.ret then
logger:log(ngx.ERR, "antibot:content() failed : " .. ret.msg)
logger:log(ERR, "antibot:content() failed : " .. ret.msg)
else
logger:log(ngx.INFO, "antibot:content() success : " .. ret.msg)
logger:log(INFO, "antibot:content() success : " .. ret.msg)
end
ngx.ctx = ctx
save_ctx(ctx)
}
}
{% endif %}

View file

@ -4,6 +4,16 @@ local utils = require "bunkerweb.utils"
local badbehavior = class("badbehavior", plugin)
local ngx = ngx
local ERR = ngx.ERR
local WARN = ngx.WARN
local NOTICE = ngx.NOTICE
local timer_at = ngx.timer.at
local add_ban = utils.add_ban
local is_whitelisted = utils.is_whitelisted
local is_banned = utils.is_banned
local tostring = tostring
function badbehavior:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "badbehavior", ctx)
@ -11,7 +21,7 @@ end
function badbehavior:log()
-- Check if we are whitelisted
if self.ctx.bw.is_whitelisted == "yes" then
if is_whitelisted(self.ctx) == "yes" then
return self:ret(true, "client is whitelisted")
end
-- Check if bad behavior is activated
@ -23,11 +33,11 @@ function badbehavior:log()
return self:ret(true, "not increasing counter")
end
-- Check if we are already banned
if self.ctx.bw.is_banned then
if is_banned(self.ctx.bw.remote_addr) then
return self:ret(true, "already banned")
end
-- Call increase function later and with cosocket enabled
local ok, err = ngx.timer.at(
local ok, err = timer_at(
0,
badbehavior.increase,
self.ctx.bw.remote_addr,
@ -55,13 +65,14 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us
-- Instantiate objects
local logger = require "bunkerweb.logger":new("badbehavior")
local datastore = require "bunkerweb.datastore":new()
-- Declare counter
local counter = false
-- Redis case
if use_redis then
local redis_counter, err = badbehavior.redis_increase(ip, count_time, ban_time)
if not redis_counter then
logger:log(ngx.ERR, "(increase) redis_increase failed, falling back to local : " .. err)
logger:log(ERR, "(increase) redis_increase failed, falling back to local : " .. err)
else
counter = redis_counter
end
@ -70,7 +81,7 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us
if not counter then
local local_counter, err = datastore:get("plugin_badbehavior_count_" .. ip)
if not local_counter and err ~= "not found" then
logger:log(ngx.ERR, "(increase) can't get counts from the datastore : " .. err)
logger:log(ERR, "(increase) can't get counts from the datastore : " .. err)
end
if local_counter == nil then
local_counter = 0
@ -78,25 +89,25 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us
counter = local_counter + 1
end
-- Call decrease later
local ok, err = ngx.timer.at(count_time, badbehavior.decrease, ip, count_time, threshold, use_redis)
local ok, err = timer_at(count_time, badbehavior.decrease, ip, count_time, threshold, use_redis)
if not ok then
logger:log(ngx.ERR, "(increase) can't create decrease timer : " .. err)
logger:log(ERR, "(increase) can't create decrease timer : " .. err)
end
-- Store local counter
local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, counter, count_time)
if not ok then
logger:log(ngx.ERR, "(increase) can't save counts to the datastore : " .. err)
logger:log(ERR, "(increase) can't save counts to the datastore : " .. err)
return
end
-- Store local ban
if counter > threshold then
ok, err = utils.add_ban(ip, "bad behavior", ban_time)
ok, err = add_ban(ip, "bad behavior", ban_time)
if not ok then
logger:log(ngx.ERR, "(increase) can't save ban : " .. err)
logger:log(ERR, "(increase) can't save ban : " .. err)
return
end
logger:log(
ngx.WARN,
WARN,
"IP "
.. ip
.. " is banned for "
@ -109,7 +120,7 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us
)
end
logger:log(
ngx.NOTICE,
NOTICE,
"increased counter for IP " .. ip .. " (" .. tostring(counter) .. "/" .. tostring(threshold) .. ")"
)
end
@ -124,7 +135,7 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis)
if use_redis then
local redis_counter, err = badbehavior.redis_decrease(ip, count_time)
if not redis_counter then
logger:log(ngx.ERR, "(decrease) redis_decrease failed, falling back to local : " .. err)
logger:log(ERR, "(decrease) redis_decrease failed, falling back to local : " .. err)
else
counter = redis_counter
end
@ -133,7 +144,7 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis)
if not counter then
local local_counter, err = datastore:get("plugin_badbehavior_count_" .. ip)
if not local_counter and err ~= "not found" then
logger:log(ngx.ERR, "(decrease) can't get counts from the datastore : " .. err)
logger:log(ERR, "(decrease) can't get counts from the datastore : " .. err)
end
if local_counter == nil or local_counter <= 1 then
counter = 0
@ -148,19 +159,19 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis)
else
local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, counter, count_time)
if not ok then
logger:log(ngx.ERR, "(decrease) can't save counts to the datastore : " .. err)
logger:log(ERR, "(decrease) can't save counts to the datastore : " .. err)
return
end
end
logger:log(
ngx.NOTICE,
NOTICE,
"decreased counter for IP " .. ip .. " (" .. tostring(counter) .. "/" .. tostring(threshold) .. ")"
)
end
function badbehavior.redis_increase(ip, count_time, ban_time)
-- Instantiate objects
local clusterstore = require "bunkerweb.clusterstore":new(false)
local clusterstore = require "bunkerweb.clusterstore":new()
-- Our LUA script to execute on redis
local redis_script = [[
local ret_incr = redis.pcall("INCR", KEYS[1])
@ -201,7 +212,7 @@ end
function badbehavior.redis_decrease(ip, count_time)
-- Instantiate objects
local clusterstore = require "bunkerweb.clusterstore":new(false)
local clusterstore = require "bunkerweb.clusterstore":new()
-- Our LUA script to execute on redis
local redis_script = [[
local ret_decr = redis.pcall("DECR", KEYS[1])

View file

@ -5,14 +5,26 @@ local utils = require "bunkerweb.utils"
local blacklist = class("blacklist", plugin)
local ngx = ngx
local ERR = ngx.ERR
local get_phase = ngx.get_phase
local has_variable = utils.has_variable
local get_deny_status = utils.get_deny_status
local get_rdns = utils.get_rdns
local get_asn = utils.get_asn
local regex_match = utils.regex_match
local ipmatcher_new = ipmatcher.new
local tostring = tostring
local open = io.open
function blacklist:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "blacklist", ctx)
-- Decode lists
if ngx.get_phase() ~= "init" and self:is_needed() then
if get_phase() ~= "init" and self:is_needed() then
local lists, err = self.datastore:get("plugin_blacklist_lists", true)
if not lists then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
self.lists = {}
else
self.lists = lists
@ -50,9 +62,9 @@ function blacklist:is_needed()
return self.variables["USE_BLACKLIST"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_BLACKLIST", "yes")
local is_needed, err = has_variable("USE_BLACKLIST", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_BLACKLIST variable : " .. err)
self.logger:log(ERR, "can't check USE_BLACKLIST variable : " .. err)
end
return is_needed
end
@ -78,7 +90,7 @@ function blacklist:init()
}
local i = 0
for kind, _ in pairs(blacklists) do
local f, _ = io.open("/var/cache/bunkerweb/blacklist/" .. kind .. ".list", "r")
local f, _ = open("/var/cache/bunkerweb/blacklist/" .. kind .. ".list", "r")
if f then
for line in f:lines() do
table.insert(blacklists[kind], line)
@ -118,12 +130,12 @@ function blacklist:access()
for k, v in pairs(checks) do
local ok, cached = self:is_in_cache(v)
if not ok then
self.logger:log(ngx.ERR, "error while checking cache : " .. cached)
self.logger:log(ERR, "error while checking cache : " .. cached)
elseif cached and cached ~= "ok" then
return self:ret(
true,
k .. " is in cached blacklist (info : " .. cached .. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
if ok and cached then
@ -139,18 +151,18 @@ function blacklist:access()
if not already_cached[k] then
local ok, blacklisted = self:is_blacklisted(k)
if ok == nil then
self.logger:log(ngx.ERR, "error while checking if " .. k .. " is blacklisted : " .. blacklisted)
self.logger:log(ERR, "error while checking if " .. k .. " is blacklisted : " .. blacklisted)
else
-- luacheck: ignore 421
local ok, err = self:add_to_cache(self:kind_to_ele(k), blacklisted)
if not ok then
self.logger:log(ngx.ERR, "error while adding element to cache : " .. err)
self.logger:log(ERR, "error while adding element to cache : " .. err)
end
if blacklisted ~= "ok" then
return self:ret(
true,
k .. " is blacklisted (info : " .. blacklisted .. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
end
@ -204,7 +216,7 @@ end
function blacklist:is_blacklisted_ip()
-- Check if IP is in ignore list
local ipm, err = ipmatcher.new(self.lists["IGNORE_IP"])
local ipm, err = ipmatcher_new(self.lists["IGNORE_IP"])
if not ipm then
return nil, err
end
@ -235,7 +247,7 @@ function blacklist:is_blacklisted_ip()
if check_rdns then
-- Get rDNS
-- luacheck: ignore 421
local rdns_list, err = utils.get_rdns(self.ctx.bw.remote_addr)
local rdns_list, err = get_rdns(self.ctx.bw.remote_addr, self.ctx, true)
if rdns_list then
-- Check if rDNS is in ignore list
local ignore = false
@ -258,13 +270,13 @@ function blacklist:is_blacklisted_ip()
end
end
else
self.logger:log(ngx.ERR, "error while getting rdns : " .. err)
self.logger:log(ERR, "error while getting rdns : " .. err)
end
end
-- Check if ASN is in ignore list
if self.ctx.bw.ip_is_global then
local asn, err = utils.get_asn(self.ctx.bw.remote_addr)
local asn, err = get_asn(self.ctx.bw.remote_addr)
if not asn then
self.logger:log(ngx.ERR, "can't get ASN of IP " .. self.ctx.bw.remote_addr .. " : " .. err)
else
@ -294,7 +306,7 @@ function blacklist:is_blacklisted_uri()
-- Check if URI is in ignore list
local ignore = false
for _, ignore_uri in ipairs(self.lists["IGNORE_URI"]) do
if utils.regex_match(self.ctx.bw.uri, ignore_uri) then
if regex_match(self.ctx.bw.uri, ignore_uri) then
ignore = true
break
end
@ -302,7 +314,7 @@ function blacklist:is_blacklisted_uri()
-- Check if URI is in blacklist
if not ignore then
for _, uri in ipairs(self.lists["URI"]) do
if utils.regex_match(self.ctx.bw.uri, uri) then
if regex_match(self.ctx.bw.uri, uri) then
return true, "URI " .. uri
end
end
@ -315,7 +327,7 @@ function blacklist:is_blacklisted_ua()
-- Check if UA is in ignore list
local ignore = false
for _, ignore_ua in ipairs(self.lists["IGNORE_USER_AGENT"]) do
if utils.regex_match(self.ctx.bw.http_user_agent, ignore_ua) then
if regex_match(self.ctx.bw.http_user_agent, ignore_ua) then
ignore = true
break
end
@ -323,7 +335,7 @@ function blacklist:is_blacklisted_ua()
-- Check if UA is in blacklist
if not ignore then
for _, ua in ipairs(self.lists["USER_AGENT"]) do
if utils.regex_match(self.ctx.bw.http_user_agent, ua) then
if regex_match(self.ctx.bw.http_user_agent, ua) then
return true, "UA " .. ua
end
end

View file

@ -6,18 +6,41 @@ local utils = require "bunkerweb.utils"
local bunkernet = class("bunkernet", plugin)
local ngx = ngx
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local WARN = ngx.WARN
local timer_at = ngx.timer.at
local get_phase = ngx.get_phase
local get_version = utils.get_version
local get_integration = utils.get_integration
local get_deny_status = utils.get_deny_status
local is_ipv4 = utils.is_ipv4
local is_ipv6 = utils.is_ipv6
local ip_is_global = utils.ip_is_global
local has_variable = utils.has_variable
local is_whitelisted = utils.is_whitelisted
local is_ip_in_networks = utils.is_ip_in_networks
local get_reason = utils.get_reason
local get_variable = utils.get_variable
local tostring = tostring
local open = io.open
local encode = cjson.encode
local decode = cjson.decode
local http_new = http.new
function bunkernet:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "bunkernet", ctx)
-- Get BunkerNet ID and save info
if ngx.get_phase() ~= "init" and self:is_needed() then
if get_phase() ~= "init" and self:is_needed() then
local id, err = self.datastore:get("plugin_bunkernet_id", true)
if id then
self.bunkernet_id = id
self.version = (self.ctx and self.ctx.bw.version) or utils.get_version()
self.integration = (self.ctx and self.ctx.bw.integration) or utils.get_integration()
self.version = get_version(self.ctx)
self.integration = get_integration(self.ctx)
else
self.logger:log(ngx.ERR, "can't get BunkerNet ID from datastore : " .. err)
self.logger:log(ERR, "can't get BunkerNet ID from datastore : " .. err)
end
end
end
@ -32,9 +55,9 @@ function bunkernet:is_needed()
return self.variables["USE_BUNKERNET"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_BUNKERNET", "yes")
local is_needed, err = has_variable("USE_BUNKERNET", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_BUNKERNET variable : " .. err)
self.logger:log(ERR, "can't check USE_BUNKERNET variable : " .. err)
end
return is_needed
end
@ -59,7 +82,7 @@ function bunkernet:init_worker()
"received status " .. tostring(status) .. " from API using instance ID " .. self.bunkernet_id
)
end
self.logger:log(ngx.NOTICE, "connectivity with API using instance ID " .. self.bunkernet_id .. " is successful")
self.logger:log(NOTICE, "connectivity with API using instance ID " .. self.bunkernet_id .. " is successful")
return self:ret(true, "connectivity with API using instance ID " .. self.bunkernet_id .. " is successful")
end
@ -69,7 +92,7 @@ function bunkernet:init()
return self:ret(true, "no service uses BunkerNet, skipping init")
end
-- Check if instance ID is present
local f, err = io.open("/var/cache/bunkerweb/bunkernet/instance.id", "r")
local f, err = open("/var/cache/bunkerweb/bunkernet/instance.id", "r")
if not f then
return self:ret(false, "can't read instance id : " .. err)
end
@ -87,12 +110,12 @@ function bunkernet:init()
local db = {
ip = {},
}
local f, err = io.open("/var/cache/bunkerweb/bunkernet/ip.list", "r")
local f, err = open("/var/cache/bunkerweb/bunkernet/ip.list", "r")
if not f then
ret = false
else
for line in f:lines() do
if (utils.is_ipv4(line) or utils.is_ipv6(line)) and utils.ip_is_global(line) then
if (is_ipv4(line) or is_ipv6(line)) and ip_is_global(line) then
table.insert(db.ip, line)
i = i + 1
end
@ -123,7 +146,7 @@ function bunkernet:access()
return self:ret(true, "IP is not global")
end
-- Check if whitelisted
if self.ctx.bw.is_whitelisted == "yes" then
if is_whitelisted(self.ctx) then
return self:ret(true, "client is whitelisted")
end
-- Extract DB
@ -132,12 +155,12 @@ function bunkernet:access()
-- Check if is IP is present
if #db.ip > 0 then
-- luacheck: ignore 421
local present, err = utils.is_ip_in_networks(self.ctx.bw.remote_addr, db.ip)
local present, err = is_ip_in_networks(self.ctx.bw.remote_addr, db.ip)
if present == nil then
return self:ret(false, "can't check if ip is in db : " .. err)
end
if present then
return self:ret(true, "ip is in db", utils.get_deny_status(self.ctx))
return self:ret(true, "ip is in db", get_deny_status())
end
end
else
@ -158,7 +181,7 @@ function bunkernet:log(bypass_checks)
end
end
-- Check if IP has been blocked
local reason = utils.get_reason(self.ctx)
local reason = get_reason(self.ctx)
if not reason then
return self:ret(true, "ip is not blocked")
end
@ -169,20 +192,30 @@ function bunkernet:log(bypass_checks)
if not self.ctx.bw.ip_is_global then
return self:ret(true, "IP is not global")
end
-- TODO : check if IP has been reported recently
-- Check if IP has been reported recently
local ok, data = self.cachestore:get("plugin_bunkernet_" .. self.ctx.bw.remote_addr .. "_" .. reason)
if not ok then
self.logger:log(ERR, "can't check cachestore : " .. data)
elseif data then
return self:ret(true, "already reported recently")
end
-- luacheck: ignore 212 431
local function report_callback(premature, obj, ip, reason, method, url, headers)
local function report_callback(premature, obj, ip, reason, method, url, headers, use_redis)
local ok, err, status, _ = obj:report(ip, reason, method, url, headers)
if status == 429 then
obj.logger:log(ngx.WARN, "bunkernet API is rate limiting us")
obj.logger:log(WARN, "bunkernet API is rate limiting us")
elseif not ok then
obj.logger:log(ngx.ERR, "can't report IP : " .. err)
obj.logger:log(ERR, "can't report IP : " .. err)
else
obj.logger:log(ngx.NOTICE, "successfully reported IP " .. ip .. " (reason : " .. reason .. ")")
obj.logger:log(NOTICE, "successfully reported IP " .. ip .. " (reason : " .. reason .. ")")
local cachestore = require "bunkerweb.cachestore":new(use_redis, nil, true)
local ok, err = cachestore:set("plugin_bunkernet_" .. ip .. "_" .. reason)
if not ok then
obj.logger:log(ERR, "error from cachestore : " .. err)
end
end
end
local hdr, err = ngx.timer.at(
local hdr, err = timer_at(
0,
report_callback,
self,
@ -208,7 +241,7 @@ function bunkernet:log_default()
return self:ret(false, "missing instance ID")
end
-- Check if default server is disabled
local check, err = utils.get_variable("DISABLE_DEFAULT_SERVER", false)
local check, err = get_variable("DISABLE_DEFAULT_SERVER", false)
if check == nil then
return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER : " .. err)
end
@ -224,7 +257,7 @@ function bunkernet:log_stream()
end
function bunkernet:request(method, url, data)
local httpc, err = http.new()
local httpc, err = http_new()
if not httpc then
return false, "can't instantiate http object : " .. err
end
@ -240,7 +273,7 @@ function bunkernet:request(method, url, data)
end
local res, err = httpc:request_uri(self.variables["BUNKERNET_SERVER"] .. url, {
method = method,
body = cjson.encode(all_data),
body = encode(all_data),
headers = {
["Content-Type"] = "application/json",
["User-Agent"] = "BunkerWeb/" .. self.version,
@ -253,7 +286,7 @@ function bunkernet:request(method, url, data)
if res.status ~= 200 then
return false, "status code != 200", res.status, nil
end
local ok, ret = pcall(cjson.decode, res.body)
local ok, ret = pcall(decode, res.body)
if not ok then
return false, "error while decoding json : " .. ret
end

View file

@ -4,6 +4,12 @@ local utils = require "bunkerweb.utils"
local cors = class("cors", plugin)
local ngx = ngx
local HTTP_NO_CONTENT = ngx.HTTP_NO_CONTENT
local WARN = ngx.WARN
local regex_match = utils.regex_match
local get_deny_status = utils.get_deny_status
function cors:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "cors", ctx)
@ -31,50 +37,51 @@ function cors:header()
return self:ret(true, "origin header not present")
end
-- Always include Vary header to prevent caching
local vary = ngx.header.Vary
local ngx_header = ngx.header
local vary = ngx_header.Vary
if vary then
if type(vary) == "string" then
ngx.header.Vary = { vary, "Origin" }
ngx_header.Vary = { vary, "Origin" }
else
table.insert(vary, "Origin")
ngx.header.Vary = vary
ngx_header.Vary = vary
end
else
ngx.header.Vary = "Origin"
ngx_header.Vary = "Origin"
end
-- Check if Origin is allowed
if
self.ctx.bw.http_origin
and self.variables["CORS_DENY_REQUEST"] == "yes"
and self.variables["CORS_ALLOW_ORIGIN"] ~= "*"
and not utils.regex_match(self.ctx.bw.http_origin, self.variables["CORS_ALLOW_ORIGIN"])
and not regex_match(self.ctx.bw.http_origin, self.variables["CORS_ALLOW_ORIGIN"])
then
self.logger:log(ngx.WARN, "origin " .. self.ctx.bw.http_origin .. " is not allowed")
self.logger:log(WARN, "origin " .. self.ctx.bw.http_origin .. " is not allowed")
return self:ret(true, "origin " .. self.ctx.bw.http_origin .. " is not allowed")
end
-- Set headers
if self.variables["CORS_ALLOW_ORIGIN"] == "*" then
ngx.header["Access-Control-Allow-Origin"] = "*"
ngx_header["Access-Control-Allow-Origin"] = "*"
else
ngx.header["Access-Control-Allow-Origin"] = self.ctx.bw.http_origin
ngx_header["Access-Control-Allow-Origin"] = self.ctx.bw.http_origin
end
for variable, header in pairs(self.all_headers) do
if self.variables[variable] ~= "" then
ngx.header[header] = self.variables[variable]
ngx_header[header] = self.variables[variable]
end
end
if self.ctx.bw.request_method == "OPTIONS" then
for variable, header in pairs(self.preflight_headers) do
if variable == "CORS_ALLOW_CREDENTIALS" then
if self.variables["CORS_ALLOW_CREDENTIALS"] == "yes" then
ngx.header[header] = "true"
ngx_header[header] = "true"
end
elseif self.variables[variable] ~= "" then
ngx.header[header] = self.variables[variable]
ngx_header[header] = self.variables[variable]
end
end
ngx.header["Content-Type"] = "text/html; charset=UTF-8"
ngx.header["Content-Length"] = "0"
ngx_header["Content-Type"] = "text/html; charset=UTF-8"
ngx_header["Content-Length"] = "0"
return self:ret(true, "edited headers for preflight request")
end
return self:ret(true, "edited headers for standard request")
@ -90,17 +97,17 @@ function cors:access()
self.ctx.bw.http_origin
and self.variables["CORS_DENY_REQUEST"] == "yes"
and self.variables["CORS_ALLOW_ORIGIN"] ~= "*"
and not utils.regex_match(self.ctx.bw.http_origin, self.variables["CORS_ALLOW_ORIGIN"])
and not regex_match(self.ctx.bw.http_origin, self.variables["CORS_ALLOW_ORIGIN"])
then
return self:ret(
true,
"origin " .. self.ctx.bw.http_origin .. " is not allowed, denying access",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
-- Send CORS policy with a 204 (no content) status
if self.ctx.bw.request_method == "OPTIONS" and self.ctx.bw.http_origin then
return self:ret(true, "preflight request", ngx.HTTP_NO_CONTENT)
return self:ret(true, "preflight request", HTTP_NO_CONTENT)
end
return self:ret(true, "standard request")
end

View file

@ -5,6 +5,12 @@ local utils = require "bunkerweb.utils"
local country = class("country", plugin)
local ngx = ngx
local get_country = utils.get_country
local get_deny_status = utils.get_deny_status
local decode = cjson.decode
local encode = cjson.encode
function country:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "country", ctx)
@ -18,7 +24,7 @@ function country:access()
-- Check if IP is in cache
local _, data = self:is_in_cache(self.ctx.bw.remote_addr)
if data then
data = cjson.decode(data)
data = decode(data)
if data.result == "ok" then
return self:ret(
true,
@ -36,7 +42,7 @@ function country:access()
.. " is in country cache (blacklisted, country = "
.. data.country
.. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
@ -50,7 +56,7 @@ function country:access()
end
-- Get the country of client
local country_data, err = utils.get_country(self.ctx.bw.remote_addr)
local country_data, err = get_country(self.ctx.bw.remote_addr)
if not country_data then
return self:ret(false, "can't get country of client IP " .. self.ctx.bw.remote_addr .. " : " .. err)
end
@ -78,7 +84,7 @@ function country:access()
return self:ret(
true,
"client IP " .. self.ctx.bw.remote_addr .. " is not whitelisted (country = " .. country_data .. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
@ -93,7 +99,7 @@ function country:access()
return self:ret(
true,
"client IP " .. self.ctx.bw.remote_addr .. " is blacklisted (country = " .. country_data .. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
end
@ -125,7 +131,7 @@ end
function country:add_to_cache(ip, country_data, result)
local ok, err = self.cachestore:set(
"plugin_country_" .. self.ctx.bw.server_name .. ip,
cjson.encode { country = country_data, result = result },
encode({ country = country_data, result = result }),
86400
)
if not ok then

View file

@ -5,6 +5,16 @@ local ssl = require "ngx.ssl"
local customcert = class("customcert", plugin)
local ngx = ngx
local ERR = ngx.ERR
local parse_pem_cert = ssl.parse_pem_cert
local parse_pem_priv_key = ssl.parse_pem_priv_key
local ssl_server_name = ssl.server_name
local get_variable = utils.get_variable
local get_multiple_variables = utils.get_multiple_variables
local has_variable = utils.has_variable
local open = io.open
function customcert:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "customcert", ctx)
@ -12,13 +22,13 @@ end
function customcert:init()
local ret_ok, ret_err = true, "success"
if utils.has_variable("USE_CUSTOM_SSL", "yes") then
local multisite, err = utils.get_variable("MULTISITE", false)
if has_variable("USE_CUSTOM_SSL", "yes") then
local multisite, err = get_variable("MULTISITE", false)
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"USE_CUSTOM_SSL", "SERVER_NAME"})
local vars, err = get_multiple_variables({"USE_CUSTOM_SSL", "SERVER_NAME"})
if not vars then
return self:ret(false, "can't get USE_CUSTOM_SSL variables : " .. err)
end
@ -26,13 +36,13 @@ function customcert:init()
if multisite_vars["USE_CUSTOM_SSL"] == "yes" and server_name ~= "global" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -40,19 +50,19 @@ function customcert:init()
end
end
else
local server_name, err = utils.get_variable("SERVER_NAME", false)
local server_name, err = get_variable("SERVER_NAME", false)
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local check, data = self:read_files(server_name:match("%S+"))
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -65,7 +75,7 @@ function customcert:init()
end
function customcert:ssl_certificate()
local server_name, err = ssl.server_name()
local server_name, err = ssl_server_name()
if not server_name then
return self:ret(false, "can't get server_name : " .. err)
end
@ -86,7 +96,7 @@ function customcert:read_files(server_name)
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
local f, err = open(file, "r")
if not f then
return false, file .. " = " .. err
end
@ -98,12 +108,12 @@ end
function customcert:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
local cert_chain, err = parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_pem_priv_key(data[2])
local priv_key, err = parse_pem_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end

View file

@ -5,9 +5,20 @@ local utils = require "bunkerweb.utils"
local dnsbl = class("dnsbl", plugin)
local ngx = ngx
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local spawn = ngx.thread.spawn
local wait = ngx.thread.wait
local arpa_str = resolver.arpa_str
local get_ips = utils.get_ips
local has_variable = utils.has_variable
local get_deny_status = utils.get_deny_status
local kill_all_threads = utils.kill_all_threads
local is_in_dnsbl = function(addr, server)
local request = resolver.arpa_str(addr):gsub("%.in%-addr%.arpa", ""):gsub("%.ip6%.arpa", "") .. "." .. server
local ips, err = utils.get_ips(request, false)
local request = arpa_str(addr):gsub("%.in%-addr%.arpa", ""):gsub("%.ip6%.arpa", "") .. "." .. server
local ips, err = get_ips(request, false, nil, true)
if not ips then
return nil, server, err
end
@ -30,7 +41,7 @@ function dnsbl:init_worker()
return self:ret(false, "BW is loading")
end
-- Check if at least one service uses it
local is_needed, err = utils.has_variable("USE_DNSBL", "yes")
local is_needed, err = has_variable("USE_DNSBL", "yes")
if is_needed == nil then
return self:ret(false, "can't check USE_DNSBL variable : " .. err)
elseif not is_needed then
@ -40,21 +51,21 @@ function dnsbl:init_worker()
local threads = {}
for server in self.variables["DNSBL_LIST"]:gmatch("%S+") do
-- Create thread
local thread = ngx.thread.spawn(is_in_dnsbl, "127.0.0.2", server)
local thread = spawn(is_in_dnsbl, "127.0.0.2", server)
threads[server] = thread
end
-- Wait for threads
for data, thread in pairs(threads) do
-- luacheck: ignore 421
local ok, result, server, err = ngx.thread.wait(thread)
local ok, result, server, err = wait(thread)
if not ok then
self.logger:log(ngx.ERR, "error while waiting thread of " .. data .. " check : " .. result)
self.logger:log(ERR, "error while waiting thread of " .. data .. " check : " .. result)
elseif result == nil then
self.logger:log(ngx.ERR, "error while sending DNS request to " .. server .. " : " .. err)
self.logger:log(ERR, "error while sending DNS request to " .. server .. " : " .. err)
elseif not result then
self.logger:log(ngx.ERR, "dnsbl check for " .. server .. " failed")
self.logger:log(ERR, "dnsbl check for " .. server .. " failed")
else
self.logger:log(ngx.NOTICE, "dnsbl check for " .. server .. " is successful")
self.logger:log(NOTICE, "dnsbl check for " .. server .. " is successful")
end
end
return self:ret(true, "success")
@ -83,14 +94,14 @@ function dnsbl:access()
return self:ret(
true,
"client IP " .. self.ctx.bw.remote_addr .. " is in DNSBL cache (server = " .. cached .. ")",
utils.get_deny_status(self.ctx)
get_deny_status()
)
end
-- Loop on DNSBL list
local threads = {}
for server in self.variables["DNSBL_LIST"]:gmatch("%S+") do
-- Create thread
local thread = ngx.thread.spawn(is_in_dnsbl, self.ctx.bw.remote_addr, server)
local thread = spawn(is_in_dnsbl, self.ctx.bw.remote_addr, server)
threads[server] = thread
end
-- Wait for threads
@ -109,7 +120,7 @@ function dnsbl:access()
end
-- Wait for first thread
-- luacheck: ignore 421
local ok, result, server, err = ngx.thread.wait(unpack(wait_threads))
local ok, result, server, err = wait(unpack(wait_threads))
-- Error case
if not ok then
ret_threads = false
@ -120,7 +131,7 @@ function dnsbl:access()
threads[server] = nil
-- DNS error
if result == nil then
self.logger:log(ngx.ERR, "error while sending DNS request to " .. server .. " : " .. err)
self.logger:log(ERR, "error while sending DNS request to " .. server .. " : " .. err)
end
-- IP is in DNSBL
if result then
@ -137,7 +148,7 @@ function dnsbl:access()
for _, thread in pairs(threads) do
table.insert(wait_threads, thread)
end
utils.kill_all_threads(wait_threads)
kill_all_threads(wait_threads)
end
-- Blacklisted by a server : add to cache and deny access
if ret_threads then
@ -145,7 +156,7 @@ function dnsbl:access()
if not ok then
return self:ret(false, "error while adding element to cache : " .. err)
end
return self:ret(true, "IP is blacklisted by " .. ret_server, utils.get_deny_status(self.ctx))
return self:ret(true, "IP is blacklisted by " .. ret_server, get_deny_status())
end
-- Error case
return self:ret(false, ret_err)

View file

@ -1,8 +1,14 @@
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local ngx = ngx
local subsystem = ngx.config.subsystem
local template = nil
if ngx.shared.datastore then
local render = nil
if subsystem == "http" then
template = require "resty.template"
render = template.render
end
local errors = class("errors", plugin)
@ -65,7 +71,7 @@ end
function errors:render_template(code)
-- Render template
template.render("error.html", {
render("error.html", {
title = code .. " - " .. self.default_errors[code].title,
error_title = self.default_errors[code].title,
error_code = code,

View file

@ -5,14 +5,26 @@ local utils = require "bunkerweb.utils"
local greylist = class("greylist", plugin)
local ngx = ngx
local ERR = ngx.ERR
local get_phase = ngx.get_phase
local has_variable = utils.has_variable
local get_deny_status = utils.get_deny_status
local get_rdns = utils.get_rdns
local get_asn = utils.get_asn
local regex_match = utils.regex_match
local ipmatcher_new = ipmatcher.new
local tostring = tostring
local open = io.open
function greylist:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "greylist", ctx)
-- Decode lists
if ngx.get_phase() ~= "init" and self:is_needed() then
if get_phase() ~= "init" and self:is_needed() then
local lists, err = self.datastore:get("plugin_greylist_lists", true)
if not lists then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
self.lists = {}
else
self.lists = lists
@ -45,9 +57,9 @@ function greylist:is_needed()
return self.variables["USE_GREYLIST"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_GREYLIST", "yes")
local is_needed, err = has_variable("USE_GREYLIST", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_GREYLIST variable : " .. err)
self.logger:log(ERR, "can't check USE_GREYLIST variable : " .. err)
end
return is_needed
end
@ -67,7 +79,7 @@ function greylist:init()
}
local i = 0
for kind, _ in pairs(greylists) do
local f, _ = io.open("/var/cache/bunkerweb/greylist/" .. kind .. ".list", "r")
local f, _ = open("/var/cache/bunkerweb/greylist/" .. kind .. ".list", "r")
if f then
for line in f:lines() do
table.insert(greylists[kind], line)
@ -107,7 +119,7 @@ function greylist:access()
for k, v in pairs(checks) do
local ok, cached = self:is_in_cache(v)
if not ok then
self.logger:log(ngx.ERR, "error while checking cache : " .. cached)
self.logger:log(ERR, "error while checking cache : " .. cached)
elseif cached and cached ~= "ko" then
return self:ret(true, k .. " is in cached greylist (info : " .. cached .. ")")
end
@ -124,12 +136,12 @@ function greylist:access()
if not already_cached[k] then
local ok, greylisted = self:is_greylisted(k)
if ok == nil then
self.logger:log(ngx.ERR, "error while checking if " .. k .. " is greylisted : " .. greylisted)
self.logger:log(ERR, "error while checking if " .. k .. " is greylisted : " .. greylisted)
else
-- luacheck: ignore 421
local ok, err = self:add_to_cache(self:kind_to_ele(k), greylisted)
if not ok then
self.logger:log(ngx.ERR, "error while adding element to cache : " .. err)
self.logger:log(ERR, "error while adding element to cache : " .. err)
end
if greylisted ~= "ko" then
return self:ret(true, k .. " is in greylist")
@ -139,7 +151,7 @@ function greylist:access()
end
-- Return
return self:ret(true, "not in greylist", utils.get_deny_status(self.ctx))
return self:ret(true, "not in greylist", get_deny_status())
end
function greylist:preread()
@ -169,7 +181,7 @@ end
function greylist:is_greylisted_ip()
-- Check if IP is in greylist
local ipm, err = ipmatcher.new(self.lists["IP"])
local ipm, err = ipmatcher_new(self.lists["IP"])
if not ipm then
return nil, err
end
@ -189,7 +201,7 @@ function greylist:is_greylisted_ip()
if check_rdns then
-- Get rDNS
-- luacheck: ignore 421
local rdns_list, err = utils.get_rdns(self.ctx.bw.remote_addr)
local rdns_list, err = get_rdns(self.ctx.bw.remote_addr, self.ctx, true)
-- Check if rDNS is in greylist
if rdns_list then
for _, rdns in ipairs(rdns_list) do
@ -200,15 +212,15 @@ function greylist:is_greylisted_ip()
end
end
else
self.logger:log(ngx.ERR, "error while getting rdns : " .. err)
self.logger:log(ERR, "error while getting rdns : " .. err)
end
end
-- Check if ASN is in greylist
if self.ctx.bw.ip_is_global then
local asn, err = utils.get_asn(self.ctx.bw.remote_addr)
local asn, err = get_asn(self.ctx.bw.remote_addr)
if not asn then
self.logger:log(ngx.ERR, "can't get ASN of IP " .. self.ctx.bw.remote_addr .. " : " .. err)
self.logger:log(ERR, "can't get ASN of IP " .. self.ctx.bw.remote_addr .. " : " .. err)
else
for _, bl_asn in ipairs(self.lists["ASN"]) do
if bl_asn == tostring(asn) then
@ -225,7 +237,7 @@ end
function greylist:is_greylisted_uri()
-- Check if URI is in greylist
for _, uri in ipairs(self.lists["URI"]) do
if utils.regex_match(self.ctx.bw.uri, uri) then
if regex_match(self.ctx.bw.uri, uri) then
return true, "URI " .. uri
end
end
@ -236,7 +248,7 @@ end
function greylist:is_greylisted_ua()
-- Check if UA is in greylist
for _, ua in ipairs(self.lists["USER_AGENT"]) do
if utils.regex_match(self.ctx.bw.http_user_agent, ua) then
if regex_match(self.ctx.bw.http_user_agent, ua) then
return true, "UA " .. ua
end
end

View file

@ -4,6 +4,13 @@ local utils = require "bunkerweb.utils"
local headers = class("headers", plugin)
local ngx = ngx
local ERR = ngx.ERR
local get_phase = ngx.get_phase
local regex_match = utils.regex_match
local get_multiple_variables = utils.get_multiple_variables
local tostring = tostring
function headers:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "headers", ctx)
@ -18,11 +25,11 @@ function headers:initialize(ctx)
["X_XSS_PROTECTION"] = "X-XSS-Protection",
}
-- Load data from datastore if needed
if ngx.get_phase() ~= "init" then
if get_phase() ~= "init" then
-- Get custom headers from datastore
local custom_headers, err = self.datastore:get("plugin_headers_custom_headers", true)
if not custom_headers then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
return
end
self.custom_headers = {}
@ -43,7 +50,7 @@ end
function headers:init()
-- Get variables
local variables, err = utils.get_multiple_variables({ "CUSTOM_HEADER" })
local variables, err = get_multiple_variables({ "CUSTOM_HEADER" })
if variables == nil then
return self:ret(false, err)
end
@ -55,7 +62,7 @@ function headers:init()
if data[srv] == nil then
data[srv] = {}
end
local m = utils.regex_match(value, "([\\w-]+): ([^,]+)")
local m = regex_match(value, "([\\w-]+): ([^,]+)")
if m then
data[srv][m[1]] = m[2]
end
@ -72,14 +79,15 @@ end
function headers:header()
-- Override upstream headers if needed
local ngx_header = ngx.header
local ssl = self.ctx.bw.scheme == "https"
for variable, header in pairs(self.all_headers) do
if
ngx.header[header] == nil
ngx_header[header] == nil
or (
self.variables[variable] ~= ""
and self.variables["KEEP_UPSTREAM_HEADERS"] ~= "*"
and utils.regex_match(self.variables["KEEP_UPSTREAM_HEADERS"], "(^| )" .. header .. "($| )") == nil
and regex_match(self.variables["KEEP_UPSTREAM_HEADERS"], "(^| )" .. header .. "($| )") == nil
)
then
if header ~= "Strict-Transport-Security" or ssl then
@ -87,21 +95,21 @@ function headers:header()
header == "Content-Security-Policy"
and self.variables["CONTENT_SECURITY_POLICY_REPORT_ONLY"] == "yes"
then
ngx.header["Content-Security-Policy-Report-Only"] = self.variables[variable]
ngx_header["Content-Security-Policy-Report-Only"] = self.variables[variable]
else
ngx.header[header] = self.variables[variable]
ngx_header[header] = self.variables[variable]
end
end
end
end
-- Add custom headers
for header, value in pairs(self.custom_headers) do
ngx.header[header] = value
ngx_header[header] = value
end
-- Remove headers
if self.variables["REMOVE_HEADERS"] ~= "" then
for header in self.variables["REMOVE_HEADERS"]:gmatch("%S+") do
ngx.header[header] = nil
ngx_header[header] = nil
end
end
return self:ret(true, "edited headers for request")

View file

@ -6,6 +6,27 @@ local ssl = require "ngx.ssl"
local letsencrypt = class("letsencrypt", plugin)
local ngx = ngx
local ERR = ngx.ERR
local NOTICE = ngx.NOTICE
local OK = ngx.OK
local HTTP_NOT_FOUND = ngx.HTTP_NOT_FOUND
local HTTP_OK = ngx.HTTP_OK
local HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST
local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR
local parse_pem_cert = ssl.parse_pem_cert
local parse_pem_priv_key = ssl.parse_pem_priv_key
local ssl_server_name = ssl.server_name
local get_variable = utils.get_variable
local get_multiple_variables = utils.get_multiple_variables
local has_variable = utils.has_variable
local open = io.open
local sub = string.sub
local match = string.match
local decode = cjson.decode
local execute = os.execute
local remove = os.remove
function letsencrypt:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "letsencrypt", ctx)
@ -13,13 +34,13 @@ end
function letsencrypt:init()
local ret_ok, ret_err = true, "success"
if utils.has_variable("AUTO_LETS_ENCRYPT", "yes") then
local multisite, err = utils.get_variable("MULTISITE", false)
if has_variable("AUTO_LETS_ENCRYPT", "yes") then
local multisite, err = get_variable("MULTISITE", false)
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"AUTO_LETS_ENCRYPT", "SERVER_NAME"})
local vars, err = get_multiple_variables({"AUTO_LETS_ENCRYPT", "SERVER_NAME"})
if not vars then
return self:ret(false, "can't get AUTO_LETS_ENCRYPT variables : " .. err)
end
@ -27,13 +48,13 @@ function letsencrypt:init()
if multisite_vars["AUTO_LETS_ENCRYPT"] == "yes" and server_name ~= "global" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -41,19 +62,19 @@ function letsencrypt:init()
end
end
else
local server_name, err = utils.get_variable("SERVER_NAME", false)
local server_name, err = get_variable("SERVER_NAME", false)
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local check, data = self:read_files(server_name:match("%S+"))
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -66,7 +87,7 @@ function letsencrypt:init()
end
function letsencrypt:ssl_certificate()
local server_name, err = ssl.server_name()
local server_name, err = ssl_server_name()
if not server_name then
return self:ret(false, "can't get server_name : " .. err)
end
@ -87,7 +108,7 @@ function letsencrypt:read_files(server_name)
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
local f, err = open(file, "r")
if not f then
return false, file .. " = " .. err
end
@ -99,12 +120,12 @@ end
function letsencrypt:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
local cert_chain, err = parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_pem_priv_key(data[2])
local priv_key, err = parse_pem_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end
@ -120,48 +141,45 @@ function letsencrypt:load_data(data, server_name)
end
function letsencrypt:access()
if string.sub(self.ctx.bw.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
self.logger:log(ngx.NOTICE, "got a visit from Let's Encrypt, let's whitelist it")
return self:ret(true, "visit from LE", ngx.OK)
if sub(self.ctx.bw.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
self.logger:log(NOTICE, "got a visit from Let's Encrypt, let's whitelist it")
return self:ret(true, "visit from LE", OK)
end
return self:ret(true, "success")
end
-- luacheck: ignore 212
function letsencrypt:api(ctx)
function letsencrypt:api()
if
not string.match(ctx.bw.uri, "^/lets%-encrypt/challenge$")
or (ctx.bw.request_method ~= "POST" and ctx.bw.request_method ~= "DELETE")
not match(self.ctx.bw.uri, "^/lets%-encrypt/challenge$")
or (self.ctx.bw.request_method ~= "POST" and self.ctx.bw.request_method ~= "DELETE")
then
return false, nil, nil
return self:ret(false, "success")
end
local acme_folder = "/var/tmp/bunkerweb/lets-encrypt/.well-known/acme-challenge/"
ngx.req.read_body()
local ret, data = pcall(cjson.decode, ngx.req.get_body_data())
local ngx_req = ngx.req
ngx_req.read_body()
local ret, data = pcall(decode, ngx_req.get_body_data())
if not ret then
return true, ngx.HTTP_BAD_REQUEST, { status = "error", msg = "json body decoding failed" }
return self:ret(true, "json body decoding failed", HTTP_BAD_REQUEST)
end
os.execute("mkdir -p " .. acme_folder)
if ctx.bw.request_method == "POST" then
local file, err = io.open(acme_folder .. data.token, "w+")
execute("mkdir -p " .. acme_folder)
if self.ctx.bw.request_method == "POST" then
local file, err = open(acme_folder .. data.token, "w+")
if not file then
return true,
ngx.HTTP_INTERNAL_SERVER_ERROR,
{ status = "error", msg = "can't write validation token : " .. err }
return self:ret(true, "can't write validation token : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
file:write(data.validation)
file:close()
return true, ngx.HTTP_OK, { status = "success", msg = "validation token written" }
return self:ret(true, "validation token written", HTTP_OK)
elseif ctx.bw.request_method == "DELETE" then
local ok, err = os.remove(acme_folder .. data.token)
local ok, err = remove(acme_folder .. data.token)
if not ok then
return true,
ngx.HTTP_INTERNAL_SERVER_ERROR,
{ status = "error", msg = "can't remove validation token : " .. err }
return self:ret(true, "can't remove validation token : " .. err, HTTP_INTERNAL_SERVER_ERROR)
end
return true, ngx.HTTP_OK, { status = "success", msg = "validation token removed" }
return true, HTTP_OK, { status = "success", msg = "validation token removed" }
end
return true, ngx.HTTP_NOT_FOUND, { status = "error", msg = "unknown request" }
return true, HTTP_NOT_FOUND, { status = "error", msg = "unknown request" }
end
return letsencrypt

View file

@ -5,11 +5,24 @@ local utils = require "bunkerweb.utils"
local limit = class("limit", plugin)
local ngx = ngx
local ERR = ngx.ERR
local HTTP_TOO_MANY_REQUESTS = ngx.HTTP_TOO_MANY_REQUESTS
local get_phase = ngx.get_phase
local has_variable = utils.has_variable
local get_multiple_variables = utils.get_multiple_variables
local is_whitelisted = utils.is_whitelisted
local regex_match = utils.regex_match
local time = os.time
local date = os.date
local encode = cjson.encode
local decode = cjson.decode
local limit_req_timestamps = function(rate_max, rate_time, timestamps)
-- Compute new timestamps
local updated = false
local new_timestamps = {}
local current_timestamp = os.time(os.date "!*t")
local current_timestamp = time(date("!*t"))
local delay = 0
if rate_time == "s" then
delay = 1
@ -40,11 +53,11 @@ function limit:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "limit", ctx)
-- Load rules if needed
if ngx.get_phase() ~= "init" and self:is_needed() then
if get_phase() ~= "init" and self:is_needed() then
-- Get all rules from datastore
local all_rules, err = self.datastore:get("plugin_limit_rules", true)
if not all_rules then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
return
end
self.rules = {}
@ -73,7 +86,7 @@ function limit:is_needed()
return self.variables["USE_LIMIT_REQ"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_LIMIT_REQ", "yes")
local is_needed, err = has_variable("USE_LIMIT_REQ", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_LIMIT_REQ variable : " .. err)
end
@ -86,7 +99,7 @@ function limit:init()
return self:ret(true, "no service uses limit for requests, skipping init")
end
-- Get variables
local variables, err = utils.get_multiple_variables({ "LIMIT_REQ_URL", "LIMIT_REQ_RATE" })
local variables, err = get_multiple_variables({ "LIMIT_REQ_URL", "LIMIT_REQ_RATE" })
if variables == nil then
return self:ret(false, err)
end
@ -95,7 +108,7 @@ function limit:init()
local i = 0
for srv, vars in pairs(variables) do
for var, value in pairs(vars) do
if utils.regex_match(var, "LIMIT_REQ_URL") then
if regex_match(var, "LIMIT_REQ_URL") then
local url = value
local rate = vars[var:gsub("URL", "RATE")]
if data[srv] == nil then
@ -115,7 +128,7 @@ end
function limit:access()
-- Check if we are whitelisted
if self.ctx.bw.is_whitelisted == "yes" then
if is_whitelisted(self.ctx) then
return self:ret(true, "client is whitelisted")
end
-- Check if access is needed
@ -125,7 +138,7 @@ function limit:access()
-- Check if URI is limited
local rate
for k, v in pairs(self.rules) do
if k ~= "/" and utils.regex_match(self.ctx.bw.uri, k) then
if k ~= "/" and regex_match(self.ctx.bw.uri, k) then
rate = v
break
end
@ -158,7 +171,7 @@ function limit:access()
.. " and max rate = "
.. rate
.. ")",
ngx.HTTP_TOO_MANY_REQUESTS
HTTP_TOO_MANY_REQUESTS
)
end
-- Limit not reached
@ -184,14 +197,14 @@ function limit:limit_req(rate_max, rate_time)
if self.use_redis then
local redis_timestamps, err = self:limit_req_redis(rate_max, rate_time)
if redis_timestamps == nil then
self.logger:log(ngx.ERR, "limit_req_redis failed, falling back to local : " .. err)
self.logger:log(ERR, "limit_req_redis failed, falling back to local : " .. err)
else
timestamps = redis_timestamps
-- Save the new timestamps
-- luacheck: ignore 421
local ok, err = self.datastore:set(
"plugin_limit_" .. self.ctx.bw.server_name .. self.ctx.bw.remote_addr .. self.ctx.bw.uri,
cjson.encode(timestamps),
encode(timestamps),
delay
)
if not ok then
@ -222,7 +235,7 @@ function limit:limit_req_local(rate_max, rate_time)
elseif err == "not found" then
timestamps = "{}"
end
timestamps = cjson.decode(timestamps)
timestamps = decode(timestamps)
-- Compute new timestamps
local updated, new_timestamps, delay = limit_req_timestamps(rate_max, rate_time, timestamps)
-- Save new timestamps if needed
@ -230,7 +243,7 @@ function limit:limit_req_local(rate_max, rate_time)
-- luacheck: ignore 421
local ok, err = self.datastore:set(
"plugin_limit_" .. self.ctx.bw.server_name .. self.ctx.bw.remote_addr .. self.ctx.bw.uri,
cjson.encode(new_timestamps),
encode(new_timestamps),
delay
)
if not ok then
@ -303,7 +316,7 @@ function limit:limit_req_redis(rate_max, rate_time)
"plugin_limit_" .. self.ctx.bw.server_name .. self.ctx.bw.remote_addr .. self.ctx.bw.uri,
rate_max,
rate_time,
os.time(os.date("!*t"))
time(date("!*t"))
)
if not timestamps then
self.clusterstore:close()

View file

@ -4,6 +4,11 @@ local utils = require "bunkerweb.utils"
local misc = class("misc", plugin)
local ngx = ngx
local HTTP_NOT_ALLOWED = ngx.HTTP_NOT_ALLOWED
local HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST
local regex_match = utils.regex_match
function misc:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "misc", ctx)
@ -12,8 +17,8 @@ end
function misc:access()
-- Check if method is valid
local method = self.ctx.bw.request_method
if not method or not utils.regex_match(method, "^[A-Z]+$") then
return self:ret(true, "method is not valid", ngx.HTTP_BAD_REQUEST)
if not method or not regex_match(method, "^[A-Z]+$") then
return self:ret(true, "method is not valid", HTTP_BAD_REQUEST)
end
-- Check if method is allowed
for allowed_method in self.variables["ALLOWED_METHODS"]:gmatch("[^|]+") do
@ -21,7 +26,7 @@ function misc:access()
return self:ret(true, "method " .. method .. " is allowed")
end
end
return self:ret(true, "method " .. method .. " is not allowed", ngx.HTTP_NOT_ALLOWED)
return self:ret(true, "method " .. method .. " is not allowed", HTTP_NOT_ALLOWED)
end
return misc

View file

@ -76,6 +76,60 @@
"label": "Redis keepalive pool",
"regex": "^[0-9]+$",
"type": "text"
},
"REDIS_USERNAME": {
"context": "global",
"default": "",
"help": "Redis username used in AUTH command.",
"id": "redis-username",
"label": "Redis username",
"regex": "^.*$",
"type": "text"
},
"REDIS_PASSWORD": {
"context": "global",
"default": "",
"help": "Redis password used in AUTH command.",
"id": "redis-password",
"label": "Redis password",
"regex": "^.*$",
"type": "password"
},
"REDIS_SENTINEL_HOSTS": {
"context": "global",
"default": "",
"help": "Redis sentinel hosts with format host:[port] separated with spaces.",
"id": "redis-sentinel-hosts",
"label": "Redis sentinel hosts",
"regex": "^.*$",
"type": "text"
},
"REDIS_SENTINEL_USERNAME": {
"context": "global",
"default": "",
"help": "Redis sentinel username.",
"id": "redis-sentinel-username",
"label": "Redis sentinel username",
"regex": "^.*$",
"type": "text"
},
"REDIS_SENTINEL_PASSWORD": {
"context": "global",
"default": "",
"help": "Redis sentinel password.",
"id": "redis-sentinel-password",
"label": "Redis sentinel password",
"regex": "^.*$",
"type": "password"
},
"REDIS_SENTINEL_MASTER": {
"context": "global",
"default": "",
"help": "Redis sentinel master name.",
"id": "redis-sentinel-master",
"label": "Redis sentinel master",
"regex": "^.*$",
"type": "text"
}
}
}

View file

@ -3,6 +3,9 @@ local plugin = require "bunkerweb.plugin"
local redis = class("redis", plugin)
local ngx = ngx
local NOTICE = ngx.NOTICE
function redis:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "redis", ctx)
@ -27,7 +30,7 @@ function redis:init_worker()
if not ok then
return self:ret(false, "redis ping command failed")
end
self.logger:log(ngx.NOTICE, "connectivity with redis server " .. self.variables["REDIS_HOST"] .. " is successful")
self.logger:log(NOTICE, "connectivity with redis server " .. self.variables["REDIS_HOST"] .. " is successful")
return self:ret(true, "success")
end

View file

@ -4,6 +4,14 @@ local utils = require "bunkerweb.utils"
local reversescan = class("reversescan", plugin)
local ngx = ngx
local spawn = ngx.thread.spawn
local wait = ngx.thread.wait
local ngx_socket = ngx.socket
local kill_all_threads = utils.kill_all_threads
local get_deny_status = utils.get_deny_status
local tonumber = tonumber
function reversescan:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "reversescan", ctx)
@ -32,7 +40,7 @@ function reversescan:access()
break
-- Perform scan in a thread
elseif not cached then
local thread = ngx.thread.spawn(
local thread = spawn(
self.scan,
self.ctx.bw.remote_addr,
tonumber(port),
@ -47,11 +55,11 @@ function reversescan:access()
for _, thread in pairs(threads) do
table.insert(wait_threads, thread)
end
utils.kill_all_threads(wait_threads)
kill_all_threads(wait_threads)
end
-- Open port case
if ret_threads then
return self:ret(true, ret_err, utils.get_deny_status(self.ctx))
return self:ret(true, ret_err, get_deny_status())
end
-- Error case
return self:ret(false, ret_err)
@ -71,7 +79,7 @@ function reversescan:access()
break
end
-- Wait for first thread
local ok, open, port = ngx.thread.wait(unpack(wait_threads))
local ok, open, port = wait(unpack(wait_threads))
-- Error case
if not ok then
ret_threads = false
@ -100,7 +108,7 @@ function reversescan:access()
for _, thread in pairs(threads) do
table.insert(wait_threads, thread)
end
utils.kill_all_threads(wait_threads)
kill_all_threads(wait_threads)
end
-- Cache results
for port, result in pairs(results) do
@ -112,7 +120,7 @@ function reversescan:access()
if ret_threads ~= nil then
-- Open port case
if ret_threads then
return self:ret(true, ret_err, utils.get_deny_status(self.ctx))
return self:ret(true, ret_err, get_deny_status())
end
-- Error case
return self:ret(false, ret_err)
@ -126,7 +134,7 @@ function reversescan:preread()
end
function reversescan.scan(ip, port, timeout)
local tcpsock = ngx.socket.tcp()
local tcpsock = ngx_socket.tcp()
tcpsock:settimeout(timeout)
local ok, _ = tcpsock:connect(ip, port)
tcpsock:close()

View file

@ -5,6 +5,16 @@ local ssl = require "ngx.ssl"
local selfsigned = class("selfsigned", plugin)
local ngx = ngx
local ERR = ngx.ERR
local parse_pem_cert = ssl.parse_pem_cert
local parse_pem_priv_key = ssl.parse_pem_priv_key
local ssl_server_name = ssl.server_name
local get_variable = utils.get_variable
local get_multiple_variables = utils.get_multiple_variables
local has_variable = utils.has_variable
local open = io.open
function selfsigned:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "selfsigned", ctx)
@ -12,13 +22,13 @@ end
function selfsigned:init()
local ret_ok, ret_err = true, "success"
if utils.has_variable("GENERATE_SELF_SIGNED_SSL", "yes") then
local multisite, err = utils.get_variable("MULTISITE", false)
if has_variable("GENERATE_SELF_SIGNED_SSL", "yes") then
local multisite, err = get_variable("MULTISITE", false)
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars, err = utils.get_multiple_variables({"GENERATE_SELF_SIGNED_SSL", "SERVER_NAME"})
local vars, err = get_multiple_variables({"GENERATE_SELF_SIGNED_SSL", "SERVER_NAME"})
if not vars then
return self:ret(false, "can't get GENERATE_SELF_SIGNED_SSL variables : " .. err)
end
@ -26,13 +36,13 @@ function selfsigned:init()
if multisite_vars["GENERATE_SELF_SIGNED_SSL"] == "yes" and server_name ~= "global" then
local check, data = self:read_files(server_name)
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -40,19 +50,19 @@ function selfsigned:init()
end
end
else
local server_name, err = utils.get_variable("SERVER_NAME", false)
local server_name, err = get_variable("SERVER_NAME", false)
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local check, data = self:read_files(server_name:match("%S+"))
if not check then
self.logger:log(ngx.ERR, "error while reading files : " .. data)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
local check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ngx.ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
@ -65,7 +75,7 @@ function selfsigned:init()
end
function selfsigned:ssl_certificate()
local server_name, err = ssl.server_name()
local server_name, err = ssl_server_name()
if not server_name then
return self:ret(false, "can't get server_name : " .. err)
end
@ -86,7 +96,7 @@ function selfsigned:read_files(server_name)
}
local data = {}
for i, file in ipairs(files) do
local f, err = io.open(file, "r")
local f, err = open(file, "r")
if not f then
return false, file .. " = " .. err
end
@ -98,12 +108,12 @@ end
function selfsigned:load_data(data, server_name)
-- Load certificate
local cert_chain, err = ssl.parse_pem_cert(data[1])
local cert_chain, err = parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key, err = ssl.parse_pem_priv_key(data[2])
local priv_key, err = parse_pem_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end

View file

@ -5,6 +5,12 @@ local utils = require "bunkerweb.utils"
local sessions = class("sessions", plugin)
local ngx = ngx
local ERR = ngx.ERR
local get_variable = utils.get_variable
local session_init = session.init
local tonumber = tonumber
function sessions:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "sessions", ctx)
@ -57,7 +63,7 @@ function sessions:init()
["REDIS_KEEPALIVE_POOL"] = "",
}
for k, _ in pairs(redis_vars) do
local value, err = utils.get_variable(k, false)
local value, err = get_variable(k, false)
if value == nil then
return self:ret(false, "can't get " .. k .. " variable : " .. err)
end
@ -78,7 +84,7 @@ function sessions:init()
config.secret = utils.rand(16)
local ok, err = self.datastore:set("storage_sessions_SESSIONS_SECRET", config.secret)
if not ok then
self.logger:log(ngx.ERR, "error from datastore:set : " .. err)
self.logger:log(ERR, "error from datastore:set : " .. err)
end
end
end
@ -89,7 +95,7 @@ function sessions:init()
config.cookie_name = utils.rand(16)
local ok, err = self.datastore:set("storage_sessions_SESSIONS_NAME", config.cookie_name)
if not ok then
self.logger:log(ngx.ERR, "error from datastore:set : " .. err)
self.logger:log(ERR, "error from datastore:set : " .. err)
end
end
end
@ -111,7 +117,7 @@ function sessions:init()
database = tonumber(redis_vars["REDIS_DATABASE"]),
}
end
session.init(config)
session_init(config)
return self:ret(true, "sessions init successful")
end

View file

@ -6,14 +6,29 @@ local utils = require "bunkerweb.utils"
local whitelist = class("whitelist", plugin)
local ngx = ngx
local ERR = ngx.ERR
local OK = ngx.OK
local WARN = ngx.WARN
local get_phase = ngx.get_phase
local has_variable = utils.has_variable
local get_ips = utils.get_ips
local get_rdns = utils.get_rdns
local get_asn = utils.get_asn
local regex_match = utils.regex_match
local ipmatcher_new = ipmatcher.new
local tostring = tostring
local open = io.open
local env_set = env.set
function whitelist:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "whitelist", ctx)
-- Decode lists
if ngx.get_phase() ~= "init" and self:is_needed() then
if get_phase() ~= "init" and self:is_needed() then
local lists, err = self.datastore:get("plugin_whitelist_lists", true)
if not lists then
self.logger:log(ngx.ERR, err)
self.logger:log(ERR, err)
self.lists = {}
else
self.lists = lists
@ -46,9 +61,9 @@ function whitelist:is_needed()
return self.variables["USE_WHITELIST"] == "yes"
end
-- Other cases : at least one service uses it
local is_needed, err = utils.has_variable("USE_WHITELIST", "yes")
local is_needed, err = has_variable("USE_WHITELIST", "yes")
if is_needed == nil then
self.logger:log(ngx.ERR, "can't check USE_WHITELIST variable : " .. err)
self.logger:log(ERR, "can't check USE_WHITELIST variable : " .. err)
end
return is_needed
end
@ -68,7 +83,7 @@ function whitelist:init()
}
local i = 0
for kind, _ in pairs(whitelists) do
local f, _ = io.open("/var/cache/bunkerweb/whitelist/" .. kind .. ".list", "r")
local f, _ = open("/var/cache/bunkerweb/whitelist/" .. kind .. ".list", "r")
if f then
for line in f:lines() do
table.insert(whitelists[kind], line)
@ -86,10 +101,11 @@ function whitelist:init()
end
function whitelist:set()
local ngx_var = ngx.var
-- Set default value
ngx.var.is_whitelisted = "no"
ngx_var.is_whitelisted = "no"
self.ctx.bw.is_whitelisted = "no"
env.set("is_whitelisted", "no")
env_set("is_whitelisted", "no")
-- Check if set is needed
if not self:is_needed() then
return self:ret(true, "whitelist not activated")
@ -99,9 +115,9 @@ function whitelist:set()
if whitelisted == nil then
return self:ret(false, err)
elseif whitelisted then
ngx.var.is_whitelisted = "yes"
ngx_var.is_whitelisted = "yes"
self.ctx.bw.is_whitelisted = "yes"
env.set("is_whitelisted", "yes")
env_set("is_whitelisted", "yes")
return self:ret(true, err)
end
return self:ret(true, "not in whitelist cache")
@ -113,14 +129,15 @@ function whitelist:access()
return self:ret(true, "whitelist not activated")
end
-- Check cache
local ngx_var = ngx.var
local whitelisted, err, already_cached = self:check_cache()
if whitelisted == nil then
return self:ret(false, err)
elseif whitelisted then
ngx.var.is_whitelisted = "yes"
ngx_var.is_whitelisted = "yes"
self.ctx.bw.is_whitelisted = "yes"
env.set("is_whitelisted", "yes")
return self:ret(true, err, ngx.OK)
env_set("is_whitelisted", "yes")
return self:ret(true, err, OK)
end
-- Perform checks
local ok
@ -128,16 +145,16 @@ function whitelist:access()
if not already_cached[k] then
ok, whitelisted = self:is_whitelisted(k)
if ok == nil then
self.logger:log(ngx.ERR, "error while checking if " .. k .. " is whitelisted : " .. whitelisted)
self.logger:log(ERR, "error while checking if " .. k .. " is whitelisted : " .. whitelisted)
else
ok, err = self:add_to_cache(self:kind_to_ele(k), whitelisted)
if not ok then
self.logger:log(ngx.ERR, "error while adding element to cache : " .. err)
self.logger:log(ERR, "error while adding element to cache : " .. err)
end
if whitelisted ~= "ok" then
ngx.var.is_whitelisted = "yes"
ngx_var.is_whitelisted = "yes"
self.ctx.bw.is_whitelisted = "yes"
env.set("is_whitelisted", "yes")
env_set("is_whitelisted", "yes")
return self:ret(true, k .. " is whitelisted (info : " .. whitelisted .. ")", ngx.OK)
end
end
@ -179,7 +196,7 @@ function whitelist:check_cache()
for k, v in pairs(checks) do
local ok, cached = self:is_in_cache(v)
if not ok then
self.logger:log(ngx.ERR, "error while checking cache : " .. cached)
self.logger:log(ERR, "error while checking cache : " .. cached)
elseif cached and cached ~= "ok" then
return true, k .. " is in cached whitelist (info : " .. cached .. ")"
end
@ -224,7 +241,7 @@ end
function whitelist:is_whitelisted_ip()
-- Check if IP is in whitelist
local ipm, err = ipmatcher.new(self.lists["IP"])
local ipm, err = ipmatcher_new(self.lists["IP"])
if not ipm then
return nil, err
end
@ -244,7 +261,7 @@ function whitelist:is_whitelisted_ip()
if check_rdns then
-- Get rDNS
-- luacheck: ignore 421
local rdns_list, err = utils.get_rdns(self.ctx.bw.remote_addr)
local rdns_list, err = get_rdns(self.ctx.bw.remote_addr, self.ctx, true)
-- Check if rDNS is in whitelist
if rdns_list then
local forward_check = nil
@ -262,7 +279,7 @@ function whitelist:is_whitelisted_ip()
end
end
if forward_check then
local ip_list, err = utils.get_ips(forward_check)
local ip_list, err = get_ips(forward_check, nil, self.ctx, true)
if ip_list then
for _, ip in ipairs(ip_list) do
if ip == self.ctx.bw.remote_addr then
@ -270,23 +287,23 @@ function whitelist:is_whitelisted_ip()
end
end
self.logger:log(
ngx.WARN,
WARN,
"IP " .. self.ctx.bw.remote_addr .. " may spoof reverse DNS " .. forward_check
)
else
self.logger:log(ngx.ERR, "error while getting rdns (forward check) : " .. err)
self.logger:log(ERR, "error while getting rdns (forward check) : " .. err)
end
end
else
self.logger:log(ngx.ERR, "error while getting rdns : " .. err)
self.logger:log(ERR, "error while getting rdns : " .. err)
end
end
-- Check if ASN is in whitelist
if self.ctx.bw.ip_is_global then
local asn, err = utils.get_asn(self.ctx.bw.remote_addr)
local asn, err = get_asn(self.ctx.bw.remote_addr)
if not asn then
self.logger:log(ngx.ERR, "can't get ASN of IP " .. self.ctx.bw.remote_addr .. " : " .. err)
self.logger:log(ERR, "can't get ASN of IP " .. self.ctx.bw.remote_addr .. " : " .. err)
else
for _, bl_asn in ipairs(self.lists["ASN"]) do
if bl_asn == tostring(asn) then
@ -303,7 +320,7 @@ end
function whitelist:is_whitelisted_uri()
-- Check if URI is in whitelist
for _, uri in ipairs(self.lists["URI"]) do
if utils.regex_match(self.ctx.bw.uri, uri) then
if regex_match(self.ctx.bw.uri, uri) then
return true, "URI " .. uri
end
end
@ -314,7 +331,7 @@ end
function whitelist:is_whitelisted_ua()
-- Check if UA is in whitelist
for _, ua in ipairs(self.lists["USER_AGENT"]) do
if utils.regex_match(self.ctx.bw.http_user_agent, ua) then
if regex_match(self.ctx.bw.http_user_agent, ua) then
return true, "UA " .. ua
end
end

View file

@ -234,6 +234,12 @@
"name": "zlib v1.3",
"url": "https://github.com/madler/zlib.git",
"commit": "09155eaa2f9270dc4ed1fa13e2b4b2613e6e4851"
},
{
"id": "lua-resty-redis-connector",
"name": "lua-resty-redis-connector v0.11.0",
"url": "https://github.com/ledgetech/lua-resty-redis-connector.git",
"commit": "02a29f93253d1f6ad392c5ac2b643c57e62b5979"
}
]
}

View file

@ -43,22 +43,24 @@ do
url="$(echo "$repo" | jq -r .url)"
commit="$(echo "$repo" | jq -r .commit)"
post_install="$(echo "$repo" | jq -r .post_install)"
post="no"
echo " Clone ${name} from $url at commit/version $commit"
if [ ! -d "src/deps/src/$id" ] ; then
do_and_check_cmd git subtree add --prefix "src/deps/src/$id" "$url" "$commit" --squash
post="yes"
else
echo "⚠️ Skipping clone of $url because target directory is already present"
echo " Updating ${name} from $url at commit/version $commit"
do_and_check_cmd git subtree pull --prefix "src/deps/src/$id" "$url" "$commit" --squash
# echo " Updating ${name} from $url at commit/version $commit"
# do_and_check_cmd git subtree pull --prefix "src/deps/src/$id" "$url" "$commit" --squash
fi
if [ -d "src/deps/src/$id/.git" ] ; then
do_and_check_cmd rm -rf "src/deps/src/$id/.git"
fi
if [ "$post_install" != "null" ] ; then
if [ "$post_install" != "null" ] && [ "$post" != "no" ]; then
echo " Running post install script for ${name}"
bash -c "$post_install"
fi

View file

@ -183,6 +183,12 @@ export CHANGE_DIR="/tmp/bunkerweb/deps/src/lua-resty-signal"
do_and_check_cmd make PREFIX=/usr/share/bunkerweb/deps -j "$NTASK"
do_and_check_cmd make PREFIX=/usr/share/bunkerweb/deps LUA_LIB_DIR=/usr/share/bunkerweb/deps/lib/lua install
# Installing lua-resty-redis-connector
echo " Installing lua-resty-redis-connector"
export CHANGE_DIR="/tmp/bunkerweb/deps/src/lua-resty-redis-connector"
do_and_check_cmd make PREFIX=/usr/share/bunkerweb/deps LUA_LIB_DIR=/usr/share/bunkerweb/deps/lib/lua install
# Patch modsec module
export CHANGE_DIR="/tmp/bunkerweb/deps/misc"
do_and_check_cmd bash -c "mv ngx_http_modsecurity_access.c /tmp/bunkerweb/deps/src/modsecurity-nginx/src/"

View file

@ -0,0 +1 @@
*.t linguist-language=lua

View file

@ -0,0 +1 @@
github: pintsized

View file

@ -0,0 +1,5 @@
t/servroot/
t/error.log
luacov.*
*.src.rock
lua-resty-redis-connector*.tar.gz

View file

@ -0,0 +1,2 @@
std = "ngx_lua"
redefined = false

View file

@ -0,0 +1,4 @@
modules = {
["resty.redis.connector"] = "lib/resty/redis/connector.lua",
["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua",
}

View file

@ -0,0 +1,84 @@
sudo: required
dist: focal
os: linux
language: c
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:redislabs/redis'
packages:
- luarocks
- lsof
cache:
directories:
- download-cache
env:
global:
- JOBS=3
- NGX_BUILD_JOBS=$JOBS
- LUAJIT_PREFIX=/opt/luajit21
- LUAJIT_LIB=$LUAJIT_PREFIX/lib
- LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1
- LUA_INCLUDE_DIR=$LUAJIT_INC
- OPENSSL_PREFIX=/opt/ssl
- OPENSSL_LIB=$OPENSSL_PREFIX/lib
- OPENSSL_INC=$OPENSSL_PREFIX/include
- OPENSSL_VER=1.1.1f
- LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH
- TEST_NGINX_SLEEP=0.006
- LUACHECK_VER=0.21.1
jobs:
- NGINX_VERSION=1.19.9
before_install:
# we can't update redis in addons.apt.packages as updated package automatically tries to start and immediately fails
- echo exit 101 | sudo tee /usr/sbin/policy-rc.d
- sudo chmod +x /usr/sbin/policy-rc.d
- sudo apt-get install -y redis-server
- sudo luarocks install luacov
- sudo luarocks install lua-resty-redis
- sudo luarocks install luacheck $LUACHECK_VER
- luacheck -q .
install:
- if [ ! -d download-cache ]; then mkdir download-cache; fi
- if [ ! -f download-cache/openssl-$OPENSSL_VER.tar.gz ]; then wget -O download-cache/openssl-$OPENSSL_VER.tar.gz https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz; fi
- sudo apt-get install -qq -y cpanminus axel
- sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1)
- git clone https://github.com/openresty/openresty.git ../openresty
- git clone https://github.com/openresty/nginx-devel-utils.git
- git clone https://github.com/openresty/lua-cjson.git
- git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module
- git clone https://github.com/openresty/stream-lua-nginx-module.git ../stream-lua-nginx-module
- git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core
- git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache
- git clone https://github.com/openresty/echo-nginx-module.git ../echo-nginx-module
- git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx
- git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git
script:
- sudo iptables -A OUTPUT -p tcp --dst 127.0.0.2 --dport 12345 -j DROP
- cd luajit2/
- make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' > build.log 2>&1 || (cat build.log && exit 1)
- sudo make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1)
- cd ../lua-cjson && make && sudo PATH=$PATH make install && cd ..
- tar zxf download-cache/openssl-$OPENSSL_VER.tar.gz
- cd openssl-$OPENSSL_VER/
- ./config shared --prefix=$OPENSSL_PREFIX -DPURIFY > build.log 2>&1 || (cat build.log && exit 1)
- make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1)
- sudo make PATH=$PATH install_sw > build.log 2>&1 || (cat build.log && exit 1)
- cd ..
- export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH
- export NGX_BUILD_CC=$CC
- ngx-build $NGINX_VERSION --with-ipv6 --with-http_realip_module --with-http_ssl_module --with-cc-opt="-I$OPENSSL_INC" --with-ld-opt="-L$OPENSSL_LIB -Wl,-rpath,$OPENSSL_LIB" --add-module=../echo-nginx-module --add-module=../lua-nginx-module --add-module=../stream-lua-nginx-module --with-stream --with-stream_ssl_module --with-debug > build.log 2>&1 || (cat build.log && exit 1)
- nginx -V
- ldd `which nginx`|grep -E 'luajit|ssl|pcre'
- mkdir -p tmp
- TMP_DIR=$PWD/tmp make test_all

View file

@ -0,0 +1,189 @@
SHELL := /bin/bash # Cheat by using bash :)
OPENRESTY_PREFIX = /usr/local/openresty
TEST_FILE ?= t
TMP_DIR ?= /tmp
REDIS_CMD = redis-server
SENTINEL_CMD = $(REDIS_CMD) --sentinel
REDIS_SOCK = /redis.sock
REDIS_PID = /redis.pid
REDIS_LOG = /redis.log
REDIS_PREFIX = $(TMP_DIR)/redis-
# Overrideable redis test variables
TEST_REDIS_PORT ?= 6380
TEST_REDIS_PORT_SL1 ?= 6381
TEST_REDIS_PORT_SL2 ?= 6382
TEST_REDIS_PORT_AUTH ?= 6383
TEST_REDIS_PORTS ?= $(TEST_REDIS_PORT) $(TEST_REDIS_PORT_SL1) $(TEST_REDIS_PORT_SL2)
TEST_REDIS_PORTS_ALL ?= $(TEST_REDIS_PORTS) $(TEST_REDIS_PORT_AUTH)
TEST_REDIS_DATABASE ?= 1
TEST_REDIS_SOCKET ?= $(REDIS_PREFIX)$(TEST_REDIS_PORT)$(REDIS_SOCK)
REDIS_SLAVE_ARG := --slaveof 127.0.0.1 $(TEST_REDIS_PORT)
REDIS_CLI := redis-cli -p $(TEST_REDIS_PORT) -n $(TEST_REDIS_DATABASE)
# Overrideable redis + sentinel test variables
TEST_SENTINEL_PORT1 ?= 6390
TEST_SENTINEL_PORT2 ?= 6391
TEST_SENTINEL_PORT3 ?= 6392
TEST_SENTINEL_PORT_AUTH ?= 6393
TEST_SENTINEL_PORTS ?= $(TEST_SENTINEL_PORT1) $(TEST_SENTINEL_PORT2) $(TEST_SENTINEL_PORT3)
TEST_SENTINEL_PORTS_ALL ?= $(TEST_SENTINEL_PORTS) $(TEST_SENTINEL_PORT_AUTH)
TEST_SENTINEL_MASTER_NAME ?= mymaster
TEST_SENTINEL_PROMOTION_TIME ?= 20
# Command line arguments for redis tests
TEST_REDIS_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \
TEST_NGINX_REDIS_PORT=$(TEST_REDIS_PORT) \
TEST_NGINX_REDIS_PORT_SL1=$(TEST_REDIS_PORT_SL1) \
TEST_NGINX_REDIS_PORT_SL2=$(TEST_REDIS_PORT_SL2) \
TEST_NGINX_REDIS_PORT_AUTH=$(TEST_REDIS_PORT_AUTH) \
TEST_NGINX_REDIS_SOCKET=unix:$(TEST_REDIS_SOCKET) \
TEST_NGINX_REDIS_DATABASE=$(TEST_REDIS_DATABASE) \
TEST_NGINX_NO_SHUFFLE=1
# Command line arguments for sentinel tests
TEST_SENTINEL_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \
TEST_NGINX_REDIS_PORT=$(TEST_NGINX_REDIS_PORT) \
TEST_NGINX_REDIS_PORT_SL1=$(TEST_NGINX_REDIS_PORT_SL1) \
TEST_NGINX_REDIS_PORT_SL2=$(TEST_NGINX_REDIS_PORT_SL2) \
TEST_NGINX_SENTINEL_PORT1=$(TEST_NGINX_SENTINEL_PORT1) \
TEST_NGINX_SENTINEL_PORT2=$(TEST_NGINX_SENTINEL_PORT2) \
TEST_NGINX_SENTINEL_PORT3=$(TEST_NGINX_SENTINEL_PORT3) \
TEST_NGINX_SENTINEL_PORT_AUTH=$(TEST_NGINX_SENTINEL_AUTH) \
TEST_NGINX_SENTINEL_MASTER_NAME=$(TEST_NGINX_SENTINEL_MASTER_NAME) \
TEST_NGINX_REDIS_DATABASE=$(TEST_NGINX_REDIS_DATABASE) \
TEST_NGINX_NO_SHUFFLE=1
# Sentinel configuration can only be set by a config file
define TEST_SENTINEL_CONFIG
sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(TEST_REDIS_PORT) 2
sentinel down-after-milliseconds $(TEST_SENTINEL_MASTER_NAME) 2000
sentinel failover-timeout $(TEST_SENTINEL_MASTER_NAME) 10000
sentinel parallel-syncs $(TEST_SENTINEL_MASTER_NAME) 5
endef
define TEST_SENTINEL_AUTH_CONFIG
sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(TEST_REDIS_PORT_AUTH) 1
endef
export TEST_SENTINEL_CONFIG TEST_SENTINEL_AUTH_CONFIG
SENTINEL_CONFIG_FILE = /tmp/sentinel-test-config
SENTINEL_AUTH_CONFIG_FILE = /tmp/sentinel-auth-test-config
PREFIX ?= /usr/local
LUA_INCLUDE_DIR ?= $(PREFIX)/include
LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION)
PROVE ?= prove -I ../test-nginx/lib
INSTALL ?= install
.PHONY: all install test test_all start_redis_instances stop_redis_instances \
start_redis_instance stop_redis_instance cleanup_redis_instance flush_db \
create_sentinel_config delete_sentinel_config check_ports test_redis \
test_sentinel sleep
all: ;
install: all
$(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis
$(INSTALL) lib/resty/redis/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis
test: test_redis
test_all: start_redis_instances sleep test_redis stop_redis_instances
check:
luacheck lib
sleep:
sleep 3
start_redis_instances: check_ports create_sentinel_config
$(REDIS_CMD) --version
@$(foreach port,$(TEST_REDIS_PORTS), \
[[ "$(port)" != "$(TEST_REDIS_PORT)" ]] && \
SLAVE="$(REDIS_SLAVE_ARG)" || \
SLAVE="" && \
$(MAKE) start_redis_instance args="$$SLAVE" port=$(port) \
prefix=$(REDIS_PREFIX)$(port) && \
) true
$(MAKE) start_redis_instance \
args="--user redisuser on '>redisuserpass' '~*' '&*' '+@all'" \
port=$(TEST_REDIS_PORT_AUTH) \
prefix=$(REDIS_PREFIX)$(TEST_REDIS_PORT_AUTH)
@$(foreach port,$(TEST_SENTINEL_PORTS), \
$(MAKE) start_redis_instance \
port=$(port) args="$(SENTINEL_CONFIG_FILE) --sentinel" \
prefix=$(REDIS_PREFIX)$(port) && \
) true
$(MAKE) start_redis_instance \
args="$(SENTINEL_AUTH_CONFIG_FILE) --sentinel --user sentineluser on '>sentineluserpass' '~*' '&*' '+@all'" \
port=$(TEST_SENTINEL_PORT_AUTH) \
prefix=$(REDIS_PREFIX)$(TEST_SENTINEL_PORT_AUTH)
stop_redis_instances: delete_sentinel_config
-@$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL), \
$(MAKE) stop_redis_instance cleanup_redis_instance port=$(port) \
prefix=$(REDIS_PREFIX)$(port) && \
) true 2>&1 > /dev/null
start_redis_instance:
-@echo "Starting redis on port $(port) with args: \"$(args)\""
-@mkdir -p $(prefix)
$(REDIS_CMD) $(args) \
--pidfile $(prefix)$(REDIS_PID) \
--bind 127.0.0.1 --port $(port) \
--unixsocket $(prefix)$(REDIS_SOCK) \
--unixsocketperm 777 \
--dir $(prefix) \
--logfile $(prefix)$(REDIS_LOG) \
--loglevel debug \
--daemonize yes
stop_redis_instance:
-@echo "Stopping redis on port $(port)"
-@[[ -f "$(prefix)$(REDIS_PID)" ]] && kill -QUIT \
`cat $(prefix)$(REDIS_PID)` 2>&1 > /dev/null || true
cleanup_redis_instance: stop_redis_instance
-@echo "Cleaning up redis files in $(prefix)"
-@rm -rf $(prefix)
flush_db:
-@echo "Flushing Redis DB"
@$(REDIS_CLI) flushdb
create_sentinel_config:
-@echo "Creating $(SENTINEL_CONFIG_FILE)"
@echo "$$TEST_SENTINEL_CONFIG" > $(SENTINEL_CONFIG_FILE)
-@echo "Creating $(SENTINEL_AUTH_CONFIG_FILE)"
@echo "$$TEST_SENTINEL_AUTH_CONFIG" > $(SENTINEL_AUTH_CONFIG_FILE)
delete_sentinel_config:
-@echo "Removing $(SENTINEL_CONFIG_FILE)"
@rm -f $(SENTINEL_CONFIG_FILE)
-@echo "Removing $(SENTINEL_AUTH_CONFIG_FILE)"
@rm -f $(SENTINEL_AUTH_CONFIG_FILE)
check_ports:
-@echo "Checking ports $(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL)"
@$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL),! lsof -i :$(port) &&) true 2>&1 > /dev/null
test_redis: flush_db
util/lua-releng
@rm -f luacov.stats.out
$(TEST_REDIS_VARS) $(PROVE) $(TEST_FILE)
@luacov
@tail -7 luacov.report.out
test_leak: flush_db
$(TEST_REDIS_VARS) TEST_NGINX_CHECK_LEAK=1 $(PROVE) $(TEST_FILE)

View file

@ -0,0 +1,312 @@
# lua-resty-redis-connector
[![Build
Status](https://travis-ci.org/ledgetech/lua-resty-redis-connector.svg?branch=master)](https://travis-ci.org/ledgetech/lua-resty-redis-connector)
Connection utilities for
[lua-resty-redis](https://github.com/openresty/lua-resty-redis), making it easy
and reliable to connect to Redis hosts, either directly or via [Redis
Sentinel](http://redis.io/topics/sentinel).
## Synopsis
Quick and simple authenticated connection on localhost to DB 2:
```lua
local redis, err = require("resty.redis.connector").new({
url = "redis://PASSWORD@127.0.0.1:6379/2",
}):connect()
```
More verbose configuration, with timeouts and a default password:
```lua
local rc = require("resty.redis.connector").new({
connect_timeout = 50,
send_timeout = 5000,
read_timeout = 5000,
keepalive_timeout = 30000,
password = "mypass",
})
local redis, err = rc:connect({
url = "redis://127.0.0.1:6379/2",
})
-- ...
local ok, err = rc:set_keepalive(redis) -- uses keepalive params
```
Keep all config in a table, to easily create / close connections as needed:
```lua
local rc = require("resty.redis.connector").new({
connect_timeout = 50,
send_timeout = 5000,
read_timeout = 5000,
keepalive_timeout = 30000,
host = "127.0.0.1",
port = 6379,
db = 2,
password = "mypass",
})
local redis, err = rc:connect()
-- ...
local ok, err = rc:set_keepalive(redis)
```
[connect](#connect) can be used to override some defaults given in [new](#new),
which are pertinent to this connection only.
```lua
local rc = require("resty.redis.connector").new({
host = "127.0.0.1",
port = 6379,
db = 2,
})
local redis, err = rc:connect({
db = 5,
})
```
## DSN format
If the `params.url` field is present then it will be parsed to set the other
params. Any manually specified params will override values given in the DSN.
*Note: this is a behaviour change as of v0.06. Previously, the DSN values would
take precedence.*
### Direct Redis connections
The format for connecting directly to Redis is:
`redis://USERNAME:PASSWORD@HOST:PORT/DB`
The `USERNAME`, `PASSWORD` and `DB` fields are optional, all other components
are required.
Use of username requires Redis 6.0.0 or newer.
### Connections via Redis Sentinel
When connecting via Redis Sentinel, the format is as follows:
`sentinel://USERNAME:PASSWORD@MASTER_NAME:ROLE/DB`
Again, `USERNAME`, `PASSWORD` and `DB` are optional. `ROLE` must be either `m`
or `s` for master / slave respectively.
On versions of Redis newer than 5.0.1, Sentinels can optionally require their
own password. If enabled, provide this password in the `sentinel_password`
parameter. On Redis 6.2.0 and newer you can pass username using
`sentinel_username` parameter.
A table of `sentinels` must also be supplied. e.g.
```lua
local redis, err = rc:connect{
url = "sentinel://mymaster:a/2",
sentinels = {
{ host = "127.0.0.1", port = 26379 },
},
sentinel_username = "default",
sentinel_password = "password"
}
```
## Proxy Mode
Enable the `connection_is_proxied` parameter if connecting to Redis through a
proxy service (e.g. Twemproxy). These proxies generally only support a limited
sub-set of Redis commands, those which do not require state and do not affect
multiple keys. Databases and transactions are also not supported.
Proxy mode will disable switching to a DB on connect. Unsupported commands
(defaults to those not supported by Twemproxy) will return `nil, err`
immediately rather than being sent to the proxy, which can result in dropped
connections.
`discard` will not be sent when adding connections to the keepalive pool
## Disabled commands
If configured as a table of commands, the command methods will be replaced by a
function which immediately returns `nil, err` without forwarding the command to
the server
## Default Parameters
```lua
{
connect_timeout = 100,
send_timeout = 1000,
read_timeout = 1000,
keepalive_timeout = 60000,
keepalive_poolsize = 30,
-- ssl, ssl_verify, server_name, pool, pool_size, backlog
-- see: https://github.com/openresty/lua-resty-redis#connect
connection_options = {},
host = "127.0.0.1",
port = "6379",
path = "", -- unix socket path, e.g. /tmp/redis.sock
username = "",
password = "",
sentinel_username = "",
sentinel_password = "",
db = 0,
master_name = "mymaster",
role = "master", -- master | slave
sentinels = {},
connection_is_proxied = false,
disabled_commands = {},
}
```
## API
* [new](#new)
* [connect](#connect)
* [set_keepalive](#set_keepalive)
* [Utilities](#utilities)
* [connect_via_sentinel](#connect_via_sentinel)
* [try_hosts](#try_hosts)
* [connect_to_host](#connect_to_host)
* [sentinel.get_master](#sentinelget_master)
* [sentinel.get_slaves](#sentinelget_slaves)
### new
`syntax: rc = redis_connector.new(params)`
Creates the Redis Connector object, overring default params with the ones given.
In case of failures, returns `nil` and a string describing the error.
### connect
`syntax: redis, err = rc:connect(params)`
Attempts to create a connection, according to the [params](#parameters)
supplied, falling back to defaults given in `new` or the predefined defaults. If
a connection cannot be made, returns `nil` and a string describing the reason.
Note that `params` given here do not change the connector's own configuration,
and are only used to alter this particular connection operation. As such, the
following parameters have no meaning when given in `connect`.
* `keepalive_poolsize`
* `keepalive_timeout`
* `connection_is_proxied`
* `disabled_commands`
### set_keepalive
`syntax: ok, err = rc:set_keepalive(redis)`
Attempts to place the given Redis connection on the keepalive pool, according to
timeout and poolsize params given in `new` or the predefined defaults.
This allows an application to release resources without having to keep track of
application wide keepalive settings.
Returns `1` or in the case of error, `nil` and a string describing the error.
## Utilities
The following methods are not typically needed, but may be useful if a custom
interface is required.
### connect_via_sentinel
`syntax: redis, err = rc:connect_via_sentinel(params)`
Returns a Redis connection by first accessing a sentinel as supplied by the
`params.sentinels` table, and querying this with the `params.master_name` and
`params.role`.
### try_hosts
`syntax: redis, err = rc:try_hosts(hosts)`
Tries the hosts supplied in order and returns the first successful connection.
### connect_to_host
`syntax: redis, err = rc:connect_to_host(host)`
Attempts to connect to the supplied `host`.
### sentinel.get_master
`syntax: master, err = sentinel.get_master(sentinel, master_name)`
Given a connected Sentinel instance and a master name, will return the current
master Redis instance.
### sentinel.get_slaves
`syntax: slaves, err = sentinel.get_slaves(sentinel, master_name)`
Given a connected Sentinel instance and a master name, will return a list of
registered slave Redis instances.
# Author
James Hurst <james@pintsized.co.uk>
# Licence
This module is licensed under the 2-clause BSD license.
Copyright (c) James Hurst <james@pintsized.co.uk>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,9 @@
name=lua-resty-redis-connector
abstract=Connection utilities for lua-resty-redis, making it easy and reliable to connect to Redis hosts, either directly or via Redis Sentinel.
author=James Hurst
is_original=yes
license=2bsd
lib_dir=lib
doc_dir=lib
repo_link=https://github.com/ledgetech/lua-resty-redis-connector
main_module=lib/resty/redis/connector.lua

View file

@ -0,0 +1,459 @@
local ipairs, pcall, error, tostring, type, next, setmetatable, getmetatable =
ipairs, pcall, error, tostring, type, next, setmetatable, getmetatable
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_re_match = ngx.re.match
local str_find = string.find
local str_sub = string.sub
local tbl_remove = table.remove
local tbl_sort = table.sort
local ok, tbl_new = pcall(require, "table.new")
if not ok then
tbl_new = function (narr, nrec) return {} end -- luacheck: ignore 212
end
local redis = require("resty.redis")
redis.add_commands("sentinel")
local get_master = require("resty.redis.sentinel").get_master
local get_slaves = require("resty.redis.sentinel").get_slaves
-- A metatable which prevents undefined fields from being created / accessed
local fixed_field_metatable = {
__index =
function(t, k) -- luacheck: ignore 212
error("field " .. tostring(k) .. " does not exist", 3)
end,
__newindex =
function(t, k, v) -- luacheck: ignore 212
error("attempt to create new field " .. tostring(k), 3)
end,
}
-- Returns a new table, recursively copied from the one given, retaining
-- metatable assignment.
--
-- @param table table to be copied
-- @return table
local function tbl_copy(orig)
local orig_type = type(orig)
local copy
if orig_type == "table" then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[tbl_copy(orig_key)] = tbl_copy(orig_value)
end
setmetatable(copy, tbl_copy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
-- Returns a new table, recursively copied from the combination of the given
-- table `t1`, with any missing fields copied from `defaults`.
--
-- If `defaults` is of type "fixed field" and `t1` contains a field name not
-- present in the defults, an error will be thrown.
--
-- @param table t1
-- @param table defaults
-- @return table a new table, recursively copied and merged
local function tbl_copy_merge_defaults(t1, defaults)
if t1 == nil then t1 = {} end
if defaults == nil then defaults = {} end
if type(t1) == "table" and type(defaults) == "table" then
local copy = {}
for t1_key, t1_value in next, t1, nil do
copy[tbl_copy(t1_key)] = tbl_copy_merge_defaults(
t1_value, tbl_copy(defaults[t1_key])
)
end
for defaults_key, defaults_value in next, defaults, nil do
if t1[defaults_key] == nil then
copy[tbl_copy(defaults_key)] = tbl_copy(defaults_value)
end
end
return copy
else
return t1 -- not a table
end
end
local DEFAULTS = setmetatable({
connect_timeout = 100,
read_timeout = 1000,
send_timeout = 1000,
connection_options = {}, -- pool, etc
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = 6379,
path = "", -- /tmp/redis.sock
username = "",
password = "",
sentinel_username = "",
sentinel_password = "",
db = 0,
url = "", -- DSN url
master_name = "mymaster",
role = "master", -- master | slave
sentinels = {},
-- Redis proxies typically don't support full Redis capabilities
connection_is_proxied = false,
disabled_commands = {},
}, fixed_field_metatable)
-- This is the set of commands unsupported by Twemproxy
local default_disabled_commands = {
"migrate", "move", "object", "randomkey", "rename", "renamenx", "scan",
"bitop", "msetnx", "blpop", "brpop", "brpoplpush", "psubscribe", "publish",
"punsubscribe", "subscribe", "unsubscribe", "discard", "exec", "multi",
"unwatch", "watch", "script", "auth", "echo", "select", "bgrewriteaof",
"bgsave", "client", "config", "dbsize", "debug", "flushall", "flushdb",
"info", "lastsave", "monitor", "save", "shutdown", "slaveof", "slowlog",
"sync", "time"
}
local _M = {
_VERSION = '0.11.0',
}
local mt = { __index = _M }
local function parse_dsn(params)
local url = params.url
if url and url ~= "" then
local url_pattern = [[^(?:(redis|sentinel)://)(?:([^@]*)@)?([^:/]+)(?::(\d+|[msa]+))/?(.*)$]]
local m, err = ngx_re_match(url, url_pattern, "oj")
if not m then
return nil, "could not parse DSN: " .. tostring(err)
end
-- TODO: Support a 'protocol' for proxied Redis?
local fields
if m[1] == "redis" then
fields = { "password", "host", "port", "db" }
elseif m[1] == "sentinel" then
fields = { "password", "master_name", "role", "db" }
end
-- username/password may not be present
if #m < 5 then tbl_remove(fields, 1) end
local roles = { m = "master", s = "slave" }
local parsed_params = {}
for i, v in ipairs(fields) do
if v == "db" or v == "port" then
parsed_params[v] = tonumber(m[i + 1])
else
parsed_params[v] = m[i + 1]
end
if v == "role" then
parsed_params[v] = roles[parsed_params[v]]
end
end
local colon_pos = str_find(parsed_params.password or "", ":", 1, true)
if colon_pos then
parsed_params.username = str_sub(parsed_params.password, 1, colon_pos - 1)
parsed_params.password = str_sub(parsed_params.password, colon_pos + 1)
end
return tbl_copy_merge_defaults(params, parsed_params)
end
return params
end
_M.parse_dsn = parse_dsn
-- Fill out gaps in config with any dsn params
local function apply_dsn(config)
if config and config.url then
local err
config, err = parse_dsn(config)
if err then ngx_log(ngx_ERR, err) end
end
return config
end
-- For backwards compatability; previously send_timeout was implicitly the
-- same as read_timeout. So if only the latter is given, ensure the former
-- matches.
local function apply_fallback_send_timeout(config)
if config and not config.send_timeout and config.read_timeout then
config.send_timeout = config.read_timeout
end
end
function _M.new(config)
config = apply_dsn(config)
apply_fallback_send_timeout(config)
local ok, config = pcall(tbl_copy_merge_defaults, config, DEFAULTS)
if not ok then
return nil, config -- err
else
-- In proxied Redis mode disable default commands
if config.connection_is_proxied == true and
not next(config.disabled_commands) then
config.disabled_commands = default_disabled_commands
end
return setmetatable({
config = setmetatable(config, fixed_field_metatable)
}, mt)
end
end
function _M.connect(self, params)
params = apply_dsn(params)
apply_fallback_send_timeout(params)
params = tbl_copy_merge_defaults(params, self.config)
if #params.sentinels > 0 then
return self:connect_via_sentinel(params)
else
return self:connect_to_host(params)
end
end
local function sort_by_localhost(a, b)
if a.host == "127.0.0.1" and b.host ~= "127.0.0.1" then
return true
else
return false
end
end
function _M.connect_via_sentinel(self, params)
local sentinels = params.sentinels
local master_name = params.master_name
local role = params.role
local db = params.db
local username = params.username
local password = params.password
local sentinel_username = params.sentinel_username
local sentinel_password = params.sentinel_password
if sentinel_password then
for _, host in ipairs(sentinels) do
host.username = sentinel_username
host.password = sentinel_password
end
end
local sentnl, err, previous_errors = self:try_hosts(sentinels)
if not sentnl then
return nil, err, previous_errors
end
if role == "master" then
local master, err = get_master(sentnl, master_name)
if not master then
return nil, err
end
sentnl:set_keepalive()
master.db = db
master.username = username
master.password = password
local redis, err = self:connect_to_host(master)
if not redis then
return nil, err
end
return redis
else
-- We want a slave
local slaves, err = get_slaves(sentnl, master_name)
if not slaves then
return nil, err
end
sentnl:set_keepalive()
-- Put any slaves on 127.0.0.1 at the front
tbl_sort(slaves, sort_by_localhost)
if db or password then
for _, slave in ipairs(slaves) do
slave.db = db
slave.username = username
slave.password = password
end
end
local slave, err, previous_errors = self:try_hosts(slaves)
if not slave then
return nil, err, previous_errors
end
return slave
end
end
-- In case of errors, returns "nil, err, previous_errors" where err is
-- the last error received, and previous_errors is a table of the previous errors.
function _M.try_hosts(self, hosts)
local errors = tbl_new(#hosts, 0)
for i, host in ipairs(hosts) do
local r, err = self:connect_to_host(host)
if r and not err then
return r, nil, errors
else
errors[i] = err
end
end
return nil, "no hosts available", errors
end
function _M.connect_to_host(self, host)
local r = redis.new()
-- config options in 'host' should override the global defaults
-- host contains keys that aren't in config
-- this can break tbl_copy_merge_defaults, hence the mannual loop here
local config = tbl_copy(self.config)
for k, _ in pairs(config) do
if host[k] then
config[k] = host[k]
end
end
r:set_timeouts(
config.connect_timeout,
config.send_timeout,
config.read_timeout
)
-- Stub out methods for disabled commands
if next(config.disabled_commands) then
for _, cmd in ipairs(config.disabled_commands) do
r[cmd] = function()
return nil, ("Command "..cmd.." is disabled")
end
end
end
local ok, err
local path = host.path
local opts = config.connection_options
if path and path ~= "" then
if opts then
ok, err = r:connect(path, config.connection_options)
else
ok, err = r:connect(path)
end
else
if opts then
ok, err = r:connect(host.host, host.port, config.connection_options)
else
ok, err = r:connect(host.host, host.port)
end
end
if not ok then
return nil, err
else
local username = host.username
local password = host.password
if password and password ~= "" then
local res
-- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary
if username and username ~= "" and username ~= "default" then
res, err = r:auth(username, password)
else
res, err = r:auth(password)
end
if err then
return res, err
end
end
-- No support for DBs in proxied Redis.
if config.connection_is_proxied ~= true and host.db ~= nil then
local res, err = r:select(host.db)
-- SELECT will fail if we are connected to sentinel:
-- detect it and ignore error message it that's the case
if err and str_find(err, "ERR unknown command") then
local role = r:role()
if role and role[1] == "sentinel" then
err = nil
end
end
if err then
return res, err
end
end
return r, nil
end
end
function _M.set_keepalive(self, redis)
-- Restore connection to "NORMAL" before putting into keepalive pool,
-- ignoring any errors.
-- Proxied Redis does not support transactions.
if self.config.connection_is_proxied ~= true then
redis:discard()
end
local config = self.config
return redis:set_keepalive(
config.keepalive_timeout, config.keepalive_poolsize
)
end
-- Deprecated: use config table in new() or connect() instead.
function _M.set_connect_timeout(self, timeout)
self.config.connect_timeout = timeout
end
-- Deprecated: use config table in new() or connect() instead.
function _M.set_read_timeout(self, timeout)
self.config.read_timeout = timeout
end
-- Deprecated: use config table in new() or connect() instead.
function _M.set_connection_options(self, options)
self.config.connection_options = options
end
return setmetatable(_M, fixed_field_metatable)

View file

@ -0,0 +1,63 @@
local ipairs, type = ipairs, type
local ngx_null = ngx.null
local tbl_insert = table.insert
local ok, tbl_new = pcall(require, "table.new")
if not ok then
tbl_new = function (narr, nrec) return {} end -- luacheck: ignore 212
end
local _M = {
_VERSION = '0.11.0'
}
function _M.get_master(sentinel, master_name)
local res, err = sentinel:sentinel(
"get-master-addr-by-name",
master_name
)
if res and res ~= ngx_null and res[1] and res[2] then
return { host = res[1], port = res[2] }
elseif res == ngx_null then
return nil, "invalid master name"
else
return nil, err
end
end
function _M.get_slaves(sentinel, master_name)
local res, err = sentinel:sentinel("slaves", master_name)
if res and type(res) == "table" then
local hosts = tbl_new(#res, 0)
for _,slave in ipairs(res) do
local num_recs = #slave
local host = tbl_new(0, num_recs + 1)
for i = 1, num_recs, 2 do
host[slave[i]] = slave[i + 1]
end
local master_link_status_ok = host["master-link-status"] == "ok"
local is_down = host["flags"] and (string.find(host["flags"],"s_down")
or string.find(host["flags"],"disconnected"))
if master_link_status_ok and not is_down then
host.host = host.ip -- for parity with other functions
tbl_insert(hosts, host)
end
end
if hosts[1] ~= nil then
return hosts
else
return nil, "no slaves available"
end
else
return nil, err
end
end
return _M

View file

@ -0,0 +1,27 @@
package = "lua-resty-redis-connector"
version = "0.11.0-0"
source = {
url = "git://github.com/ledgetech/lua-resty-redis-connector",
tag = "v0.11.0"
}
description = {
summary = "Connection utilities for lua-resty-redis.",
detailed = [[
Connection utilities for lua-resty-redis, making it easy and
reliable to connect to Redis hosts, either directly or via Redis
Sentinel.
]],
homepage = "https://github.com/ledgetech/lua-resty-redis-connector",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "builtin",
modules = {
["resty.redis.connector"] = "lib/resty/redis/connector.lua",
["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua"
}
}

View file

@ -0,0 +1,229 @@
use Test::Nginx::Socket::Lua;
use Cwd qw(cwd);
repeat_each(2);
plan tests => repeat_each() * blocks() * 2;
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
no_long_string();
run_tests();
__DATA__
=== TEST 1: Default config
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = assert(require("resty.redis.connector").new())
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 2: Defaults via new
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local config = {
connect_timeout = 500,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
}
local rc = require("resty.redis.connector").new(config)
assert(config ~= rc.config, "config should not equal rc.config")
assert(rc.config.connect_timeout == 500, "connect_timeout should be 500")
assert(rc.config.db == 6, "db should be 6")
assert(rc.config.role == "master", "role should be master")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 3: Config via connect still overrides
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
connect_timeout = 500,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(config ~= rc.config, "config should not equal rc.config")
assert(rc.config.connect_timeout == 500, "connect_timeout should be 500")
assert(rc.config.db == 6, "db should be 6")
assert(rc.config.role == "master", "role should be master")
assert(rc.config.keepalive_poolsize == 10,
"keepalive_poolsize should be 10")
local redis, err = rc:connect({
port = $TEST_NGINX_REDIS_PORT,
disabled_commands = { "set" }
})
if not redis or err then
ngx.log(ngx.ERR, "connect failed: ", err)
return
end
local ok, err = redis:set("foo", "bar")
assert( ok == nil and (string.find(err, "disabled") ~= nil) , "Disabled commands not passed through" )
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 4: Unknown config errors, all config does not error
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc, err = require("resty.redis.connector").new({
connect_timeout = 500,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
foo = "bar",
})
assert(rc == nil, "rc should be nil")
assert(string.find(err, "field foo does not exist"),
"err should contain error")
-- Provide all options, without errors
assert(require("resty.redis.connector").new({
connect_timeout = 100,
send_timeout = 500,
read_timeout = 1000,
connection_options = { pool = "<host>::<port>" },
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = $TEST_NGINX_REDIS_PORT,
path = "",
username = "",
password = "",
db = 0,
url = "",
master_name = "mymaster",
role = "master",
sentinels = {},
}), "new should return positively")
-- Provide all options via connect, without errors
local rc = require("resty.redis.connector").new()
assert(rc:connect({
connect_timeout = 100,
send_timeout = 500,
read_timeout = 1000,
connection_options = { pool = "<host>::<port>" },
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = $TEST_NGINX_REDIS_PORT,
path = "",
username = "",
password = "",
db = 0,
url = "",
master_name = "mymaster",
role = "master",
sentinels = {},
}), "rc:connect should return positively")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 5: timeout defaults
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
-- global defaults
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 1000, "send_timeout should be 1000")
assert(rc.config.read_timeout == 1000, "read_timeout should be 1000")
local redis = assert(rc:connect(), "rc:connect should return positively")
assert(redis:set("foo", "bar"))
rc:set_keepalive(redis)
-- send_timeout defaults to read_timeout
rc = require("resty.redis.connector").new({
read_timeout = 500,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 500, "send_timeout should be 500")
assert(rc.config.read_timeout == 500, "read_timeout should be 500")
local redis = assert(rc:connect(), "rc:connect should return positively")
assert(redis:set("foo", "bar"))
rc:set_keepalive(redis)
-- send_timeout can be set separately from read_timeout
rc = require("resty.redis.connector").new({
send_timeout = 500,
read_timeout = 200,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 500, "send_timeout should be 500")
assert(rc.config.read_timeout == 200, "read_timeout should be 200")
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,442 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
$ENV{TEST_NGINX_REDIS_PORT_AUTH} ||= 6393;
$ENV{TEST_NGINX_REDIS_SOCKET} ||= 'unix://tmp/redis/redis.sock';
no_long_string();
run_tests();
__DATA__
=== TEST 1: basic connect
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT
})
local redis, err = assert(rc:connect(params),
"connect should return positively")
assert(redis:set("dog", "an animal"),
"redis:set should return positively")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 2: try_hosts
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors off;
content_by_lua_block {
local rc = require("resty.redis.connector").new({
connect_timeout = 100,
})
local hosts = {
{ host = "127.0.0.1", port = 1 },
{ host = "127.0.0.1", port = 2 },
{ host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT },
}
local redis, err, previous_errors = rc:try_hosts(hosts)
assert(redis and not err,
"try_hosts should return a connection and no error")
assert(string.len(previous_errors[1]) > 0,
"previous_errors[1] should contain an error")
assert(string.len(previous_errors[2]) > 0,
"previous_errors[2] should contain an error")
assert(redis:set("dog", "an animal"),
"redis connection should be working")
redis:close()
local hosts = {
{ host = "127.0.0.1", port = 1 },
{ host = "127.0.0.1", port = 2 },
}
local redis, err, previous_errors = rc:try_hosts(hosts)
assert(not redis and err == "no hosts available",
"no available hosts should return an error")
assert(string.len(previous_errors[1]) > 0,
"previous_errors[1] should contain an error")
assert(string.len(previous_errors[2]) > 0,
"previous_errors[2] should contain an error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 3: connect_to_host
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local host = { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT }
local redis, err = rc:connect_to_host(host)
assert(redis and not err,
"connect_to_host should return positively")
assert(redis:set("dog", "an animal"),
"redis connection should be working")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 4: connect_to_host options ignore defaults
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
db = 2,
})
local redis, err = assert(rc:connect_to_host({
host = "127.0.0.1",
db = 1,
port = $TEST_NGINX_REDIS_PORT
}), "connect_to_host should return positively")
assert(redis:set("dog", "an animal") == "OK",
"set should return 'OK'")
redis:select(2)
assert(redis:get("dog") == ngx.null,
"dog should not exist in db 2")
redis:select(1)
assert(redis:get("dog") == "an animal",
"dog should be 'an animal' in db 1")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 5: Test set_keepalive method
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
})
local redis = assert(rc:connect(),
"rc:connect should return positively")
local ok, err = rc:set_keepalive(redis)
assert(not err, "set_keepalive error should be nil")
local ok, err = redis:set("foo", "bar")
assert(not ok, "ok should be nil")
assert(string.find(err, "closed"), "error should contain 'closed'")
local redis = assert(rc:connect(), "connect should return positively")
assert(redis:subscribe("channel"), "subscribe should return positively")
local ok, err = rc:set_keepalive(redis)
assert(not ok, "ok should be nil")
assert(string.find(err, "subscribed state"),
"error should contain 'subscribed state'")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 6: password
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
password = "foo",
})
local redis, err = rc:connect()
assert(not redis and string.find(err, "ERR") and string.find(err, "AUTH"),
"connect should fail with password error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 7: username and password
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
username = "x",
password = "foo",
})
local redis, err = rc:connect()
assert(not redis and string.find(err, "WRONGPASS"),
"connect should fail with invalid username-password error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 8: Bad unix domain socket path should fail
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local redis, err = require("resty.redis.connector").new({
path = "unix://GARBAGE_PATH_AKFDKAJSFKJSAFLKJSADFLKJSANCKAJSNCKJSANCLKAJS",
}):connect()
assert(not redis and err == "no such file or directory",
"bad domain socket should fail")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 8.1: Good unix domain socket path should succeed
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local redis, err = require("resty.redis.connector").new({
path = "$TEST_NGINX_REDIS_SOCKET",
}):connect()
assert (redis and not err,
"connection should be valid")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 9: parse_dsn
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector")
local user_params = {
url = "redis://foo@127.0.0.1:$TEST_NGINX_REDIS_PORT/4"
}
local params, err = rc.parse_dsn(user_params)
assert(params and not err,
"url should parse without error: " .. tostring(err))
assert(params.host == "127.0.0.1", "host should be localhost")
assert(tonumber(params.port) == $TEST_NGINX_REDIS_PORT,
"port should be $TEST_NGINX_REDIS_PORT")
assert(tonumber(params.db) == 4, "db should be 4")
assert(params.password == "foo", "password should be foo")
local user_params = {
url = "sentinel://foo:bar@foomaster:s/2"
}
local params, err = rc.parse_dsn(user_params)
assert(params and not err,
"url should parse without error: " .. tostring(err))
assert(params.master_name == "foomaster", "master_name should be foomaster")
assert(params.role == "slave", "role should be slave")
assert(tonumber(params.db) == 2, "db should be 2")
assert(params.username == "foo", "username should be foo")
assert(params.password == "bar", "password should be bar")
local params = {
url = "sentinels:/wrongformat",
}
local ok, err = rc.parse_dsn(params)
assert(not ok and err == "could not parse DSN: nil",
"url should fail to parse")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 10: params override dsn components
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector")
local user_params = {
url = "redis://foo@127.0.0.1:$TEST_NGINX_REDIS_PORT/4",
db = 2,
password = "bar",
host = "example.com",
}
local params, err = rc.parse_dsn(user_params)
assert(params and not err,
"url should parse without error: " .. tostring(err))
assert(tonumber(params.db) == 2, "db should be 2")
assert(params.password == "bar", "password should be bar")
assert(params.host == "example.com", "host should be example.com")
assert(tonumber(params.port) == $TEST_NGINX_REDIS_PORT, "port should still be $TEST_NGINX_REDIS_PORT")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 11: Integration test for parse_dsn
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local user_params = {
url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4",
db = 2,
host = "127.0.0.1",
}
local rc, err = require("resty.redis.connector").new(user_params)
assert(rc and not err, "new should return positively")
local redis, err = rc:connect()
assert(redis and not err, "connect should return positively")
assert(redis:set("cat", "dog") and redis:get("cat") == "dog")
local redis, err = rc:connect({
url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4",
db = 2,
host = "127.0.0.1",
})
assert(redis and not err, "connect should return positively")
assert(redis:set("cat", "dog") and redis:get("cat") == "dog")
local rc2, err = require("resty.redis.connector").new()
local redis, err = rc2:connect({
url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4",
db = 2,
host = "127.0.0.1",
})
assert(redis and not err, "connect should return positively")
assert(redis:set("cat", "dog") and redis:get("cat") == "dog")
local redis, err = rc2:connect({
url = "redis://redisuser:redisuserpass@127.0.0.1:$TEST_NGINX_REDIS_PORT_AUTH/"
})
assert(redis and not err, "connect should return positively")
local username = assert(redis:acl("whoami"))
assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username))
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 12: DSN without DB
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local user_params = {
url = "redis://foo.example:$TEST_NGINX_REDIS_PORT",
host = "127.0.0.1",
}
local rc, err = require("resty.redis.connector").new(user_params)
assert(rc and not err, "new should return positively")
local redis, err = rc:connect()
assert(redis and not err, "connect should return positively")
assert(redis:set("cat", "dog") and redis:get("cat") == "dog")
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,156 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
no_long_string();
run_tests();
__DATA__
=== TEST 1: Proxy mode disables commands
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
connection_is_proxied = true
})
local redis, err = assert(rc:connect(params),
"connect should return positively")
assert(redis:set("dog", "an animal"),
"redis:set should return positively")
local ok, err = redis:multi()
assert(ok == nil, "redis:multi should return nil")
assert(err == "Command multi is disabled")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 2: Proxy mode disables custom commands
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
connection_is_proxied = true,
disabled_commands = { "foobar", "hget"}
})
local redis, err = assert(rc:connect(params),
"connect should return positively")
assert(redis:set("dog", "an animal"),
"redis:set should return positively")
assert(redis:multi(),
"redis:multi should return positively")
local ok, err = redis:hget()
assert(ok == nil, "redis:hget should return nil")
assert(err == "Command hget is disabled")
local ok, err = redis:foobar()
assert(ok == nil, "redis:foobar should return nil")
assert(err == "Command foobar is disabled")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 3: Proxy mode does not switch DB
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local redis = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
db = 2
}):connect()
local proxy = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
connection_is_proxied = true,
db = 2
}):connect()
assert(redis:set("proxy", "test"),
"redis:set should return positively")
assert(proxy:get("proxy") == ngx.null,
"proxy key should not exist in proxy")
redis:seelct(2)
assert(redis:get("proxy") == "test",
"proxy key should be 'test' in db 1")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 4: Commands are disabled without proxy mode
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
disabled_commands = { "foobar", "hget"}
})
local redis, err = assert(rc:connect(params),
"connect should return positively")
assert(redis:set("dog", "an animal"),
"redis:set should return positively")
assert(redis:multi(),
"redis:multi should return positively")
local ok, err = redis:hget()
assert(ok == nil, "redis:hget should return nil")
assert(err == "Command hget is disabled")
local ok, err = redis:foobar()
assert(ok == nil, "redis:foobar should return nil")
assert(err == "Command foobar is disabled")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,279 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
$ENV{TEST_NGINX_REDIS_PORT_SL1} ||= 6381;
$ENV{TEST_NGINX_REDIS_PORT_SL2} ||= 6382;
$ENV{TEST_NGINX_SENTINEL_PORT1} ||= 6390;
$ENV{TEST_NGINX_SENTINEL_PORT2} ||= 6391;
$ENV{TEST_NGINX_SENTINEL_PORT3} ||= 6392;
$ENV{TEST_NGINX_SENTINEL_PORT_AUTH} ||= 6393;
no_long_string();
run_tests();
__DATA__
=== TEST 1: Get the master
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local rs = require("resty.redis.sentinel")
local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1" }
assert(sentinel and not err, "sentinel should connect without errors but got " .. tostring(err))
local master, err = rs.get_master(sentinel, "mymaster")
assert(master and not err, "get_master should return the master")
assert(master.host == "127.0.0.1" and tonumber(master.port) == $TEST_NGINX_REDIS_PORT,
"host should be 127.0.0.1 and port should be $TEST_NGINX_REDIS_PORT")
master, err = rs.get_master(sentinel, "invalid-mymaster")
assert(not master and err, "invalid master name should result in error")
sentinel:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 1b: Get the master directly
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local master, err = rc:connect({
url = "sentinel://mymaster:m/3",
sentinels = {
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 }
}
})
assert(master and not err, "get_master should return the master")
assert(master:set("foo", "bar"), "set should run without error")
assert(master:get("foo") == "bar", "get(foo) should return bar")
master:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 2: Get slaves
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local rs = require("resty.redis.sentinel")
local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1" }
assert(sentinel and not err, "sentinel should connect without error")
local slaves, err = rs.get_slaves(sentinel, "mymaster")
assert(slaves and not err, "slaves should be returned without error")
local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"slaves should both be found")
slaves, err = rs.get_slaves(sentinel, "invalid-mymaster")
assert(not slaves and err, "invalid master name should result in error")
sentinel:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 3: Get only healthy slaves
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local sentinel, err = rc:connect({ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1" })
assert(sentinel and not err, "sentinel should connect without error")
local slaves, err = require("resty.redis.sentinel").get_slaves(
sentinel,
"mymaster"
)
assert(slaves and not err, "slaves should be returned without error")
local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"slaves should both be found")
-- connect to one and remove it
local r = require("resty.redis.connector").new():connect({
port = $TEST_NGINX_REDIS_PORT_SL1,
})
r:slaveof("127.0.0.1", 7000)
ngx.sleep(9)
local slaves, err = require("resty.redis.sentinel").get_slaves(
sentinel,
"mymaster"
)
assert(slaves and not err, "slaves should be returned without error")
local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == false and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"only $TEST_NGINX_REDIS_PORT_SL2 should be found")
r:slaveof("127.0.0.1", $TEST_NGINX_REDIS_PORT)
sentinel:close()
}
}
--- request
GET /t
--- timeout: 10
--- no_error_log
[error]
=== TEST 4: connector.connect_via_sentinel
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local params = {
sentinels = {
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 },
},
master_name = "mymaster",
role = "master",
}
local redis, err = rc:connect_via_sentinel(params)
assert(redis and not err, "redis should connect without error")
params.role = "slave"
local redis, err = rc:connect_via_sentinel(params)
assert(redis and not err, "redis should connect without error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 5: regression for slave sorting (iss12)
--- http_config eval: $::HttpConfig
--- config
location /t {
lua_socket_log_errors Off;
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local params = {
sentinels = {
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 },
},
master_name = "mymaster",
role = "slave",
}
-- hotwire get_slaves to expose sorting issue
local sentinel = require("resty.redis.sentinel")
sentinel.get_slaves = function()
return {
{ host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT_SL1 },
{ host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT_SL2 },
{ host = "134.123.51.2", port = $TEST_NGINX_REDIS_PORT_SL1 },
}
end
local redis, err = rc:connect_via_sentinel(params)
assert(redis and not err, "redis should connect without error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 6: connect with acl
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local redis, err = rc:connect({
username = "redisuser",
password = "redisuserpass",
sentinels = {
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT_AUTH }
},
master_name = "mymaster",
sentinel_username = "sentineluser",
sentinel_username = "sentineluserpass",
})
assert(redis and not err, "redis should connect without error")
local username = assert(redis:acl("whoami"))
assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username))
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,63 @@
#!/usr/bin/env perl
use strict;
use warnings;
sub file_contains ($$);
my $version;
for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) {
# Check the sanity of each .lua file
open my $in, $file or
die "ERROR: Can't open $file for reading: $!\n";
my $found_ver;
while (<$in>) {
my ($ver, $skipping);
if (/(?x) (?:_VERSION) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) {
my $orig_ver = $ver = $1;
$found_ver = 1;
# $skipping = $2;
$ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e;
warn "$file: $orig_ver ($ver)\n";
} elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) {
warn "$file: $1\n";
$found_ver = 1;
last;
}
if ($ver and $version and !$skipping) {
if ($version ne $ver) {
# die "$file: $ver != $version\n";
}
} elsif ($ver and !$version) {
$version = $ver;
}
}
if (!$found_ver) {
warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n";
}
close $in;
#print "Checking use of Lua global variables in file $file ...\n";
system("luac -p -l $file | grep ETGLOBAL | grep -vE '(require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|select|rawset|rawget|debug)\$'");
#file_contains($file, "attempt to write to undeclared variable");
system("grep -H -n -E --color '.{120}' $file");
}
sub file_contains ($$) {
my ($file, $regex) = @_;
open my $in, $file
or die "Cannot open $file fo reading: $!\n";
my $content = do { local $/; <$in> };
close $in;
#print "$content";
return scalar ($content =~ /$regex/);
}
if (-d 't') {
for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) {
system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file});
}
}

View file

@ -0,0 +1,27 @@
FROM python:3.12.1-alpine3.18@sha256:af0d8da43677e3000ebdf4045508d891a87e7bd2d3ec87bc6e40403be97291b8
# Install firefox and geckodriver
RUN apk add --no-cache --virtual .build-deps curl grep zip wget && \
apk add --no-cache firefox
# Installing geckodriver for firefox...
RUN GECKODRIVER_VERSION=`curl -i https://github.com/mozilla/geckodriver/releases/latest | grep -Po 'v[0-9]+\.[0-9]+\.[0-9]+'` && \
wget -O geckodriver.tar.gz -w 5 https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz && \
tar -C /usr/local/bin -xzvf geckodriver.tar.gz && \
chmod +x /usr/local/bin/geckodriver && \
rm geckodriver.tar.gz
WORKDIR /tmp
COPY requirements.txt .
RUN MAKEFLAGS="-j $(nproc)" pip install --no-cache-dir --require-hashes --no-deps -r requirements.txt && \
rm -f requirements.txt
WORKDIR /opt/tests
COPY main.py .
EXPOSE 8080
ENTRYPOINT [ "python3", "main.py" ]

View file

@ -0,0 +1,9 @@
FROM redis:7-alpine@sha256:2d148c557c85309c7cf1bbf15ebc21d5fc370ab1cb913a6c19b74bd29d10801c
RUN apk add --no-cache bash openssl
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT [ "./entrypoint.sh" ]

View file

@ -0,0 +1,23 @@
version: "3.5"
services:
tests:
build: .
environment:
PYTHONUNBUFFERED: "1"
USE_REVERSE_SCAN: "no"
USE_ANTIBOT: "no"
REDIS_SENTINEL_HOSTS: "bw-sentinel-1 bw-sentinel-2 bw-sentinel-3"
REDIS_SENTINEL_MASTER: "mymasterset"
REDIS_DATABASE: "0"
REDIS_SSL: "no"
extra_hosts:
- "www.example.com:1.0.0.2"
networks:
bw-services:
ipv4_address: 1.0.0.3
networks:
bw-services:
external: true

View file

@ -0,0 +1,135 @@
version: "3.5"
services:
bw:
image: bunkerity/bunkerweb:1.5.4
pull_policy: never
depends_on:
- bw-redis
labels:
- "bunkerweb.INSTANCE=yes"
volumes:
- ./index.html:/var/www/html/index.html
environment:
API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24 1.0.0.3"
HTTP_PORT: "80"
USE_BUNKERNET: "no"
SEND_ANONYMOUS_REPORT: "no"
BLACKLIST_IP_URLS: ""
LOG_LEVEL: "info"
SESSIONS_NAME: "test"
USE_REVERSE_SCAN: "no"
USE_ANTIBOT: "no"
USE_GREYLIST: "yes"
GREYLIST_IP: "0.0.0.0/0"
WHITELIST_COUNTRY: "AU"
# ? REDIS settings
USE_REDIS: "yes"
REDIS_SENTINEL_HOSTS: "bw-sentinel-1 bw-sentinel-2 bw-sentinel-3"
REDIS_SENTINEL_MASTER: "mymasterset"
REDIS_DATABASE: "0"
REDIS_SSL: "no"
CUSTOM_CONF_SERVER_HTTP_ready: |
location /ready {
default_type 'text/plain';
rewrite_by_lua_block {
ngx.print('ready')
ngx.flush(true)
ngx.exit(ngx.HTTP_OK)
}
}
networks:
bw-universe:
bw-services:
ipv4_address: 1.0.0.2
bw-scheduler:
image: bunkerity/bunkerweb-scheduler:1.5.4
pull_policy: never
depends_on:
- bw
- bw-docker
environment:
DOCKER_HOST: "tcp://bw-docker:2375"
LOG_LEVEL: "info"
networks:
- bw-universe
- bw-docker
bw-docker:
image: tecnativa/docker-socket-proxy:nightly
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: "1"
networks:
- bw-docker
bw-redis-master:
image: 'bitnami/redis:latest'
environment:
- REDIS_REPLICATION_MODE=master
- ALLOW_EMPTY_PASSWORD=yes
networks:
- bw-services
bw-redis-slave:
image: 'bitnami/redis:latest'
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=bw-redis-master
- ALLOW_EMPTY_PASSWORD=yes
depends_on:
- bw-redis-master
networks:
- bw-services
bw-sentinel-1:
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_MASTER_HOST=bw-redis-master
- REDIS_MASTER_SET=mymasterset
depends_on:
- bw-redis-master
- bw-redis-slave
networks:
- bw-services
bw-sentinel-2:
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_MASTER_HOST=bw-redis-master
- REDIS_MASTER_SET=mymasterset
depends_on:
- bw-redis-master
- bw-redis-slave
networks:
- bw-services
bw-sentinel-3:
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_MASTER_HOST=bw-redis-master
- REDIS_MASTER_SET=mymasterset
depends_on:
- bw-redis-master
- bw-redis-slave
networks:
- bw-services
networks:
bw-universe:
name: bw-universe
ipam:
driver: default
config:
- subnet: 10.20.30.0/24
bw-services:
name: bw-services
ipam:
driver: default
config:
- subnet: 1.0.0.0/24
bw-docker:
name: bw-docker

View file

@ -0,0 +1,31 @@
#!/bin/bash
set -e
command="redis-server"
if [ "$REDIS_SSL" = "yes" ]; then
mkdir /tls
openssl genrsa -out /tls/ca.key 4096
openssl req \
-x509 -new -nodes -sha256 \
-key /tls/ca.key \
-days 365 \
-subj /CN=bw-redis/ \
-out /tls/ca.crt
openssl req \
-x509 -nodes -newkey rsa:4096 \
-keyout /tls/redis.key \
-out /tls/redis.pem \
-days 365 \
-subj /CN=bw-redis/
chmod -R 640 /tls
command+=" --tls-port ${REDIS_PORT:-6379} --port 0 --tls-cert-file /tls/redis.pem --tls-key-file /tls/redis.key --tls-ca-cert-file /tls/ca.crt --tls-auth-clients no"
else
command+=" --port ${REDIS_PORT:-6379}"
fi
$command

View file

View file

@ -0,0 +1,397 @@
from fastapi import FastAPI
from multiprocessing import Process
from os import getenv
from redis import Redis
from requests import get
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from time import sleep
from traceback import format_exc
from contextlib import suppress
from requests.exceptions import RequestException
from uvicorn import run
fastapi_proc = None
ip_to_check = "1.0.0.3" if getenv("TEST_TYPE", "docker") == "docker" else "127.0.0.1"
try:
ready = False
retries = 0
while not ready:
with suppress(RequestException):
resp = get("http://www.example.com/ready", headers={"Host": "www.example.com"})
status_code = resp.status_code
text = resp.text
if status_code >= 500:
print("❌ An error occurred with the server, exiting ...", flush=True)
exit(1)
ready = status_code < 400 or status_code == 403 and text == "ready"
if retries > 10:
print("❌ The service took too long to be ready, exiting ...", flush=True)
exit(1)
elif not ready:
retries += 1
print("⚠️ Waiting for the service to be ready, retrying in 5s ...", flush=True)
sleep(5)
redis_host = getenv("REDIS_HOST", "127.0.0.1")
if not redis_host:
print("❌ Redis host is not set, exiting ...", flush=True)
exit(1)
redis_port = getenv("REDIS_PORT", "6379")
if not redis_port.isdigit():
print("❌ Redis port doesn't seem to be a number, exiting ...", flush=True)
exit(1)
redis_port = int(redis_port)
redis_db = getenv("REDIS_DATABASE", "0")
if not redis_db.isdigit():
print("❌ Redis database doesn't seem to be a number, exiting ...", flush=True)
exit(1)
redis_db = int(redis_db)
redis_ssl = getenv("REDIS_SSL", "no") == "yes"
print(
f" Trying to connect to Redis with the following parameters:\nhost: {redis_host}\nport: {redis_port}\ndb: {redis_db}\nssl: {redis_ssl}",
flush=True,
)
redis_client = Redis(
host=redis_host,
port=redis_port,
db=redis_db,
ssl=redis_ssl,
socket_timeout=1,
ssl_cert_reqs=None,
)
if not redis_client.ping():
print("❌ Redis is not reachable, exiting ...", flush=True)
exit(1)
use_reverse_scan = getenv("USE_REVERSE_SCAN", "no") == "yes"
if use_reverse_scan:
if ip_to_check == "1.0.0.3":
print(" Testing Reverse Scan, starting FastAPI ...", flush=True)
app = FastAPI()
fastapi_proc = Process(target=run, args=(app,), kwargs=dict(host="0.0.0.0", port=8080))
fastapi_proc.start()
sleep(2)
print(
" FastAPI started, sending a request to http://www.example.com ...",
flush=True,
)
response = get(
"http://www.example.com",
headers={"Host": "www.example.com"},
)
if response.status_code != 403:
response.raise_for_status()
print("❌ The request was not blocked, exiting ...", flush=True)
exit(1)
sleep(0.5)
print(" The request was blocked, checking Redis ...", flush=True)
port_to_check = "8080" if ip_to_check == "1.0.0.3" else "80"
key_value = redis_client.get(f"plugin_reverse_scan_{ip_to_check}:{port_to_check}")
if key_value is None:
print(
f'❌ The Reverse Scan key ("plugin_reverse_scan_{ip_to_check}:{port_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
elif key_value != b"open":
print(
f'❌ The Reverse Scan key ("plugin_reverse_scan_{ip_to_check}:{port_to_check}") was found, but the value is not "open" ({key_value.decode()}), exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The Reverse Scan key was found, the value is {key_value.decode()}",
flush=True,
)
exit(0)
use_antibot = getenv("USE_ANTIBOT", "no") != "no"
if use_antibot:
print(" Testing Antibot ...", flush=True)
firefox_options = Options()
firefox_options.add_argument("--headless")
print(" Starting Firefox ...", flush=True)
with webdriver.Firefox(options=firefox_options) as driver:
driver.delete_all_cookies()
driver.maximize_window()
print(" Navigating to http://www.example.com ...", flush=True)
driver.get("http://www.example.com")
sleep(0.5)
print(" Checking Redis ...", flush=True)
keys = redis_client.keys("sessions_:test:*")
if not keys:
print(
f"❌ No Antibot keys were found, exiting ...\nkeys: {redis_client.keys()}",
flush=True,
)
exit(1)
key_value = redis_client.get(keys[0])
if key_value is None:
print(
f"❌ The Antibot key ({keys[0].decode()}) was not found, exiting ...\nkeys: {redis_client.keys()}",
flush=True,
)
exit(1)
print(
f"✅ The Antibot key was found, the value is {key_value.decode()}",
flush=True,
)
exit(0)
print(
" Sending a request to http://www.example.com/?id=/etc/passwd ...",
flush=True,
)
response = get(
"http://www.example.com/?id=/etc/passwd",
headers={"Host": "www.example.com"},
)
if response.status_code != 403:
response.raise_for_status()
print("❌ The request was not blocked, exiting ...", flush=True)
exit(1)
sleep(0.5)
print(" The request was blocked, checking Redis ...", flush=True)
key_value = redis_client.get(f"plugin_bad_behavior_{ip_to_check}")
if key_value is None:
print(
f'❌ The Bad Behavior key ("plugin_bad_behavior_{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The Bad Behavior key was found, the value is {key_value.decode()}",
flush=True,
)
print(
" Sending another request to http://www.example.com/?id=/etc/passwd ...",
flush=True,
)
response = get(
"http://www.example.com/?id=/etc/passwd",
headers={"Host": "www.example.com"},
)
if response.status_code != 403:
response.raise_for_status()
print("❌ The request was not blocked, exiting ...", flush=True)
exit(1)
sleep(0.5)
second_key_value = redis_client.get(f"plugin_bad_behavior_{ip_to_check}")
if second_key_value <= key_value:
print(
f'❌ The Bad Behavior key ("plugin_bad_behavior_{ip_to_check}") was not incremented, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The Bad Behavior key was incremented, the value is {second_key_value.decode()}",
flush=True,
)
print(
" Sending requests to http://www.example.com until we reach the limit ...",
flush=True,
)
status_code = 0
while status_code != 429:
response = get(
"http://www.example.com",
headers={"Host": "www.example.com"},
)
if response.status_code not in (200, 429):
response.raise_for_status()
status_code = response.status_code
sleep(0.5)
key_value = redis_client.get(f"plugin_limit_www.example.com{ip_to_check}/")
if key_value is None:
print(
f'❌ The limit key ("plugin_limit_www.example.com{ip_to_check}/") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The limit key was found, the value is {key_value.decode()}",
flush=True,
)
print(
" Checking if the country key was created and has the correct value ...",
flush=True,
)
key_value = redis_client.get(f"plugin_country_www.example.com{ip_to_check}")
if key_value is None:
print(
f'❌ The country key ("plugin_country_www.example.com{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The country key was found, the value is {key_value.decode()}",
flush=True,
)
print(
" Checking if the whitelist key was created and has the correct value ...",
flush=True,
)
key_value = redis_client.get(f"plugin_whitelist_www.example.comip{ip_to_check}")
if key_value is None:
print(
f'❌ The whitelist key ("plugin_whitelist_www.example.comip{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
if key_value != b"ok":
print(
f'❌ The whitelist key ("plugin_whitelist_www.example.comip{ip_to_check}") was found, but the value is not "ok" ({key_value.decode()}), exiting ...\nkeys: {redis_client.keys()}',
)
print(
f"✅ The whitelist key was found, the value is {key_value.decode()}",
flush=True,
)
print(
" Checking if the blacklist key was created and has the correct value ...",
flush=True,
)
key_value = redis_client.get(f"plugin_blacklist_www.example.comip{ip_to_check}")
if key_value is None:
print(
f'❌ The blacklist key ("plugin_blacklist_www.example.comip{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
if key_value != b"ok":
print(
f'❌ The blacklist key ("plugin_blacklist_www.example.comip{ip_to_check}") was found, but the value is not "ok" ({key_value.decode()}), exiting ...\nkeys: {redis_client.keys()}',
)
print(
f"✅ The blacklist key was found, the value is {key_value.decode()}",
flush=True,
)
print(
" Checking if the greylist key was created and has the correct value ...",
flush=True,
)
key_value = redis_client.get(f"plugin_greylist_www.example.comip{ip_to_check}")
if key_value is None:
print(
f'❌ The greylist key ("plugin_greylist_www.example.comip{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
if key_value != b"ip":
print(
f'❌ The greylist key ("plugin_greylist_www.example.comip{ip_to_check}") was found, but the value is not "ip" ({key_value.decode()}), exiting ...\nkeys: {redis_client.keys()}',
)
print(
f"✅ The greylist key was found, the value is {key_value.decode()}",
flush=True,
)
if ip_to_check == "1.0.0.3":
print(
" Checking if the dnsbl keys were created ...",
flush=True,
)
key_value = redis_client.get(f"plugin_dnsbl_www.example.com{ip_to_check}")
if key_value is None:
print(
f'❌ The dnsbl key ("plugin_dnsbl_www.example.com{ip_to_check}") was not found, exiting ...\nkeys: {redis_client.keys()}',
flush=True,
)
exit(1)
print(
f"✅ The dnsbl key was found, the value is {key_value.decode()}",
flush=True,
)
except SystemExit as e:
exit(e.code)
except:
print(f"❌ Something went wrong, exiting ...\n{format_exc()}", flush=True)
exit(1)
finally:
if fastapi_proc:
fastapi_proc.terminate()

View file

@ -0,0 +1,8 @@
location /ready {
default_type 'text/plain';
rewrite_by_lua_block {
ngx.print('ready')
ngx.flush(true)
ngx.exit(ngx.HTTP_OK)
}
}

View file

@ -0,0 +1,5 @@
fastapi==0.108.0
redis==5.0.1
requests==2.31.0
selenium==4.16.0
uvicorn[standard]==0.25.0

View file

@ -0,0 +1,612 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements.in
#
annotated-types==0.6.0 \
--hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \
--hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d
# via pydantic
anyio==4.2.0 \
--hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \
--hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f
# via
# starlette
# watchfiles
async-timeout==4.0.3 \
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
# via redis
attrs==23.1.0 \
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
# via
# outcome
# trio
certifi==2023.11.17 \
--hash=sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1 \
--hash=sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474
# via
# requests
# selenium
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via requests
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via uvicorn
exceptiongroup==1.2.0 \
--hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
--hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
# via
# anyio
# trio
# trio-websocket
fastapi==0.108.0 \
--hash=sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296 \
--hash=sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7
# via -r requirements.in
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
# via
# uvicorn
# wsproto
httptools==0.6.1 \
--hash=sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563 \
--hash=sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142 \
--hash=sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d \
--hash=sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b \
--hash=sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4 \
--hash=sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb \
--hash=sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658 \
--hash=sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084 \
--hash=sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2 \
--hash=sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97 \
--hash=sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837 \
--hash=sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3 \
--hash=sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58 \
--hash=sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da \
--hash=sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d \
--hash=sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90 \
--hash=sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0 \
--hash=sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1 \
--hash=sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2 \
--hash=sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e \
--hash=sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0 \
--hash=sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf \
--hash=sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc \
--hash=sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3 \
--hash=sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503 \
--hash=sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a \
--hash=sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3 \
--hash=sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949 \
--hash=sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84 \
--hash=sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb \
--hash=sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a \
--hash=sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f \
--hash=sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e \
--hash=sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81 \
--hash=sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185 \
--hash=sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3
# via uvicorn
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
# via
# anyio
# requests
# trio
outcome==1.3.0.post0 \
--hash=sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8 \
--hash=sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b
# via trio
pydantic==2.5.3 \
--hash=sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a \
--hash=sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4
# via fastapi
pydantic-core==2.14.6 \
--hash=sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556 \
--hash=sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e \
--hash=sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411 \
--hash=sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245 \
--hash=sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c \
--hash=sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66 \
--hash=sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd \
--hash=sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d \
--hash=sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b \
--hash=sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06 \
--hash=sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948 \
--hash=sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341 \
--hash=sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0 \
--hash=sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f \
--hash=sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a \
--hash=sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2 \
--hash=sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51 \
--hash=sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80 \
--hash=sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8 \
--hash=sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d \
--hash=sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8 \
--hash=sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb \
--hash=sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590 \
--hash=sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87 \
--hash=sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534 \
--hash=sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b \
--hash=sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145 \
--hash=sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba \
--hash=sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b \
--hash=sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2 \
--hash=sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e \
--hash=sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052 \
--hash=sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622 \
--hash=sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab \
--hash=sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b \
--hash=sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66 \
--hash=sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e \
--hash=sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4 \
--hash=sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e \
--hash=sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec \
--hash=sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c \
--hash=sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed \
--hash=sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937 \
--hash=sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f \
--hash=sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9 \
--hash=sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4 \
--hash=sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96 \
--hash=sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277 \
--hash=sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23 \
--hash=sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7 \
--hash=sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b \
--hash=sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91 \
--hash=sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d \
--hash=sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e \
--hash=sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1 \
--hash=sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2 \
--hash=sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160 \
--hash=sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9 \
--hash=sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670 \
--hash=sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7 \
--hash=sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c \
--hash=sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb \
--hash=sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42 \
--hash=sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d \
--hash=sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8 \
--hash=sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1 \
--hash=sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6 \
--hash=sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8 \
--hash=sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf \
--hash=sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e \
--hash=sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a \
--hash=sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9 \
--hash=sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1 \
--hash=sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40 \
--hash=sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2 \
--hash=sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d \
--hash=sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f \
--hash=sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f \
--hash=sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af \
--hash=sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7 \
--hash=sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda \
--hash=sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a \
--hash=sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95 \
--hash=sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0 \
--hash=sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60 \
--hash=sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149 \
--hash=sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975 \
--hash=sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4 \
--hash=sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe \
--hash=sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94 \
--hash=sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03 \
--hash=sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c \
--hash=sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b \
--hash=sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a \
--hash=sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24 \
--hash=sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391 \
--hash=sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c \
--hash=sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab \
--hash=sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd \
--hash=sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786 \
--hash=sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08 \
--hash=sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8 \
--hash=sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6 \
--hash=sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0 \
--hash=sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421
# via pydantic
pysocks==1.7.1 \
--hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via urllib3
python-dotenv==1.0.0 \
--hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \
--hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a
# via uvicorn
pyyaml==6.0.1 \
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
--hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
--hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
--hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
--hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
--hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
--hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
--hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
--hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via uvicorn
redis==5.0.1 \
--hash=sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f \
--hash=sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f
# via -r requirements.in
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
# via -r requirements.in
selenium==4.16.0 \
--hash=sha256:aec71f4e6ed6cb3ec25c9c1b5ed56ae31b6da0a7f17474c7566d303f84e6219f \
--hash=sha256:b2e987a445306151f7be0e6dfe2aa72a479c2ac6a91b9d5ef2d6dd4e49ad0435
# via -r requirements.in
sniffio==1.3.0 \
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
# via
# anyio
# trio
sortedcontainers==2.4.0 \
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
# via trio
starlette==0.32.0.post1 \
--hash=sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09 \
--hash=sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02
# via fastapi
trio==0.23.2 \
--hash=sha256:5a0b566fa5d50cf231cfd6b08f3b03aa4179ff004b8f3144059587039e2b26d3 \
--hash=sha256:da1d35b9a2b17eb32cae2e763b16551f9aa6703634735024e32f325c9285069e
# via
# selenium
# trio-websocket
trio-websocket==0.11.1 \
--hash=sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f \
--hash=sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638
# via selenium
typing-extensions==4.9.0 \
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
# via
# anyio
# fastapi
# pydantic
# pydantic-core
# starlette
# uvicorn
urllib3==2.1.0 \
--hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \
--hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54
# via
# requests
# selenium
uvicorn==0.25.0 \
--hash=sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2 \
--hash=sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c
# via
# -r requirements.in
# uvicorn
uvloop==0.19.0 \
--hash=sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd \
--hash=sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec \
--hash=sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b \
--hash=sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc \
--hash=sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797 \
--hash=sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5 \
--hash=sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2 \
--hash=sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d \
--hash=sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be \
--hash=sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd \
--hash=sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12 \
--hash=sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17 \
--hash=sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef \
--hash=sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24 \
--hash=sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428 \
--hash=sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1 \
--hash=sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849 \
--hash=sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593 \
--hash=sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd \
--hash=sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67 \
--hash=sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6 \
--hash=sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3 \
--hash=sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd \
--hash=sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8 \
--hash=sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7 \
--hash=sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533 \
--hash=sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957 \
--hash=sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650 \
--hash=sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e \
--hash=sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7 \
--hash=sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256
# via uvicorn
watchfiles==0.21.0 \
--hash=sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc \
--hash=sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365 \
--hash=sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0 \
--hash=sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e \
--hash=sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124 \
--hash=sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c \
--hash=sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317 \
--hash=sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094 \
--hash=sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7 \
--hash=sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235 \
--hash=sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c \
--hash=sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c \
--hash=sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c \
--hash=sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235 \
--hash=sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293 \
--hash=sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa \
--hash=sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef \
--hash=sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19 \
--hash=sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8 \
--hash=sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d \
--hash=sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915 \
--hash=sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429 \
--hash=sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097 \
--hash=sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe \
--hash=sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0 \
--hash=sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d \
--hash=sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99 \
--hash=sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1 \
--hash=sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a \
--hash=sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895 \
--hash=sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94 \
--hash=sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562 \
--hash=sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab \
--hash=sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360 \
--hash=sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1 \
--hash=sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7 \
--hash=sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f \
--hash=sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03 \
--hash=sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01 \
--hash=sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58 \
--hash=sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052 \
--hash=sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e \
--hash=sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765 \
--hash=sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6 \
--hash=sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137 \
--hash=sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85 \
--hash=sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca \
--hash=sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f \
--hash=sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214 \
--hash=sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7 \
--hash=sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7 \
--hash=sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3 \
--hash=sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b \
--hash=sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7 \
--hash=sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6 \
--hash=sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994 \
--hash=sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9 \
--hash=sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec \
--hash=sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128 \
--hash=sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c \
--hash=sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2 \
--hash=sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078 \
--hash=sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3 \
--hash=sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e \
--hash=sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a \
--hash=sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6 \
--hash=sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49 \
--hash=sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b \
--hash=sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28 \
--hash=sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9 \
--hash=sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586 \
--hash=sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400 \
--hash=sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165 \
--hash=sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303 \
--hash=sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d
# via uvicorn
websockets==12.0 \
--hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \
--hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \
--hash=sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df \
--hash=sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b \
--hash=sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205 \
--hash=sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892 \
--hash=sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53 \
--hash=sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2 \
--hash=sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed \
--hash=sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c \
--hash=sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd \
--hash=sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b \
--hash=sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931 \
--hash=sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30 \
--hash=sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370 \
--hash=sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be \
--hash=sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec \
--hash=sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf \
--hash=sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62 \
--hash=sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b \
--hash=sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402 \
--hash=sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f \
--hash=sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123 \
--hash=sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9 \
--hash=sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603 \
--hash=sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45 \
--hash=sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558 \
--hash=sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4 \
--hash=sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438 \
--hash=sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137 \
--hash=sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480 \
--hash=sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447 \
--hash=sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8 \
--hash=sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04 \
--hash=sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c \
--hash=sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb \
--hash=sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967 \
--hash=sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b \
--hash=sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d \
--hash=sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def \
--hash=sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c \
--hash=sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92 \
--hash=sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2 \
--hash=sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113 \
--hash=sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b \
--hash=sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28 \
--hash=sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7 \
--hash=sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d \
--hash=sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f \
--hash=sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468 \
--hash=sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8 \
--hash=sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae \
--hash=sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611 \
--hash=sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d \
--hash=sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9 \
--hash=sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca \
--hash=sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f \
--hash=sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2 \
--hash=sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077 \
--hash=sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2 \
--hash=sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6 \
--hash=sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374 \
--hash=sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc \
--hash=sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e \
--hash=sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53 \
--hash=sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399 \
--hash=sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547 \
--hash=sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3 \
--hash=sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870 \
--hash=sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5 \
--hash=sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8 \
--hash=sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7
# via uvicorn
wsproto==1.2.0 \
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
--hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736
# via trio-websocket

View file

@ -0,0 +1,327 @@
#!/bin/bash
integration=$1
if [ -z "$integration" ] ; then
echo "🧰 Please provide an integration name as argument ❌"
exit 1
elif [ "$integration" != "docker" ] && [ "$integration" != "linux" ] ; then
echo "🧰 Integration \"$integration\" is not supported ❌"
exit 1
fi
echo "🧰 Building redis stack for integration \"$integration\" ..."
# Starting stack
if [ "$integration" == "docker" ] ; then
docker compose pull bw-docker
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Pull failed ❌"
exit 1
fi
echo "🧰 Building custom redis image ..."
docker compose build bw-redis
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Build failed ❌"
exit 1
fi
echo "🧰 Building tests images ..."
docker compose -f docker-compose.test.yml build
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Build failed ❌"
exit 1
fi
else
sudo systemctl stop bunkerweb
sudo sed -i "/^USE_BLACKLIST=/d" /etc/bunkerweb/variables.env
echo "BLACKLIST_IP_URLS=" | sudo tee -a /etc/bunkerweb/variables.env
echo "SESSIONS_NAME=test" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_REVERSE_SCAN=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "REVERSE_SCAN_PORTS=80" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_ANTIBOT=no" | sudo tee -a /etc/bunkerweb/variables.env
echo "USE_GREYLIST=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "GREYLIST_IP=0.0.0.0/0" | sudo tee -a /etc/bunkerweb/variables.env
echo "WHITELIST_COUNTRY=AU" | sudo tee -a /etc/bunkerweb/variables.env
echo "🧰 Installing Redis ..."
sudo apt install --no-install-recommends -y redis
redis-server --daemonize yes
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Redis start failed ❌"
exit 1
fi
echo "🧰 Redis installed ✅"
echo "🧰 Generating redis certs ..."
mkdir tls
openssl genrsa -out tls/ca.key 4096
openssl req \
-x509 -new -nodes -sha256 \
-key tls/ca.key \
-days 365 \
-subj /CN=bw-redis/ \
-out tls/ca.crt
openssl req \
-x509 -nodes -newkey rsa:4096 \
-keyout tls/redis.key \
-out tls/redis.pem \
-days 365 \
-subj /CN=bw-redis/
sudo chmod -R 777 tls
echo "🧰 Certs generated ✅"
echo "USE_REDIS=yes" | sudo tee -a /etc/bunkerweb/variables.env
echo "REDIS_HOST=127.0.0.1" | sudo tee -a /etc/bunkerweb/variables.env
echo "REDIS_PORT=6379" | sudo tee -a /etc/bunkerweb/variables.env
echo "REDIS_DATABASE=0" | sudo tee -a /etc/bunkerweb/variables.env
echo "REDIS_SSL=no" | sudo tee -a /etc/bunkerweb/variables.env
sudo touch /var/www/html/index.html
export TEST_TYPE="linux"
sudo cp ready.conf /etc/bunkerweb/configs/server-http
fi
manual=0
end=0
cleanup_stack () {
exit_code=$?
if [[ $end -eq 1 || $exit_code = 1 ]] || [[ $end -eq 0 && $exit_code = 0 ]] && [ $manual = 0 ] ; then
if [ "$integration" == "docker" ] ; then
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_REVERSE_SCAN: "yes"@USE_REVERSE_SCAN: "no"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_ANTIBOT: "cookie"@USE_ANTIBOT: "no"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_PORT: "[0-9]*"@REDIS_PORT: "6379"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_DATABASE: "1"@REDIS_DATABASE: "0"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_SSL: "yes"@REDIS_SSL: "no"@' {} \;
else
sudo rm -rf tls
sudo sed -i 's@USE_REVERSE_SCAN=.*$@USE_REVERSE_SCAN=no@' /etc/bunkerweb/variables.env
sudo sed -i 's@USE_ANTIBOT=.*$@USE_ANTIBOT=no@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_PORT=.*$@REDIS_PORT=6379@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_DATABASE=.*$@REDIS_DATABASE=0@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_SSL=.*$@REDIS_SSL=no@' /etc/bunkerweb/variables.env
unset USE_REVERSE_SCAN
unset USE_ANTIBOT
unset REDIS_PORT
unset REDIS_DATABASE
unset REDIS_SSL
sudo killall redis-server
fi
if [[ $end -eq 1 && $exit_code = 0 ]] ; then
return
fi
fi
echo "🧰 Cleaning up current stack ..."
if [ "$integration" == "docker" ] ; then
docker compose down -v --remove-orphans
else
sudo systemctl stop bunkerweb
sudo truncate -s 0 /var/log/bunkerweb/error.log
fi
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Cleanup failed ❌"
exit 1
fi
echo "🧰 Cleaning up current stack done ✅"
}
# Cleanup stack on exit
trap cleanup_stack EXIT
for test in "activated" "reverse_scan" "antibot" "tweaked"
do
if [ "$test" = "activated" ] ; then
echo "🧰 Running tests with redis with default values ..."
elif [ "$test" = "reverse_scan" ] ; then
echo "🧰 Running tests with redis with reverse scan activated ..."
if [ "$integration" == "docker" ] ; then
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_REVERSE_SCAN: "no"@USE_REVERSE_SCAN: "yes"@' {} \;
else
sudo sed -i 's@USE_REVERSE_SCAN=.*$@USE_REVERSE_SCAN=yes@' /etc/bunkerweb/variables.env
export USE_REVERSE_SCAN="yes"
fi
elif [ "$test" = "antibot" ] ; then
echo "🧰 Running tests with redis with antibot cookie activated ..."
if [ "$integration" == "docker" ] ; then
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_REVERSE_SCAN: "yes"@USE_REVERSE_SCAN: "no"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_ANTIBOT: "no"@USE_ANTIBOT: "cookie"@' {} \;
else
sudo sed -i 's@USE_REVERSE_SCAN=.*$@USE_REVERSE_SCAN=no@' /etc/bunkerweb/variables.env
sudo sed -i 's@USE_ANTIBOT=.*$@USE_ANTIBOT=cookie@' /etc/bunkerweb/variables.env
export USE_REVERSE_SCAN="no"
export USE_ANTIBOT="cookie"
fi
elif [ "$test" = "tweaked" ] ; then
echo "🧰 Running tests with redis' settings tweaked ..."
if [ "$integration" == "docker" ] ; then
find . -type f -name 'docker-compose.*' -exec sed -i 's@USE_ANTIBOT: "cookie"@USE_ANTIBOT: "no"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_PORT: "[0-9]*"@REDIS_PORT: "6380"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_DATABASE: "0"@REDIS_DATABASE: "1"@' {} \;
find . -type f -name 'docker-compose.*' -exec sed -i 's@REDIS_SSL: "no"@REDIS_SSL: "yes"@' {} \;
else
sudo sed -i 's@USE_ANTIBOT=.*$@USE_ANTIBOT=no@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_PORT=.*$@REDIS_PORT=6380@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_DATABASE=.*$@REDIS_DATABASE=1@' /etc/bunkerweb/variables.env
sudo sed -i 's@REDIS_SSL=.*$@REDIS_SSL=yes@' /etc/bunkerweb/variables.env
unset USE_ANTIBOT
export REDIS_PORT="6380"
export REDIS_DATABASE="1"
export REDIS_SSL="yes"
echo "🧰 Stopping redis ..."
sudo killall redis-server
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Redis stop failed ❌"
exit 1
fi
echo "🧰 Redis stopped ✅"
echo "🧰 Starting redis with tweaked settings ..."
redis-server --tls-port 6380 --port 0 --tls-cert-file tls/redis.pem --tls-key-file tls/redis.key --tls-ca-cert-file tls/ca.crt --tls-auth-clients no --daemonize yes
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Redis start failed ❌"
exit 1
fi
echo "🧰 Redis started ✅"
fi
fi
echo "🧰 Starting stack ..."
if [ "$integration" == "docker" ] ; then
docker compose up -d
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Up failed, retrying ... ⚠️"
manual=1
cleanup_stack
manual=0
docker compose up -d
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Up failed ❌"
exit 1
fi
fi
else
sudo systemctl start bunkerweb
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Start failed ❌"
exit 1
fi
fi
# Check if stack is healthy
echo "🧰 Waiting for stack to be healthy ..."
i=0
if [ "$integration" == "docker" ] ; then
while [ $i -lt 120 ] ; do
containers=("redis-bw-1" "redis-bw-scheduler-1")
healthy="true"
for container in "${containers[@]}" ; do
check="$(docker inspect --format "{{json .State.Health }}" "$container" | grep "healthy")"
if [ "$check" = "" ] ; then
healthy="false"
break
fi
done
if [ "$healthy" = "true" ] ; then
echo "🧰 Docker stack is healthy ✅"
break
fi
sleep 1
i=$((i+1))
done
if [ $i -ge 120 ] ; then
docker compose logs
echo "🧰 Docker stack is not healthy ❌"
exit 1
fi
else
healthy="false"
retries=0
while [[ $healthy = "false" && $retries -lt 5 ]] ; do
while [ $i -lt 120 ] ; do
if sudo grep -q "BunkerWeb is ready" "/var/log/bunkerweb/error.log" ; then
echo "🧰 Linux stack is healthy ✅"
break
fi
sleep 1
i=$((i+1))
done
if [ $i -ge 120 ] ; then
sudo journalctl -u bunkerweb --no-pager
echo "🛡️ Showing BunkerWeb error logs ..."
sudo cat /var/log/bunkerweb/error.log
echo "🛡️ Showing BunkerWeb access logs ..."
sudo cat /var/log/bunkerweb/access.log
echo "🧰 Linux stack is not healthy ❌"
exit 1
fi
if sudo journalctl -u bunkerweb --no-pager | grep -q "SYSTEMCTL - ❌ " ; then
echo "🧰 ⚠ Linux stack got an issue, restarting ..."
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s
manual=1
cleanup_stack
manual=0
sudo systemctl start bunkerweb
retries=$((retries+1))
else
healthy="true"
fi
done
if [ "$retries" -ge 5 ] ; then
echo "🧰 Linux stack could not be healthy ❌"
exit 1
fi
fi
# Start tests
if [ "$integration" == "docker" ] ; then
docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from tests
else
python3 main.py
fi
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
echo "🧰 Test \"$test\" failed ❌"
echo "🛡️ Showing BunkerWeb and BunkerWeb Scheduler logs ..."
if [ "$integration" == "docker" ] ; then
docker compose logs bw bw-scheduler
else
sudo journalctl -u bunkerweb --no-pager
echo "🛡️ Showing BunkerWeb error logs ..."
sudo cat /var/log/bunkerweb/error.log
echo "🛡️ Showing BunkerWeb access logs ..."
sudo cat /var/log/bunkerweb/access.log
echo "🛡️ Showing Geckodriver logs ..."
sudo cat geckodriver.log
fi
exit 1
else
echo "🧰 Test \"$test\" succeeded ✅"
fi
manual=1
cleanup_stack
manual=0
echo " "
done
end=1
echo "🧰 Tests are done ! ✅"

View file

@ -1,7 +1,7 @@
from contextlib import suppress
from datetime import datetime, timedelta
from functools import partial
from os import getenv, listdir
from os import getenv, listdir, sep
from os.path import join
from pathlib import Path
from time import sleep
@ -20,11 +20,20 @@ from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException, WebDriverException
default_server = "127.0.0.1"
integration_path = Path(sep, "usr", "share", "bunkerweb", "INTEGRATION")
os_release_path = Path(sep, "etc", "os-release")
if getenv("KUBERNETES_MODE", "no").lower() == "yes" or getenv("SWARM_MODE", "no").lower() == "yes" or getenv("AUTOCONF_MODE", "no").lower() == "yes":
default_server = "192.168.0.2"
elif os_release_path.is_file() and "Alpine" in os_release_path.read_text(encoding="utf-8"):
default_server = "192.168.0.2"
ready = False
retries = 0
while not ready:
with suppress(RequestException):
status_code = get("http://127.0.0.1/setup").status_code
status_code = get(f"http://{default_server}/setup").status_code
if status_code > 500 and status_code != 502:
print("An error occurred with the server, exiting ...", flush=True)
@ -179,9 +188,9 @@ with driver_func() as driver:
driver.maximize_window()
driver_wait = WebDriverWait(driver, 60)
print("Navigating to http://127.0.0.1/setup ...", flush=True)
print(f"Navigating to http://{default_server}/setup ...", flush=True)
driver.get("http://127.0.0.1/setup")
driver.get(f"http://{default_server}/setup")
### WIZARD PAGE