221 lines
5.8 KiB
Lua
221 lines
5.8 KiB
Lua
-- Copyright (C) Kong Inc.
|
|
local timestamp = require "kong.tools.timestamp"
|
|
local policies = require "kong.plugins.rate-limiting.policies"
|
|
local kong_meta = require "kong.meta"
|
|
local pdk_private_rl = require "kong.pdk.private.rate_limiting"
|
|
|
|
|
|
local kong = kong
|
|
local ngx = ngx
|
|
local max = math.max
|
|
local time = ngx.time
|
|
local floor = math.floor
|
|
local pairs = pairs
|
|
local error = error
|
|
local tostring = tostring
|
|
local timer_at = ngx.timer.at
|
|
local SYNC_RATE_REALTIME = -1
|
|
|
|
|
|
local pdk_rl_store_response_header = pdk_private_rl.store_response_header
|
|
local pdk_rl_apply_response_headers = pdk_private_rl.apply_response_headers
|
|
|
|
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
local EXPIRATION = require "kong.plugins.rate-limiting.expiration"
|
|
|
|
|
|
local RATELIMIT_LIMIT = "RateLimit-Limit"
|
|
local RATELIMIT_REMAINING = "RateLimit-Remaining"
|
|
local RATELIMIT_RESET = "RateLimit-Reset"
|
|
local RETRY_AFTER = "Retry-After"
|
|
|
|
|
|
local X_RATELIMIT_LIMIT = {
|
|
second = "X-RateLimit-Limit-Second",
|
|
minute = "X-RateLimit-Limit-Minute",
|
|
hour = "X-RateLimit-Limit-Hour",
|
|
day = "X-RateLimit-Limit-Day",
|
|
month = "X-RateLimit-Limit-Month",
|
|
year = "X-RateLimit-Limit-Year",
|
|
}
|
|
|
|
local X_RATELIMIT_REMAINING = {
|
|
second = "X-RateLimit-Remaining-Second",
|
|
minute = "X-RateLimit-Remaining-Minute",
|
|
hour = "X-RateLimit-Remaining-Hour",
|
|
day = "X-RateLimit-Remaining-Day",
|
|
month = "X-RateLimit-Remaining-Month",
|
|
year = "X-RateLimit-Remaining-Year",
|
|
}
|
|
|
|
|
|
local RateLimitingHandler = {}
|
|
|
|
|
|
RateLimitingHandler.VERSION = kong_meta.version
|
|
RateLimitingHandler.PRIORITY = 910
|
|
|
|
|
|
local function get_identifier(conf)
|
|
local identifier
|
|
|
|
if conf.limit_by == "service" then
|
|
identifier = (kong.router.get_service() or
|
|
EMPTY).id
|
|
elseif conf.limit_by == "consumer" then
|
|
identifier = (kong.client.get_consumer() or
|
|
kong.client.get_credential() or
|
|
EMPTY).id
|
|
|
|
elseif conf.limit_by == "credential" then
|
|
identifier = (kong.client.get_credential() or
|
|
EMPTY).id
|
|
|
|
elseif conf.limit_by == "header" then
|
|
identifier = kong.request.get_header(conf.header_name)
|
|
|
|
elseif conf.limit_by == "path" then
|
|
local req_path = kong.request.get_path()
|
|
if req_path == conf.path then
|
|
identifier = req_path
|
|
end
|
|
end
|
|
|
|
return identifier or kong.client.get_forwarded_ip()
|
|
end
|
|
|
|
|
|
local function get_usage(conf, identifier, current_timestamp, limits)
|
|
local usage = {}
|
|
local stop
|
|
|
|
for period, limit in pairs(limits) do
|
|
local current_usage, err = policies[conf.policy].usage(conf, identifier, period, current_timestamp)
|
|
if err then
|
|
return nil, nil, err
|
|
end
|
|
|
|
-- What is the current usage for the configured limit name?
|
|
local remaining = limit - current_usage
|
|
|
|
-- Recording usage
|
|
usage[period] = {
|
|
limit = limit,
|
|
remaining = remaining,
|
|
}
|
|
|
|
if remaining <= 0 then
|
|
stop = period
|
|
end
|
|
end
|
|
|
|
return usage, stop
|
|
end
|
|
|
|
|
|
local function increment(premature, conf, ...)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
policies[conf.policy].increment(conf, ...)
|
|
end
|
|
|
|
|
|
function RateLimitingHandler:access(conf)
|
|
local current_timestamp = time() * 1000
|
|
|
|
-- Consumer is identified by ip address or authenticated_credential id
|
|
local identifier = get_identifier(conf)
|
|
local fault_tolerant = conf.fault_tolerant
|
|
|
|
-- Load current metric for configured period
|
|
local limits = {
|
|
second = conf.second,
|
|
minute = conf.minute,
|
|
hour = conf.hour,
|
|
day = conf.day,
|
|
month = conf.month,
|
|
year = conf.year,
|
|
}
|
|
|
|
local usage, stop, err = get_usage(conf, identifier, current_timestamp, limits)
|
|
if err then
|
|
if not fault_tolerant then
|
|
return error(err)
|
|
end
|
|
|
|
kong.log.err("failed to get usage: ", tostring(err))
|
|
end
|
|
|
|
if usage then
|
|
local ngx_ctx = ngx.ctx
|
|
local reset
|
|
if not conf.hide_client_headers then
|
|
local timestamps
|
|
local limit
|
|
local window
|
|
local remaining
|
|
for k, v in pairs(usage) do
|
|
local current_limit = v.limit
|
|
local current_window = EXPIRATION[k]
|
|
local current_remaining = v.remaining
|
|
if stop == nil or stop == k then
|
|
current_remaining = current_remaining - 1
|
|
end
|
|
current_remaining = max(0, current_remaining)
|
|
|
|
if not limit or (current_remaining < remaining)
|
|
or (current_remaining == remaining and
|
|
current_window > window)
|
|
then
|
|
limit = current_limit
|
|
window = current_window
|
|
remaining = current_remaining
|
|
|
|
if not timestamps then
|
|
timestamps = timestamp.get_timestamps(current_timestamp)
|
|
end
|
|
|
|
reset = max(1, window - floor((current_timestamp - timestamps[k]) / 1000))
|
|
end
|
|
|
|
pdk_rl_store_response_header(ngx_ctx, X_RATELIMIT_LIMIT[k], current_limit)
|
|
pdk_rl_store_response_header(ngx_ctx, X_RATELIMIT_REMAINING[k], current_remaining)
|
|
end
|
|
|
|
pdk_rl_store_response_header(ngx_ctx, RATELIMIT_LIMIT, limit)
|
|
pdk_rl_store_response_header(ngx_ctx, RATELIMIT_REMAINING, remaining)
|
|
pdk_rl_store_response_header(ngx_ctx, RATELIMIT_RESET, reset)
|
|
end
|
|
|
|
-- If limit is exceeded, terminate the request
|
|
if stop then
|
|
if not conf.hide_client_headers then
|
|
pdk_rl_store_response_header(ngx_ctx, RETRY_AFTER, reset)
|
|
pdk_rl_apply_response_headers(ngx_ctx)
|
|
end
|
|
|
|
return kong.response.error(conf.error_code, conf.error_message)
|
|
end
|
|
|
|
if not conf.hide_client_headers then
|
|
pdk_rl_apply_response_headers(ngx_ctx)
|
|
end
|
|
end
|
|
|
|
if conf.sync_rate ~= SYNC_RATE_REALTIME and conf.policy == "redis" then
|
|
increment(false, conf, limits, identifier, current_timestamp, 1)
|
|
|
|
else
|
|
local ok, err = timer_at(0, increment, conf, limits, identifier, current_timestamp, 1)
|
|
if not ok then
|
|
kong.log.err("failed to create timer: ", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
return RateLimitingHandler
|