1
0
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:
zhengkunwang 2024-02-28 14:34:09 +08:00 committed by GitHub
parent c510a028db
commit 2affd2bc79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 13907 additions and 0 deletions

5
.gitignore vendored
View File

@ -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/

View 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
}
}

View 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;

View 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

View 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

View 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>

View 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();
}
}

View 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>

View 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>

View 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>

View 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()}})()};

View File

@ -0,0 +1,9 @@
local db = require "db"
local config = require "config"
config.load_config_file()
db.init_db()

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;

View 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

View 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

View 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

View 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的请求"
}
]
}

View 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)"
}
]
}

View File

@ -0,0 +1,3 @@
{
}

View 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)"
}
]
}

View 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"
}
]
}

View 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)"}
]
}

View File

@ -0,0 +1,10 @@
{
"rules": [
{
"name": "拦截IP",
"state": "on",
"type": "ipv4",
"ipv4": "123"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"rules": [
{
"state": "on",
"type": "ipv4",
"ipv4": "123"
}
]
}

View 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"
}
]
}

View 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)\\="}
]
}

View File

@ -0,0 +1,5 @@
{
"rules": [
]
}

View File

@ -0,0 +1,8 @@
{
"rules": [
{
"state": "on",
"rule": "PostmanRuntime/7.36.1"
}
]
}

View 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"
}
]
}

View 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"
}
]
}

View 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)

View File

@ -0,0 +1,4 @@
{
"name": "1Panel WAF",
"version": "1.0.0"
}

View 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