Files
2026-07-13 12:32:21 +08:00

222 lines
9.2 KiB
Lua

local typedefs = require "kong.db.schema.typedefs"
local redis_schema = require "kong.tools.redis.schema"
local SYNC_RATE_REALTIME = -1
local ORDERED_PERIODS = { "second", "minute", "hour", "day", "month", "year"}
local function validate_periods_order(config)
for i, lower_period in ipairs(ORDERED_PERIODS) do
local v1 = config[lower_period]
if type(v1) == "number" then
for j = i + 1, #ORDERED_PERIODS do
local upper_period = ORDERED_PERIODS[j]
local v2 = config[upper_period]
if type(v2) == "number" and v2 < v1 then
return nil, string.format("The limit for %s(%.1f) cannot be lower than the limit for %s(%.1f)",
upper_period, v2, lower_period, v1)
end
end
end
end
if config.policy ~= "redis" and config.sync_rate ~= SYNC_RATE_REALTIME then
return nil, "sync_rate can only be used with the redis policy"
end
if config.policy == "redis" then
if config.sync_rate ~= SYNC_RATE_REALTIME and config.sync_rate < 0.02 then
return nil, "sync_rate must be greater than 0.02, or -1 to disable"
end
end
return true
end
local function is_dbless()
local _, database, role = pcall(function()
return kong.configuration.database,
kong.configuration.role
end)
return database == "off" or role == "control_plane"
end
local policy
if is_dbless() then
policy = { description = "The rate-limiting policies to use for retrieving and incrementing the limits.", type = "string",
default = "local",
len_min = 0,
one_of = {
"local",
"redis",
},
}
else
policy = { description = "The rate-limiting policies to use for retrieving and incrementing the limits.", type = "string",
default = "local",
len_min = 0,
one_of = {
"local",
"cluster",
"redis",
},
}
end
return {
name = "rate-limiting",
fields = {
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
{ second = { description = "The number of HTTP requests that can be made per second.", type = "number", gt = 0 }, },
{ minute = { description = "The number of HTTP requests that can be made per minute.", type = "number", gt = 0 }, },
{ hour = { description = "The number of HTTP requests that can be made per hour.", type = "number", gt = 0 }, },
{ day = { description = "The number of HTTP requests that can be made per day.", type = "number", gt = 0 }, },
{ month = { description = "The number of HTTP requests that can be made per month.", type = "number", gt = 0 }, },
{ year = { description = "The number of HTTP requests that can be made per year.", type = "number", gt = 0 }, },
{ limit_by = { description = "The entity that is used when aggregating the limits.", type = "string",
default = "consumer",
one_of = { "consumer", "credential", "ip", "service", "header", "path" },
}, },
{ header_name = typedefs.header_name },
{ path = typedefs.path },
{ policy = policy },
{ fault_tolerant = { description = "A boolean value that determines if the requests should be proxied even if Kong has troubles connecting a third-party data store. If `true`, requests will be proxied anyway, effectively disabling the rate-limiting function until the data store is working again. If `false`, then the clients will see `500` errors.", type = "boolean", required = true, default = true }, },
{ redis = redis_schema.config_schema },
{ hide_client_headers = { description = "Optionally hide informative response headers.", type = "boolean", required = true, default = false }, },
{ error_code = { description = "Set a custom error code to return when the rate limit is exceeded.", type = "number", default = 429, gt = 0 }, },
{ error_message = { description = "Set a custom error message to return when the rate limit is exceeded.", type = "string", default = "API rate limit exceeded" }, },
{ sync_rate = { description = "How often to sync counter data to the central data store. A value of -1 results in synchronous behavior.", type = "number", required = true, default = -1 }, },
},
custom_validator = validate_periods_order,
shorthand_fields = {
-- TODO: deprecated forms, to be removed in Kong 4.0
{ redis_host = {
type = "string",
deprecation = {
replaced_with = { { path = { 'redis', 'host' } } },
message = "rate-limiting: config.redis_host is deprecated, please use config.redis.host instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { host = value } }
end
} },
{ redis_port = {
type = "integer",
deprecation = {
replaced_with = { { path = { 'redis', 'port' } } },
message = "rate-limiting: config.redis_port is deprecated, please use config.redis.port instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { port = value } }
end
} },
{ redis_password = {
type = "string",
len_min = 0,
deprecation = {
replaced_with = { { path = { 'redis', 'password' } } },
message = "rate-limiting: config.redis_password is deprecated, please use config.redis.password instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { password = value } }
end
} },
{ redis_username = {
type = "string",
deprecation = {
replaced_with = { { path = { 'redis', 'username' } } },
message = "rate-limiting: config.redis_username is deprecated, please use config.redis.username instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { username = value } }
end
} },
{ redis_ssl = {
type = "boolean",
deprecation = {
replaced_with = { { path = { 'redis', 'ssl' } } },
message = "rate-limiting: config.redis_ssl is deprecated, please use config.redis.ssl instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { ssl = value } }
end
} },
{ redis_ssl_verify = {
type = "boolean",
deprecation = {
replaced_with = { { path = { 'redis', 'ssl_verify' } } },
message = "rate-limiting: config.redis_ssl_verify is deprecated, please use config.redis.ssl_verify instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { ssl_verify = value } }
end
} },
{ redis_server_name = {
type = "string",
deprecation = {
replaced_with = { { path = { 'redis', 'server_name' } } },
message = "rate-limiting: config.redis_server_name is deprecated, please use config.redis.server_name instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { server_name = value } }
end
} },
{ redis_timeout = {
type = "integer",
deprecation = {
replaced_with = { { path = { 'redis', 'timeout' } } },
message = "rate-limiting: config.redis_timeout is deprecated, please use config.redis.timeout instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { timeout = value } }
end
} },
{ redis_database = {
type = "integer",
deprecation = {
replaced_with = { { path = { 'redis', 'database' } } },
message = "rate-limiting: config.redis_database is deprecated, please use config.redis.database instead",
removal_in_version = "4.0", },
func = function(value)
return { redis = { database = value } }
end
} },
},
},
},
},
entity_checks = {
{ at_least_one_of = { "config.second", "config.minute", "config.hour", "config.day", "config.month", "config.year" } },
{ conditional = {
if_field = "config.policy", if_match = { eq = "redis" },
then_field = "config.redis.host", then_match = { required = true },
} },
{ conditional = {
if_field = "config.policy", if_match = { eq = "redis" },
then_field = "config.redis.port", then_match = { required = true },
} },
{ conditional = {
if_field = "config.policy", if_match = { eq = "redis" },
then_field = "config.redis.timeout", then_match = { required = true },
} },
{ conditional = {
if_field = "config.limit_by", if_match = { eq = "header" },
then_field = "config.header_name", then_match = { required = true },
} },
{ conditional = {
if_field = "config.limit_by", if_match = { eq = "path" },
then_field = "config.path", then_match = { required = true },
} },
},
}