mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
183 lines
6.1 KiB
Lua
183 lines
6.1 KiB
Lua
local _M = {}
|
|
|
|
function _M.check()
|
|
local source = debug.getinfo(1, "S").source or ""
|
|
if string.sub(source, 1, 1) == "@" then
|
|
local script_path = string.sub(source, 2)
|
|
local base_dir = string.match(script_path, "^(.*)/pow/[^/]+%.lua$")
|
|
if base_dir and base_dir ~= "" and not string.find(package.path, base_dir, 1, true) then
|
|
package.path = base_dir .. "/?.lua;" .. base_dir .. "/?/init.lua;" .. package.path
|
|
end
|
|
end
|
|
|
|
local cjson = require "cjson.safe"
|
|
local policy = require "pow.policy"
|
|
|
|
local pow_config_dict = ngx.shared.openflare_pow_config
|
|
local pow_sessions = ngx.shared.openflare_pow_sessions
|
|
|
|
local function session_cookie(value, ttl)
|
|
local cookie = "__openflare_pow=" .. value .. "; Path=/; HttpOnly; SameSite=Lax; Max-Age=" .. tostring(ttl)
|
|
if ngx.var.scheme == "https" then
|
|
cookie = cookie .. "; Secure"
|
|
end
|
|
return cookie
|
|
end
|
|
|
|
-- Lazy-load pow_config from file; reload when content changes
|
|
local function load_pow_config()
|
|
local config_paths = {
|
|
"/data/etc/openflare/waf_config.json",
|
|
"/etc/nginx/openflare-lua/waf_config.json",
|
|
"/usr/local/openresty/nginx/conf/waf_config.json"
|
|
}
|
|
for _, config_path in ipairs(config_paths) do
|
|
local f = io.open(config_path, "r")
|
|
if f then
|
|
local content = f:read("*a")
|
|
f:close()
|
|
local current_hash = ngx.md5(content or "")
|
|
|
|
if current_hash == pow_config_dict:get("_config_hash") then
|
|
return
|
|
end
|
|
|
|
-- Clear old domain/site entries
|
|
local old_keys = pow_config_dict:get("_domain_keys")
|
|
if old_keys then
|
|
for domain in string.gmatch(old_keys, "[^\n]+") do
|
|
pow_config_dict:delete(domain)
|
|
end
|
|
end
|
|
|
|
local domain_keys = {}
|
|
if content and content ~= "" and content ~= "{}" then
|
|
local ok, decoded = pcall(cjson.decode, content)
|
|
if ok and decoded and decoded.rule_groups and decoded.site_rule_groups then
|
|
-- Build rule groups map (group ID -> PoWConfig)
|
|
local groups = {}
|
|
for _, group in ipairs(decoded.rule_groups) do
|
|
if group.pow_enabled then
|
|
groups[tostring(group.id)] = group.pow_config
|
|
end
|
|
end
|
|
-- Build site name to pow_config map
|
|
for site, group_ids in pairs(decoded.site_rule_groups) do
|
|
local pow_config = nil
|
|
-- Check custom group IDs first
|
|
for _, id in ipairs(group_ids) do
|
|
pow_config = groups[tostring(id)]
|
|
if pow_config then
|
|
break
|
|
end
|
|
end
|
|
-- If not found, check global group IDs
|
|
if not pow_config then
|
|
for _, group in ipairs(decoded.rule_groups) do
|
|
if group.is_global and group.pow_enabled then
|
|
pow_config = group.pow_config
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if pow_config then
|
|
pow_config_dict:set(site, cjson.encode({enabled = true, config = pow_config}), 0)
|
|
domain_keys[#domain_keys+1] = site
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
pow_config_dict:set("_domain_keys", table.concat(domain_keys, "\n"), 0)
|
|
pow_config_dict:set("_config_hash", current_hash, 0)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
load_pow_config()
|
|
|
|
local host = ngx.var.host
|
|
if not host or host == "" then
|
|
return
|
|
end
|
|
|
|
local site = ngx.var.openflare_waf_site or ""
|
|
if site == "" then
|
|
site = host
|
|
end
|
|
|
|
local config_raw = pow_config_dict:get(site)
|
|
if not config_raw then
|
|
return
|
|
end
|
|
|
|
local ok, route_config = pcall(cjson.decode, config_raw)
|
|
if not ok or not route_config then
|
|
return
|
|
end
|
|
|
|
if not route_config.enabled then
|
|
return
|
|
end
|
|
|
|
local config = route_config.config or {}
|
|
local session_ttl = config.session_ttl or 600
|
|
local uri = ngx.var.uri or ""
|
|
local ua = ngx.var.http_user_agent or ""
|
|
local remote_ip = ngx.var.remote_addr or ""
|
|
|
|
-- Check whitelist: if matched, skip PoW
|
|
local whitelist = config.whitelist or {}
|
|
if policy.match_any(remote_ip, ua, uri, whitelist) then
|
|
return
|
|
end
|
|
|
|
-- Check blacklist: if matched, require PoW
|
|
local blacklist = config.blacklist or {}
|
|
local has_blacklist = policy.has_entries(blacklist)
|
|
local need_pow = false
|
|
if has_blacklist then
|
|
need_pow = policy.match_any(remote_ip, ua, uri, blacklist)
|
|
else
|
|
-- No blacklist means all non-whitelisted need PoW
|
|
need_pow = true
|
|
end
|
|
|
|
if not need_pow then
|
|
return
|
|
end
|
|
|
|
-- Check valid session cookie
|
|
local cookie_val = ngx.var["cookie___openflare_pow"]
|
|
if cookie_val and cookie_val ~= "" then
|
|
local session_key = host .. ":" .. cookie_val
|
|
local session_data = pow_sessions:get(session_key)
|
|
if session_data then
|
|
pow_sessions:set(session_key, "1", session_ttl)
|
|
ngx.header["Set-Cookie"] = session_cookie(cookie_val, session_ttl)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- If requesting the challenge API endpoints, let them through (handled by content_by_lua)
|
|
local anubis_api_prefix = "/.within.website/x/cmd/anubis/api/"
|
|
local anubis_static_prefix = "/.within.website/x/cmd/anubis/static/"
|
|
if string.sub(uri, 1, #anubis_api_prefix) == anubis_api_prefix then
|
|
return
|
|
end
|
|
if string.sub(uri, 1, #anubis_static_prefix) == anubis_static_prefix then
|
|
return
|
|
end
|
|
|
|
-- Render the challenge page through an internal redirect so the browser stays
|
|
-- on the originally requested URL instead of seeing a 302 hop.
|
|
ngx.req.set_uri_args({
|
|
redir = ngx.var.scheme .. "://" .. host .. uri .. (ngx.var.args and ("?" .. ngx.var.args) or ""),
|
|
host = host
|
|
})
|
|
return ngx.exec("/.within.website/x/cmd/anubis/api/make-challenge")
|
|
end
|
|
|
|
return _M
|