bunkerweb/src/bw/lua/bunkerweb/api.lua
2024-02-21 17:48:06 +01:00

350 lines
9.6 KiB
Lua

local ngx = ngx
local ngx_req = ngx.req
local cdatastore = require "bunkerweb.datastore"
local cjson = require "cjson"
local class = require "middleclass"
local clogger = require "bunkerweb.logger"
local helpers = require "bunkerweb.helpers"
local process = require "ngx.process"
local rsignal = require "resty.signal"
local upload = require "resty.upload"
local utils = require "bunkerweb.utils"
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 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(ctx)
self.ctx = ctx
local data, err = get_variable("API_WHITELIST_IP", false)
self.ips = {}
if not data then
logger:log(ERR, "can't get API_WHITELIST_IP variable : " .. err)
else
for ip in data:gmatch("%S+") do
table.insert(self.ips, ip)
end
end
end
-- luacheck: ignore 212
function api:log_cmd(cmd, status, stdout, stderr)
local level = NOTICE
local prefix = "success"
if status ~= 0 then
level = ERR
prefix = "error"
end
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 = run(cmd, nil, 10000)
self:log_cmd(cmd, status, stdout, stderr)
-- Timeout
if ok == nil then
return nil, reason
end
-- Other cases : exit 0, exit !0 and killed by signal
return status == 0, reason, status
end
-- luacheck: ignore 212
function api:response(http_status, api_status, msg)
local resp = {}
resp["status"] = api_status
resp["msg"] = msg
return http_status, resp
end
api.global.GET["^/ping$"] = function(self)
return self:response(HTTP_OK, "success", "pong")
end
api.global.POST["^/reload$"] = function(self)
-- Check config
local status = execute("nginx -t")
if status ~= 0 then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "config check failed")
end
-- 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 = kill(get_master_pid(), "QUIT")
if not ok then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "err = " .. err)
end
return self:response(HTTP_OK, "success", "stop successful")
end
api.global.POST["^/confs$"] = function(self)
local tmp = "/var/tmp/bunkerweb/api_" .. self.ctx.bw.uri:sub(2) .. ".tar.gz"
local destination = "/usr/share/bunkerweb/" .. self.ctx.bw.uri:sub(2)
if self.ctx.bw.uri == "/confs" then
destination = "/etc/nginx"
elseif self.ctx.bw.uri == "/data" then
destination = "/data"
elseif self.ctx.bw.uri == "/cache" then
destination = "/var/cache/bunkerweb"
elseif self.ctx.bw.uri == "/custom_configs" then
destination = "/etc/bunkerweb/configs"
elseif self.ctx.bw.uri == "/plugins" then
destination = "/etc/bunkerweb/plugins"
elseif self.ctx.bw.uri == "/pro_plugins" then
destination = "/etc/bunkerweb/pro/plugins"
end
local form, err = upload:new(4096)
if not form then
return self:response(HTTP_BAD_REQUEST, "error", err)
end
form:set_timeout(1000)
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(HTTP_BAD_REQUEST, "error", err)
end
if typ == "eof" then
break
end
if typ == "body" then
file:write(res)
end
end
file:flush()
file:close()
local cmds = {
"rm -rf " .. destination .. "/*",
"tar xzf " .. tmp .. " -C " .. destination,
}
for _, cmd in ipairs(cmds) do
local status = execute(cmd)
if status ~= 0 then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
end
end
return self:response(HTTP_OK, "success", "saved data at " .. destination)
end
api.global.POST["^/data$"] = api.global.POST["^/confs$"]
api.global.POST["^/cache$"] = api.global.POST["^/confs$"]
api.global.POST["^/custom_configs$"] = api.global.POST["^/confs$"]
api.global.POST["^/plugins$"] = api.global.POST["^/confs$"]
api.global.POST["^/pro_plugins$"] = api.global.POST["^/confs$"]
api.global.POST["^/unban$"] = function(self)
read_body()
local data = get_body_data()
if not data then
local data_file = get_body_file()
if data_file then
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(decode, data)
if not ok then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
end
datastore:delete("bans_ip_" .. ip["ip"])
return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " unbanned")
end
api.global.POST["^/ban$"] = function(self)
read_body()
local data = get_body_data()
if not data then
local data_file = get_body_file()
if data_file then
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(decode, data)
if not ok then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
end
local ban = {
ip = "",
exp = 86400,
reason = "manual",
}
ban.ip = ip["ip"]
if ip["exp"] then
ban.exp = ip["exp"]
end
if ip["reason"] then
ban.reason = ip["reason"]
end
datastore:set(
"bans_ip_" .. ban["ip"],
encode({
reason = ban["reason"],
date = os.time(),
}),
ban["exp"]
)
return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " banned")
end
api.global.GET["^/bans$"] = function(self)
local data = {}
for _, k in ipairs(datastore:keys()) do
if k:find("^bans_ip_") then
local result, err = datastore:get(k)
if err then
return self:response(
HTTP_INTERNAL_SERVER_ERROR,
"error",
"can't access " .. k .. " from datastore : " .. result
)
end
local ok, ttl = datastore:ttl(k)
if not ok then
return self:response(
HTTP_INTERNAL_SERVER_ERROR,
"error",
"can't access ttl " .. k .. " from datastore : " .. ttl
)
end
local ban_data
ok, ban_data = pcall(decode, result)
if not ok then
ban_data = { reason = result, date = -1 }
end
table.insert(
data,
{ ip = k:sub(9, #k), reason = ban_data["reason"], date = ban_data["date"], exp = math.floor(ttl) }
)
end
end
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(HTTP_INTERNAL_SERVER_ERROR, "error", "can't access variables from datastore : " .. err)
end
return self:response(HTTP_OK, "success", variables)
end
function api:is_allowed_ip()
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"
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 match(self.ctx.bw.uri, uri) then
local status, resp = api_fun(self)
local ret = true
if status ~= HTTP_OK then
ret = false
end
if #resp["msg"] == 0 then
resp["msg"] = ""
elseif type(resp["msg"]) == "table" then
resp["data"] = resp["msg"]
resp["msg"] = resp["status"]
end
return ret, resp["msg"], status, encode(resp)
end
end
end
local list, err = datastore:get("plugins", true)
if not list then
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
local plugin_lua, _ = 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 ret
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
end
end
end
end
local resp = {}
resp["status"] = "error"
resp["msg"] = "not found"
return false, "error", HTTP_NOT_FOUND, encode(resp)
end
return api