http { include lua/req.conf; } # lua/req.conf # ( , LRU ) lua_shared_dict req_limit 1024m; # ( , ) lua_shared_dict ban_list 128m; # . , geo $lua_req_whitelist { default 0; 12.34.56.78/24 1; } # init_by_lua ' -- lua_req_priv_key = "secretpassphrase" -- lua_req_cookie_name = "reqcookiename" -- lua_req_ban_log = "/path/to/log/file" -- ( ) -- lua_req_d_one = 42 -- URI lua_req_d_mul = 84 -- URI lua_req_s_one = 100 -- URI lua_req_s_mul = 200 -- URI lua_req_d_ip = 200 -- IP lua_req_s_ip = 400 -- IP -- 10 lua_req_ban_ttl = 600 -- math.randomseed(math.floor(ngx.now()*1000)) '; # , access access_by_lua_file /path/to/nginx/lua/req.lua;
if ngx.var.lua_req_whitelist ~= '1' then -- IP , end
function string.endswith(haystack, needle) return (needle == '') or (needle == string.sub(haystack, -string.len(needle))) end local function path_is_static(path) local exts = {'js', 'css', 'png', 'jpg', 'jpeg', 'gif', 'xml', 'ico', 'swf'} path = path:lower() for _,ext in ipairs(exts) do if path:endswith(ext) then return true end end return false end local uri_path = ngx.var.request_uri if ngx.var.is_args == '?' then uri_path = uri_path:gsub('^([^?]+)\\?.*$', '%1') end local is_static = path_is_static(uri_path)
local function gen_cookie_rand() return tostring(math.random(2147483647)) end local function gen_cookie(prefix, rnd) return ngx.encode_base64( -- IP UserAgent, ngx.sha1_bin(ngx.today() .. prefix .. lua_req_priv_key .. rnd) ) end local uri = ngx.var.request_uri -- URI local host = ngx.var.http_host -- ( nginx ) local ip = ngx.var.remote_addr local user_agent = ngx.var.http_user_agent or '' if user_agent:len() > 0 then user_agent = ngx.encode_base64(ngx.sha1_bin(user_agent)) end local key_prefix = ip .. ':' .. user_agent -- local user_cookie = ngx.unescape_uri(ngx.var['cookie_' .. lua_req_cookie_name]) or '' local rnd = gen_cookie_rand() local p = user_cookie:find('_') if p then rnd = user_cookie:sub(p+1) user_cookie = user_cookie:sub(1, p-1) end local control_cookie = gen_cookie(key_prefix, rnd) if user_cookie ~= control_cookie then user_cookie = '' rnd = gen_cookie_rand() control_cookie = gen_cookie(key_prefix, rnd) end key_prefix = key_prefix .. ':' .. user_cookie ngx.header['Set-Cookie'] = string.format('%s=%s; path=/; expires=%s', lua_req_cookie_name, ngx.escape_uri(control_cookie .. '_' .. rnd), ngx.cookie_time(ngx.time()+24*3600) )
local ban_key = key_prefix..':ban' if ban_list:get(ban_key) or ban_list:get(ip..':ban') then -- IP return ngx.exit(ngx.HTTP_FORBIDDEN) end
-- : URI URI local limits = { [false] = { [false] = lua_req_d_mul, -- URI [true] = lua_req_d_one, -- URI }, [true] = { [false] = lua_req_s_mul, -- URI [true] = lua_req_s_one, -- URI } } for _,one_path in ipairs({true, false}) do local limit = limits[is_static][one_path] local key = {key_prefix} -- if is_static then table.insert(key, 'S') else table.insert(key, 'D') end -- ( API ) if one_path then table.insert(key, host..uri) end -- "12.34.56.78:useragentsha1base64:cookiesha1base64:S:site.com/path/to/file" key = table.concat(key, ':') local exhaust = check_limit_exhaust(key, limit, ban_ttl) if exhaust then return ngx.exit(ngx.HTTP_FORBIDDEN) end end
local function check_limit_exhaust(key, limit, cnt_ttl) local key_ts = key..':ts' local cnt, _ = req_limit:incr(key, 1) -- , -- if cnt == nil then if req_limit:add(key, 1, cnt_ttl) then req_limit:set(key_ts, ngx.now(), cnt_ttl) end return false end -- ( ) if cnt <= limit then return false end -- ( ), -- local key_lock = key..':lock' local key_lock_ttl = 0.5 local ts local try_until = ngx.now() + key_lock_ttl local locked while true do locked = req_limit:add(key_lock, 1, key_lock_ttl) cnt = req_limit:get(key) ts = req_limit:get(key_ts) if locked or (try_until < ngx.now()) then break end ngx.sleep(0.01) end -- - , , . -- IP blacklist -- if (not locked) and ((not cnt) or (not ts)) then return true, 'lock_failed' end -- ( ) local ts_diff = math.max(0.001, ngx.now() - ts) -- local cnt_norm = math.floor(cnt / ts_diff) -- if cnt_norm <= limit then -- ts cnt ( set' - ) req_limit:set(key, cnt_norm, cnt_ttl) req_limit:set(key_ts, ngx.now() - 1, cnt_ttl) -- ; blacklist ; if locked then req_limit:delete(key_lock) end return false end -- . , , req_limit:delete(key) req_limit:delete(key_ts) if locked then req_limit:delete(key_lock) end return true, cnt_norm end
Source: https://habr.com/ru/post/215235/