mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
287 lines
8.2 KiB
Lua
287 lines
8.2 KiB
Lua
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
|