Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate Limit headers #1166

Merged
merged 8 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Fixed

### Added

- Added Rate-limit headers policy [THREESCALE-3795](https://issues.jboss.org/browse/THREESCALE-3795) [PR #1166](https://github.com/3scale/APIcast/pull/1166)

## [3.8.0] - 2020-03-24

`3.8.0-rc1` was considered final and became `3.8.0`.
`3.8.0-cr1` was considered final and became `3.8.0`.

## [3.8.0-rc1] - 2020-03-07
## [3.8.0-cr1] - 2020-03-07

### Fixed
- Fixed naming issues in policies [THREESCALE-4150](https://issues.jboss.org/browse/THREESCALE-4150) [PR #1167](https://github.com/3scale/APIcast/pull/1167)
Expand Down Expand Up @@ -795,4 +800,4 @@ expressed might change in future releases.
[3.7.0]: https://github.com/3scale/apicast/compare/v3.7.0-cr2...v3.7.0
[3.8.0-alpha1]: https://github.com/3scale/apicast/compare/v3.7.0...v3.8.0-alpha1
[3.8.0-alpha2]: https://github.com/3scale/apicast/compare/v3.8.0-alpha1...v3.8.0-alpha2
[3.8.0-rc1]: https://github.com/3scale/apicast/compare/v3.8.0-alpha2...v3.8.0-rc1
[3.8.0-cr1]: https://github.com/3scale/apicast/compare/v3.8.0-alpha2...v3.8.0-cr1
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ $(S2I_CONTEXT)/Roverfile.lock : $(S2I_CONTEXT)/Roverfile $(S2I_CONTEXT)/apicast-
$(ROVER) lock --roverfile=$(S2I_CONTEXT)/Roverfile

lua_modules: $(ROVER) $(S2I_CONTEXT)/Roverfile.lock
# This variable is to skip issues with openssl 1.1.1
# https://github.com/wahern/luaossl/issues/175
EXTRA_CFLAGS="-DHAVE_EVP_KDF_CTX=1" $(ROVER) install --roverfile=$(S2I_CONTEXT)/Roverfile
# This variable is to skip issues with openssl 1.1.1
# https://github.com/wahern/luaossl/issues/175
EXTRA_CFLAGS="-DHAVE_EVP_KDF_CTX=1" $(ROVER) install --roverfile=$(S2I_CONTEXT)/Roverfile > /dev/null

lua_modules/bin/rover:
@LUAROCKS_CONFIG=$(S2I_CONTEXT)/config-5.1.lua luarocks install --server=http://luarocks.org/dev lua-rover --tree=lua_modules 1>&2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ function _M:access(context)
else
local formatted_usage = usage:format()
local backend_res = backend:authorize(formatted_usage, credentials)
context:publish_backend_auth(backend_res)
local backend_status = backend_res.status
local cache_handler = context.cache_handler -- Set by Caching policy

Expand Down
23 changes: 22 additions & 1 deletion gateway/src/apicast/policy/apicast/apicast.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local errors = require('apicast.errors')
local math = math
local setmetatable = setmetatable
local assert = assert
local table_insert = table.insert

local user_agent = require('apicast.user_agent')

Expand Down Expand Up @@ -102,7 +103,7 @@ function _M:access(context)
local p = context and context.proxy or ctx.proxy or self.proxy

if p then
return p:access(context.service, context.usage, context.credentials, context.ttl)
return p:access(context, context.service, context.usage, context.credentials, context.ttl)
davidor marked this conversation as resolved.
Show resolved Hide resolved
end
end

Expand All @@ -119,6 +120,26 @@ function _M:content(context)
end
end

function _M:export()
return {
add_backend_auth_subscriber = function(context, handler)
if not context.backend_auth_subscribers then
context.backend_auth_subscribers = {}
end
table_insert(context.backend_auth_subscribers, handler)
end,
publish_backend_auth = function(context, response)
if not context.backend_auth_subscribers then
return
end

for _,handler in ipairs(context.backend_auth_subscribers) do
handler(context, response)
end
end
}
end

_M.balancer = balancer.call

return _M
20 changes: 20 additions & 0 deletions gateway/src/apicast/policy/rate_limit_headers/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Rate limits headers

This policy send the headers back to the user with the rate limit information.
This policy implements the [RateLimit Header Fields for HTTP draft]
(https://ioggstream.github.io/draft-polli-ratelimit-headers/draft-polli-ratelimit-headers.html)


## Headers accuracy:

This header information is retrieved from
[APISonator](https://github.com/3scale/apisonator), but is not always sync, on
second request we cached the information, so it's possible that the information
is not 100% accurate with APISonator, but APIcast always try to sync with
backend.

The main reason for this is performance, in this case, call to APISonator in
request time is time-consuming, and it's not needed at all.

If data accurate is needed, caching can be disabled, so data will always be 100%
accurate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "http://apicast.io/policy-v1.1/schema#manifest#",
"name": "Rate Limits Headers",
"summary": "Set rate limit headers on response",
"description":
["This policy implements the `RateLimit Header Fields for HTTP` draft in ",
"responses."],
"version": "builtin",
"configuration": {}
}
55 changes: 55 additions & 0 deletions gateway/src/apicast/policy/rate_limit_headers/cache.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local lrucache = require("resty.lrucache")
local cache_entry = require("apicast.policy.rate_limit_headers.cache_entry")

local _M = {}

local mt = { __index = _M }
local default_namespace = "rate_limit_namespace"


function _M.new(size, namespace)
local cache_size = tonumber(size) or 1000
local self = setmetatable({}, mt)
local cache, err = lrucache.new(cache_size)
if err then
ngx.log(ngx.ERR, "Cannot start cache for usage metrics, err=", err)
return err
end
self.cache = cache
self.namespace = namespace or default_namespace
return self
end

function _M:get_key(usage)
davidor marked this conversation as resolved.
Show resolved Hide resolved
return self.namespace .. "::".. usage:encoded_format()
end

function _M:decrement_usage_metric(usage)
if not usage then
return cache_entry.Init_empty(usage)
end

local key = self:get_key(usage)
local data = self.cache:get(key)

if not data then
-- If it's here should not, because this should be called in the
-- post_action, so return an empty one0
return cache_entry.Init_empty(usage)
end
-- data:decrement(delta)
-- Take care here, delta can be from usage.delta

data:decrement(1)
self.cache:set(key, data)
return data
end

function _M:reset_or_create_usage_metric(usage, max, remaining, reset)
local key = self:get_key(usage)
local data = cache_entry.new(usage, max, remaining, reset)
self.cache:set(key, data)
return data
end

return _M
44 changes: 44 additions & 0 deletions gateway/src/apicast/policy/rate_limit_headers/cache_entry.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local now = ngx.now

local counter = require "resty.counter"
local countdown_counter = require("apicast.policy.rate_limit_headers.countdown_counter")

local _M = {}
local mt = { __index = _M }

function _M.new(usage, max, remaining, reset)
local self = setmetatable({}, mt)

self.usage = usage
self:reset(max, remaining, reset)

return self
end

function _M.Init_empty(usage)
local self = setmetatable({}, mt)

self.usage = usage
self:reset(0, 0, now())
return self
end

function _M:decrement(delta)
self.remaining:decrement(delta)
end

function _M:dump_data()
return {
limit = self.limit:__tostring(),
remaining = self.remaining:__tostring(),
reset = self.reset:remaining_secs_positive(now()),
}
end

function _M:reset(max, remaining, reset)
self.limit = counter.new(max)
self.remaining = counter.new(remaining)
self.reset = countdown_counter.new(reset, now())
end

return _M
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local now = ngx.now
local _M = {}

local mt = { __index = _M }

function _M.new(limit_time_delta, initial_time)
if not initial_time then
initial_time = now()
end

local self = setmetatable({}, mt)
self.limit_time = tonumber(initial_time) + tonumber(limit_time_delta)
return self
end

function _M:remaining_secs(time)
return self.limit_time - time
end

function _M:remaining_secs_positive(time)
local result = self:remaining_secs(time)
if result >= 0 then
return tonumber(string.format("%i", result))
end
return 0
end

function _M:__tostring()
return tostring(self.limit_time)
end

return _M
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/rate_limit_headers/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('rate_limit_headers')
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
local policy = require('apicast.policy')
local _M = policy.new('Rate Limit Headers', 'builtin')

local usage_cache = require "cache"

local new = _M.new
local get_phase = ngx.get_phase

-- Headers returned by APISonator with the information.
-- https://github.com/3scale/apisonator/blob/master/docs/extensions.md#limit_headers-boolean
local threescale_limit_header = "3scale-Limit-Max-Value"
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
local threescale_remaining_header = "3scale-Limit-Remaining"
local threescale_reset_header = "3scale-Limit-Reset"

-- Headers spec in RFC
-- https://ioggstream.github.io/draft-polli-ratelimit-headers/draft-polli-ratelimit-headers.html#name-header-specifications
local limit_header = "RateLimit-Limit"
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
local remaining_header = "RateLimit-Remaining"
local reset_header = "RateLimit-Reset"

function _M.new(config)
local self = new(config)
self.cache = usage_cache.new(1000, "rate_limit_headers")
return self
end

local function handler(self)
local _self = self
local callback = function(context, response)
if not response.headers then
-- This is why callback is called from batching policy, and only returns
-- 200 status
ngx.log(ngx.ERR, "No headers in reset rate limit headers, discard")
return nil
end
local limit = response.headers[threescale_limit_header] or 0
local remaining = response.headers[threescale_remaining_header] or 0
local reset = response.headers[threescale_reset_header] or 0

local data = _self.cache:reset_or_create_usage_metric(
context.usage, limit, remaining, reset)

if get_phase() == "access" then
context.rate_limit_info = data
end
end
return callback
end

function _M:access(context)
context:add_backend_auth_subscriber(handler(self))
end

local function decrement(self, usage)
return self.cache:decrement_usage_metric(usage)
end

local function add_headers(info)
ngx.header[limit_header] = info.limit
ngx.header[remaining_header] = info.remaining
ngx.header[reset_header] = info.reset
end

function _M:content(context)
if context.rate_limit_info then
add_headers(context.rate_limit_info:dump_data())
else
add_headers(decrement(self, context.usage):dump_data())
end
end

return _M
Loading