132 lines
3.5 KiB
Lua
132 lines
3.5 KiB
Lua
![]() |
local resty_global_throttle = require("resty.global_throttle")
|
||
|
local resty_ipmatcher = require("resty.ipmatcher")
|
||
|
local util = require("util")
|
||
|
|
||
|
local ngx = ngx
|
||
|
local ngx_exit = ngx.exit
|
||
|
local ngx_log = ngx.log
|
||
|
local ngx_ERR = ngx.ERR
|
||
|
local ngx_INFO = ngx.INFO
|
||
|
|
||
|
local _M = {}
|
||
|
|
||
|
local DECISION_CACHE = ngx.shared.global_throttle_cache
|
||
|
|
||
|
-- it does not make sense to cache decision for too little time
|
||
|
-- the benefit of caching likely is negated if we cache for too little time
|
||
|
-- Lua Shared Dict's time resolution for expiry is 0.001.
|
||
|
local CACHE_THRESHOLD = 0.001
|
||
|
|
||
|
local DEFAULT_RAW_KEY = "remote_addr"
|
||
|
|
||
|
local function should_ignore_request(ignored_cidrs)
|
||
|
if not ignored_cidrs or #ignored_cidrs == 0 then
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local ignored_cidrs_matcher, err = resty_ipmatcher.new(ignored_cidrs)
|
||
|
if not ignored_cidrs_matcher then
|
||
|
ngx_log(ngx_ERR, "failed to initialize resty-ipmatcher: ", err)
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local is_ignored
|
||
|
is_ignored, err = ignored_cidrs_matcher:match(ngx.var.remote_addr)
|
||
|
if err then
|
||
|
ngx_log(ngx_ERR, "failed to match ip: '",
|
||
|
ngx.var.remote_addr, "': ", err)
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
return is_ignored
|
||
|
end
|
||
|
|
||
|
local function is_enabled(config, location_config)
|
||
|
if config.memcached.host == "" or config.memcached.port == 0 then
|
||
|
return false
|
||
|
end
|
||
|
if location_config.limit == 0 or
|
||
|
location_config.window_size == 0 then
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
if should_ignore_request(location_config.ignored_cidrs) then
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
local function get_namespaced_key_value(namespace, key_value)
|
||
|
return namespace .. key_value
|
||
|
end
|
||
|
|
||
|
function _M.throttle(config, location_config)
|
||
|
if not is_enabled(config, location_config) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local key_value = util.generate_var_value(location_config.key)
|
||
|
if not key_value or key_value == "" then
|
||
|
key_value = ngx.var[DEFAULT_RAW_KEY]
|
||
|
end
|
||
|
|
||
|
local namespaced_key_value =
|
||
|
get_namespaced_key_value(location_config.namespace, key_value)
|
||
|
|
||
|
local is_limit_exceeding = DECISION_CACHE:get(namespaced_key_value)
|
||
|
if is_limit_exceeding then
|
||
|
ngx.var.global_rate_limit_exceeding = "c"
|
||
|
return ngx_exit(config.status_code)
|
||
|
end
|
||
|
|
||
|
local my_throttle, err = resty_global_throttle.new(
|
||
|
location_config.namespace,
|
||
|
location_config.limit,
|
||
|
location_config.window_size,
|
||
|
{
|
||
|
provider = "memcached",
|
||
|
host = config.memcached.host,
|
||
|
port = config.memcached.port,
|
||
|
connect_timeout = config.memcached.connect_timeout,
|
||
|
max_idle_timeout = config.memcached.max_idle_timeout,
|
||
|
pool_size = config.memcached.pool_size,
|
||
|
}
|
||
|
)
|
||
|
if err then
|
||
|
ngx.log(ngx.ERR, "faled to initialize resty_global_throttle: ", err)
|
||
|
-- fail open
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local desired_delay, estimated_final_count
|
||
|
estimated_final_count, desired_delay, err = my_throttle:process(key_value)
|
||
|
if err then
|
||
|
ngx.log(ngx.ERR, "error while processing key: ", err)
|
||
|
-- fail open
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if desired_delay then
|
||
|
if desired_delay > CACHE_THRESHOLD then
|
||
|
local ok
|
||
|
ok, err =
|
||
|
DECISION_CACHE:safe_add(namespaced_key_value, true, desired_delay)
|
||
|
if not ok then
|
||
|
if err ~= "exists" then
|
||
|
ngx_log(ngx_ERR, "failed to cache decision: ", err)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
ngx.var.global_rate_limit_exceeding = "y"
|
||
|
ngx_log(ngx_INFO, "limit is exceeding for ",
|
||
|
location_config.namespace, "/", key_value,
|
||
|
" with estimated_final_count: ", estimated_final_count)
|
||
|
|
||
|
return ngx_exit(config.status_code)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return _M
|