mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 14:56:38 +08:00
refactor
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
local _M = {}
|
||||
|
||||
function _M.check()
|
||||
local cjson = require "cjson.safe"
|
||||
|
||||
local config_dict = ngx.shared.openflare_waf_config
|
||||
|
||||
local function read_file(path)
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function load_config()
|
||||
local paths = {
|
||||
"/data/etc/openflare/waf_config.json",
|
||||
"/etc/nginx/openflare-lua/waf_config.json",
|
||||
"/usr/local/openresty/nginx/conf/waf_config.json"
|
||||
}
|
||||
for _, path in ipairs(paths) do
|
||||
local content = read_file(path)
|
||||
if content and content ~= "" then
|
||||
local hash = ngx.md5(content)
|
||||
if config_dict:get("_config_hash") == hash then
|
||||
local cached = config_dict:get("_config_json")
|
||||
if cached then
|
||||
local decoded = cjson.decode(cached)
|
||||
if decoded then
|
||||
return decoded
|
||||
end
|
||||
end
|
||||
end
|
||||
local decoded = cjson.decode(content)
|
||||
if decoded then
|
||||
config_dict:set("_config_hash", hash, 0)
|
||||
config_dict:set("_config_json", content, 0)
|
||||
return decoded
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function load_ip_groups()
|
||||
local paths = {
|
||||
"/data/etc/openflare/waf_ip_groups.json",
|
||||
"/etc/nginx/openflare-lua/waf_ip_groups.json",
|
||||
"/usr/local/openresty/nginx/conf/waf_ip_groups.json"
|
||||
}
|
||||
for _, path in ipairs(paths) do
|
||||
local content = read_file(path)
|
||||
if content and content ~= "" then
|
||||
local hash = ngx.md5(content)
|
||||
if config_dict:get("_ip_groups_hash") == hash then
|
||||
local cached = config_dict:get("_ip_groups_json")
|
||||
if cached then
|
||||
local decoded = cjson.decode(cached)
|
||||
if decoded then
|
||||
return decoded
|
||||
end
|
||||
end
|
||||
end
|
||||
local decoded = cjson.decode(content)
|
||||
if decoded then
|
||||
config_dict:set("_ip_groups_hash", hash, 0)
|
||||
config_dict:set("_ip_groups_json", content, 0)
|
||||
return decoded
|
||||
end
|
||||
end
|
||||
end
|
||||
return { groups = {} }
|
||||
end
|
||||
|
||||
local function list_contains(items, value)
|
||||
if not items or type(items) ~= "table" or not value or value == "" then
|
||||
return false
|
||||
end
|
||||
for _, item in ipairs(items) do
|
||||
if item == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function table_has_items(items)
|
||||
return type(items) == "table" and #items > 0
|
||||
end
|
||||
|
||||
local function parse_ipv4(value)
|
||||
local a, b, c, d = string.match(value or "", "^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
|
||||
if not a then
|
||||
return nil
|
||||
end
|
||||
a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d)
|
||||
if a > 255 or b > 255 or c > 255 or d > 255 then
|
||||
return nil
|
||||
end
|
||||
return ((a * 256 + b) * 256 + c) * 256 + d
|
||||
end
|
||||
|
||||
local function ipv4_in_cidr(ip, cidr)
|
||||
local base, bits = string.match(cidr or "", "^([^/]+)/(%d+)$")
|
||||
if not base then
|
||||
return false
|
||||
end
|
||||
bits = tonumber(bits)
|
||||
if not bits or bits < 0 or bits > 32 then
|
||||
return false
|
||||
end
|
||||
local ip_num = parse_ipv4(ip)
|
||||
local base_num = parse_ipv4(base)
|
||||
if not ip_num or not base_num then
|
||||
return false
|
||||
end
|
||||
if bits == 0 then
|
||||
return true
|
||||
end
|
||||
local mask = 4294967295 - (2 ^ (32 - bits) - 1)
|
||||
return (ip_num - (ip_num % (2 ^ (32 - bits)))) == (base_num - (base_num % (2 ^ (32 - bits))))
|
||||
end
|
||||
|
||||
local function ip_matches(items, ip)
|
||||
if not items or type(items) ~= "table" or not ip or ip == "" then
|
||||
return false
|
||||
end
|
||||
for _, item in ipairs(items) do
|
||||
if item == ip then
|
||||
return true
|
||||
end
|
||||
if string.find(item, "/", 1, true) and ipv4_in_cidr(ip, item) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function ip_matches_group_ids(group_ids, ip, ip_groups_config)
|
||||
if not group_ids or type(group_ids) ~= "table" or not ip or ip == "" then
|
||||
return false
|
||||
end
|
||||
local groups = (ip_groups_config or {}).groups or {}
|
||||
for _, id in ipairs(group_ids) do
|
||||
local group = groups[tostring(id)]
|
||||
if group and group.enabled and ip_matches(group.ip_list, ip) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function lookup_country(ip)
|
||||
local ok, maxminddb = pcall(require, "resty.maxminddb")
|
||||
if not ok or not maxminddb then
|
||||
return nil
|
||||
end
|
||||
local paths = {
|
||||
"/data/etc/openflare/GeoLite2-Country.mmdb",
|
||||
"/etc/openflare/GeoLite2-Country.mmdb",
|
||||
"/usr/local/share/openflare/GeoLite2-Country.mmdb"
|
||||
}
|
||||
for _, path in ipairs(paths) do
|
||||
local opened = pcall(maxminddb.init, path)
|
||||
if opened then
|
||||
local res, err = maxminddb.lookup(ip)
|
||||
if res and res.country and res.country.iso_code then
|
||||
return string.upper(res.country.iso_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function group_by_id(config)
|
||||
local result = {}
|
||||
for _, group in ipairs(config.rule_groups or {}) do
|
||||
result[tostring(group.id)] = group
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function active_groups(config, groups)
|
||||
local site = ngx.var.openflare_waf_site or ""
|
||||
local ids = (config.site_rule_groups or {})[site]
|
||||
local result = {}
|
||||
for _, group in ipairs(config.rule_groups or {}) do
|
||||
if group.is_global then
|
||||
result[#result + 1] = group
|
||||
end
|
||||
end
|
||||
if ids then
|
||||
local by_id = group_by_id(config)
|
||||
for _, id in ipairs(ids) do
|
||||
local group = by_id[tostring(id)]
|
||||
if group and not group.is_global then
|
||||
result[#result + 1] = group
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function exit_with_group(group)
|
||||
ngx.ctx.openflare_waf_blocked = true
|
||||
ngx.status = tonumber(group.block_status_code) or 418
|
||||
local body = group.block_response_body or ""
|
||||
if body ~= "" then
|
||||
ngx.header["Content-Type"] = "text/html; charset=utf-8"
|
||||
ngx.say(body)
|
||||
end
|
||||
return ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
local function first_allowlist_group(groups)
|
||||
for _, group in ipairs(groups) do
|
||||
if table_has_items(group.ip_whitelist)
|
||||
or table_has_items(group.ip_whitelist_group_ids)
|
||||
or table_has_items(group.country_whitelist) then
|
||||
return group
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local config = load_config()
|
||||
if not config then
|
||||
if config_dict:add("_missing_config_logged", true, 60) then
|
||||
ngx.log(ngx.WARN, "openflare waf config is missing or invalid; requests will be allowed")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local ip = ngx.var.remote_addr or ""
|
||||
local groups = active_groups(config)
|
||||
local ip_groups_config = load_ip_groups()
|
||||
if #groups == 0 then
|
||||
if config_dict:add("_empty_groups_logged", true, 60) then
|
||||
ngx.log(ngx.WARN, "openflare waf has no active rule group for site: ", ngx.var.openflare_waf_site or "")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
if ip_matches(group.ip_whitelist, ip) or ip_matches_group_ids(group.ip_whitelist_group_ids, ip, ip_groups_config) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local country = nil
|
||||
for _, group in ipairs(groups) do
|
||||
if type(group.country_whitelist) == "table" and #group.country_whitelist > 0 then
|
||||
country = country or lookup_country(ip)
|
||||
if list_contains(group.country_whitelist, country) then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local allowlist_group = first_allowlist_group(groups)
|
||||
if allowlist_group then
|
||||
return exit_with_group(allowlist_group)
|
||||
end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
if ip_matches(group.ip_blacklist, ip) or ip_matches_group_ids(group.ip_blacklist_group_ids, ip, ip_groups_config) then
|
||||
return exit_with_group(group)
|
||||
end
|
||||
end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
if type(group.country_blacklist) == "table" and #group.country_blacklist > 0 then
|
||||
country = country or lookup_country(ip)
|
||||
if list_contains(group.country_blacklist, country) then
|
||||
return exit_with_group(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return "ok"
|
||||
end
|
||||
|
||||
return _M
|
||||
Reference in New Issue
Block a user