mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
350 lines
9.6 KiB
Lua
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
|