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

1500 lines
44 KiB
Lua

local helpers = require "spec.helpers"
local cjson = require "cjson"
local redis_helper = require "spec.helpers.redis_helper"
local REDIS_HOST = helpers.redis_host
local REDIS_PORT = helpers.redis_port
local REDIS_SSL_PORT = helpers.redis_ssl_port
local REDIS_SSL_SNI = helpers.redis_ssl_sni
local REDIS_PASSWORD = ""
local REDIS_DATABASE = 1
local UPSTREAM_HOST = "localhost"
local fmt = string.format
local proxy_client = helpers.proxy_client
local table_insert = table.insert
local tonumber = tonumber
local ngx_sleep = ngx.sleep
local ngx_now = ngx.now
-- This performs the test up to two times (and no more than two).
-- We are **not** retrying to "give it another shot" in case of a flaky test.
-- The reason why we allow for a single retry in this test suite is because
-- tests are dependent on the value of the current minute. If the minute
-- flips during the test (i.e. going from 03:43:59 to 03:44:00), the result
-- will fail. Since each test takes less than a minute to run, running it
-- a second time right after that failure ensures that another flip will
-- not occur. If the second execution failed as well, this means that there
-- was an actual problem detected by the test.
local function retry(fn)
if not pcall(fn) then
ngx_sleep(61 - (ngx_now() % 60)) -- Wait for minute to expire
fn()
end
end
local function GET(url, opt)
local client = proxy_client()
local res, err = client:get(url, opt)
if not res then
client:close()
return nil, err
end
assert(res:read_body())
client:close()
return res
end
local function client_requests(n, proxy_fn)
local ret = {
minute_limit = {},
minute_remaining = {},
hour_limit = {},
hour_remaining = {},
limit = {},
remaining = {},
status = {},
reset = {},
}
for _ = 1, n do
local res = assert(proxy_fn())
table_insert(ret.reset, tonumber(res.headers["RateLimit-Reset"]))
table_insert(ret.status, res.status)
table_insert(ret.minute_limit, tonumber(res.headers["X-RateLimit-Limit-Minute"]))
table_insert(ret.minute_remaining, tonumber(res.headers["X-RateLimit-Remaining-Minute"]))
table_insert(ret.hour_limit, tonumber(res.headers["X-RateLimit-Limit-Hour"]))
table_insert(ret.hour_remaining, tonumber(res.headers["X-RateLimit-Remaining-Hour"]))
table_insert(ret.limit, tonumber(res.headers["RateLimit-Limit"]))
table_insert(ret.remaining, tonumber(res.headers["RateLimit-Remaining"]))
helpers.wait_timer("rate-limiting", true, "any-finish")
end
return ret
end
local function validate_headers(headers, check_minute, check_hour)
if check_minute then
assert.same({
6, 6, 6, 6, 6, 6, 6,
}, headers.minute_limit)
assert.same({
5, 4, 3, 2, 1, 0, 0,
}, headers.minute_remaining)
end
if check_hour then
for _, v in ipairs(headers.hour_limit) do
assert(v > 0)
end
for _, v in ipairs(headers.hour_remaining) do
assert(v >= 0)
end
end
assert.same({
6, 6, 6, 6, 6, 6, 6,
}, headers.limit)
assert.same({
5, 4, 3, 2, 1, 0, 0,
}, headers.remaining)
assert.same({
200, 200, 200, 200, 200, 200, 429,
}, headers.status)
for _, reset in ipairs(headers.reset) do
if check_hour then
assert.equal(true, reset <= 3600 and reset >= 0)
elseif check_minute then
assert.equal(true, reset <= 60 and reset >= 0)
else
error("check_hour or check_minute must be true")
end
end
end
local function setup_service(admin_client, url)
local service = assert(admin_client:send({
method = "POST",
path = "/services",
body = {
url = url,
},
headers = {
["Content-Type"] = "application/json",
},
}))
return cjson.decode(assert.res_status(201, service))
end
local function setup_route(admin_client, service, paths, protocol)
protocol = protocol or "http"
local route = assert(admin_client:send({
method = "POST",
path = "/routes",
body = {
protocols = { protocol },
service = { id = service.id, },
paths = paths,
},
headers = {
["Content-Type"] = "application/json",
},
}))
return cjson.decode(assert.res_status(201, route))
end
local function setup_rl_plugin(admin_client, conf, service, consumer)
local plugin
if service then
plugin = assert(admin_client:send({
method = "POST",
path = "/plugins",
body = {
name = "rate-limiting",
service = { id = service.id, },
config = conf,
},
headers = {
["Content-Type"] = "application/json",
},
}))
elseif consumer then
plugin = assert(admin_client:send({
method = "POST",
path = "/plugins",
body = {
name = "rate-limiting",
consumer = { id = consumer.id, },
config = conf,
},
headers = {
["Content-Type"] = "application/json",
},
}))
else
plugin = assert(admin_client:send({
method = "POST",
path = "/plugins",
body = {
name = "rate-limiting",
config = conf,
},
headers = {
["Content-Type"] = "application/json",
},
}))
end
return cjson.decode(assert.res_status(201, plugin))
end
local function setup_key_auth_plugin(admin_client, conf, service)
local plugin
if service then
plugin = assert(admin_client:send({
method = "POST",
path = "/plugins",
body = {
name = "key-auth",
service = { id = service.id, },
config = conf,
},
headers = {
["Content-Type"] = "application/json",
},
}))
else
plugin = assert(admin_client:send({
method = "POST",
path = "/plugins",
body = {
name = "key-auth",
config = conf,
},
headers = {
["Content-Type"] = "application/json",
},
}))
end
return cjson.decode(assert.res_status(201, plugin))
end
local function setup_consumer(admin_client, username)
local consumer = assert(admin_client:send({
method = "POST",
path = "/consumers",
body = {
username = username,
},
headers = {
["Content-Type"] = "application/json",
},
}))
return cjson.decode(assert.res_status(201, consumer))
end
local function setup_credential(admin_client, consumer, key)
local credential = assert(admin_client:send({
method = "POST",
path = "/consumers/" .. consumer.id .. "/key-auth",
body = {
key = key,
},
headers = {
["Content-Type"] = "application/json",
},
}))
return cjson.decode(assert.res_status(201, credential))
end
local function delete_service(admin_client, service)
local res = assert(admin_client:send({
method = "DELETE",
path = "/services/" .. service.id,
}))
assert.res_status(204, res)
end
local function delete_route(admin_client, route)
local res = assert(admin_client:send({
method = "DELETE",
path = "/routes/" .. route.id,
}))
assert.res_status(204, res)
end
local function delete_plugin(admin_client, plugin)
local res = assert(admin_client:send({
method = "DELETE",
path = "/plugins/" .. plugin.id,
}))
assert.res_status(204, res)
end
local function delete_consumer(admin_client, consumer)
local res = assert(admin_client:send({
method = "DELETE",
path = "/consumers/" .. consumer.id,
}))
assert.res_status(204, res)
end
local function delete_credential(admin_client, credential)
local res = assert(admin_client:send({
method = "DELETE",
path = "/consumers/" .. credential.consumer.id .. "/key-auth/" .. credential.id,
}))
assert.res_status(204, res)
end
local limit_by_confs = {
"ip",
"consumer",
"credential",
"service",
"header",
"path",
}
local ssl_confs = {
no_ssl = {
redis_port = REDIS_PORT,
},
ssl_verify = {
redis_ssl = true,
redis_ssl_verify = true,
redis_server_name = REDIS_SSL_SNI,
redis_port = REDIS_SSL_PORT,
},
ssl_no_verify = {
redis_ssl = true,
redis_ssl_verify = false,
redis_server_name = "really.really.really.does.not.exist.host.test",
redis_port = REDIS_SSL_PORT,
},
}
local desc
for _, strategy in helpers.each_strategy() do
for __, policy in ipairs({ "local", "cluster", "redis" }) do
for ___, limit_by in ipairs(limit_by_confs) do
for ssl_conf_name, ssl_conf in pairs(ssl_confs) do
if ssl_conf_name ~= "no_ssl" and policy ~= "redis" then
goto continue
end
desc = fmt("Plugin: rate-limiting #db (access) [strategy: %s] [policy: %s] [limit_by: %s] [redis: %s]",
strategy, policy, limit_by, ssl_conf_name)
describe(desc, function()
local db, https_server, admin_client
local UPSTREAM_PORT
local UPSTREAM_URL
lazy_setup(function()
UPSTREAM_PORT = helpers.get_available_port()
UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)
_, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
if policy == "redis" then
redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
elseif policy == "cluster" then
db:truncate("ratelimiting_metrics")
end
https_server = helpers.https_server.new(UPSTREAM_PORT)
https_server:start()
helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
plugins = "bundled,rate-limiting,key-auth",
trusted_ips = "0.0.0.0/0,::/0",
lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
})
end)
lazy_teardown(function()
assert(https_server, "unexpected error")
https_server:shutdown()
assert(helpers.stop_kong(), "failed to stop Kong")
end)
before_each(function ()
admin_client = helpers.admin_client()
if strategy == "cluster" then
db:truncate("ratelimiting_metrics")
end
if policy == "redis" then
redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
end
end)
after_each(function()
admin_client:close()
end)
it(fmt("blocks if exceeding limit (single %s)", limit_by), function()
local test_path = "/test"
local test_header = "test-header"
local test_key_name = "test-key"
local test_credential = "test_credential"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = limit_by,
path = test_path, -- only for limit_by = "path"
header_name = test_header, -- only for limit_by = "header"
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
local auth_plugin
local consumer
local credential
if limit_by == "consumer" or limit_by == "credential" then
auth_plugin = setup_key_auth_plugin(admin_client, {
key_names = { test_key_name },
}, service)
consumer = setup_consumer(admin_client, "Bob")
credential = setup_credential(admin_client, consumer, test_credential)
end
finally(function()
if limit_by == "consumer" or limit_by == "credential" then
delete_credential(admin_client, credential)
delete_consumer(admin_client, consumer)
delete_plugin(admin_client, auth_plugin)
end
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local function proxy_fn()
if limit_by == "ip" or
limit_by == "path" or
limit_by == "service" then
return GET(test_path)
end
if limit_by == "header" then
return GET(test_path, { [test_header] = "test" })
end
if limit_by == "consumer" or limit_by == "credential" then
return GET(test_path, { headers = { [test_key_name] = test_credential }})
end
error("unexpected limit_by: " .. limit_by)
end
retry(function ()
validate_headers(client_requests(7, proxy_fn), true)
end)
end)
if limit_by == "ip" then
it("blocks if exceeding limit (multiple ip)", function()
local test_path = "/test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "ip",
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
for _, ip in ipairs({ "127.0.0.1", "127.0.0.2" }) do
validate_headers(client_requests(7, function ()
return GET(test_path, { headers = { ["X-Real-IP"] = ip }})
end), true)
end -- for _, ip in ipairs({ "127.0.0.1", "127.0.0.2" }) do
end) -- retry(function ()
end) -- it("blocks if exceeding limit (multiple ip)", function()
it("blocks if exceeding limit #grpc (single ip)", function()
local test_path = "/hello.HelloService/"
local service = setup_service(admin_client, helpers.grpcbin_url)
local route = setup_route(admin_client, service, { test_path }, "grpc")
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "ip",
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
for i = 1, 6 do
local ok, res = helpers.proxy_client_grpc(){
service = "hello.HelloService.SayHello",
opts = {
["-v"] = true,
},
}
assert.is_true(ok, res)
assert.matches("x%-ratelimit%-limit%-minute: 6", res)
assert.matches("x%-ratelimit%-remaining%-minute: " .. (6 - i), res)
assert.matches("ratelimit%-limit: 6", res)
assert.matches("ratelimit%-remaining: " .. (6 - i), res)
local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
assert.equal(true, reset <= 60 and reset >= 0)
-- wait for zero-delay timer
helpers.wait_timer("rate-limiting", true, "any-finish")
end
-- Additional request, while limit is 6/minute
local ok, res = helpers.proxy_client_grpc(){
service = "hello.HelloService.SayHello",
opts = {
["-v"] = true,
},
}
assert.falsy(ok)
assert.matches("Code: ResourceExhausted", res)
assert.matches("ratelimit%-limit: 6", res)
assert.matches("ratelimit%-remaining: 0", res)
local retry = tonumber(string.match(res, "retry%-after: (%d+)"))
assert.equal(true, retry <= 60 and retry > 0)
local reset = tonumber(string.match(res, "ratelimit%-reset: (%d+)"))
assert.equal(true, reset <= 60 and reset > 0)
end)
end)
it("hide_client_headers (single ip)", function ()
local test_path = "/test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "ip",
hide_client_headers = true,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local res = assert(GET(test_path))
assert.res_status(200, res)
assert.is_nil(res.headers["X-Ratelimit-Limit-Minute"])
assert.is_nil(res.headers["X-Ratelimit-Remaining-Minute"])
assert.is_nil(res.headers["Ratelimit-Limit"])
assert.is_nil(res.headers["Ratelimit-Remaining"])
assert.is_nil(res.headers["Ratelimit-Reset"])
assert.is_nil(res.headers["Retry-After"])
-- repeat until get rate-limited
helpers.wait_until(function()
res = assert(GET(test_path))
return res.status == 429, "should be rate-limited (429), got " .. res.status
end, 10)
assert.res_status(429, res)
assert.is_nil(res.headers["X-Ratelimit-Limit-Minute"])
assert.is_nil(res.headers["X-Ratelimit-Remaining-Minute"])
assert.is_nil(res.headers["Ratelimit-Limit"])
assert.is_nil(res.headers["Ratelimit-Remaining"])
assert.is_nil(res.headers["Ratelimit-Reset"])
assert.is_nil(res.headers["Retry-After"])
end)
it("handles multiple limits (single ip)", function()
local test_path = "/test"
local test_header = "test-header"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
hour = 99999,
policy = policy,
limit_by = limit_by,
path = test_path,
header_name = test_header,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local function proxy_fn()
return GET(test_path)
end
retry(function ()
validate_headers(client_requests(7, proxy_fn), true, true)
end)
end)
it("expire counter", function()
local test_path = "/test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
second = 1,
policy = policy,
limit_by = "ip",
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
assert
.with_timeout(15)
.with_max_tries(10)
.with_step(0.5) -- the windows is 1 second, we wait 0.5 seconds between each retry,
-- that can avoid some unlucky case (we are at the end of the window)
.ignore_exceptions(false)
.eventually(function()
local res1 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
assert.res_status(200, res1)
assert.are.same(1, tonumber(res1.headers["RateLimit-Limit"]))
assert.are.same(0, tonumber(res1.headers["RateLimit-Remaining"]))
assert.is_true(tonumber(res1.headers["ratelimit-reset"]) >= 0)
assert.are.same(1, tonumber(res1.headers["X-RateLimit-Limit-Second"]))
assert.are.same(0, tonumber(res1.headers["X-RateLimit-Remaining-Second"]))
local res2 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
local body2 = assert.res_status(429, res2)
local json2 = cjson.decode(body2)
assert.not_nil(json2.message)
assert.matches("API rate limit exceeded", json2.message)
ngx_sleep(1)
local res3 = GET(test_path, { headers = { ["X-Real-IP"] = "127.0.0.3" }})
assert.res_status(200, res3)
end)
.has_no_error("counter should have been cleared after current window")
end)
it("blocks with a custom error code and message", function()
local test_path = "/test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 1,
policy = policy,
limit_by = limit_by,
path = test_path,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
},
error_code = 404,
error_message = "Fake Not Found",
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local res = GET(test_path)
assert.res_status(200, res)
helpers.wait_timer("rate-limiting", true, "any-finish")
res = GET(test_path)
local json = cjson.decode(assert.res_status(404, res))
assert.matches("Fake Not Found", json.message)
end) -- it("blocks with a custom error code and message", function()
end -- if limit_by == "ip" then
if limit_by == "service" then
it("blocks if exceeding limit (multiple service)", function ()
local test_path_1, test_path_2 = "/1-test", "/2-test"
local service_1, service_2 = setup_service(admin_client, UPSTREAM_URL),
setup_service(admin_client, UPSTREAM_URL)
local route_1, route_2 = setup_route(admin_client, service_1, { test_path_1 }),
setup_route(admin_client, service_2, { test_path_2 })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "service",
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
})
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route_1)
delete_route(admin_client, route_2)
delete_service(admin_client, service_1)
delete_service(admin_client, service_2)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
for _, path in ipairs({ test_path_1, test_path_2 }) do
validate_headers(client_requests(7, function()
return GET(path)
end), true)
end -- for _, path in ipairs({ test_path_1, test_path_2 }) do
end) -- retry(function ()
end) -- it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function ()
end -- if limit_by == "service" then
if limit_by == "path" then
it("blocks if exceeding limit (multiple path)", function()
local test_path_1, test_path_2 = "/1-test", "/2-test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route_1, route_2 = setup_route(admin_client, service, { test_path_1 }),
setup_route(admin_client, service, { test_path_2 })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "path",
path = test_path_1,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route_1)
delete_route(admin_client, route_2)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
for _, path in ipairs({ test_path_1, test_path_2 }) do
validate_headers(client_requests(7, function()
return GET(path)
end), true)
end -- for _, path in ipairs({ test_path_1, test_path_2 }) do
end) -- retry(function ()
end) -- it("blocks if exceeding limit (multiple path)", function()
end -- if limit_by == "path" then
if limit_by == "header" then
it("blocks if exceeding limit (multiple header)", function()
local test_path = "/test"
local test_header_1, test_header_2 = "test-header-1", "test-header-2"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = "header",
header_name = test_header_1,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
for _, header_name in ipairs({ test_header_1, test_header_2 }) do
validate_headers(client_requests(7, function()
return GET(test_path, { headers = { [header_name] = "test" }})
end), true)
end -- for _, header_name in ipairs({ test_header_1, test_header_2 }) do
end) -- retry(function ()
end) -- it("blocks if exceeding limit (multiple header)", function()
end -- if limit_by == "header" then
if limit_by == "consumer" or limit_by == "credential" then
it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function()
local test_path = "/test"
local test_key_name = "test-key"
local test_credential_1, test_credential_2 = "test_credential_1", "test_credential_2"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = policy,
limit_by = limit_by,
redis = {
host = REDIS_HOST,
port = ssl_conf.redis_port,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = ssl_conf.redis_ssl,
ssl_verify = ssl_conf.redis_ssl_verify,
server_name = ssl_conf.redis_server_name,
}
}, service)
local auth_plugin = setup_key_auth_plugin(admin_client, {
key_names = { test_key_name },
}, service)
local consumer_1, consumer_2 = setup_consumer(admin_client, "Bob"), setup_consumer(admin_client, "Alice")
local credential_1, credential_2 = setup_credential(admin_client, consumer_1, test_credential_1),
setup_credential(admin_client, consumer_2, test_credential_2)
finally(function()
delete_credential(admin_client, credential_1)
delete_credential(admin_client, credential_2)
delete_consumer(admin_client, consumer_1)
delete_consumer(admin_client, consumer_2)
delete_plugin(admin_client, auth_plugin)
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function()
for _, credential in ipairs({ test_credential_1, test_credential_2 }) do
validate_headers(client_requests(7, function()
return GET(test_path, { headers = { [test_key_name] = credential }})
end), true)
end -- for _, credential in ipairs({ test_credential_1, test_credential_2 }) do
end) -- retry(function()
end) -- it(fmt("blocks if exceeding limit (multiple %s)", limit_by), function()
end -- if limit_by == "consumer" and limit_by == "credential" then
end)
::continue::
end -- for ssl_conf_name, ssl_conf in pairs(ssl_confs) do
end -- for ___, limit_by in ipairs(limit_by_confs) do
desc = fmt("Plugin: rate-limiting fault tolerancy #db (access) [strategy: %s] [policy: %s]",
strategy, policy)
describe(desc, function ()
local db, https_server, admin_client
local test_path = "/test"
local service
local UPSTREAM_PORT
local UPSTREAM_URL
local function start_kong()
return helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
plugins = "bundled,rate-limiting,key-auth",
trusted_ips = "0.0.0.0/0,::/0",
lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
})
end
local stop_kong = helpers.stop_kong
lazy_setup(function()
UPSTREAM_PORT = helpers.get_available_port()
UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)
https_server = helpers.https_server.new(UPSTREAM_PORT)
https_server:start()
end)
lazy_teardown(function()
assert(https_server, "unexpected error")
https_server:shutdown()
local _
_, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
db:reset()
end)
before_each(function ()
local _
_, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
db:reset()
_, db = helpers.get_db_utils(strategy, nil, { "rate-limiting", "key-auth" })
if policy == "redis" then
redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
elseif policy == "cluster" then
db:truncate("ratelimiting_metrics")
end
assert(start_kong())
admin_client = helpers.admin_client()
service = setup_service(admin_client, UPSTREAM_URL)
setup_route(admin_client, service, { test_path })
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
end)
after_each(function()
admin_client:close()
assert(stop_kong())
end)
if policy == "cluster" then
it("does not work if an error occurs", function ()
setup_rl_plugin(admin_client, {
minute = 6,
limit_by = "ip",
policy = policy,
fault_tolerant = false,
}, service)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function()
local ret = client_requests(2, function()
return GET(test_path)
end)
assert.same({6, 6}, ret.minute_limit)
assert.same({5, 4}, ret.minute_remaining)
assert.same({6, 6}, ret.limit)
assert.same({5, 4}, ret.remaining)
for _, reset in ipairs(ret.reset) do
assert.equal(true, reset <= 60 and reset >= 0)
end
end)
assert(db.connector:query("DROP TABLE ratelimiting_metrics"))
local res = assert(GET(test_path))
local body = assert.res_status(500, res)
local json = cjson.decode(body)
assert.not_nil(json)
assert.matches("An unexpected error occurred", json.message)
assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
assert.falsy(res.headers["Ratelimit-Limit"])
assert.falsy(res.headers["Ratelimit-Remaining"])
assert.falsy(res.headers["Ratelimit-Reset"])
end)
it("keeps working if an error occurs", function ()
setup_rl_plugin(admin_client, {
minute = 6,
limit_by = "ip",
policy = policy,
fault_tolerant = true,
}, service)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function()
local ret = client_requests(2, function()
return GET(test_path)
end)
assert.same({6, 6}, ret.minute_limit)
assert.same({5, 4}, ret.minute_remaining)
assert.same({6, 6}, ret.limit)
assert.same({5, 4}, ret.remaining)
assert.same({200, 200}, ret.status)
for _, reset in ipairs(ret.reset) do
assert.equal(true, reset <= 60 and reset >= 0)
end
end)
assert(db.connector:query("DROP TABLE ratelimiting_metrics"))
local res = assert(GET(test_path))
assert.res_status(200, res)
assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
assert.falsy(res.headers["Ratelimit-Limit"])
assert.falsy(res.headers["Ratelimit-Remaining"])
assert.falsy(res.headers["Ratelimit-Reset"])
end)
end -- if policy == "cluster" then
if policy == "redis" then
it("does not work if an error occurs", function ()
setup_rl_plugin(admin_client, {
minute = 6,
policy = "redis",
limit_by = "ip",
redis = {
host = "127.0.0.1",
port = 80, -- bad redis port
ssl = false,
},
fault_tolerant = false,
}, service)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local res = assert(GET(test_path))
local body = assert.res_status(500, res)
local json = cjson.decode(body)
assert.not_nil(json)
assert.matches("An unexpected error occurred", json.message)
assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
assert.falsy(res.headers["Ratelimit-Limit"])
assert.falsy(res.headers["Ratelimit-Remaining"])
assert.falsy(res.headers["Ratelimit-Reset"])
end)
it("keeps working if an error occurs", function ()
setup_rl_plugin(admin_client, {
minute = 6,
policy = "redis",
limit_by = "ip",
redis = {
host = "127.0.0.1",
port = 80, -- bad redis port
ssl = false,
},
fault_tolerant = true,
}, service)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
local res = assert(GET(test_path))
assert.res_status(200, res)
assert.falsy(res.headers["X-Ratelimit-Limit-Minute"])
assert.falsy(res.headers["X-Ratelimit-Remaining-Minute"])
assert.falsy(res.headers["Ratelimit-Limit"])
assert.falsy(res.headers["Ratelimit-Remaining"])
assert.falsy(res.headers["Ratelimit-Reset"])
end)
end -- if policy == "redis" then
end)
if policy == "redis" then
desc = fmt("Plugin: rate-limiting with sync_rate #db (access) [strategy: %s]", strategy)
describe(desc, function ()
local https_server, admin_client
local UPSTREAM_PORT
local UPSTREAM_URL
lazy_setup(function()
UPSTREAM_PORT = helpers.get_available_port()
UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)
helpers.get_db_utils(strategy, nil, {
"rate-limiting",
})
https_server = helpers.https_server.new(UPSTREAM_PORT)
https_server:start()
assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
plugins = "bundled,rate-limiting,key-auth",
trusted_ips = "0.0.0.0/0,::/0",
lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
log_level = "error"
}))
end)
lazy_teardown(function()
https_server:shutdown()
assert(helpers.stop_kong())
end)
before_each(function()
redis_helper.reset_redis(REDIS_HOST, REDIS_PORT)
admin_client = helpers.admin_client()
end)
after_each(function()
admin_client:close()
end)
it("blocks if exceeding limit", function ()
local test_path = "/test"
local service = setup_service(admin_client, UPSTREAM_URL)
local route = setup_route(admin_client, service, { test_path })
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = "redis",
limit_by = "ip",
redis = {
host = REDIS_HOST,
port = REDIS_PORT,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = false,
},
sync_rate = 10,
}, service)
local red = redis_helper.connect(REDIS_HOST, REDIS_PORT)
local ok, err = red:select(REDIS_DATABASE)
if not ok then
error("failed to change Redis database: " .. err)
end
finally(function()
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route)
delete_service(admin_client, service)
red:close()
local shell = require "resty.shell"
shell.run("cat servroot/logs/error.log", nil, 0)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
})
-- initially, the metrics are not written to the redis
assert(red:dbsize() == 0, "redis db should be empty, but got " .. red:dbsize())
retry(function ()
-- exceed the limit
for _i = 0, 7 do
GET(test_path)
end
-- exceed the limit locally
assert.res_status(429, GET(test_path))
-- wait for the metrics to be written to the redis
helpers.pwait_until(function()
GET(test_path)
assert(red:dbsize() == 1, "redis db should have 1 key, but got " .. red:dbsize())
end, 15)
-- wait for the metrics expire
helpers.pwait_until(function()
assert.res_status(200, GET(test_path))
end, 61)
end)
end) -- it("blocks if exceeding limit", function ()
end)
end -- if policy == "redis" then
end -- for __, policy in ipairs({ "local", "cluster", "redis" }) do
desc = fmt("Plugin: rate-limiting enable globally #db (access) [strategy: %s]", strategy)
describe(desc, function ()
local https_server, admin_client
local UPSTREAM_PORT
local UPSTREAM_URL
lazy_setup(function()
UPSTREAM_PORT = helpers.get_available_port()
UPSTREAM_URL = string.format("http://%s:%d/always_200", UPSTREAM_HOST, UPSTREAM_PORT)
helpers.get_db_utils(strategy, nil, {
"rate-limiting", "key-auth",
})
https_server = helpers.https_server.new(UPSTREAM_PORT)
https_server:start()
assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
plugins = "bundled,rate-limiting,key-auth",
trusted_ips = "0.0.0.0/0,::/0",
lua_ssl_trusted_certificate = "spec/fixtures/redis/ca.crt",
}))
end)
lazy_teardown(function()
https_server:shutdown()
assert(helpers.stop_kong())
end)
before_each(function()
admin_client = helpers.admin_client()
end)
after_each(function()
admin_client:close()
end)
it("global for single consumer", function()
local test_path_1, test_path_2 = "/1-test", "/2-test"
local test_key_name = "test-key"
local test_credential = "test-credential"
local service_1, service_2 = setup_service(admin_client, UPSTREAM_URL),
setup_service(admin_client, UPSTREAM_URL)
local route_1, route_2 = setup_route(admin_client, service_1, { test_path_1 }),
setup_route(admin_client, service_2, { test_path_2 })
local consumer = setup_consumer(admin_client, "Bob")
local key_auth_plugin = setup_key_auth_plugin(admin_client, {
key_names = { test_key_name },
})
local rl_plugin = setup_rl_plugin(admin_client, {
minute = 6,
policy = "local",
limit_by = "credential",
redis = {
host = REDIS_HOST,
port = REDIS_PORT,
password = REDIS_PASSWORD,
database = REDIS_DATABASE,
ssl = false,
}
})
local credential = setup_credential(admin_client, consumer, test_credential)
finally(function()
delete_credential(admin_client, credential)
delete_consumer(admin_client, consumer)
delete_plugin(admin_client, key_auth_plugin)
delete_plugin(admin_client, rl_plugin)
delete_route(admin_client, route_1)
delete_route(admin_client, route_2)
delete_service(admin_client, service_1)
delete_service(admin_client, service_2)
end)
helpers.wait_for_all_config_update({
override_global_rate_limiting_plugin = true,
override_global_key_auth_plugin = true,
})
retry(function ()
validate_headers(client_requests(7, function()
return GET(test_path_1, {
headers = {
[test_key_name] = test_credential,
}
})
end), true)
local ret = client_requests(7, function()
return GET(test_path_2, {
headers = {
[test_key_name] = test_credential,
}
})
end)
assert.same({
6, 6, 6, 6, 6, 6, 6,
}, ret.minute_limit)
assert.same({
0, 0, 0, 0, 0, 0, 0,
}, ret.minute_remaining)
assert.same({
6, 6, 6, 6, 6, 6, 6,
}, ret.limit)
assert.same({
0, 0, 0, 0, 0, 0, 0,
}, ret.remaining)
assert.same({
429, 429, 429, 429, 429, 429, 429,
}, ret.status)
for _, reset in ipairs(ret.reset) do
assert.equal(true, reset <= 60 and reset >= 0)
end
end)
end)
end)
end -- for _, strategy in helpers.each_strategy() do