mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat: 增加 waf 模块 (#4008)
This commit is contained in:
parent
c510a028db
commit
2affd2bc79
5
.gitignore
vendored
5
.gitignore
vendored
@ -41,6 +41,11 @@ frontend/src/xpack
|
||||
backend/xpack
|
||||
backend/router/entry_xpack.go
|
||||
backend/server/init_xpack.go
|
||||
plugins/openresty/waf/db
|
||||
plugins/openresty/waf/data
|
||||
plugins/openresty/waf/sites
|
||||
plugins/openresty/waf/conf/websites.json
|
||||
|
||||
|
||||
.history/
|
||||
dist/
|
||||
|
160
plugins/openresty/waf/conf/global.json
Normal file
160
plugins/openresty/waf/conf/global.json
Normal file
@ -0,0 +1,160 @@
|
||||
{
|
||||
"waf": "on",
|
||||
"mode": "protection",
|
||||
"secret": "qwer1234",
|
||||
"redis": {
|
||||
"state": "off",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"password": "Calong@2015",
|
||||
"ssl": false,
|
||||
"poolSize": 10
|
||||
},
|
||||
"ipWhite": {
|
||||
"state": "on"
|
||||
},
|
||||
"ipBlack": {
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"action": "deny",
|
||||
"type": "ipBlack",
|
||||
"res": "ip"
|
||||
},
|
||||
"urlWhite": {
|
||||
"state": "on"
|
||||
},
|
||||
"urlBlack": {
|
||||
"type": "urlBlack",
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"action": "deny"
|
||||
},
|
||||
"uaWhite": {
|
||||
"state": "off"
|
||||
},
|
||||
"uaBlack": {
|
||||
"type": "uaBlack",
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"action": "deny"
|
||||
},
|
||||
"notFoundCount": {
|
||||
"state": "on",
|
||||
"type": "notFoundCount",
|
||||
"threshold": 10,
|
||||
"duration": 60,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"methodWhite": {
|
||||
"type": "methodWhite",
|
||||
"state": "on",
|
||||
"code": 444,
|
||||
"action": "deny"
|
||||
},
|
||||
"bot": {
|
||||
"state": "on",
|
||||
"type": "bot",
|
||||
"uri": "/1pwaf/bot/trap",
|
||||
"action": "REDIRECT_JS",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"geoRestrict": {
|
||||
"state": "on",
|
||||
"rules": [],
|
||||
"action": "deny"
|
||||
},
|
||||
"defaultIpBlack": {
|
||||
"state": "on",
|
||||
"type": "defaultIpBlack",
|
||||
"code": 444,
|
||||
"action": "deny"
|
||||
},
|
||||
"xss": {
|
||||
"state": "on",
|
||||
"type": "xss",
|
||||
"code": 444,
|
||||
"action": "deny"
|
||||
},
|
||||
"sql": {
|
||||
"state": "on",
|
||||
"type": "sql",
|
||||
"code": 444,
|
||||
"action": "deny"
|
||||
},
|
||||
"cc": {
|
||||
"state": "on",
|
||||
"type": "cc",
|
||||
"rule": "cc",
|
||||
"tokenTimeOut": 1800,
|
||||
"threshold": 100,
|
||||
"duration": 60,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"ccurl": {
|
||||
"state": "on",
|
||||
"type": "urlcc",
|
||||
"rule": "urlcc",
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"attackCount": {
|
||||
"state": "on",
|
||||
"type": "attackCount",
|
||||
"threshold": 20,
|
||||
"duration": 60,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"fileExtCheck": {
|
||||
"state": "on",
|
||||
"action": "deny",
|
||||
"code": 403,
|
||||
"type": "fileExtCheck",
|
||||
"extList": [
|
||||
"php",
|
||||
"jsp",
|
||||
"asp",
|
||||
"exe",
|
||||
"sh"
|
||||
]
|
||||
},
|
||||
"cookie": {
|
||||
"type": "cookie",
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"header": {
|
||||
"state": "on",
|
||||
"type": "header",
|
||||
"code": 403,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
},
|
||||
"defaultUaBlack": {
|
||||
"type": "defaultUaBlack",
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600,
|
||||
"action": "deny"
|
||||
},
|
||||
"args": {
|
||||
"type": "args",
|
||||
"state": "on",
|
||||
"code": 403,
|
||||
"action": "deny",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTime": 600
|
||||
}
|
||||
}
|
11
plugins/openresty/waf/conf/waf.conf
Normal file
11
plugins/openresty/waf/conf/waf.conf
Normal file
@ -0,0 +1,11 @@
|
||||
lua_shared_dict dict_locks 100k;
|
||||
lua_shared_dict dict_req_count 10m;
|
||||
lua_shared_dict waf 30m;
|
||||
lua_shared_dict waf_black_ip 10m;
|
||||
lua_shared_dict waf_limit 10m;
|
||||
lua_shared_dict waf_accesstoken 10m;
|
||||
|
||||
lua_package_path "/usr/local/openresty/1pwaf/?.lua;/usr/local/openresty/1pwaf/lib/?.lua;;";
|
||||
init_by_lua_file /usr/local/openresty/1pwaf/init.lua;
|
||||
access_by_lua_file /usr/local/openresty/1pwaf/waf.lua;
|
||||
log_by_lua_file /usr/local/openresty/1pwaf/log_and_traffic.lua;
|
155
plugins/openresty/waf/config.lua
Normal file
155
plugins/openresty/waf/config.lua
Normal file
@ -0,0 +1,155 @@
|
||||
local file_utils = require "file"
|
||||
local lfs = require "lfs"
|
||||
local cjson = require "cjson"
|
||||
|
||||
local read_rule = file_utils.read_rule
|
||||
local read_file2string = file_utils.read_file2string
|
||||
local read_file2table = file_utils.read_file2table
|
||||
local list_dir = lfs.dir
|
||||
local attributes = lfs.attributes
|
||||
local match_str = string.match
|
||||
|
||||
local waf_dir = "/usr/local/openresty/1pwaf/"
|
||||
local config_dir = waf_dir .. 'conf/'
|
||||
local global_rule_dir = waf_dir .. 'rules/'
|
||||
local site_dir = waf_dir .. 'sites/'
|
||||
|
||||
local _M = {}
|
||||
local config = {}
|
||||
|
||||
local function init_sites_config()
|
||||
local site_config = {}
|
||||
local site_rules = {}
|
||||
for entry in list_dir(site_dir) do
|
||||
if entry ~= "." and entry ~= ".." then
|
||||
local site_path = site_dir .. entry .. "/"
|
||||
if attributes(site_path, "mode") == "directory" then
|
||||
local site_key = entry
|
||||
for s_entry in list_dir(site_path) do
|
||||
local s_entry_path = site_path .. s_entry
|
||||
if attributes(s_entry_path, "mode") == "file" and s_entry == "config.json" then
|
||||
local s_config = read_file2table(s_entry_path)
|
||||
site_config[site_key] = s_config
|
||||
end
|
||||
if attributes(s_entry_path, "mode") == "directory" and s_entry == "rules" then
|
||||
local s_rules = {}
|
||||
local rule_dir = s_entry_path .. "/"
|
||||
for r_file in list_dir(rule_dir) do
|
||||
if r_file ~= "." and r_file ~= ".." then
|
||||
local rule_path = rule_dir .. r_file
|
||||
local rule_type = match_str(r_file, "(.-)%.json$")
|
||||
if attributes(rule_path, "mode") == "file" then
|
||||
local s_rule = nil
|
||||
if rule_type == "methodWhite" then
|
||||
s_rule = read_rule(rule_dir, rule_type, true)
|
||||
else
|
||||
s_rule = read_rule(rule_dir, rule_type)
|
||||
end
|
||||
s_rules[rule_type] = s_rule
|
||||
end
|
||||
end
|
||||
end
|
||||
site_rules[site_key] = s_rules
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ngx.log(ngx.NOTICE, "Load config" .. cjson.encode(site_config))
|
||||
config.site_config = site_config
|
||||
ngx.log(ngx.NOTICE, "Load rules" .. cjson.encode(site_rules))
|
||||
config.site_rules = site_rules
|
||||
end
|
||||
|
||||
local function ini_waf_info()
|
||||
local waf_info = read_file2table(waf_dir .. 'waf.json')
|
||||
if waf_info then
|
||||
ngx.log(ngx.NOTICE, "Load " .. waf_info.name .. " Version:" .. waf_info.version)
|
||||
end
|
||||
end
|
||||
|
||||
local function init_global_config()
|
||||
local global_config = read_file2table(config_dir .. 'global.json')
|
||||
config.global_config = global_config
|
||||
config.isProtectionMode = global_config["mode"] == "protection" and true or false
|
||||
|
||||
local rules = {}
|
||||
rules.uaBlack = read_rule(global_rule_dir, "uaBlack")
|
||||
rules.uaWhite = read_rule(global_rule_dir, "uaWhite")
|
||||
rules.urlBlack = read_rule(global_rule_dir, "urlBlack")
|
||||
rules.urlWhite = read_rule(global_rule_dir, "urlWhite")
|
||||
rules.ipBlack = read_rule(global_rule_dir, "ipBlack")
|
||||
rules.ipWhite = read_rule(global_rule_dir, "ipWhite")
|
||||
rules.args = read_rule(global_rule_dir, "args")
|
||||
rules.cookie = read_rule(global_rule_dir, "cookie")
|
||||
rules.defaultUaBlack = read_rule(global_rule_dir, "defaultUaBlack")
|
||||
rules.header = read_rule(global_rule_dir, "header")
|
||||
|
||||
config.global_rules = rules
|
||||
|
||||
local html_res = {}
|
||||
local htmDir = waf_dir .. "html/"
|
||||
html_res.slide = read_file2string(htmDir .. "slide.html")
|
||||
html_res.slide_js = read_file2string(htmDir .. "slide.js")
|
||||
html_res.five_second = read_file2string(htmDir .. "5s.html")
|
||||
html_res.five_second_js = read_file2string(htmDir .. "5s.js")
|
||||
html_res.redirect = read_file2string(htmDir .. "redirect.html")
|
||||
html_res.ip = read_file2string(htmDir .. "ip.html")
|
||||
|
||||
config.html_res = html_res
|
||||
|
||||
_M.waf_dir = waf_dir
|
||||
_M.config_dir = config_dir
|
||||
end
|
||||
|
||||
function _M.load_config_file()
|
||||
ini_waf_info()
|
||||
init_global_config()
|
||||
init_sites_config()
|
||||
end
|
||||
|
||||
function _M.get_site_config(website_key)
|
||||
return config.site_config[website_key]
|
||||
end
|
||||
|
||||
function _M.get_site_rules(website_key)
|
||||
return config.site_rules[website_key]
|
||||
end
|
||||
|
||||
function _M.get_global_config(name)
|
||||
return config.global_config[name]
|
||||
end
|
||||
|
||||
function _M.get_global_rules(name)
|
||||
return config.global_rules[name]
|
||||
end
|
||||
|
||||
function _M.is_global_state_on(name)
|
||||
return config.global_config[name]["state"] == "on" and true or false
|
||||
end
|
||||
|
||||
function _M.is_site_state_on(name)
|
||||
return config.site_config[name]["state"] == "on" and true or false
|
||||
end
|
||||
|
||||
function _M.get_redis_config()
|
||||
return config.global_config["redis"]
|
||||
end
|
||||
|
||||
function _M.get_html_res(name)
|
||||
return config.html_res[name]
|
||||
end
|
||||
|
||||
function _M.is_waf_on()
|
||||
return config.global_config["waf"] == "on" and true or false
|
||||
end
|
||||
|
||||
function _M.is_redis_on()
|
||||
return config.global_config["redis"] == "on" and true or false
|
||||
end
|
||||
|
||||
function _M.get_secret()
|
||||
return config.global_config["secret"]
|
||||
end
|
||||
|
||||
return _M
|
89
plugins/openresty/waf/db.lua
Normal file
89
plugins/openresty/waf/db.lua
Normal file
@ -0,0 +1,89 @@
|
||||
local config = require "config"
|
||||
|
||||
local open_file = io.open
|
||||
local exec = os.execute
|
||||
local pcall = pcall
|
||||
|
||||
local _M = {}
|
||||
|
||||
local function init_dir(path)
|
||||
local file = open_file(path, "rb")
|
||||
if not file then
|
||||
exec("mkdir -p " .. path)
|
||||
end
|
||||
end
|
||||
|
||||
local function check_table(table_name)
|
||||
if wafdb == nil then
|
||||
return false
|
||||
end
|
||||
local stmt = wafdb:prepare("SELECT COUNT(*) FROM sqlite_master where type='table' and name=?")
|
||||
local rows = 0
|
||||
if stmt ~= nil then
|
||||
stmt:bind_values(table_name)
|
||||
stmt:step()
|
||||
rows = stmt:get_uvalues()
|
||||
stmt:finalize()
|
||||
end
|
||||
return rows > 0
|
||||
end
|
||||
|
||||
function _M.init_db()
|
||||
local ok, sqlite3 = pcall(function()
|
||||
return require "lsqlite3"
|
||||
end)
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
if wafdb then
|
||||
return false
|
||||
end
|
||||
local path = config.waf_dir .. "db/"
|
||||
init_dir(path)
|
||||
local db_path = path .. "1pwaf.db"
|
||||
if wafdb == nil or not wafdb:isopen() then
|
||||
wafdb = sqlite3.open(db_path)
|
||||
if wafdb == nil then
|
||||
return false
|
||||
end
|
||||
wafdb:exec([[PRAGMA journal_mode = wal]])
|
||||
wafdb:exec([[PRAGMA synchronous = 0]])
|
||||
wafdb:exec([[PRAGMA page_size = 8192]])
|
||||
wafdb:exec([[PRAGMA journal_size_limit = 2147483648]])
|
||||
end
|
||||
local status = {}
|
||||
if not check_table("attack_log") then
|
||||
status = wafdb:exec([[
|
||||
CREATE TABLE attack_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip TEXT,
|
||||
ip_city TEXT,
|
||||
ip_country TEXT,
|
||||
ip_subdivisions TEXT,
|
||||
ip_continent TEXT,
|
||||
ip_longitude TEXT,
|
||||
ip_latitude TEXT,
|
||||
time INTEGER,
|
||||
localtime TEXT,
|
||||
server_name TEXT,
|
||||
website_key TEXT,
|
||||
host TEXT,
|
||||
method TEXT,
|
||||
uri TEXT,
|
||||
user_agent TEXT,
|
||||
rule TEXT,
|
||||
nginx_log TEXT,
|
||||
blocking_time INTEGER,
|
||||
action TEXT,
|
||||
msg TEXT,
|
||||
params TEXT,
|
||||
is_block INTEGER
|
||||
)]])
|
||||
|
||||
ngx.log(ngx.ERR, "init db status" .. status)
|
||||
end
|
||||
|
||||
ngx.log(ngx.ERR, "init db success")
|
||||
end
|
||||
|
||||
return _M
|
21
plugins/openresty/waf/html/5s.html
Normal file
21
plugins/openresty/waf/html/5s.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>5s</title>
|
||||
<style>
|
||||
#loadingContainer { position: absolute; top: 50%%; left: 50%%; transform: translate(-50%%, -50%%); display: flex; align-items: center; justify-content: center; flex-direction: column; background: #e8e8e8; width: 300px; height: 100px; border: 2px solid #e8e8e8; }
|
||||
#loadingText { font-size: 18px; margin-top: 10px; }
|
||||
#loadingSuccess { display: none; font-size: 24px; color: #7ac23c; margin-top: 10px; }
|
||||
.loadingSpinner { border: 4px solid rgba(0, 0, 0, 0.1); border-top: 4px solid #7ac23c; border-radius: 50%%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin-top: 10px; }
|
||||
@keyframes spin { 0%% { transform: rotate(0deg); } 100%% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingContainer">
|
||||
<div id="loadingText">正在验证...</div>
|
||||
<div id="loadingSuccess">验证成功</div>
|
||||
<div class="loadingSpinner"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="/5s_check_%s.js"></script>
|
||||
</body>
|
||||
</html>
|
24
plugins/openresty/waf/html/5s.js
Normal file
24
plugins/openresty/waf/html/5s.js
Normal file
@ -0,0 +1,24 @@
|
||||
window.onload = function () {
|
||||
setTimeout(function () {
|
||||
showSuccess();
|
||||
verifySucc();
|
||||
}, 5000);
|
||||
|
||||
function showSuccess() {
|
||||
document.getElementById("loadingText").style.display = "none";
|
||||
document.getElementById("loadingSuccess").style.display = "block";
|
||||
document.querySelector(".loadingSpinner").style.display = "none";
|
||||
}
|
||||
|
||||
function verifySucc() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
const requestUrl = "%s-%s-%s";
|
||||
xhr.open("GET", requestUrl, true);
|
||||
xhr.send();
|
||||
}
|
||||
}
|
29
plugins/openresty/waf/html/ip.html
Normal file
29
plugins/openresty/waf/html/ip.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<title>访问被拒绝</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: '微软雅黑', sans-serif; background-color: #282c34; color: #fff; text-align: center; padding: 50px; }
|
||||
.main { max-width: 600px; margin: 10% auto; background-color: #3a3a3a; border-radius: 8px; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); }
|
||||
.title { background: #ff4d4d; color: #fff; font-size: 24px; height: 60px; line-height: 60px; border-radius: 8px 8px 0 0; }
|
||||
.content { background-color: #444; border: 1px solid #666; border-radius: 0 0 8px 8px; padding: 20px; margin-top: -1px; }
|
||||
.t1 { color: #ff9999; font-weight: bold; margin: 0 0 20px; padding-bottom: 18px; }
|
||||
ol { margin: 0; padding: 0; list-style: none; }
|
||||
ol li { line-height: 30px; background-color: #555; border-radius: 5px; margin-bottom: 10px; padding: 10px; }
|
||||
.footer { margin-top: 20px; font-size: 12px; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="title">无法访问</div>
|
||||
<div class="content">
|
||||
<p class="t1">很抱歉,您的 IP 已被禁止访问</p>
|
||||
<ol>
|
||||
<li>如被误封,请联系网站管理员解封</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="footer">此防护来自 1Panel</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
24
plugins/openresty/waf/html/redirect.html
Normal file
24
plugins/openresty/waf/html/redirect.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<title>网站防火墙</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: '微软雅黑', sans-serif; background-color: #282c34; color: #fff; text-align: center; padding: 50px; }
|
||||
.main { max-width: 600px; margin: 10% auto; background-color: #3a3a3a; border-radius: 8px; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); }
|
||||
.title { background: #ff4d4d; color: #fff; font-size: 24px; height: 60px; line-height: 60px; border-radius: 8px 8px 0 0; }
|
||||
.content { background-color: #444; border: 1px solid #666; border-radius: 0 0 8px 8px; padding: 20px; margin-top: -1px; }
|
||||
.t1 { color: #ff9999; font-weight: bold; margin: 0 0 20px; }
|
||||
.footer { margin-top: 10px; font-size: 12px; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="title">网站防火墙</div>
|
||||
<div class="content">
|
||||
<p class="t1">您的请求不合法,已被拒绝</p>
|
||||
</div>
|
||||
<div class="footer">此网站防护来自 1Panel</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
26
plugins/openresty/waf/html/slide.html
Normal file
26
plugins/openresty/waf/html/slide.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>滑动认证</title>
|
||||
<style>
|
||||
#dragContainer {position:absolute;top:50%%;left:50%%;transform:translate(-50%%, -50%%);display:inline-block;background:#e8e8e8;width:300px;height:33px;border:2px solid #e8e8e8;}
|
||||
#dragBg {position:absolute;background-color:#7ac23c;height:100%%;}
|
||||
#dragText {position:absolute;width:100%%;height:100%%;text-align:center;line-height:33px;user-select:none;-webkit-user-select:none;}
|
||||
#dragHandler {position:absolute;width:40px;height:100%%;cursor:pointer;box-sizing:border-box;overflow:hidden;}
|
||||
#dragHandler.dragHandlerBg {background-color:#c0c0c0;}
|
||||
#dragHandler.dragHandlerBg::before {content:'»';font-size:24px;position:absolute;top:50%%;left:50%%;transform:translate(-50%%, -50%%);color:#7ac23c;}
|
||||
.dragHandlerOkBg {position:absolute;border-radius:50%%;background-color:#7ac23c;display:flex;justify-content:center;align-items:center;}
|
||||
.dragHandlerOkBg::before {content:'\2713';font-size:16px;color:white;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div id="dragContainer">
|
||||
<div id="dragBg"></div>
|
||||
<div id="dragText"></div>
|
||||
<div id="dragHandler" class="dragHandlerBg"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="/slide_check_%s.js"></script>
|
||||
</body>
|
||||
</html>
|
1
plugins/openresty/waf/html/slide.js
Normal file
1
plugins/openresty/waf/html/slide.js
Normal file
@ -0,0 +1 @@
|
||||
window.onload=function(){(function(){const dragContainer=document.getElementById("dragContainer");const dragBg=document.getElementById("dragBg");const dragText=document.getElementById("dragText");const dragHandler=document.getElementById("dragHandler");const maxHandleOffset=dragContainer.clientWidth-dragHandler.clientWidth;let isVertifySucc=false;initDrag();function initDrag(){dragText.textContent="拖动滑块验证";dragHandler.addEventListener("mousedown",onDragStart);dragHandler.addEventListener("touchstart",onDragStart)}function onDragStart(a){a.preventDefault();if(a.type==="mousedown"||(a.type==="touchstart"&&a.touches.length===1)){document.addEventListener("mousemove",onDragMove);document.addEventListener("touchmove",onDragMove);document.addEventListener("mouseup",onDragEnd);document.addEventListener("touchend",onDragEnd)}}function onDragMove(a){let clientX;if(a.type==="mousemove"){clientX=a.clientX}else if(a.type==="touchmove"&&a.touches.length===1){clientX=a.touches[0].clientX}let containerOffsetX=clientX-dragContainer.getBoundingClientRect().left;let left=containerOffsetX-dragHandler.clientWidth/2;if(left<0){left=0}else if(left>maxHandleOffset){left=maxHandleOffset}dragHandler.style.left=left+"px";dragBg.style.width=dragHandler.style.left}function onDragEnd(){document.removeEventListener("mousemove",onDragMove);document.removeEventListener("touchmove",onDragMove);document.removeEventListener("mouseup",onDragEnd);document.removeEventListener("touchend",onDragEnd);if(!isVertifySucc){let left=dragHandler.offsetLeft;if(left>=maxHandleOffset){verifySucc()}else{dragHandler.style.left="0px";dragBg.style.width="0px"}}}function verifySucc(){isVertifySucc=true;dragText.textContent="验证通过";dragText.style.color="white";dragHandler.setAttribute("class","dragHandlerOkBg");dragHandler.removeEventListener("mousedown",onDragStart);dragHandler.removeEventListener("touchstart",onDragStart);let xhr=new XMLHttpRequest();xhr.onreadystatechange=function(){if(xhr.readyState===4&&xhr.status===200){window.location.reload()}};const requestUrl="%s-%s-%s";xhr.open("GET",requestUrl,true);xhr.send()}})()};
|
9
plugins/openresty/waf/init.lua
Normal file
9
plugins/openresty/waf/init.lua
Normal file
@ -0,0 +1,9 @@
|
||||
local db = require "db"
|
||||
local config = require "config"
|
||||
|
||||
config.load_config_file()
|
||||
|
||||
db.init_db()
|
||||
|
||||
|
||||
|
184
plugins/openresty/waf/lib/action.lua
Normal file
184
plugins/openresty/waf/lib/action.lua
Normal file
@ -0,0 +1,184 @@
|
||||
local config = require "config"
|
||||
local redis_util = require "redis_util"
|
||||
local format_str = string.format
|
||||
|
||||
local _M = {}
|
||||
|
||||
local function deny(status_code, res)
|
||||
ngx.status = status_code
|
||||
if res then
|
||||
ngx.header.content_type = "text/html; charset=UTF-8"
|
||||
ngx.say(config.get_html_res(res))
|
||||
end
|
||||
ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
local function redirect(status_code)
|
||||
ngx.header.content_type = "text/html; charset=UTF-8"
|
||||
ngx.say(config.get_html_res("redirect"))
|
||||
ngx.status = status_code
|
||||
ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
local function slide()
|
||||
ngx.header.content_type = "text/html; charset=UTF-8"
|
||||
ngx.header.Cache_Control = "no-cache"
|
||||
ngx.status = 200
|
||||
ngx.say(format_str(config.get_html_res("slide"), ngx.md5(ngx.ctx.ip)))
|
||||
ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
local function five_second()
|
||||
ngx.header.content_type = "text/html; charset=UTF-8"
|
||||
ngx.header.Cache_Control = "no-cache"
|
||||
ngx.status = 200
|
||||
ngx.say(format_str(config.get_html_res("five_second"), ngx.md5(ngx.ctx.ip)))
|
||||
ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
function _M.block_ip(ip, rule)
|
||||
local ok, err = nil, nil
|
||||
local msg = "拉黑IP : " .. ip .. "国家 " .. ngx.ctx.geoip.country["zh"]
|
||||
if rule then
|
||||
msg = msg .. " 规则 " .. rule.type
|
||||
end
|
||||
|
||||
ngx.log(ngx.ERR, msg)
|
||||
|
||||
if config.redis_on then
|
||||
local red, err1 = redis_util.get_conn()
|
||||
if not red then
|
||||
return nil, err1
|
||||
end
|
||||
local key = "black_ip:" .. ip
|
||||
|
||||
local exists = red:exists(key)
|
||||
if exists == 0 then
|
||||
ok, err = red:set(key, 1)
|
||||
if ok then
|
||||
ngx.ctx.ipBlocked = true
|
||||
else
|
||||
ngx.log(ngx.ERR, "failed to set redis key " .. key, err)
|
||||
end
|
||||
end
|
||||
|
||||
if rule.ipBlockTime > 0 then
|
||||
ok, err = red:expire(key, rule.ipBlockTime)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to expire redis key " .. key, err)
|
||||
end
|
||||
end
|
||||
|
||||
redis_util.close_conn(red)
|
||||
else
|
||||
local wafBlackIp = ngx.shared.waf_black_ip
|
||||
local exists = wafBlackIp:get(ip)
|
||||
if not exists then
|
||||
ok, err = wafBlackIp:set(ip, 1, rule.ipBlockTime)
|
||||
if ok then
|
||||
ngx.ctx.ipBlocked = true
|
||||
else
|
||||
ngx.log(ngx.ERR, "failed to set key " .. ip, err)
|
||||
end
|
||||
elseif rule.ipBlockTime > 0 then
|
||||
ok, err = wafBlackIp:expire(ip, rule.ipBlockTime)
|
||||
if ok then
|
||||
ngx.ctx.ipBlocked = true
|
||||
else
|
||||
ngx.log(ngx.ERR, "failed to expire key " .. ip, err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
local function attack_count(config_type)
|
||||
if config_type == "ipBlack" then
|
||||
return
|
||||
end
|
||||
if config.is_global_state_on("attackCount") then
|
||||
local ip = ngx.ctx.ip
|
||||
local attack_config = config.get_global_config("attackCount")
|
||||
local key = ip
|
||||
|
||||
if config.is_redis_on() then
|
||||
key = "cc_attack_count:" .. key
|
||||
local count, _ = redis_util.incr(key, attack_config.duration)
|
||||
if not count then
|
||||
redis_util.set(key, 1, attack_config.duration)
|
||||
elseif count >= attack_config.threshold then
|
||||
_M.block_ip(ip, attack_config)
|
||||
return
|
||||
end
|
||||
else
|
||||
key = ip .. "attack"
|
||||
local limit = ngx.shared.waf_limit
|
||||
local count, _ = limit:incr(key, 1, 0, attack_config.duration)
|
||||
|
||||
if not count then
|
||||
limit:set(key, 1, attack_config.duration)
|
||||
elseif count >= attack_config.threshold then
|
||||
_M.block_ip(ip, attack_config)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.exec_action(rule_config, match_rule, data)
|
||||
local action = rule_config.action
|
||||
|
||||
if match_rule then
|
||||
rule_config.rule = match_rule.rule
|
||||
else
|
||||
rule_config.rule = "默认"
|
||||
end
|
||||
|
||||
ngx.ctx.rule_table = rule_config
|
||||
ngx.ctx.action = action
|
||||
ngx.ctx.hitData = data
|
||||
ngx.ctx.isAttack = true
|
||||
|
||||
if rule_config.ipBlock and rule_config.ipBlock == 'on' then
|
||||
_M.block_ip(ngx.ctx.ip, rule_config)
|
||||
end
|
||||
|
||||
if rule_config.type == nil then
|
||||
rule_config.type = "默认"
|
||||
end
|
||||
|
||||
attack_count(rule_config.type)
|
||||
|
||||
local msg = "访问 IP " .. ngx.ctx.ip .. " 访问 URL" .. ngx.var.uri .. " 触发动作 " .. action .. "User-Agent" .. ngx.ctx.ua .. " 规则类型 " .. rule_config.type .. " 规则 " .. rule_config.rule
|
||||
if ngx.ctx.country then
|
||||
msg = "国家" .. ngx.ctx.country["zh"]
|
||||
end
|
||||
if ngx.ctx.city then
|
||||
msg = "省份" .. ngx.ctx.city["zh"]
|
||||
end
|
||||
|
||||
ngx.log(ngx.ERR, msg)
|
||||
if action == "allow" then
|
||||
return
|
||||
|
||||
elseif action == "deny" then
|
||||
if rule_config.code and rule_config.code ~= 444 then
|
||||
deny(rule_config.code, rule_config.res)
|
||||
else
|
||||
deny(444)
|
||||
end
|
||||
|
||||
elseif action == "slide" then
|
||||
slide()
|
||||
|
||||
elseif action == "fives" then
|
||||
five_second()
|
||||
|
||||
else
|
||||
redirect(444)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return _M
|
67
plugins/openresty/waf/lib/cc.lua
Normal file
67
plugins/openresty/waf/lib/cc.lua
Normal file
@ -0,0 +1,67 @@
|
||||
local config = require "config"
|
||||
local redis_util = require "redis_util"
|
||||
local utils = require "utils"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.set_access_token(k, v)
|
||||
local secret = config.get_secret()
|
||||
local key = ngx.md5(ngx.ctx.ip .. ngx.var.server_name .. ngx.ctx.website_key
|
||||
.. ngx.ctx.ua .. ngx.ctx.today .. secret)
|
||||
local value = ngx.md5(ngx.time() .. ngx.ctx.ip)
|
||||
--TODO check value
|
||||
if key ~= k then
|
||||
ngx.exit(444)
|
||||
end
|
||||
ngx.log(ngx.ERR, "set cc key: ", key)
|
||||
if config.redis_on then
|
||||
--local prefix = "ac_token:"
|
||||
--redis_util.set(prefix .. accesstoken, accesstoken, timeout)
|
||||
else
|
||||
local limit = ngx.shared.waf_accesstoken
|
||||
limit:set(key, value, 7200)
|
||||
end
|
||||
|
||||
local cookie_expire = ngx.cookie_time(ngx.time() + 86400)
|
||||
ngx.header['Set-Cookie'] = { key .. '=' .. value .. '; path=/; Expires=' .. cookie_expire }
|
||||
ngx.exit(200)
|
||||
end
|
||||
|
||||
function _M.check_access_token()
|
||||
local secret = config.get_secret()
|
||||
local key = ngx.md5(ngx.ctx.ip .. ngx.var.server_name .. ngx.ctx.website_key
|
||||
.. ngx.ctx.ua .. ngx.ctx.today .. secret)
|
||||
if not ngx.var.http_cookie then
|
||||
return false
|
||||
end
|
||||
local cookies = utils.get_cookie_list(ngx.var.http_cookie)
|
||||
if not cookies then
|
||||
return false
|
||||
end
|
||||
if not cookies[key] then
|
||||
return false
|
||||
end
|
||||
local accesstoken = cookies[key]
|
||||
local value = nil
|
||||
|
||||
if config.redis_on then
|
||||
local prefix = "ac_token:"
|
||||
value = redis_util.get(prefix .. key)
|
||||
if value and value == accesstoken then
|
||||
return true
|
||||
end
|
||||
else
|
||||
local limit = ngx.shared.waf_accesstoken
|
||||
value = limit:get(key)
|
||||
end
|
||||
if value and value == accesstoken then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.clear_access_token()
|
||||
ngx.header['Set-Cookie'] = { 'a_token=; path=/; Expires=Thu, 01-Jan-1970 00:00:00 GMT' }
|
||||
end
|
||||
|
||||
return _M
|
93
plugins/openresty/waf/lib/file.lua
Normal file
93
plugins/openresty/waf/lib/file.lua
Normal file
@ -0,0 +1,93 @@
|
||||
local cjson = require "cjson"
|
||||
local pairs = pairs
|
||||
local insert_table = table.insert
|
||||
local lower_str = string.lower
|
||||
local open_file = io.open
|
||||
local decode = cjson.decode
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.read_rule(file_path, file_name, read_all)
|
||||
local file, err = open_file(file_path .. file_name .. ".json", "r")
|
||||
if not file then
|
||||
ngx.log(ngx.ERR, "Failed to open file ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local rules_table = {}
|
||||
local other_table = {}
|
||||
local text = file:read('*a')
|
||||
|
||||
file:close()
|
||||
|
||||
if #text > 0 then
|
||||
local result = decode(text)
|
||||
|
||||
if result then
|
||||
for key, value in pairs(result) do
|
||||
if key == "rules" then
|
||||
for _, r in pairs(value) do
|
||||
if read_all then
|
||||
r.hits = 0
|
||||
r.totalHits = 0
|
||||
insert_table(rules_table, r)
|
||||
else
|
||||
if lower_str(r.state) == 'on' then
|
||||
r.hits = 0
|
||||
r.totalHits = 0
|
||||
insert_table(rules_table, r)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
other_table[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return rules_table, other_table
|
||||
end
|
||||
|
||||
function _M.read_file2table(file_path)
|
||||
local file = open_file(file_path, 'r')
|
||||
if file == nil then
|
||||
return nil
|
||||
end
|
||||
str = file:read("*a")
|
||||
file:close()
|
||||
return decode(str)
|
||||
end
|
||||
|
||||
function _M.read_file2string(file_path, binary)
|
||||
if not file_path then
|
||||
ngx.log(ngx.ERR, "No file found ", file_path)
|
||||
return
|
||||
end
|
||||
|
||||
local mode = "r"
|
||||
if binary == true then
|
||||
mode = "rb"
|
||||
end
|
||||
|
||||
local file, err = open_file(file_path, mode)
|
||||
if not file then
|
||||
ngx.log(ngx.ERR, "Failed to open file ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local content = ""
|
||||
repeat
|
||||
local chunk = file:read(8192) -- 读取 8KB 的块
|
||||
if chunk then
|
||||
content = content .. chunk
|
||||
else
|
||||
break
|
||||
end
|
||||
until not chunk
|
||||
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
return _M
|
51
plugins/openresty/waf/lib/geoip.lua
Normal file
51
plugins/openresty/waf/lib/geoip.lua
Normal file
@ -0,0 +1,51 @@
|
||||
local geo = require "resty.maxminddb"
|
||||
|
||||
local pcall = pcall
|
||||
|
||||
local _M = {}
|
||||
local geo_ip_file = "/usr/local/openresty/1pwaf/data/GeoIP.mmdb"
|
||||
local black_ip_file = "/usr/local/openresty/1pwaf/data/BlackIP.mmdb"
|
||||
|
||||
function _M.init()
|
||||
if not geo.initted() then
|
||||
geo.init({
|
||||
geo_ip = geo_ip_file,
|
||||
black_ip = black_ip_file
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function _M.is_default_black_ip(ip)
|
||||
local pass, res, err = pcall(geo.lookup, "black_ip", ip)
|
||||
if not pass then
|
||||
ngx.log(ngx.ERR, 'failed to lookup black ip,reason:', err)
|
||||
elseif res and res['isBlack'] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.lookup(ip)
|
||||
local geo_res = {
|
||||
iso = "",
|
||||
country = "",
|
||||
city = "",
|
||||
longitude = 0,
|
||||
latitude = 0,
|
||||
province = ""
|
||||
}
|
||||
local pass, res, err = pcall(geo.lookup, "geo_ip", ip)
|
||||
if not pass then
|
||||
ngx.log(ngx.ERR, 'failed to lookup by ip,reason:', err)
|
||||
elseif res and res['iso'] then
|
||||
geo_res.iso = res['iso']
|
||||
geo_res.country = res['country']
|
||||
geo_res.province = res['province']
|
||||
geo_res.longitude = res['longitude']
|
||||
geo_res.latitude = res['latitude']
|
||||
return geo_res
|
||||
end
|
||||
return geo_res
|
||||
end
|
||||
|
||||
return _M
|
622
plugins/openresty/waf/lib/lib.lua
Normal file
622
plugins/openresty/waf/lib/lib.lua
Normal file
@ -0,0 +1,622 @@
|
||||
local redis_util = require "redis_util"
|
||||
local action = require "action"
|
||||
local cc = require "cc"
|
||||
local fileUtils = require "file"
|
||||
local ck = require "resty.cookie"
|
||||
local geo = require "geoip"
|
||||
local libinjection = require "resty.libinjection"
|
||||
local config = require "config"
|
||||
local cjson = require "cjson"
|
||||
local utils = require "utils"
|
||||
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local tostring = tostring
|
||||
local type = type
|
||||
local next = next
|
||||
local tonumber = tonumber
|
||||
local concat_table = table.concat
|
||||
local ngx_re_find = ngx.re.find
|
||||
local decode = cjson.decode
|
||||
local ngx_re_gmatch = ngx.re.gmatch
|
||||
local ngx_re_match = ngx.re.match
|
||||
|
||||
local exec_action = action.exec_action
|
||||
|
||||
local _M = {}
|
||||
|
||||
local function is_global_state_on(name)
|
||||
return config.is_global_state_on(name)
|
||||
end
|
||||
|
||||
local function is_site_state_on(name)
|
||||
local site_config = config.get_site_config(ngx.ctx.website_key)
|
||||
if site_config ~= nil then
|
||||
return site_config[name]["state"] == "on" and true or false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function is_state_on(name)
|
||||
return is_site_state_on(name) and is_global_state_on(name)
|
||||
end
|
||||
|
||||
local function get_site_config(name)
|
||||
local site_config = config.get_site_config(ngx.ctx.website_key)
|
||||
if site_config ~= nil then
|
||||
return site_config[name]
|
||||
end
|
||||
return config.get_global_config(name)
|
||||
end
|
||||
|
||||
local function get_site_rule(name)
|
||||
local site_rules = config.get_site_rules(ngx.ctx.website_key)
|
||||
if site_rules ~= nil then
|
||||
return site_rules[name]
|
||||
end
|
||||
return config.get_global_rules(name)
|
||||
end
|
||||
|
||||
local function get_global_rules(name)
|
||||
return config.get_global_rules(name)
|
||||
end
|
||||
|
||||
local function get_global_config(name)
|
||||
return config.get_global_config(name)
|
||||
end
|
||||
|
||||
local function is_rule_state_on(rule_table)
|
||||
return rule_table["state"] == "on" and true or false
|
||||
end
|
||||
|
||||
local function matches(input, regex, ctx, nth)
|
||||
if not nth then
|
||||
nth = 0
|
||||
end
|
||||
return ngx_re_find(input, regex, "isjo", ctx, nth)
|
||||
end
|
||||
|
||||
local function match_rule(rule_table, str)
|
||||
if str == nil or next(rule_table) == nil then
|
||||
return false
|
||||
end
|
||||
for _, t in ipairs(rule_table) do
|
||||
if matches(str, t.rule) then
|
||||
return true, t
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function match_ip(ip_rule, ip, ipn)
|
||||
if ip_rule == nil or ip == nil then
|
||||
return false
|
||||
end
|
||||
if is_rule_state_on(ip_rule) == false then
|
||||
return false
|
||||
end
|
||||
local ip_rule_type = ip_rule.type
|
||||
if utils.is_ipv6(ip) and ip_rule_type == "ipv6" then
|
||||
if ip == ip_rule.ipv6 then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if ip_rule.type == "ipv4" then
|
||||
if ipn == tonumber(ip_rule.ipv4) then
|
||||
return true
|
||||
end
|
||||
elseif ip_rule.type == "ipArr" then
|
||||
local ipArr = ip_rule.ipArr
|
||||
if utils.is_ip_in_array(ipn, ipArr.start, ipArr["end"]) then
|
||||
return true
|
||||
end
|
||||
elseif ip_rule.type == "ipGroup" then
|
||||
--TODO 匹配 IP 组
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function xss_and_sql_check(body)
|
||||
if body then
|
||||
if is_global_state_on("xss") or is_global_state_on("sql") then
|
||||
for k, v in pairs(body) do
|
||||
if type(v) == 'string' then
|
||||
if is_site_state_on("xss") then
|
||||
local is_xss, fingerprint = libinjection.xss(tostring(v))
|
||||
local xss_config = get_site_config("xss")
|
||||
if is_xss then
|
||||
exec_action(xss_config, { rule = tostring(k) .. '=' .. tostring(v) })
|
||||
return
|
||||
end
|
||||
end
|
||||
if is_site_state_on("sql") then
|
||||
local is_sqli, fingerprint = libinjection.sqli(tostring(v))
|
||||
local sql_config = get_site_config("sql")
|
||||
if is_sqli then
|
||||
exec_action(sql_config, { rule = tostring(k) .. '=' .. tostring(v) })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local function get_request_body()
|
||||
ngx.req.read_body()
|
||||
local body_data = ngx.req.get_body_data()
|
||||
if not body_data then
|
||||
local body_file = ngx.req.get_body_file()
|
||||
if body_file then
|
||||
body_data = fileUtils.read_file2string(body_file, true)
|
||||
end
|
||||
end
|
||||
return body_data
|
||||
end
|
||||
|
||||
function _M.is_white_ip()
|
||||
if is_global_state_on("ipWhite") then
|
||||
local ip = ngx.ctx.ip
|
||||
if ip == "unknown" then
|
||||
return false
|
||||
end
|
||||
if ip == "127.0.0.1" then
|
||||
return true
|
||||
end
|
||||
local ipn = utils.ipv4_to_int(ip)
|
||||
local ip_rules = get_global_rules("ipWhite")
|
||||
for _, ip_rule in pairs(ip_rules) do
|
||||
if match_ip(ip_rule, ip, ipn) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.allow_location_check()
|
||||
if is_state_on("geoRestrict") then
|
||||
local geo_ip = ngx.ctx.geoip
|
||||
if geo_ip and geo_ip.iso and geo_ip.iso ~= "" then
|
||||
local iso = geo_ip.iso
|
||||
local geo_config = get_site_config("geoRestrict")
|
||||
local exist = false
|
||||
for _, rule in ipairs(geo_config.rules) do
|
||||
if iso == rule then
|
||||
exist = true
|
||||
break
|
||||
|
||||
end
|
||||
end
|
||||
local default_geo_config = {
|
||||
action = "deny",
|
||||
code = 444,
|
||||
type = "geoRestrict",
|
||||
state = "on",
|
||||
rule = iso
|
||||
}
|
||||
if exist then
|
||||
if geo_config.action == "allow" then
|
||||
return true
|
||||
end
|
||||
if geo_config.action == "deny" then
|
||||
exec_action(default_geo_config, default_geo_config)
|
||||
return false
|
||||
end
|
||||
else
|
||||
if geo_config.action == "allow" then
|
||||
exec_action(default_geo_config, default_geo_config)
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.default_ip_black()
|
||||
if is_state_on("defaultIpBlack") then
|
||||
if geo.is_default_black_ip(ngx.ctx.ip) then
|
||||
exec_action(get_site_config("defaultIpBlack"), { rule = ngx.ctx.ip })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.black_ip()
|
||||
if is_global_state_on("ipBlack") then
|
||||
local ip = ngx.ctx.ip
|
||||
if ip == "unknown" then
|
||||
return false
|
||||
end
|
||||
local exists = nil
|
||||
|
||||
if config.is_redis_on() then
|
||||
exists = redis_util.get("black_ip:" .. ip)
|
||||
else
|
||||
exists = ngx.shared.waf_black_ip:get(ip)
|
||||
end
|
||||
|
||||
if not exists then
|
||||
local ip_black_list = get_global_rules("ipBlack")
|
||||
local ipn = utils.ipv4_to_int(ip)
|
||||
for _, ip_rule in pairs(ip_black_list) do
|
||||
if match_ip(ip_rule, ip, ipn) then
|
||||
exists = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if exists then
|
||||
exec_action(get_global_config("ipBlack"))
|
||||
end
|
||||
|
||||
return exists
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.method_check()
|
||||
local method = ngx.req.get_method()
|
||||
local method_white_list = get_site_rule("methodWhite")
|
||||
for _, method_rule in ipairs(method_white_list) do
|
||||
if method_rule.rule == method and method_rule.state == 'off' then
|
||||
local method_config = get_global_config("methodWhite")
|
||||
exec_action(method_config, method_rule)
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function _M.bot_check()
|
||||
if is_state_on("bot") then
|
||||
local ruri = ngx.var.request_uri
|
||||
local uri = ngx.var.uri
|
||||
local bot_rule = get_site_config("bot")
|
||||
if uri == bot_rule.uri or ruri == bot_rule.uri then
|
||||
exec_action(bot_rule)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.black_ua()
|
||||
if is_global_state_on("uaBlack") then
|
||||
if type(ngx.ctx.ua) ~= 'string' then
|
||||
ngx.exit(200)
|
||||
end
|
||||
local m, mr = match_rule(get_global_rules("uaBlack"), ngx.ctx.ua)
|
||||
if m then
|
||||
exec_action(get_global_config("uaBlack"), mr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.default_ua_black()
|
||||
if is_state_on("defaultUaBlack") then
|
||||
if type(ngx.ctx.ua) ~= 'string' then
|
||||
ngx.exit(200)
|
||||
end
|
||||
local m, mr = match_rule(get_global_rules('defaultUaBlack'), ngx.ctx.ua)
|
||||
if m then
|
||||
exec_action(get_global_config('defaultUaBlack'), mr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.is_white_ua()
|
||||
if is_global_state_on("uaWhite") then
|
||||
local ua = utils.get_header("user-agent")
|
||||
if not ua then
|
||||
return false
|
||||
end
|
||||
if type(ua) ~= 'string' then
|
||||
ngx.exit(200)
|
||||
end
|
||||
for _, wa in ipairs(get_global_rules("uaWhite")) do
|
||||
if ngx.ctx.ua == wa then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.cc()
|
||||
if is_state_on("cc") then
|
||||
if cc.check_access_token() then
|
||||
return
|
||||
end
|
||||
local ip = ngx.ctx.ip
|
||||
local cc_config = get_site_config("cc")
|
||||
local key = ip
|
||||
|
||||
if config.is_redis_on() then
|
||||
key = "cc_req_count:" .. key
|
||||
local count, _ = redis_util.incr(key, cc_config.duration)
|
||||
if not count then
|
||||
redis_util.set(key, 1, cc_config.duration)
|
||||
elseif count > cc_config.threshold then
|
||||
exec_action(cc_config, { rule = cc_config.rule })
|
||||
return
|
||||
end
|
||||
else
|
||||
local limit = ngx.shared.waf_limit
|
||||
local count, _ = limit:incr(key, 1, 0, cc_config.duration)
|
||||
if not count then
|
||||
limit:set(key, 1, cc_config.duration)
|
||||
elseif count > cc_config.threshold then
|
||||
exec_action(cc_config, { rule = cc_config.rule })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.cc_url()
|
||||
if is_state_on("ccurl") then
|
||||
local ip = ngx.ctx.ip
|
||||
local key = ip
|
||||
local urlcc_rules = get_site_rule("ccurl")
|
||||
local urlcc_config = get_site_config("ccurl")
|
||||
local uri = ngx.var.uri
|
||||
|
||||
ngx.log(ngx.ERR, "ccrules is" .. cjson.encode(urlcc_rules))
|
||||
|
||||
local m, mr = match_rule(urlcc_rules, uri)
|
||||
if not m or not mr then
|
||||
return
|
||||
end
|
||||
key = uri .. key
|
||||
if config.is_redis_on() then
|
||||
key = "url_cc_req_count:" .. key
|
||||
local count, _ = redis_util.incr(key, mr.duration)
|
||||
if not count then
|
||||
redis_util.set(key, 1, mr.duration)
|
||||
elseif count > mr.threshold then
|
||||
exec_action(urlcc_config, { rule = mr.rule })
|
||||
return
|
||||
end
|
||||
else
|
||||
local limit = ngx.shared.waf_limit
|
||||
local count, _ = limit:incr(key, 1, 0, mr.duration)
|
||||
if not count then
|
||||
limit:set(key, 1, urlcc_config.duration)
|
||||
elseif count > mr.threshold then
|
||||
exec_action(urlcc_config, { rule = mr.rule })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.is_white_url()
|
||||
if is_global_state_on("urlWhite") then
|
||||
local url = ngx.var.uri
|
||||
if url == nil or url == " " then
|
||||
return false
|
||||
end
|
||||
local m, _ = match_rule(get_global_rules("urlWhite"), url)
|
||||
if m then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.black_url()
|
||||
if is_global_state_on("urlBlack") then
|
||||
local url = ngx.var.uri
|
||||
if url == nil or url == "" then
|
||||
return false
|
||||
end
|
||||
local m, mr = match_rule(get_global_rules("urlBlack"), url)
|
||||
if m then
|
||||
exec_action(get_global_config("urlBlack"), mr)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.args_check()
|
||||
if is_state_on("args") then
|
||||
local args = ngx.req.get_uri_args()
|
||||
if args then
|
||||
local args_list = get_global_rules("args")
|
||||
for _, val in pairs(args) do
|
||||
local val_arr = val
|
||||
if type(val) == "table" then
|
||||
val_arr = concat_table(val, ", ")
|
||||
end
|
||||
if val_arr and type(val_arr) ~= "boolean" and val_arr ~= "" then
|
||||
local m, mr = match_rule(args_list, utils.unescape_uri(val_arr))
|
||||
if m then
|
||||
exec_action(get_global_config("args"), mr)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
xss_and_sql_check(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.cookie_check()
|
||||
local cookie = ngx.var.http_cookie
|
||||
if cookie and is_state_on("cookie") then
|
||||
local cookieList = get_site_rule('cookie')
|
||||
local m, mr = match_rule(cookieList, cookie)
|
||||
if m then
|
||||
local rule_config = get_global_config('cookie')
|
||||
exec_action(rule_config, mr)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.header_check()
|
||||
if is_state_on("header") then
|
||||
local headers_rule = get_site_rule("header")
|
||||
local headers_config = get_site_config("header")
|
||||
local referer = ngx.var.http_referer
|
||||
if referer and referer ~= "" then
|
||||
local m = match_rule(headers_rule, referer)
|
||||
if m then
|
||||
exec_action(headers_config)
|
||||
end
|
||||
end
|
||||
local headers = utils.get_headers()
|
||||
if headers then
|
||||
for k, v in pairs(headers) do
|
||||
local m1, mr1 = match_rule(headers_rule, k)
|
||||
if m1 then
|
||||
exec_action(headers_config, mr1)
|
||||
end
|
||||
local m2, mr2 = match_rule(headers_rule, v)
|
||||
if m2 then
|
||||
exec_action(headers_config, mr2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _M.post_check()
|
||||
local content_type = ngx.ctx.content_type
|
||||
local content_length = ngx.ctx.content_length
|
||||
|
||||
if ngx.ctx.method == "GET" or not content_type or type(content_type) ~= 'string' then
|
||||
return
|
||||
end
|
||||
|
||||
if content_length == nil or content_length == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if ngx_re_find(content_type, '^application/json', "ijo") then
|
||||
local data = get_request_body()
|
||||
if data then
|
||||
xss_and_sql_check(decode(data))
|
||||
end
|
||||
end
|
||||
|
||||
if is_site_state_on('fileExtCheck') and ngx_re_find(content_type, [[multipart]], 'ijo') then
|
||||
if not ngx_re_match(content_type, '^multipart/form-data; boundary=') then
|
||||
return
|
||||
end
|
||||
local boundary_value = ngx_re_match(content_type, '^multipart/form-data; boundary=(.+)')
|
||||
if boundary_value == nil then
|
||||
return
|
||||
end
|
||||
local data = get_request_body()
|
||||
if data == nil then
|
||||
return
|
||||
end
|
||||
local iterator = ngx_re_gmatch(data, [[Content-Disposition.+filename=.+]], 'ijo')
|
||||
if not iterator then
|
||||
return
|
||||
end
|
||||
|
||||
local rule = get_site_config("fileExtCheck")
|
||||
while true do
|
||||
local m = iterator()
|
||||
if m then
|
||||
local match = ngx_re_match(m[0], 'Content-Disposition: form-data; (.+)filename="(.+)\\.(.*)"', 'ijo')
|
||||
if match then
|
||||
local extension = match[3]
|
||||
for _, ext in ipairs(rule.extList) do
|
||||
if extension == ext then
|
||||
exec_action(rule)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function _M.acl()
|
||||
local rules = get_site_rule("acl")
|
||||
for _, rule in pairs(rules) do
|
||||
if rule.state == nil or rule.state == "off" then
|
||||
goto continue
|
||||
end
|
||||
local conditions = rule.conditions
|
||||
local match = true
|
||||
for _, condition in pairs(conditions) do
|
||||
local field = condition.field
|
||||
local field_name = condition.name
|
||||
local pattern = condition.pattern
|
||||
local match_value = ''
|
||||
if field == 'URL' then
|
||||
match_value = ngx.var.request_uri
|
||||
|
||||
elseif field == 'Cookie' then
|
||||
if field_name ~= nil and field_name ~= '' then
|
||||
local cookies, _ = ck:new()
|
||||
if not cookies then
|
||||
match = false
|
||||
break
|
||||
else
|
||||
match_value, _ = cookies:get(field_name)
|
||||
end
|
||||
else
|
||||
match_value = ngx.var.http_cookie
|
||||
end
|
||||
|
||||
elseif field == 'Header' then
|
||||
local headers = ngx.req.get_headers()
|
||||
if headers then
|
||||
if field_name ~= nil and field_name ~= '' then
|
||||
match_value = headers[field_name]
|
||||
else
|
||||
match_value = concat_table(headers, '')
|
||||
end
|
||||
else
|
||||
match = false
|
||||
break
|
||||
end
|
||||
|
||||
elseif field == 'Referer' then
|
||||
match_value = ngx.var.http_referer
|
||||
|
||||
elseif field == 'User-Agent' then
|
||||
match_value = ngx.var.http_user_agent
|
||||
|
||||
elseif field == 'IP' then
|
||||
match_value = ngx.ctx.ip
|
||||
end
|
||||
|
||||
if pattern == '' then
|
||||
if match_value ~= nil and match_value ~= '' then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
else
|
||||
if not matches(match_value, pattern) then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if match then
|
||||
rule.type = "acl"
|
||||
exec_action(rule)
|
||||
end
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
148
plugins/openresty/waf/lib/logger.lua
Normal file
148
plugins/openresty/waf/lib/logger.lua
Normal file
@ -0,0 +1,148 @@
|
||||
local type = type
|
||||
local concat_table = table.concat
|
||||
local new_table = table.new
|
||||
local tostring = tostring
|
||||
local setmetatable = setmetatable
|
||||
local open_file = io.open
|
||||
local ngx_timer_at = ngx.timer.at
|
||||
|
||||
local _M = {}
|
||||
|
||||
local mt = { __index = _M }
|
||||
|
||||
function _M:new(log_path, host, rolling)
|
||||
local t = {
|
||||
flush_limit = 4096, -- 4kb
|
||||
flush_timeout = 1,
|
||||
|
||||
buffered_size = 0,
|
||||
buffer_index = 0,
|
||||
buffer_data = new_table(20000, 0),
|
||||
|
||||
log_path = log_path,
|
||||
prefix = log_path .. host .. '_',
|
||||
rolling = rolling or false,
|
||||
host = host,
|
||||
timer = nil }
|
||||
|
||||
setmetatable(t, mt)
|
||||
return t
|
||||
end
|
||||
|
||||
local function needFlush(self)
|
||||
if self.buffered_size > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function flush_lock(self)
|
||||
local dic_lock = ngx.shared.dict_locks
|
||||
local locked = dic_lock:get(self.host)
|
||||
if not locked then
|
||||
local succ, err = dic_lock:set(self.host, true)
|
||||
if not succ then
|
||||
ngx.log(ngx.ERR, "failed to lock logfile " .. self.host .. ": ", err)
|
||||
end
|
||||
return succ
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function flush_unlock(self)
|
||||
local dic_lock = ngx.shared.dict_locks
|
||||
local success, err = dic_lock:set(self.host, false)
|
||||
if not success then
|
||||
ngx.log(ngx.ERR, "failed to unlock logfile " .. self.host .. ": ", err)
|
||||
end
|
||||
return success
|
||||
end
|
||||
|
||||
local function write_file(self, value)
|
||||
local file_name = ''
|
||||
if self.rolling then
|
||||
file_name = self.prefix .. ngx.today() .. ".log"
|
||||
else
|
||||
file_name = self.log_path
|
||||
end
|
||||
|
||||
local file = open_file(file_name, "a+")
|
||||
|
||||
if file == nil or value == nil then
|
||||
return
|
||||
end
|
||||
|
||||
file:write(value)
|
||||
file:flush()
|
||||
file:close()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local function flushBuffer(self)
|
||||
if not needFlush(self) then
|
||||
return true
|
||||
end
|
||||
|
||||
if not flush_lock(self) then
|
||||
return true
|
||||
end
|
||||
|
||||
local buffer = concat_table(self.buffer_data, "", 1, self.buffer_index)
|
||||
write_file(self, buffer)
|
||||
|
||||
self.buffered_size = 0
|
||||
self.buffer_index = 0
|
||||
self.buffer_data = new_table(20000, 0)
|
||||
|
||||
flush_unlock(self)
|
||||
end
|
||||
|
||||
local function flushPeriod(premature, self)
|
||||
flushBuffer(self)
|
||||
self.timer = false
|
||||
end
|
||||
|
||||
local function writeBuffer(self, msg, msg_len)
|
||||
self.buffer_index = self.buffer_index + 1
|
||||
|
||||
self.buffer_data[self.buffer_index] = msg
|
||||
|
||||
self.buffered_size = self.buffered_size + msg_len
|
||||
|
||||
return self.buffered_size
|
||||
end
|
||||
|
||||
local function startTimer(self)
|
||||
if not self.timer then
|
||||
local ok, err = ngx_timer_at(self.flush_timeout, flushPeriod, self)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to create the timer: ", err)
|
||||
return
|
||||
end
|
||||
if ok then
|
||||
self.timer = true
|
||||
end
|
||||
end
|
||||
return self.timer
|
||||
end
|
||||
|
||||
function _M:log(msg)
|
||||
if type(msg) ~= "string" then
|
||||
msg = tostring(msg)
|
||||
end
|
||||
|
||||
local msg_len = #msg
|
||||
local len = msg_len + self.buffered_size
|
||||
|
||||
if len < self.flush_limit then
|
||||
writeBuffer(self, msg, msg_len)
|
||||
startTimer(self)
|
||||
elseif len >= self.flush_limit then
|
||||
writeBuffer(self, msg, msg_len)
|
||||
flushBuffer(self)
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
16
plugins/openresty/waf/lib/logger_factory.lua
Normal file
16
plugins/openresty/waf/lib/logger_factory.lua
Normal file
@ -0,0 +1,16 @@
|
||||
local logger = require "logger"
|
||||
|
||||
local loggers = {}
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.get_logger(log_path, host, rolling)
|
||||
local host_logger = loggers[host]
|
||||
if not host_logger then
|
||||
host_logger = logger:new(log_path, host, rolling)
|
||||
loggers[host] = host_logger
|
||||
end
|
||||
return host_logger
|
||||
end
|
||||
|
||||
return _M
|
131
plugins/openresty/waf/lib/redis_util.lua
Normal file
131
plugins/openresty/waf/lib/redis_util.lua
Normal file
@ -0,0 +1,131 @@
|
||||
local redis = require "resty.redis"
|
||||
local config = require "config"
|
||||
|
||||
local _M = {}
|
||||
|
||||
local connect_timeout, send_timeout, read_timeout = 1000, 1000, 1000
|
||||
|
||||
function _M.get_conn()
|
||||
local red, err1 = redis:new()
|
||||
if not red then
|
||||
ngx.log(ngx.ERR, "failed to new redis:", err1)
|
||||
return nil, err1
|
||||
end
|
||||
|
||||
local redis_config = config.get_redis_config()
|
||||
|
||||
red:set_timeouts(connect_timeout, send_timeout, read_timeout)
|
||||
|
||||
local ok, err = red:connect(redis_config.host, redis_config.port, { ssl = redis_config.ssl, pool_size = redis_config.poolSize })
|
||||
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to connect: ", err .. "\n")
|
||||
return nil, err
|
||||
end
|
||||
|
||||
if redis_config.password ~= nil and #redis_config.password ~= 0 then
|
||||
local times = 0
|
||||
times, err = red:get_reused_times()
|
||||
|
||||
if times == 0 then
|
||||
local res, err2 = red:auth(redis_config.password)
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "failed to authenticate: ", err2)
|
||||
return nil, err2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return red, err
|
||||
end
|
||||
|
||||
function _M.close_conn(red)
|
||||
local ok, err = red:set_keepalive(10000, 100)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to set keepalive: ", err)
|
||||
end
|
||||
|
||||
return ok, err
|
||||
end
|
||||
|
||||
function _M.set(key, value, expire_time)
|
||||
local red, _ = _M.get_conn()
|
||||
local ok, err = nil, nil
|
||||
if red then
|
||||
ok, err = red:set(key, value)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to set key: " .. key .. " ", err)
|
||||
elseif expire_time and expire_time > 0 then
|
||||
red:expire(key, expire_time)
|
||||
end
|
||||
|
||||
_M.close_conn(red)
|
||||
end
|
||||
|
||||
return ok, err
|
||||
end
|
||||
|
||||
function _M.bath_set(keyTable, value, keyPrefix)
|
||||
local red, _ = _M.get_conn()
|
||||
local results, err = nil, nil
|
||||
if red then
|
||||
red:init_pipeline()
|
||||
|
||||
if keyPrefix then
|
||||
for _, ip in ipairs(keyTable) do
|
||||
red:set(keyPrefix .. ip, value)
|
||||
end
|
||||
else
|
||||
for _, ip in ipairs(keyTable) do
|
||||
red:set(ip, value)
|
||||
end
|
||||
end
|
||||
|
||||
results, err = red:commit_pipeline()
|
||||
if not results then
|
||||
ngx.log(ngx.ERR, "failed to set keys: ", err)
|
||||
end
|
||||
|
||||
_M.close_conn(red)
|
||||
end
|
||||
|
||||
return results, err
|
||||
end
|
||||
|
||||
function _M.get(key)
|
||||
local red, err = _M.get_conn()
|
||||
local value = nil
|
||||
if red then
|
||||
value, err = red:get(key)
|
||||
if not value then
|
||||
ngx.log(ngx.ERR, "failed to get key: " .. key, err)
|
||||
return value, err
|
||||
end
|
||||
if value == ngx.null then
|
||||
value = nil
|
||||
end
|
||||
|
||||
_M.close_conn(red)
|
||||
end
|
||||
|
||||
return value, err
|
||||
end
|
||||
|
||||
function _M.incr(key, expire_time)
|
||||
local red, err = _M.get_conn()
|
||||
local res = nil
|
||||
if red then
|
||||
res, err = red:incr(key)
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "failed to incr key: " .. key, err)
|
||||
elseif res == 1 and expire_time and expire_time > 0 then
|
||||
red:expire(key, expire_time)
|
||||
end
|
||||
|
||||
_M.close_conn(red)
|
||||
end
|
||||
|
||||
return res, err
|
||||
end
|
||||
|
||||
return _M
|
213
plugins/openresty/waf/lib/resty/cookie.lua
Normal file
213
plugins/openresty/waf/lib/resty/cookie.lua
Normal file
@ -0,0 +1,213 @@
|
||||
-- Copyright (C) 2013-2016 Jiale Zhi (calio), CloudFlare Inc.
|
||||
-- See RFC6265 http://tools.ietf.org/search/rfc6265
|
||||
-- require "luacov"
|
||||
|
||||
local type = type
|
||||
local byte = string.byte
|
||||
local sub = string.sub
|
||||
local format = string.format
|
||||
local log = ngx.log
|
||||
local ERR = ngx.ERR
|
||||
local WARN = ngx.WARN
|
||||
local ngx_header = ngx.header
|
||||
|
||||
local EQUAL = byte("=")
|
||||
local SEMICOLON = byte(";")
|
||||
local SPACE = byte(" ")
|
||||
local HTAB = byte("\t")
|
||||
|
||||
-- table.new(narr, nrec)
|
||||
local ok, new_tab = pcall(require, "table.new")
|
||||
if not ok then
|
||||
new_tab = function () return {} end
|
||||
end
|
||||
|
||||
local ok, clear_tab = pcall(require, "table.clear")
|
||||
if not ok then
|
||||
clear_tab = function(tab) for k, _ in pairs(tab) do tab[k] = nil end end
|
||||
end
|
||||
|
||||
local _M = new_tab(0, 2)
|
||||
|
||||
_M._VERSION = '0.01'
|
||||
|
||||
|
||||
local function get_cookie_table(text_cookie)
|
||||
if type(text_cookie) ~= "string" then
|
||||
log(ERR, format("expect text_cookie to be \"string\" but found %s",
|
||||
type(text_cookie)))
|
||||
return {}
|
||||
end
|
||||
|
||||
local EXPECT_KEY = 1
|
||||
local EXPECT_VALUE = 2
|
||||
local EXPECT_SP = 3
|
||||
|
||||
local n = 0
|
||||
local len = #text_cookie
|
||||
|
||||
for i=1, len do
|
||||
if byte(text_cookie, i) == SEMICOLON then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
local cookie_table = new_tab(0, n + 1)
|
||||
|
||||
local state = EXPECT_SP
|
||||
local i = 1
|
||||
local j = 1
|
||||
local key, value
|
||||
|
||||
while j <= len do
|
||||
if state == EXPECT_KEY then
|
||||
if byte(text_cookie, j) == EQUAL then
|
||||
key = sub(text_cookie, i, j - 1)
|
||||
state = EXPECT_VALUE
|
||||
i = j + 1
|
||||
end
|
||||
elseif state == EXPECT_VALUE then
|
||||
if byte(text_cookie, j) == SEMICOLON
|
||||
or byte(text_cookie, j) == SPACE
|
||||
or byte(text_cookie, j) == HTAB
|
||||
then
|
||||
value = sub(text_cookie, i, j - 1)
|
||||
cookie_table[key] = value
|
||||
|
||||
key, value = nil, nil
|
||||
state = EXPECT_SP
|
||||
i = j + 1
|
||||
end
|
||||
elseif state == EXPECT_SP then
|
||||
if byte(text_cookie, j) ~= SPACE
|
||||
and byte(text_cookie, j) ~= HTAB
|
||||
then
|
||||
state = EXPECT_KEY
|
||||
i = j
|
||||
j = j - 1
|
||||
end
|
||||
end
|
||||
j = j + 1
|
||||
end
|
||||
|
||||
if key ~= nil and value == nil then
|
||||
cookie_table[key] = sub(text_cookie, i)
|
||||
end
|
||||
|
||||
return cookie_table
|
||||
end
|
||||
|
||||
function _M.new(self)
|
||||
local _cookie = ngx.var.http_cookie
|
||||
--if not _cookie then
|
||||
--return nil, "no cookie found in current request"
|
||||
--end
|
||||
return setmetatable({ _cookie = _cookie, set_cookie_table = new_tab(4, 0) },
|
||||
{ __index = self })
|
||||
end
|
||||
|
||||
function _M.get(self, key)
|
||||
if not self._cookie then
|
||||
return nil, "no cookie found in the current request"
|
||||
end
|
||||
if self.cookie_table == nil then
|
||||
self.cookie_table = get_cookie_table(self._cookie)
|
||||
end
|
||||
|
||||
return self.cookie_table[key]
|
||||
end
|
||||
|
||||
function _M.get_all(self)
|
||||
if not self._cookie then
|
||||
return nil, "no cookie found in the current request"
|
||||
end
|
||||
|
||||
if self.cookie_table == nil then
|
||||
self.cookie_table = get_cookie_table(self._cookie)
|
||||
end
|
||||
|
||||
return self.cookie_table
|
||||
end
|
||||
|
||||
function _M.get_cookie_size(self)
|
||||
if not self._cookie then
|
||||
return 0
|
||||
end
|
||||
|
||||
return string.len(self._cookie)
|
||||
end
|
||||
|
||||
local function bake(cookie)
|
||||
if not cookie.key or not cookie.value then
|
||||
return nil, 'missing cookie field "key" or "value"'
|
||||
end
|
||||
|
||||
if cookie["max-age"] then
|
||||
cookie.max_age = cookie["max-age"]
|
||||
end
|
||||
|
||||
if (cookie.samesite) then
|
||||
local samesite = cookie.samesite
|
||||
|
||||
-- if we don't have a valid-looking attribute, ignore the attribute
|
||||
if (samesite ~= "Strict" and samesite ~= "Lax" and samesite ~= "None") then
|
||||
log(WARN, "SameSite value must be 'Strict', 'Lax' or 'None'")
|
||||
cookie.samesite = nil
|
||||
end
|
||||
end
|
||||
|
||||
local str = cookie.key .. "=" .. cookie.value
|
||||
.. (cookie.expires and "; Expires=" .. cookie.expires or "")
|
||||
.. (cookie.max_age and "; Max-Age=" .. cookie.max_age or "")
|
||||
.. (cookie.domain and "; Domain=" .. cookie.domain or "")
|
||||
.. (cookie.path and "; Path=" .. cookie.path or "")
|
||||
.. (cookie.secure and "; Secure" or "")
|
||||
.. (cookie.httponly and "; HttpOnly" or "")
|
||||
.. (cookie.samesite and "; SameSite=" .. cookie.samesite or "")
|
||||
.. (cookie.extension and "; " .. cookie.extension or "")
|
||||
return str
|
||||
end
|
||||
|
||||
function _M.set(self, cookie)
|
||||
local cookie_str, err = bake(cookie)
|
||||
if not cookie_str then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local set_cookie = ngx_header['Set-Cookie']
|
||||
local set_cookie_type = type(set_cookie)
|
||||
local t = self.set_cookie_table
|
||||
clear_tab(t)
|
||||
|
||||
if set_cookie_type == "string" then
|
||||
-- only one cookie has been setted
|
||||
if set_cookie ~= cookie_str then
|
||||
t[1] = set_cookie
|
||||
t[2] = cookie_str
|
||||
ngx_header['Set-Cookie'] = t
|
||||
end
|
||||
elseif set_cookie_type == "table" then
|
||||
-- more than one cookies has been setted
|
||||
local size = #set_cookie
|
||||
|
||||
-- we can not set cookie like ngx.header['Set-Cookie'][3] = val
|
||||
-- so create a new table, copy all the values, and then set it back
|
||||
for i=1, size do
|
||||
t[i] = ngx_header['Set-Cookie'][i]
|
||||
if t[i] == cookie_str then
|
||||
-- new cookie is duplicated
|
||||
return true
|
||||
end
|
||||
end
|
||||
t[size + 1] = cookie_str
|
||||
ngx_header['Set-Cookie'] = t
|
||||
else
|
||||
-- no cookie has been setted
|
||||
ngx_header['Set-Cookie'] = cookie_str
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
_M.get_cookie_string = bake
|
||||
|
||||
return _M
|
326
plugins/openresty/waf/lib/resty/libinjection.lua
Normal file
326
plugins/openresty/waf/lib/resty/libinjection.lua
Normal file
@ -0,0 +1,326 @@
|
||||
local _M = {}
|
||||
|
||||
local bit = require "bit"
|
||||
local ffi = require "ffi"
|
||||
|
||||
local ffi_new = ffi.new
|
||||
local ffi_string = ffi.string
|
||||
|
||||
-- enum sqli_flags
|
||||
local FLAG_NONE = 0
|
||||
local FLAG_QUOTE_NONE = 1
|
||||
local FLAG_QUOTE_SINGLE = 2
|
||||
local FLAG_QUOTE_DOUBLE = 4
|
||||
local FLAG_SQL_ANSI = 8
|
||||
local FLAG_SQL_MYSQL = 16
|
||||
|
||||
-- enum lookup_type
|
||||
local LOOKUP_FINGERPRINT = 4
|
||||
|
||||
-- enum html5_flags
|
||||
local DATA_STATE = 0
|
||||
local VALUE_NO_QUOTE = 1
|
||||
local VALUE_SINGLE_QUOTE = 2
|
||||
local VALUE_DOUBLE_QUOTE = 3
|
||||
local VALUE_BACK_QUOTE = 4
|
||||
|
||||
-- cached ORs
|
||||
local QUOTE_NONE_SQL_ANSI = bit.bor(FLAG_QUOTE_NONE, FLAG_SQL_ANSI)
|
||||
local QUOTE_NONE_SQL_MYSQL = bit.bor(FLAG_QUOTE_NONE, FLAG_SQL_MYSQL)
|
||||
local QUOTE_SINGLE_SQL_ANSI = bit.bor(FLAG_QUOTE_SINGLE, FLAG_SQL_ANSI)
|
||||
local QUOTE_SINGLE_SQL_MYSQL = bit.bor(FLAG_QUOTE_SINGLE, FLAG_SQL_MYSQL)
|
||||
local QUOTE_DOUBLE_SQL_MYSQL = bit.bor(FLAG_QUOTE_DOUBLE, FLAG_SQL_MYSQL)
|
||||
|
||||
-- libibjection.so
|
||||
ffi.cdef [[
|
||||
const char* libinjection_sqli_fingerprint(struct libinjection_sqli_state* sql_state, int flags);
|
||||
|
||||
struct libinjection_sqli_token {
|
||||
char type;
|
||||
char str_open;
|
||||
char str_close;
|
||||
size_t pos;
|
||||
size_t len;
|
||||
int count;
|
||||
char val[32];
|
||||
};
|
||||
|
||||
typedef char (*ptr_lookup_fn)(struct libinjection_sqli_state*, int lookuptype, const char* word, size_t len);
|
||||
|
||||
struct libinjection_sqli_state {
|
||||
const char *s;
|
||||
size_t slen;
|
||||
ptr_lookup_fn lookup;
|
||||
void* userdata;
|
||||
int flags;
|
||||
size_t pos;
|
||||
struct libinjection_sqli_token tokenvec[8];
|
||||
struct libinjection_sqli_token *current;
|
||||
char fingerprint[8];
|
||||
int reason;
|
||||
int stats_comment_ddw;
|
||||
int stats_comment_ddx;
|
||||
int stats_comment_c;
|
||||
int stats_comment_hash;
|
||||
int stats_folds;
|
||||
int stats_tokens;
|
||||
};
|
||||
|
||||
void libinjection_sqli_init(struct libinjection_sqli_state * sf, const char *s, size_t len, int flags);
|
||||
int libinjection_is_sqli(struct libinjection_sqli_state* sql_state);
|
||||
|
||||
int libinjection_sqli(const char* s, size_t slen, char fingerprint[]);
|
||||
|
||||
int libinjection_is_xss(const char* s, size_t len, int flags);
|
||||
int libinjection_xss(const char* s, size_t slen);
|
||||
]]
|
||||
|
||||
_M.version = "0.1.1"
|
||||
|
||||
local state_type = ffi.typeof("struct libinjection_sqli_state[1]")
|
||||
local lib, loaded
|
||||
|
||||
-- "borrowed" from CF aho-corasick lib
|
||||
local function _loadlib()
|
||||
if (not loaded) then
|
||||
local path, so_path
|
||||
local libname = "libinjection.so"
|
||||
|
||||
for k, v in string.gmatch(package.cpath, "[^;]+") do
|
||||
so_path = string.match(k, "(.*/)")
|
||||
if so_path then
|
||||
-- "so_path" could be nil. e.g, the dir path component is "."
|
||||
so_path = so_path .. libname
|
||||
|
||||
-- Don't get me wrong, the only way to know if a file exist is
|
||||
-- trying to open it.
|
||||
local f = io.open(so_path)
|
||||
if f ~= nil then
|
||||
io.close(f)
|
||||
path = so_path
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
path = "/usr/local/openresty/1pwaf/data/libinjection.so"
|
||||
|
||||
lib = ffi.load(path)
|
||||
|
||||
if (lib) then
|
||||
loaded = true
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- this function is not publicly exposed so we need to emulate it here. not great but not a measurable perf hit
|
||||
local function _reparse_as_mysql(sqli_state)
|
||||
return sqli_state[0].stats_comment_ddx ~= 0 or sqli_state[0].stats_comment_hash ~= 0
|
||||
end
|
||||
|
||||
--[[
|
||||
Secondary API: detects SQLi in a string, given a context. Given a string, returns a list of
|
||||
|
||||
* boolean indicating a match
|
||||
* SQLi fingerprint
|
||||
--]]
|
||||
local function _sqli_contextwrapper(string, char, flag1, flag2)
|
||||
if (char and not string.find(string, char, 1, true)) then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
if (not loaded) then
|
||||
if (not _loadlib()) then
|
||||
return false, nil
|
||||
end
|
||||
end
|
||||
|
||||
local issqli, lookup, sqli_state
|
||||
|
||||
-- allocate a new libinjection_sqli_state struct
|
||||
sqli_state = ffi_new(state_type)
|
||||
|
||||
-- init the state
|
||||
lib.libinjection_sqli_init(
|
||||
sqli_state,
|
||||
string,
|
||||
#string,
|
||||
FLAG_NONE
|
||||
)
|
||||
|
||||
-- initial fingerprint
|
||||
lib.libinjection_sqli_fingerprint(
|
||||
sqli_state,
|
||||
flag1
|
||||
)
|
||||
|
||||
-- lookup
|
||||
lookup = sqli_state[0].lookup(
|
||||
sqli_state,
|
||||
LOOKUP_FINGERPRINT,
|
||||
sqli_state[0].fingerprint,
|
||||
#ffi.string(sqli_state[0].fingerprint)
|
||||
)
|
||||
|
||||
-- match? great, we're done
|
||||
if (lookup > 0) then
|
||||
return true, ffi_string(sqli_state[0].fingerprint)
|
||||
end
|
||||
|
||||
-- no? reparse, fingerprint and lookup again
|
||||
if (flag2 and _reparse_as_mysql(sqli_state)) then
|
||||
lib.libinjection_sqli_fingerprint(
|
||||
sqli_state,
|
||||
flag2
|
||||
)
|
||||
|
||||
lookup = sqli_state[0].lookup(
|
||||
sqli_state,
|
||||
LOOKUP_FINGERPRINT,
|
||||
sqli_state[0].fingerprint,
|
||||
#ffi.string(sqli_state[0].fingerprint)
|
||||
)
|
||||
|
||||
if (lookup > 0) then
|
||||
return true, ffi_string(sqli_state[0].fingerprint)
|
||||
end
|
||||
end
|
||||
|
||||
return false, nil
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with no char context
|
||||
--]]
|
||||
function _M.sqli_noquote(string)
|
||||
return _sqli_contextwrapper(
|
||||
string,
|
||||
nil,
|
||||
QUOTE_NONE_SQL_ANSI,
|
||||
QUOTE_NONE_SQL_MYSQL
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with CHAR_SINGLE context
|
||||
--]]
|
||||
function _M.sqli_singlequote(string)
|
||||
return _sqli_contextwrapper(
|
||||
string,
|
||||
"'",
|
||||
QUOTE_SINGLE_SQL_ANSI,
|
||||
QUOTE_SINGLE_SQL_MYSQL
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with CHAR_DOUBLE context
|
||||
--]]
|
||||
function _M.sqli_doublequote(string)
|
||||
return _sqli_contextwrapper(
|
||||
string,
|
||||
'"',
|
||||
QUOTE_DOUBLE_SQL_MYSQL
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Simple API. Given a string, returns a list of
|
||||
|
||||
* boolean indicating a match
|
||||
* SQLi fingerprint
|
||||
--]]
|
||||
function _M.sqli(string)
|
||||
if (not loaded) then
|
||||
if (not _loadlib()) then
|
||||
return false, nil
|
||||
end
|
||||
end
|
||||
|
||||
local fingerprint = ffi_new("char [8]")
|
||||
|
||||
return lib.libinjection_sqli(string, #string, fingerprint) == 1, ffi_string(fingerprint)
|
||||
end
|
||||
|
||||
--[[
|
||||
Secondary API: detects XSS in a string, given a context. Given a string, returns a boolean denoting if XSS was detected
|
||||
--]]
|
||||
local function _xss_contextwrapper(string, flag)
|
||||
if (not loaded) then
|
||||
if (not _loadlib()) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return lib.libinjection_is_xss(string, #string, flag) == 1
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with DATA_STATE flag
|
||||
--]]
|
||||
function _M.xss_data_state(string)
|
||||
return _xss_contextwrapper(
|
||||
string,
|
||||
DATA_STATE
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with VALUE_NO_QUOTE flag
|
||||
--]]
|
||||
function _M.xss_noquote(string)
|
||||
return _xss_contextwrapper(
|
||||
string,
|
||||
VALUE_NO_QUOTE
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with VALUE_SINGLE_QUOTE flag
|
||||
--]]
|
||||
function _M.xss_singlequote(string)
|
||||
return _xss_contextwrapper(
|
||||
string,
|
||||
VALUE_SINGLE_QUOTE
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with VALUE_DOUBLE_QUOTE flag
|
||||
--]]
|
||||
function _M.xss_doublequote(string)
|
||||
return _xss_contextwrapper(
|
||||
string,
|
||||
VALUE_DOUBLE_QUOTE
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
Wrapper for second-level API with VALUE_BACK_QUOTE flag
|
||||
--]]
|
||||
function _M.xss_backquote(string)
|
||||
return _xss_contextwrapper(
|
||||
string,
|
||||
VALUE_BACK_QUOTE
|
||||
)
|
||||
end
|
||||
|
||||
--[[
|
||||
ALPHA version of XSS detector. Given a string, returns a boolean denoting if XSS was detected
|
||||
--]]
|
||||
function _M.xss(string)
|
||||
if (not loaded) then
|
||||
if (not _loadlib()) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return lib.libinjection_xss(string, #string) == 1
|
||||
end
|
||||
|
||||
return _M
|
372
plugins/openresty/waf/lib/resty/maxminddb.lua
Normal file
372
plugins/openresty/waf/lib/resty/maxminddb.lua
Normal file
@ -0,0 +1,372 @@
|
||||
--[[
|
||||
Copyright 2017-now anjia (anjia0532@gmail.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
]]
|
||||
|
||||
-- copy from https://github.com/lilien1010/lua-resty-maxminddb/blob/f96633e2428f8f7bcc1e2a7a28b747b33233a8db/resty/maxminddb.lua#L5-L12
|
||||
|
||||
local ffi = require('ffi')
|
||||
local ffi_new = ffi.new
|
||||
local ffi_str = ffi.string
|
||||
local ffi_cast = ffi.cast
|
||||
local ffi_gc = ffi.gc
|
||||
local C = ffi.C
|
||||
|
||||
local _M = {}
|
||||
local _D = {}
|
||||
|
||||
_M._VERSION = '1.3.3'
|
||||
local mt = { __index = _M }
|
||||
|
||||
-- copy from https://github.com/lilien1010/lua-resty-maxminddb/blob/f96633e2428f8f7bcc1e2a7a28b747b33233a8db/resty/maxminddb.lua#L36-L126
|
||||
ffi.cdef [[
|
||||
|
||||
typedef long int ssize_t;
|
||||
|
||||
typedef unsigned int mmdb_uint128_t __attribute__ ((__mode__(TI)));
|
||||
|
||||
typedef struct MMDB_entry_s {
|
||||
struct MMDB_s *mmdb;
|
||||
uint32_t offset;
|
||||
} MMDB_entry_s;
|
||||
|
||||
typedef struct MMDB_lookup_result_s {
|
||||
bool found_entry;
|
||||
MMDB_entry_s entry;
|
||||
uint16_t netmask;
|
||||
} MMDB_lookup_result_s;
|
||||
|
||||
typedef struct MMDB_entry_data_s {
|
||||
bool has_data;
|
||||
union {
|
||||
uint32_t pointer;
|
||||
const char *utf8_string;
|
||||
double double_value;
|
||||
const uint8_t *bytes;
|
||||
uint16_t uint16;
|
||||
uint32_t uint32;
|
||||
int32_t int32;
|
||||
uint64_t uint64;
|
||||
mmdb_uint128_t uint128;
|
||||
bool boolean;
|
||||
float float_value;
|
||||
};
|
||||
|
||||
uint32_t offset;
|
||||
uint32_t offset_to_next;
|
||||
uint32_t data_size;
|
||||
uint32_t type;
|
||||
} MMDB_entry_data_s;
|
||||
|
||||
typedef struct MMDB_entry_data_list_s {
|
||||
MMDB_entry_data_s entry_data;
|
||||
struct MMDB_entry_data_list_s *next;
|
||||
} MMDB_entry_data_list_s;
|
||||
|
||||
typedef struct MMDB_description_s {
|
||||
const char *language;
|
||||
const char *description;
|
||||
} MMDB_description_s;
|
||||
|
||||
typedef struct MMDB_metadata_s {
|
||||
uint32_t node_count;
|
||||
uint16_t record_size;
|
||||
uint16_t ip_version;
|
||||
const char *database_type;
|
||||
struct {
|
||||
size_t count;
|
||||
const char **names;
|
||||
} languages;
|
||||
uint16_t binary_format_major_version;
|
||||
uint16_t binary_format_minor_version;
|
||||
uint64_t build_epoch;
|
||||
struct {
|
||||
size_t count;
|
||||
MMDB_description_s **descriptions;
|
||||
} description;
|
||||
} MMDB_metadata_s;
|
||||
|
||||
typedef struct MMDB_ipv4_start_node_s {
|
||||
uint16_t netmask;
|
||||
uint32_t node_value;
|
||||
} MMDB_ipv4_start_node_s;
|
||||
|
||||
typedef struct MMDB_s {
|
||||
uint32_t flags;
|
||||
const char *filename;
|
||||
ssize_t file_size;
|
||||
const uint8_t *file_content;
|
||||
const uint8_t *data_section;
|
||||
uint32_t data_section_size;
|
||||
const uint8_t *metadata_section;
|
||||
uint32_t metadata_section_size;
|
||||
uint16_t full_record_byte_size;
|
||||
uint16_t depth;
|
||||
MMDB_ipv4_start_node_s ipv4_start_node;
|
||||
MMDB_metadata_s metadata;
|
||||
} MMDB_s;
|
||||
|
||||
typedef char * pchar;
|
||||
|
||||
MMDB_lookup_result_s MMDB_lookup_string(MMDB_s *const mmdb, const char *const ipstr, int *const gai_error,int *const mmdb_error);
|
||||
int MMDB_open(const char *const filename, uint32_t flags, MMDB_s *const mmdb);
|
||||
int MMDB_aget_value(MMDB_entry_s *const start, MMDB_entry_data_s *const entry_data, const char *const *const path);
|
||||
char *MMDB_strerror(int error_code);
|
||||
|
||||
int MMDB_get_entry_data_list(MMDB_entry_s *start, MMDB_entry_data_list_s **const entry_data_list);
|
||||
void MMDB_free_entry_data_list(MMDB_entry_data_list_s *const entry_data_list);
|
||||
void MMDB_close(MMDB_s *const mmdb);
|
||||
const char *gai_strerror(int errcode);
|
||||
]]
|
||||
|
||||
-- error codes
|
||||
-- https://github.com/maxmind/libmaxminddb/blob/master/include/maxminddb.h#L66
|
||||
local MMDB_SUCCESS = 0
|
||||
local MMDB_FILE_OPEN_ERROR = 1
|
||||
local MMDB_CORRUPT_SEARCH_TREE_ERROR = 2
|
||||
local MMDB_INVALID_METADATA_ERROR = 3
|
||||
local MMDB_IO_ERROR = 4
|
||||
local MMDB_OUT_OF_MEMORY_ERROR = 5
|
||||
local MMDB_UNKNOWN_DATABASE_FORMAT_ERROR = 6
|
||||
local MMDB_INVALID_DATA_ERROR = 7
|
||||
local MMDB_INVALID_LOOKUP_PATH_ERROR = 8
|
||||
local MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR = 9
|
||||
local MMDB_INVALID_NODE_NUMBER_ERROR = 10
|
||||
local MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR = 11
|
||||
|
||||
-- data type
|
||||
-- https://github.com/maxmind/libmaxminddb/blob/master/include/maxminddb.h#L40
|
||||
local MMDB_DATA_TYPE_EXTENDED = 0
|
||||
local MMDB_DATA_TYPE_POINTER = 1
|
||||
local MMDB_DATA_TYPE_UTF8_STRING = 2
|
||||
local MMDB_DATA_TYPE_DOUBLE = 3
|
||||
local MMDB_DATA_TYPE_BYTES = 4
|
||||
local MMDB_DATA_TYPE_UINT16 = 5
|
||||
local MMDB_DATA_TYPE_UINT32 = 6
|
||||
local MMDB_DATA_TYPE_MAP = 7
|
||||
local MMDB_DATA_TYPE_INT32 = 8
|
||||
local MMDB_DATA_TYPE_UINT64 = 9
|
||||
local MMDB_DATA_TYPE_UINT128 = 10
|
||||
local MMDB_DATA_TYPE_ARRAY = 11
|
||||
local MMDB_DATA_TYPE_CONTAINER = 12
|
||||
local MMDB_DATA_TYPE_END_MARKER = 13
|
||||
local MMDB_DATA_TYPE_BOOLEAN = 14
|
||||
local MMDB_DATA_TYPE_FLOAT = 15
|
||||
|
||||
-- copy from https://github.com/lilien1010/lua-resty-maxminddb/blob/f96633e2428f8f7bcc1e2a7a28b747b33233a8db/resty/maxminddb.lua#L136-L138
|
||||
|
||||
local initted = false
|
||||
|
||||
local function mmdb_strerror(profile, rc)
|
||||
return ffi_str(_D[profile].maxm.MMDB_strerror(rc))
|
||||
end
|
||||
|
||||
local function gai_strerror(rc)
|
||||
return ffi_str(C.gai_strerror(rc))
|
||||
end
|
||||
|
||||
function _M.init(profiles)
|
||||
for profile, location in pairs(profiles) do
|
||||
_D[profile] = {}
|
||||
_D[profile].maxm = ffi.load('/usr/local/openresty/1pwaf/data/libmaxminddb.so')
|
||||
_D[profile].mmdb = ffi_new('MMDB_s')
|
||||
local maxmind_ready = _D[profile].maxm.MMDB_open(location, 0, _D[profile].mmdb)
|
||||
if maxmind_ready ~= MMDB_SUCCESS then
|
||||
return nil, mmdb_strerror(profile, maxmind_ready)
|
||||
end
|
||||
ffi_gc(_D[profile].mmdb, _D[profile].maxm.MMDB_close)
|
||||
end
|
||||
|
||||
--if not initted then
|
||||
-- local maxmind_ready = maxm.MMDB_open(dbfile, 0, mmdb)
|
||||
--
|
||||
-- if maxmind_ready ~= MMDB_SUCCESS then
|
||||
-- return nil, mmdb_strerror(maxmind_ready)
|
||||
-- end
|
||||
--
|
||||
--
|
||||
--
|
||||
-- ffi_gc(mmdb, maxm.MMDB_close)
|
||||
--end
|
||||
initted = true
|
||||
return initted
|
||||
end
|
||||
|
||||
function _M.initted()
|
||||
return initted
|
||||
end
|
||||
|
||||
-- https://github.com/maxmind/libmaxminddb/blob/master/src/maxminddb.c#L1938
|
||||
-- LOCAL MMDB_entry_data_list_s *dump_entry_data_list( FILE *stream, MMDB_entry_data_list_s *entry_data_list, int indent, int *status)
|
||||
local function _dump_entry_data_list(entry_data_list, status)
|
||||
|
||||
if not entry_data_list then
|
||||
return nil, MMDB_INVALID_DATA_ERROR
|
||||
end
|
||||
|
||||
local entry_data_item = entry_data_list[0].entry_data
|
||||
local data_type = entry_data_item.type
|
||||
local data_size = entry_data_item.data_size
|
||||
local result
|
||||
|
||||
if data_type == MMDB_DATA_TYPE_MAP then
|
||||
result = {}
|
||||
|
||||
local size = entry_data_item.data_size
|
||||
|
||||
entry_data_list = entry_data_list[0].next
|
||||
|
||||
while (size > 0 and entry_data_list)
|
||||
do
|
||||
entry_data_item = entry_data_list[0].entry_data
|
||||
data_type = entry_data_item.type
|
||||
data_size = entry_data_item.data_size
|
||||
|
||||
if MMDB_DATA_TYPE_UTF8_STRING ~= data_type then
|
||||
return nil, MMDB_INVALID_DATA_ERROR
|
||||
end
|
||||
|
||||
local key = ffi_str(entry_data_item.utf8_string, data_size)
|
||||
|
||||
if not key then
|
||||
return nil, MMDB_OUT_OF_MEMORY_ERROR
|
||||
end
|
||||
|
||||
local val
|
||||
entry_data_list = entry_data_list[0].next
|
||||
entry_data_list, status, val = _dump_entry_data_list(entry_data_list)
|
||||
|
||||
if status ~= MMDB_SUCCESS then
|
||||
return nil, status
|
||||
end
|
||||
|
||||
result[key] = val
|
||||
|
||||
size = size - 1
|
||||
end
|
||||
|
||||
|
||||
elseif entry_data_list[0].entry_data.type == MMDB_DATA_TYPE_ARRAY then
|
||||
local size = entry_data_list[0].entry_data.data_size
|
||||
result = {}
|
||||
|
||||
entry_data_list = entry_data_list[0].next
|
||||
|
||||
local i = 1
|
||||
while (i <= size and entry_data_list)
|
||||
do
|
||||
local val
|
||||
entry_data_list, status, val = _dump_entry_data_list(entry_data_list)
|
||||
|
||||
if status ~= MMDB_SUCCESS then
|
||||
return nil, nil, val
|
||||
end
|
||||
|
||||
result[i] = val
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
|
||||
else
|
||||
entry_data_item = entry_data_list[0].entry_data
|
||||
data_type = entry_data_item.type
|
||||
data_size = entry_data_item.data_size
|
||||
|
||||
local val
|
||||
-- string type "key":"val"
|
||||
-- other type "key":val
|
||||
-- default other type
|
||||
if data_type == MMDB_DATA_TYPE_UTF8_STRING then
|
||||
val = ffi_str(entry_data_item.utf8_string, data_size)
|
||||
if not val then
|
||||
status = MMDB_OUT_OF_MEMORY_ERROR
|
||||
return nil, status
|
||||
end
|
||||
elseif data_type == MMDB_DATA_TYPE_BYTES then
|
||||
val = ffi_str(ffi_cast('char * ', entry_data_item.bytes), data_size)
|
||||
if not val then
|
||||
status = MMDB_OUT_OF_MEMORY_ERROR
|
||||
return nil, status
|
||||
end
|
||||
elseif data_type == MMDB_DATA_TYPE_DOUBLE then
|
||||
val = entry_data_item.double_value
|
||||
elseif data_type == MMDB_DATA_TYPE_FLOAT then
|
||||
val = entry_data_item.float_value
|
||||
elseif data_type == MMDB_DATA_TYPE_UINT16 then
|
||||
val = entry_data_item.uint16
|
||||
elseif data_type == MMDB_DATA_TYPE_UINT32 then
|
||||
val = entry_data_item.uint32
|
||||
elseif data_type == MMDB_DATA_TYPE_BOOLEAN then
|
||||
val = entry_data_item.boolean
|
||||
elseif data_type == MMDB_DATA_TYPE_UINT64 then
|
||||
val = entry_data_item.uint64
|
||||
elseif data_type == MMDB_DATA_TYPE_INT32 then
|
||||
val = entry_data_item.int32
|
||||
else
|
||||
return nil, MMDB_INVALID_DATA_ERROR
|
||||
end
|
||||
|
||||
result = val
|
||||
entry_data_list = entry_data_list[0].next
|
||||
end
|
||||
|
||||
status = MMDB_SUCCESS
|
||||
return entry_data_list, status, result
|
||||
end
|
||||
|
||||
function _M.lookup(profile, ip)
|
||||
|
||||
if not initted then
|
||||
return nil, "not initialized"
|
||||
end
|
||||
|
||||
-- copy from https://github.com/lilien1010/lua-resty-maxminddb/blob/f96633e2428f8f7bcc1e2a7a28b747b33233a8db/resty/maxminddb.lua#L159-L176
|
||||
local gai_error = ffi_new('int[1]')
|
||||
local mmdb_error = ffi_new('int[1]')
|
||||
|
||||
local result = _D[profile].maxm.MMDB_lookup_string(_D[profile].mmdb, ip, gai_error, mmdb_error)
|
||||
|
||||
if mmdb_error[0] ~= MMDB_SUCCESS then
|
||||
return nil, 'lookup failed: ' .. mmdb_strerror(profile, mmdb_error[0])
|
||||
end
|
||||
|
||||
if gai_error[0] ~= MMDB_SUCCESS then
|
||||
return nil, 'lookup failed: ' .. gai_strerror(gai_error[0])
|
||||
end
|
||||
|
||||
if true ~= result.found_entry then
|
||||
return nil, 'not found'
|
||||
end
|
||||
|
||||
local entry_data_list = ffi_cast('MMDB_entry_data_list_s **const', ffi_new("MMDB_entry_data_list_s"))
|
||||
|
||||
local status = _D[profile].maxm.MMDB_get_entry_data_list(result.entry, entry_data_list)
|
||||
|
||||
if status ~= MMDB_SUCCESS then
|
||||
return nil, 'get entry data failed: ' .. mmdb_strerror(profile, status)
|
||||
end
|
||||
|
||||
local head = entry_data_list[0] -- Save so this can be passed to free fn.
|
||||
local _, status, result = _dump_entry_data_list(entry_data_list)
|
||||
_D[profile].maxm.MMDB_free_entry_data_list(head)
|
||||
|
||||
if status ~= MMDB_SUCCESS then
|
||||
return nil, 'dump entry data failed: ' .. mmdb_strerror(profile, status)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- copy from https://github.com/lilien1010/lua-resty-maxminddb/blob/master/resty/maxminddb.lua#L208
|
||||
-- https://www.maxmind.com/en/geoip2-databases you should download the mmdb file from maxmind
|
||||
|
||||
return _M;
|
149
plugins/openresty/waf/lib/stringutf8.lua
Normal file
149
plugins/openresty/waf/lib/stringutf8.lua
Normal file
@ -0,0 +1,149 @@
|
||||
local error = error
|
||||
local str_len = string.len
|
||||
local new_table = table.new
|
||||
local concat_table = table.concat
|
||||
local insert_table = table.insert
|
||||
local byte_str = string.byte
|
||||
local sub_str = string.sub
|
||||
local type = type
|
||||
local abs = math.abs
|
||||
local match_str = string.match
|
||||
local ngx_re_gsub = ngx.re.gsub
|
||||
|
||||
local _M = {}
|
||||
|
||||
local INDEX_OUT_OF_RANGE = "String index out of range: "
|
||||
local NOT_NUMBER = "number expected, got "
|
||||
local NOT_STRING = "string expected, got "
|
||||
local NOT_STRING_NIL = "string expected, got nil"
|
||||
|
||||
function _M.to_char_array(str)
|
||||
local array
|
||||
if str then
|
||||
local length = str_len(str)
|
||||
array = new_table(length, 0)
|
||||
|
||||
local byteLength = 1
|
||||
local i, j = 1, 1
|
||||
while i <= length do
|
||||
local firstByte = byte_str(str, i)
|
||||
if firstByte >= 0 and firstByte < 128 then
|
||||
byteLength = 1
|
||||
|
||||
elseif firstByte > 191 and firstByte < 224 then
|
||||
byteLength = 2
|
||||
|
||||
elseif firstByte > 223 and firstByte < 240 then
|
||||
byteLength = 3
|
||||
|
||||
elseif firstByte > 239 and firstByte < 248 then
|
||||
byteLength = 4
|
||||
end
|
||||
|
||||
j = i + byteLength
|
||||
local char = sub_str(str, i, j - 1)
|
||||
i = j
|
||||
insert_table(array, char)
|
||||
end
|
||||
end
|
||||
|
||||
return array
|
||||
end
|
||||
|
||||
function _M.sub(str, i, j)
|
||||
local str_sub
|
||||
if str then
|
||||
if i == nil then
|
||||
i = 1
|
||||
end
|
||||
|
||||
if type(i) ~= "number" then
|
||||
error(NOT_NUMBER .. type(i))
|
||||
end
|
||||
|
||||
if i < 1 then
|
||||
error(INDEX_OUT_OF_RANGE .. i)
|
||||
end
|
||||
|
||||
if j then
|
||||
if type(j) ~= "number" then
|
||||
error(NOT_NUMBER .. type(j))
|
||||
end
|
||||
end
|
||||
|
||||
local array = _M.to_char_array(str)
|
||||
if array then
|
||||
local length = #array
|
||||
local subLen = length - i
|
||||
if subLen < 0 then
|
||||
error(INDEX_OUT_OF_RANGE .. subLen)
|
||||
end
|
||||
|
||||
if not j then
|
||||
str_sub = concat_table(array, "", i)
|
||||
else
|
||||
if abs(j) > length then
|
||||
error(INDEX_OUT_OF_RANGE .. j)
|
||||
end
|
||||
if j < 0 then
|
||||
j = length + j + 1
|
||||
end
|
||||
str_sub = concat_table(array, "", i, j)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return str_sub
|
||||
end
|
||||
|
||||
function _M.trim(str)
|
||||
if str then
|
||||
str = ngx_re_gsub(str, "^\\s*|\\s*$", "", "jo")
|
||||
end
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
function _M.len(str)
|
||||
local str_length = 0
|
||||
if str then
|
||||
if type(str) ~= "string" then
|
||||
error(NOT_STRING .. type(str))
|
||||
end
|
||||
|
||||
local length = str_len(str)
|
||||
|
||||
local i = 1
|
||||
while i <= length do
|
||||
local firstByte = byte_str(str, i)
|
||||
if firstByte >= 0 and firstByte < 128 then
|
||||
i = i + 1
|
||||
|
||||
elseif firstByte > 191 and firstByte < 224 then
|
||||
i = i + 2
|
||||
|
||||
elseif firstByte > 223 and firstByte < 240 then
|
||||
i = i + 3
|
||||
|
||||
elseif firstByte > 239 and firstByte < 248 then
|
||||
i = i + 4
|
||||
end
|
||||
|
||||
str_length = str_length + 1
|
||||
end
|
||||
else
|
||||
error(NOT_STRING_NIL)
|
||||
end
|
||||
|
||||
return str_length
|
||||
end
|
||||
|
||||
function _M.default_if_blank(str, default_str)
|
||||
if str == nil or match_str(str, "^%s*$") then
|
||||
return default_str
|
||||
end
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
return _M
|
123
plugins/openresty/waf/lib/utils.lua
Normal file
123
plugins/openresty/waf/lib/utils.lua
Normal file
@ -0,0 +1,123 @@
|
||||
local sub_str = string.sub
|
||||
local pairs = pairs
|
||||
local insert_table = table.insert
|
||||
local tonumber = tonumber
|
||||
local ipairs = ipairs
|
||||
local type = type
|
||||
local find_str = string.find
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.split(input_string, delimiter)
|
||||
local result = {}
|
||||
for part in input_string:gmatch("([^" .. delimiter .. "]+)") do
|
||||
insert_table(result, part)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function _M.get_cookie_list(cookie_str)
|
||||
local cookies = {}
|
||||
for cookie in cookie_str:gmatch("([^;]+)") do
|
||||
local key, value = cookie:match("^%s*([^=]+)=(.*)$")
|
||||
if key and value then
|
||||
cookies[key] = value
|
||||
end
|
||||
end
|
||||
return cookies
|
||||
end
|
||||
|
||||
function _M.unescape_uri(str)
|
||||
local newStr = str
|
||||
for t = 1, 2 do
|
||||
local temp = ngx.unescape_uri(newStr)
|
||||
if not temp then
|
||||
break
|
||||
end
|
||||
newStr = temp
|
||||
end
|
||||
return newStr
|
||||
end
|
||||
|
||||
function _M.get_expire_time()
|
||||
local localtime = ngx.localtime()
|
||||
local hour = sub_str(localtime, 12, 13)
|
||||
local expire_time = (24 - tonumber(hour)) * 3600
|
||||
return expire_time
|
||||
end
|
||||
|
||||
function _M.get_date_hour()
|
||||
local localtime = ngx.localtime()
|
||||
local hour = sub_str(localtime, 1, 13)
|
||||
return hour
|
||||
end
|
||||
|
||||
function _M.getHours()
|
||||
local hours = {}
|
||||
local today = ngx.today()
|
||||
local hour = nil
|
||||
for i = 0, 23 do
|
||||
if i < 10 then
|
||||
hour = today .. ' 0' .. i
|
||||
else
|
||||
hour = today .. ' ' .. i
|
||||
end
|
||||
hours[i + 1] = hour
|
||||
end
|
||||
|
||||
return hours
|
||||
end
|
||||
|
||||
function _M.ipv4_to_int(ip)
|
||||
local ipInt = 0
|
||||
for i, octet in ipairs({ ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") }) do
|
||||
ipInt = ipInt + tonumber(octet) * 256 ^ (4 - i)
|
||||
end
|
||||
return ipInt
|
||||
end
|
||||
|
||||
function _M.is_ipv6(ip)
|
||||
if find_str(ip, ':') then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.is_ip_in_array(ip, ipStart, ipEnd)
|
||||
if ip >= ipStart and ip <= ipEnd then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.get_real_ip()
|
||||
local var = ngx.var
|
||||
local ips = {
|
||||
var.http_x_forwarded_for,
|
||||
var.http_proxy_client_ip,
|
||||
var.http_wl_proxy_client_ip,
|
||||
var.http_http_client_ip,
|
||||
var.http_http_x_forwarded_for,
|
||||
var.remote_addr
|
||||
}
|
||||
|
||||
for _, ip in pairs(ips) do
|
||||
if ip and ip ~= "" then
|
||||
if type(ip) == "table" then
|
||||
ip = ip[1]
|
||||
end
|
||||
return ip
|
||||
end
|
||||
end
|
||||
|
||||
return "unknown"
|
||||
end
|
||||
|
||||
function _M.get_header(headerKey)
|
||||
return ngx.req.get_headers(20000)[headerKey]
|
||||
end
|
||||
|
||||
function _M.get_headers()
|
||||
return ngx.req.get_headers(20000)
|
||||
end
|
||||
return _M
|
232
plugins/openresty/waf/log_and_traffic.lua
Normal file
232
plugins/openresty/waf/log_and_traffic.lua
Normal file
@ -0,0 +1,232 @@
|
||||
local utils = require "utils"
|
||||
local stringutf8 = require "stringutf8"
|
||||
local logger_factory = require "logger_factory"
|
||||
local db = require "db"
|
||||
local config = require "config"
|
||||
local redis_util = require "redis_util"
|
||||
local action = require "action"
|
||||
|
||||
local upper_str = string.upper
|
||||
local concat_table = table.concat
|
||||
local tonumber = tonumber
|
||||
local get_expire_time = utils.get_expire_time
|
||||
local get_date_hour = utils.get_date_hour
|
||||
local get_today = ngx.today
|
||||
|
||||
local ATTACK_PREFIX = "attack_"
|
||||
local ATTACK_TYPE_PREFIX = "attack_type_"
|
||||
|
||||
local function writeAttackLog()
|
||||
local rule_table = ngx.ctx.rule_table
|
||||
local data = ngx.ctx.hitData
|
||||
local action = ngx.ctx.action
|
||||
local rule = rule_table.rule
|
||||
|
||||
local rule_type = rule_table.type
|
||||
if not rule_type then
|
||||
rule_type = "default"
|
||||
end
|
||||
|
||||
local realIp = ngx.ctx.ip
|
||||
|
||||
local geoip = ngx.ctx.geoip
|
||||
local country = geoip.country["zh"] or ""
|
||||
local province = geoip.province["zh"] or ""
|
||||
local city = ""
|
||||
local longitude = geoip.longitude
|
||||
local latitude = geoip.latitude
|
||||
|
||||
local method = ngx.req.get_method()
|
||||
local uri = ngx.var.request_uri
|
||||
local ua = ngx.ctx.ua
|
||||
local host = ngx.var.server_name
|
||||
local protocol = ngx.var.server_protocol
|
||||
local attackTime = ngx.localtime()
|
||||
|
||||
local website_key = ngx.ctx.website_key
|
||||
|
||||
local address = country .. province .. city
|
||||
address = stringutf8.default_if_blank(address, '-')
|
||||
ua = stringutf8.default_if_blank(ua, '-')
|
||||
data = stringutf8.default_if_blank(data, '-')
|
||||
|
||||
local log_path = "/www/sites/" .. website_key .. "/attack.log"
|
||||
local logStr = concat_table({ rule_type, realIp, address, "[" .. attackTime .. "]", '"' .. method, host, uri, protocol .. '"', data, '"' .. ua .. '"', '"' .. rule .. '"', action }, ' ')
|
||||
local host_logger = logger_factory.get_logger(log_path, host, true)
|
||||
host_logger:log(logStr .. '\n')
|
||||
|
||||
db.init_db()
|
||||
if wafdb == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local isBlock = 0
|
||||
local blocking_time = 0
|
||||
if ngx.ctx.ipBlocked then
|
||||
isBlock = 1
|
||||
blocking_time = tonumber(rule_table.ipBlockTime)
|
||||
end
|
||||
|
||||
local insertQuery = [[
|
||||
INSERT INTO attack_log (
|
||||
ip, ip_city, ip_country, ip_subdivisions, ip_continent,
|
||||
ip_longitude, ip_latitude, time, localtime, server_name,
|
||||
website_key, host, method, uri, user_agent, rule,
|
||||
nginx_log, blocking_time, action, msg, params, is_block
|
||||
) VALUES (
|
||||
:realIp, :city, :country, :subdivisions, :continent,
|
||||
:longitude, :latitude, :time, :localtime, :host,
|
||||
:website_key, :host, :method, :uri, :ua, :rule_type,
|
||||
:logStr, :blocking_time, :action, :msg, :params, :is_block
|
||||
)
|
||||
]]
|
||||
|
||||
local stmt = wafdb:prepare(insertQuery)
|
||||
|
||||
stmt:bind_names {
|
||||
realIp = realIp,
|
||||
city = city,
|
||||
country = country,
|
||||
subdivisions = "",
|
||||
continent = "",
|
||||
longitude = longitude,
|
||||
latitude = latitude,
|
||||
time = os.time(),
|
||||
localtime = os.date("%Y-%m-%d %H:%M:%S", os.time()),
|
||||
host = host,
|
||||
website_key = website_key,
|
||||
method = method,
|
||||
uri = uri,
|
||||
ua = ua,
|
||||
rule_type = rule_type,
|
||||
logStr = logStr,
|
||||
blocking_time = blocking_time or 0,
|
||||
action = action,
|
||||
msg = "msg",
|
||||
params = "Params",
|
||||
is_block = isBlock
|
||||
}
|
||||
|
||||
local code = stmt:step()
|
||||
stmt:finalize()
|
||||
|
||||
if code ~= 101 then
|
||||
local errorMsg = wafdb:errmsg()
|
||||
if errorMsg then
|
||||
ngx.log(ngx.ERR, "insert attack_log error", errorMsg)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local function writeIPBlockLog()
|
||||
local rule_table = ngx.ctx.rule_table
|
||||
local ip = ngx.ctx.ip
|
||||
local website_key = ngx.ctx.website_key
|
||||
local log_path = "/www/sites/" .. website_key .. "/attack.log"
|
||||
local host_logger = logger_factory.get_logger(log_path .. "ipBlock.log", 'ipBlock', false)
|
||||
|
||||
host_logger:log(concat_table({ ngx.localtime(), ip, rule_table.type, rule_table.ipBlockTime .. 's' }, ' ') .. "\n")
|
||||
|
||||
--todo 永久拉黑IP
|
||||
--if rule_table.ipBlockTimeout == 0 then
|
||||
-- local ipBlackLogger = logger_factory.get_logger(rulePath .. "ipBlackList", 'ipBlack', false)
|
||||
-- ipBlackLogger:log(ip .. "\n")
|
||||
--end
|
||||
end
|
||||
|
||||
-- 按小时统计当天请求流量,存入缓存,key格式:2023-05-05 09
|
||||
local function countRequestTraffic()
|
||||
local hour = get_date_hour()
|
||||
local dict = ngx.shared.dict_req_count
|
||||
local expire_time = get_expire_time()
|
||||
local count, err = dict:incr(hour, 1, 0, expire_time)
|
||||
if not count then
|
||||
dict:set(hour, 1, expire_time)
|
||||
ngx.log(ngx.ERR, "failed to count traffic ", err)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
按小时统计当天攻击请求流量,存入缓存,key格式:attack_2023-05-05 09
|
||||
按天统计当天所有攻击类型流量,存入缓存,key格式:attack_type_2023-05-05_ARGS
|
||||
]]
|
||||
local function countAttackRequestTraffic()
|
||||
local rule_table = ngx.ctx.rule_table
|
||||
local rule_type = ""
|
||||
if rule_table.rule_type then
|
||||
rule_type = upper_str(rule_table.rule_type)
|
||||
end
|
||||
if rule_table.type then
|
||||
rule_type = upper_str(rule_table.type)
|
||||
end
|
||||
local dict = ngx.shared.dict_req_count
|
||||
local count, err = nil, nil
|
||||
local expire_time = get_expire_time()
|
||||
|
||||
if rule_type ~= 'WHITEIP' then
|
||||
local hour = get_date_hour()
|
||||
local key = ATTACK_PREFIX .. hour
|
||||
count, err = dict:incr(key, 1, 0, expire_time)
|
||||
if not count then
|
||||
dict:set(key, 1, expire_time)
|
||||
ngx.log(ngx.ERR, "failed to count attack traffic ", err)
|
||||
end
|
||||
end
|
||||
|
||||
local today = get_today() .. '_'
|
||||
local type_key = ATTACK_TYPE_PREFIX .. today .. rule_type
|
||||
count, err = dict:incr(type_key, 1, 0, expire_time)
|
||||
|
||||
if not count and err == "not found" then
|
||||
dict:set(type_key, 1, expire_time)
|
||||
ngx.log(ngx.ERR, "failed to count attack traffic ", err)
|
||||
end
|
||||
end
|
||||
|
||||
local function count_not_found()
|
||||
if ngx.status ~= 404 then
|
||||
return
|
||||
end
|
||||
if config.is_global_state_on("notFoundCount") then
|
||||
local ip = ngx.ctx.ip
|
||||
local not_found_config = config.get_global_config("notFoundCount")
|
||||
local key = ip
|
||||
|
||||
if config.is_redis_on() then
|
||||
key = "cc_attack_count:" .. key
|
||||
local count, _ = redis_util.incr(key, not_found_config.duration)
|
||||
if not count then
|
||||
redis_util.set(key, 1, not_found_config.duration)
|
||||
elseif count >= not_found_config.threshold then
|
||||
action.block_ip(ip, not_found_config)
|
||||
return
|
||||
end
|
||||
else
|
||||
key = ip .. "not_found"
|
||||
local limit = ngx.shared.waf_limit
|
||||
local count, _ = limit:incr(key, 1, 0, not_found_config.duration)
|
||||
if not count then
|
||||
limit:set(key, 1, not_found_config.duration)
|
||||
elseif count >= not_found_config.threshold then
|
||||
action.block_ip(ip, not_found_config)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if config.is_waf_on() then
|
||||
count_not_found()
|
||||
countRequestTraffic()
|
||||
local isAttack = ngx.ctx.isAttack
|
||||
|
||||
if isAttack then
|
||||
writeAttackLog()
|
||||
countAttackRequestTraffic()
|
||||
end
|
||||
|
||||
-- if ngx.ctx.ipBlocked then
|
||||
-- writeIPBlockLog()
|
||||
-- end
|
||||
end
|
23
plugins/openresty/waf/rules/acl.json
Normal file
23
plugins/openresty/waf/rules/acl.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "no Cookie",
|
||||
"name": "拦截不带Cookie的请求",
|
||||
"conditions": [
|
||||
{
|
||||
"field": "URL",
|
||||
"pattern": "/test/\\d+\\.html"
|
||||
},
|
||||
{
|
||||
"field": "Cookie",
|
||||
"pattern": ""
|
||||
}
|
||||
],
|
||||
"action": "deny",
|
||||
"autoIpBlock": "off",
|
||||
"ipBlockTimeout": 60,
|
||||
"description": "拦截不带Cookie的请求"
|
||||
}
|
||||
]
|
||||
}
|
88
plugins/openresty/waf/rules/args.json
Normal file
88
plugins/openresty/waf/rules/args.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "select.+(from|limit)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:(union(.*?)select))"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "having|rongjitest"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "sleep\\((\\s*)(\\d*)(\\s*)\\)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "benchmark\\((.*)\\,(.*)\\)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:from\\W+information_schema\\W)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:(?:current_)user|database|schema|connection_id)\\s*\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:etc\\/\\W*passwd)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "into(\\s+)+(?:dump|out)file\\s*"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "group\\s+by.+\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "xwork.MethodAccessor"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "xwork\\.MethodAccessor"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "java\\.lang"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\["
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\<(iframe|script|body|img|layer|div|meta|style|base|object|input)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(onmouseover|onerror|onload)\\="
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "/shell?cd+/tmp;\\s*rm+-rf\\+\\*;\\s*wget"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "/systembc/password.php"
|
||||
},
|
||||
{
|
||||
"state":"on",
|
||||
"rule":"(Acunetix-Aspect|Acunetix-Aspect-Password|Acunetix-Aspect-Queries|X-WIPP|X-RequestManager-Memo|X-Request-Memo|X-Scan-Memo)"
|
||||
}
|
||||
]
|
||||
}
|
3
plugins/openresty/waf/rules/ccurl.json
Normal file
3
plugins/openresty/waf/rules/ccurl.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
88
plugins/openresty/waf/rules/cookie.json
Normal file
88
plugins/openresty/waf/rules/cookie.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\.\\./"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\:\\$"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\$\\{"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "select.+(from|limit)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:(union(.*?)select))"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "having|rongjitest"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "sleep\\((\\s*)(\\d*)(\\s*)\\)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "benchmark\\((.*)\\,(.*)\\)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "base64_decode\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:from\\W+information_schema\\W)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:(?:current_)user|database|schema|connection_id)\\s*\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:etc\\/\\W*passwd)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "into(\\s+)+(?:dump|out)file\\s*"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "group\\s+by.+\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "xwork.MethodAccessor"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\("
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "xwork\\.MethodAccessor"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "java\\.lang"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\["
|
||||
},
|
||||
{
|
||||
"state":"on",
|
||||
"rule":"(CustomCookie|acunetixCookie)"
|
||||
}
|
||||
]
|
||||
}
|
16
plugins/openresty/waf/rules/defaultUaBlack.json
Normal file
16
plugins/openresty/waf/rules/defaultUaBlack.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"nextId": 3,
|
||||
"rules": [
|
||||
{
|
||||
"id": 1,
|
||||
"state": "on",
|
||||
"type": "defaultUABlack",
|
||||
"action": "deny",
|
||||
"rule": "HTTrack|Apache-HttpClient|harvest|audit|dirbuster|pangolin|nmap|sqln|hydra|Parser|libwww|BBBike|sqlmap|w3af|owasp|Nikto|fimap|havij|zmeu|BabyKrokodil|netsparker|httperf| SF/",
|
||||
"ipBlock": "on",
|
||||
"ipBlockTimeout": 600,
|
||||
"code": 403,
|
||||
"description": "一些不友好ua"
|
||||
}
|
||||
]
|
||||
}
|
7
plugins/openresty/waf/rules/header.json
Normal file
7
plugins/openresty/waf/rules/header.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"rules":[
|
||||
{"state":"on","rule":"/TomcatBypass/Command/Base64"},
|
||||
{"state":"on","rule":"j\\S*ndi\\S*:\\S*(?:dap|dns)\\S+"},
|
||||
{"state":"on","rule":"(/acunetix-wvs-test-for-some-inexistent-file|netsparker|acunetix_wvs_security_test|AppScan|XSS@HERE)"}
|
||||
]
|
||||
}
|
10
plugins/openresty/waf/rules/ipBlack.json
Normal file
10
plugins/openresty/waf/rules/ipBlack.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"name": "拦截IP",
|
||||
"state": "on",
|
||||
"type": "ipv4",
|
||||
"ipv4": "123"
|
||||
}
|
||||
]
|
||||
}
|
10000
plugins/openresty/waf/rules/ipBlackList
Normal file
10000
plugins/openresty/waf/rules/ipBlackList
Normal file
File diff suppressed because it is too large
Load Diff
9
plugins/openresty/waf/rules/ipWhite.json
Normal file
9
plugins/openresty/waf/rules/ipWhite.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"type": "ipv4",
|
||||
"ipv4": "123"
|
||||
}
|
||||
]
|
||||
}
|
88
plugins/openresty/waf/rules/methodWhite.json
Normal file
88
plugins/openresty/waf/rules/methodWhite.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "GET"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "POST"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PUT"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "DELETE"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PATCH"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "HEAD"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "OPTIONS"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "TRACE"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "CONNECT"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PROPFIND"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PROPPATCH"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "MKCOL"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "COPY"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "MOVE"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "LOCK"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "UNLOCK"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "LINK"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "UNLINK"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "WRAPPED"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PROPFIND"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "SRARCH"
|
||||
}
|
||||
]
|
||||
}
|
23
plugins/openresty/waf/rules/post.json
Normal file
23
plugins/openresty/waf/rules/post.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"rules":[
|
||||
{"state":"on","action":"deny","rule":"select.+(from|limit)"},
|
||||
{"state":"on","action":"deny","rule":"(?:(union(.*?)select))"},
|
||||
{"state":"on","action":"deny","rule":"having|rongjitest"},
|
||||
{"state":"on","action":"deny","rule":"sleep\\((\\s*)(\\d*)(\\s*)\\)"},
|
||||
{"state":"on","action":"deny","rule":"benchmark\\((.*)\\,(.*)\\)"},
|
||||
{"state":"on","action":"deny","rule":"base64_decode\\("},
|
||||
{"state":"on","action":"deny","rule":"(?:from\\W+information_schema\\W)"},
|
||||
{"state":"on","action":"deny","rule":"(?:(?:current_)user|database|schema|connection_id)\\s*\\("},
|
||||
{"state":"on","action":"deny","rule":"(?:etc\\/\\W*passwd)"},
|
||||
{"state":"on","action":"deny","rule":"into(\\s+)+(?:dump|out)file\\s*"},
|
||||
{"state":"on","action":"deny","rule":"group\\s+by.+\\("},
|
||||
{"state":"on","action":"deny","rule":"xwork.MethodAccessor"},
|
||||
{"state":"on","action":"deny","rule":"(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\\("},
|
||||
{"state":"on","action":"deny","rule":"xwork\\.MethodAccessor"},
|
||||
{"state":"on","action":"deny","rule":"(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\\:\\/"},
|
||||
{"state":"on","action":"deny","rule":"java\\.lang"},
|
||||
{"state":"on","action":"deny","rule":"\\$_(GET|post|cookie|files|session|env|phplib|GLOBALS|SERVER)\\["},
|
||||
{"state":"on","action":"deny","rule":"\\<(iframe|script|body|img|layer|div|meta|style|base|object|input)"},
|
||||
{"state":"on","action":"deny","rule":"(onmouseover|onerror|onload)\\="}
|
||||
]
|
||||
}
|
5
plugins/openresty/waf/rules/uaBlack.json
Normal file
5
plugins/openresty/waf/rules/uaBlack.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": [
|
||||
|
||||
]
|
||||
}
|
8
plugins/openresty/waf/rules/uaWhite.json
Normal file
8
plugins/openresty/waf/rules/uaWhite.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "PostmanRuntime/7.36.1"
|
||||
}
|
||||
]
|
||||
}
|
44
plugins/openresty/waf/rules/urlBlack.json
Normal file
44
plugins/openresty/waf/rules/urlBlack.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\.(svn|htaccess|bash_history)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "\\.(bak|inc|old|mdb|sql|backup|java|class)$"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(vhost|bbs|host|wwwroot|www|site|root|hytop|flashfxp).*\\.rar"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(phpmyadmin|jmx-console|jmxinvokerservlet)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "(?:phpMyAdmin2|phpMyAdmin|phpmyadmin|dbadmin|pma|myadmin|admin|mysql)/scripts/setup%.php"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "java\\.lang"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "/(attachments|upimg|images|css|uploadfiles|html|uploads|templets|static|template|data|inc|forumdata|upload|includes|cache|avatar)/(\\\\w+).(php|jsp)"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "wp-includes/wlwmanifest.xml"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "<php>die(@md5(HelloThinkCMF))</php>"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"rule": "/boaform/admin/formLogin"
|
||||
}
|
||||
]
|
||||
}
|
14
plugins/openresty/waf/rules/urlWhite.json
Normal file
14
plugins/openresty/waf/rules/urlWhite.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"state": "on",
|
||||
"action": "allow",
|
||||
"rule": "/console/posts/editor"
|
||||
},
|
||||
{
|
||||
"state": "on",
|
||||
"action": "allow",
|
||||
"rule": "/apis/api.console.halo.run/v1alpha1/posts"
|
||||
}
|
||||
]
|
||||
}
|
33
plugins/openresty/waf/test.lua
Normal file
33
plugins/openresty/waf/test.lua
Normal file
@ -0,0 +1,33 @@
|
||||
function ip_to_int(ip)
|
||||
local ip_int = 0
|
||||
for i, octet in ipairs({ ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") }) do
|
||||
ip_int = ip_int + tonumber(octet) * 256 ^ (4 - i)
|
||||
end
|
||||
return ip_int
|
||||
end
|
||||
|
||||
------ 示例
|
||||
local ip_address = "222.249.139.98"
|
||||
local ip_number = ip_to_int(ip_address)
|
||||
print(ip_number)
|
||||
|
||||
--local geoip = require "lib.resty.maxminddb"
|
||||
--local cjson = require("cjson")
|
||||
--
|
||||
--geoip.init("/Users/wangzhengkun/Downloads/blackIP.mmdb")
|
||||
--
|
||||
--local geo = geoip.lookup("165.154.132.251")
|
||||
--
|
||||
--print(cjson.encode(geo))
|
||||
|
||||
--local fileUtils = require "lib.file"
|
||||
--local read_file2string = fileUtils.read_file2string
|
||||
--
|
||||
--local slideHtml = read_file2string("./html/" .. "slide.html")
|
||||
--
|
||||
--print(string.format(slideHtml, "1", "2"))
|
||||
|
||||
|
||||
--local today = os.date("%Y-%m-%d")
|
||||
--print(today)
|
||||
|
4
plugins/openresty/waf/waf.json
Normal file
4
plugins/openresty/waf/waf.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "1Panel WAF",
|
||||
"version": "1.0.0"
|
||||
}
|
163
plugins/openresty/waf/waf.lua
Normal file
163
plugins/openresty/waf/waf.lua
Normal file
@ -0,0 +1,163 @@
|
||||
local geoip = require "geoip"
|
||||
local lib = require "lib"
|
||||
local fileUtils = require "file"
|
||||
local config = require "config"
|
||||
local cc = require "cc"
|
||||
local utils = require "utils"
|
||||
local cjson = require "cjson"
|
||||
|
||||
local ipairs = ipairs
|
||||
local sub_str = string.sub
|
||||
local find_str = string.find
|
||||
local split_str = utils.split
|
||||
local encode = cjson.encode
|
||||
local read_file2table = fileUtils.read_file2table
|
||||
local tonumber = tonumber
|
||||
local date = os.date
|
||||
local format_str = string.format
|
||||
|
||||
local function get_website_key()
|
||||
local s_name = ngx.var.server_name
|
||||
local website_key = ngx.shared.waf:get(s_name)
|
||||
if website_key then
|
||||
return website_key
|
||||
end
|
||||
local websites = read_file2table(config.config_dir .. '/websites.json')
|
||||
if not websites then
|
||||
return s_name
|
||||
end
|
||||
for _, v in ipairs(websites)
|
||||
do
|
||||
for _, domain in ipairs(v['domains'])
|
||||
do
|
||||
if s_name == domain then
|
||||
ngx.shared.waf:set(s_name, v['key'], 3600)
|
||||
return v['key']
|
||||
end
|
||||
end
|
||||
end
|
||||
if s_name == '_' then
|
||||
s_name = "UNKNOWN"
|
||||
end
|
||||
return s_name
|
||||
end
|
||||
|
||||
local function init()
|
||||
local ip = utils.get_real_ip()
|
||||
ngx.ctx.ip = ip
|
||||
local ua = utils.get_header("user-agent")
|
||||
if not ua then
|
||||
ua = ""
|
||||
end
|
||||
ngx.ctx.ua = ua
|
||||
geoip.init()
|
||||
|
||||
ngx.ctx.geoip = geoip.lookup(ip)
|
||||
|
||||
local msg = "访问 IP " .. ip
|
||||
if ngx.ctx.geoip.country then
|
||||
msg = msg .. " 国家 " .. cjson.encode(ngx.ctx.geoip.country)
|
||||
end
|
||||
if ngx.ctx.geoip.province then
|
||||
msg = msg .. " 省份 " .. cjson.encode(ngx.ctx.geoip.province)
|
||||
end
|
||||
ngx.log(ngx.ERR, msg)
|
||||
|
||||
ngx.ctx.website_key = get_website_key()
|
||||
ngx.ctx.method = ngx.req.get_method()
|
||||
ngx.ctx.content_type = utils.get_header("content-type")
|
||||
if ngx.ctx.content_type then
|
||||
ngx.ctx.content_length = tonumber(utils.get_header("content-length"))
|
||||
end
|
||||
ngx.ctx.today = date("%Y-%m-%d")
|
||||
end
|
||||
|
||||
local function return_js(js_type)
|
||||
ngx.header.content_type = "text/html;charset=utf8"
|
||||
ngx.header.Cache_Control = "no-cache"
|
||||
local host = ngx.var.scheme .. "://" .. ngx.var.host
|
||||
local set_access_url = host .. "/set_access_token"
|
||||
local secret = config.get_secret()
|
||||
local key = ngx.md5(ngx.ctx.ip .. ngx.var.server_name .. ngx.ctx.website_key
|
||||
.. ngx.ctx.ua .. ngx.ctx.today .. secret)
|
||||
local value = ngx.md5(ngx.time() .. ngx.ctx.ip)
|
||||
local js = config.get_html_res(js_type)
|
||||
ngx.say(format_str(js, set_access_url, key, value))
|
||||
ngx.status = 200
|
||||
ngx.exit(200)
|
||||
end
|
||||
|
||||
local function return_json(data)
|
||||
ngx.header.content_type = "application/json;"
|
||||
ngx.header.Cache_Control = "no-cache"
|
||||
ngx.status = 200
|
||||
ngx.say(data)
|
||||
ngx.exit(200)
|
||||
end
|
||||
|
||||
local function waf_api()
|
||||
local uri = ngx.var.uri
|
||||
local prefix = sub_str(uri, 1, 15)
|
||||
if find_str(prefix, "/set_access_token") then
|
||||
local kvs = split_str(uri, "-")
|
||||
if kvs[2] and kvs[3] then
|
||||
cc.set_access_token(kvs[2], kvs[3])
|
||||
else
|
||||
ngx.exit(444)
|
||||
end
|
||||
end
|
||||
if uri == "/slide_check_" .. ngx.md5(ngx.ctx.ip) .. ".js" then
|
||||
return_js("slide_js")
|
||||
end
|
||||
|
||||
if uri == "/5s_check_" .. ngx.md5(ngx.ctx.ip) .. ".js" then
|
||||
return_js("five_second_js")
|
||||
end
|
||||
|
||||
if ngx.var.remote_addr ~= '127.0.0.1' then
|
||||
return false
|
||||
end
|
||||
if uri == '/reload_waf_config' then
|
||||
config.load_config_file()
|
||||
ngx.exit(200)
|
||||
end
|
||||
if uri == '/get_black_ip' then
|
||||
--TODO 从 redis 获取黑名单
|
||||
local data = ngx.shared.waf_black_ip:get_keys(0)
|
||||
return_json(encode(data))
|
||||
end
|
||||
end
|
||||
|
||||
if config.is_waf_on() then
|
||||
init()
|
||||
waf_api()
|
||||
|
||||
if lib.is_white_ip() then
|
||||
return true
|
||||
end
|
||||
lib.default_ip_black()
|
||||
lib.black_ip()
|
||||
|
||||
if lib.is_white_ua() then
|
||||
return true
|
||||
end
|
||||
lib.default_ua_black()
|
||||
lib.black_ua()
|
||||
|
||||
lib.cc_url()
|
||||
if lib.is_white_url() then
|
||||
return true
|
||||
end
|
||||
lib.black_url()
|
||||
|
||||
lib.allow_location_check()
|
||||
lib.acl()
|
||||
lib.bot_check()
|
||||
lib.method_check()
|
||||
lib.cc()
|
||||
lib.args_check()
|
||||
lib.cookie_check()
|
||||
lib.post_check()
|
||||
lib.header_check()
|
||||
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user