Tweak rate limiting implementation
diff --git a/api-gateway-config/scripts/lua/lib/filemgmt.lua b/api-gateway-config/scripts/lua/lib/filemgmt.lua
index 0db3451..4fb96f7 100644
--- a/api-gateway-config/scripts/lua/lib/filemgmt.lua
+++ b/api-gateway-config/scripts/lua/lib/filemgmt.lua
@@ -35,7 +35,8 @@
function _M.createRouteConf(baseConfDir, namespace, gatewayPath, routeObj)
routeObj = utils.serializeTable(cjson.decode(routeObj))
local prefix = utils.concatStrings({"\t", "include /etc/api-gateway/conf.d/commons/common-headers.conf;", "\n",
- "\t", "set $upstream https://172.17.0.1;", "\n\n"})
+ "\t", "set $upstream https://172.17.0.1;", "\n",
+ "\t", "set $namespace ", namespace, ";\n\n"})
-- Set route headers and mapping by calling routing.processCall()
local outgoingRoute = utils.concatStrings({"\t", "access_by_lua_block {", "\n",
"\t\t", "local routing = require \"routing\"", "\n",
@@ -50,7 +51,7 @@
local file, err = io.open(utils.concatStrings({baseConfDir, namespace, "/", gatewayPath, ".conf"}), "w")
if not file then
ngx.status(500)
- ngx.say("Error adding to endpoint conf file: " .. err)
+ ngx.say(utils.concatStrings({"Error adding to endpoint conf file: ", err}))
ngx.exit(ngx.status)
end
local location = utils.concatStrings({"location /api/", namespace, "/", gatewayPath, " {\n",
@@ -58,7 +59,7 @@
outgoingRoute,
proxyPass,
"}\n"})
- file:write(location .. "\n")
+ file:write(utils.concatStrings({location, "\n"}))
file:close()
end
diff --git a/api-gateway-config/scripts/lua/lib/redis.lua b/api-gateway-config/scripts/lua/lib/redis.lua
index 0576bab..4cf63db 100644
--- a/api-gateway-config/scripts/lua/lib/redis.lua
+++ b/api-gateway-config/scripts/lua/lib/redis.lua
@@ -47,7 +47,7 @@
local connect, err = red:connect(host, port)
if not connect then
ngx.status = 500
- ngx.say("Failed to connect to redis: " .. err)
+ ngx.say(utils.concatStrings({"Failed to connect to redis: ", err}))
ngx.exit(ngx.status)
end
@@ -56,7 +56,7 @@
local res, err = red:auth(password)
if not res then
ngx.status = 500
- ngx.say("Failed to authenticate: " .. err)
+ ngx.say(utils.concatStrings({"Failed to authenticate: ", err}))
ngx.exit(ngx.status)
end
end
@@ -118,7 +118,7 @@
local ok, err = red:hset(key, field, routeObj)
if not ok then
ngx.status = 500
- ngx.say("Failed adding Route to redis: " .. err)
+ ngx.say(utils.concatStrings({"Failed adding Route to redis: ", err}))
ngx.exit(ngx.status)
end
end
@@ -238,8 +238,8 @@
local routeObj = _M.getRoute(red, redisKey, "route", ngx)
_M.createRoute(red, redisKey, "route", routeObj, ngx)
filemgmt.createRouteConf(BASE_CONF_DIR, namespace, gatewayPath, routeObj)
- ngx.say(redisKey .. " updated")
- ngx.log(ngx.INFO, redisKey .. " updated")
+ ngx.say(utils.concatStrings({redisKey, " updated"}))
+ ngx.log(ngx.INFO, utils.concatStrings({redisKey, " updated"}))
ngx.flush(true)
local ok, err = red:psubscribe("__keyspace@0__:routes:*:*")
diff --git a/api-gateway-config/scripts/lua/lib/resty/limit/req.lua b/api-gateway-config/scripts/lua/lib/resty/limit/req.lua
new file mode 100644
index 0000000..5e130e0
--- /dev/null
+++ b/api-gateway-config/scripts/lua/lib/resty/limit/req.lua
@@ -0,0 +1,186 @@
+-- Copyright (C) 2014 Monkey Zhang (timebug), UPYUN Inc.
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- Redistributions of source code must retain the above copyright notice,
+-- this list of conditions and the following disclaimer.
+--
+-- Redistributions in binary form must reproduce the above copyright notice,
+-- this list of conditions and the following disclaimer in the documentation
+-- and/or other materials provided with the distribution.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+-- DEALINGS IN THE SOFTWARE.
+
+local floor = math.floor
+local tonumber = tonumber
+
+
+local _M = { _VERSION = "0.01", OK = 1, BUSY = 2, FORBIDDEN = 3 }
+
+
+local redis_limit_req_script_sha
+local redis_limit_req_script = [==[
+local key = KEYS[1]
+local rate = tonumber(KEYS[2])
+local now, interval = tonumber(KEYS[3]), tonumber(KEYS[4])
+
+local excess, last, forbidden = 0, 0, 0
+
+local res = redis.pcall('GET', key)
+if type(res) == "table" and res.err then
+ return {err=res.err}
+end
+
+if res and type(res) == "string" then
+ local v = cjson.decode(res)
+ if v and #v > 2 then
+ excess, last, forbidden = v[1], v[2], v[3]
+ end
+
+ if forbidden == 1 then
+ return {3, excess} -- FORBIDDEN
+ end
+
+ local ms = math.abs(now - last)
+ excess = excess - rate * ms / 1000 + 1000
+
+ if excess < 0 then
+ excess = 0
+ end
+
+ if excess > 0 then
+ if interval > 0 then
+ local res = redis.pcall('SET', key,
+ cjson.encode({excess, now, 1}))
+ if type(res) == "table" and res.err then
+ return {err=res.err}
+ end
+
+ local res = redis.pcall('EXPIRE', key, interval)
+ if type(res) == "table" and res.err then
+ return {err=res.err}
+ end
+ end
+
+ return {2, excess} -- BUSY
+ end
+end
+
+local res = redis.pcall('SET', key, cjson.encode({excess, now, 0}))
+if type(res) == "table" and res.err then
+ return {err=res.err}
+end
+
+local res = redis.pcall('EXPIRE', key, 60)
+if type(res) == "table" and res.err then
+ return {err=res.err}
+end
+
+return {1, excess}
+]==]
+
+
+local function redis_lookup(conn, zone, key, rate, duration)
+ local red = conn
+
+ if not redis_limit_req_script_sha then
+ local res, err = red:script("LOAD", redis_limit_req_script)
+ if not res then
+ return nil, err
+ end
+
+ ngx.log(ngx.NOTICE, "load redis limit req script")
+
+ redis_limit_req_script_sha = res
+ end
+
+ local now = ngx.now() * 1000
+ local res, err = red:evalsha(redis_limit_req_script_sha, 4,
+ zone .. ":" .. key, rate, now, duration)
+ if not res then
+ redis_limit_req_script_sha = nil
+ return nil, err
+ end
+
+ -- put it into the connection pool of size 100,
+ -- with 10 seconds max idle timeout
+ local ok, err = red:set_keepalive(10000, 100)
+ if not ok then
+ ngx.log(ngx.WARN, "failed to set keepalive: ", err)
+ end
+
+ return res
+end
+
+
+function _M.limit(cfg)
+ if not cfg.conn then
+ local ok, redis = pcall(require, "resty.redis")
+ if not ok then
+ ngx.log(ngx.ERR, "failed to require redis")
+ return _M.OK
+ end
+
+ local rds = cfg.rds or {}
+ rds.timeout = rds.timeout or 1
+ rds.host = rds.host or "127.0.0.1"
+ rds.port = rds.port or 6379
+
+ local red = redis:new()
+
+ red:set_timeout(rds.timeout * 1000)
+
+ local ok, err = red:connect(rds.host, rds.port)
+ if not ok then
+ ngx.log(ngx.WARN, "redis connect err: ", err)
+ return _M.OK
+ end
+
+ cfg.conn = red
+ end
+
+ local conn = cfg.conn
+ local zone = cfg.zone or "limit_req"
+ local key = cfg.key or ngx.var.remote_addr
+ local rate = cfg.rate or "1r/s"
+ local interval = cfg.interval or 0
+ local log_level = cfg.log_level or ngx.NOTICE
+
+ local scale = 1
+ local len = #rate
+
+ if len > 3 and rate:sub(len - 2) == "r/s" then
+ scale = 1
+ rate = rate:sub(1, len - 3)
+ elseif len > 3 and rate:sub(len - 2) == "r/m" then
+ scale = 60
+ rate = rate:sub(1, len - 3)
+ end
+
+ rate = floor((tonumber(rate) or 1) * 1000 / scale)
+
+ local res, err = redis_lookup(conn, zone, key, rate, interval)
+ if res and (res[1] == _M.BUSY or res[1] == _M.FORBIDDEN) then
+ if res[1] == _M.BUSY then
+ ngx.log(log_level, 'limiting requests, excess ' ..
+ res[2]/1000 .. ' by zone "' .. zone .. '"')
+ end
+ return
+ end
+
+ if not res and err then
+ ngx.log(ngx.WARN, "redis lookup err: ", err)
+ end
+
+ return _M.OK
+end
+
+
+return _M
\ No newline at end of file
diff --git a/api-gateway-config/scripts/lua/lib/utils.lua b/api-gateway-config/scripts/lua/lib/utils.lua
index 5ab512e..5f34e53 100644
--- a/api-gateway-config/scripts/lua/lib/utils.lua
+++ b/api-gateway-config/scripts/lua/lib/utils.lua
@@ -61,7 +61,7 @@
logger.debug(concatStrings({'did not find table: ', tostring(k), '; ', tostring(v)}))
tt[#tt+1] = concatStrings({'"', tostring(v), '"'})
else
- logger.debug(concatStrings({'did not find table: ' .. tostring(k) .. '; ' .. tostring(v)}))
+ logger.debug(concatStrings({'did not find table: ', tostring(k), '; ', tostring(v)}))
tt[#tt+1] = tostring(v)
end
end
diff --git a/api-gateway-config/scripts/lua/management.lua b/api-gateway-config/scripts/lua/management.lua
index 95b8b1a..b4d1a1e 100644
--- a/api-gateway-config/scripts/lua/management.lua
+++ b/api-gateway-config/scripts/lua/management.lua
@@ -165,6 +165,7 @@
--
function _M.subscribe()
-- Initialize and connect to redis
+ logger.debug(utils.concatStrings({'Subscribing to Redis with host: ' , REDIS_HOST, '; port: ', REDIS_PORT, '; pass:', REDIS_PASS}))
local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 60000, ngx)
redis.subscribe(red, ngx)
end
@@ -175,6 +176,7 @@
--
function _M.unsubscribe()
-- Initialize and connect to redis
+ logger.debug(utils.concatStrings({'Unsubscribing to Redis with host: ' , REDIS_HOST, '; port: ', REDIS_PORT, '; pass:', REDIS_PASS}))
local red = redis.init(REDIS_HOST, REDIS_PORT, REDIS_PASS, 1000, ngx)
redis.unsubscribe(red, ngx)
@@ -221,10 +223,10 @@
local decoded = nil
local jsonStringList = {}
for key, value in pairs(args) do
- table.insert(jsonStringList, key)
+ table.insert(jsonStringList, key)
-- Handle case where the "=" character is inside any of the strings in the json body
if(value ~= true) then
- table.insert(jsonStringList, "=" .. value)
+ table.insert(jsonStringList, utils.concatStrings({"=", value}))
end
end
return cjson.decode(utils.concatStrings(jsonStringList))
diff --git a/api-gateway-config/scripts/lua/policies/rateLimit.lua b/api-gateway-config/scripts/lua/policies/rateLimit.lua
index d376b70..c240fbe 100644
--- a/api-gateway-config/scripts/lua/policies/rateLimit.lua
+++ b/api-gateway-config/scripts/lua/policies/rateLimit.lua
@@ -20,24 +20,40 @@
--- @module rateLimit
-- Process a rateLimit object, setting the rate and interval to the request
-
-local redis = require "lib/redis"
+-- @author David Green (greend)
local REDIS_HOST = os.getenv("REDIS_HOST")
local REDIS_PORT = os.getenv("REDIS_PORT")
local REDIS_PASS = os.getenv("REDIS_PASS")
+local request = require "lib/resty/limit/req"
+local utils = require "lib/utils"
+local logger = require "lib/logger"
-local logger = require("logger")
-local request = require "resty.rate.limit"
+local _M = {}
function limit(obj)
+ local i = 60 / obj.interval
+ local r = i * obj.rate
+ r = utils.concatStrings({tostring(r), 'r/m'})
+ logger.debug(utils.concatStrings({'Limiting using rate: ', r}))
+ logger.debug(utils.concatStrings({'Limiting using interval: ', obj.interval}))
+ logger.debug(utils.concatStrings({'Limiting using redis config host: ' , REDIS_HOST, '; port: ', REDIS_PORT, '; pass:', REDIS_PASS}))
- request.limit ({ key = ngx.var.namespace,
- rate = obj.rate,
- interval = obj.interval,
- log_level = ngx.NOTICE,
- redis_config = { host = REDIS_HOST, port = REDIS_PORT, timeout = 1, pool_size = 100 },
- whitelisted_api_keys = {} })
-
+ local config = {
+ key = ngx.var.namespace,
+ zone = 'rateLimiting',
+ rate = r,
+ interval = obj.interval,
+ log_level = ngx.NOTICE,
+ rds = { host = REDIS_HOST, port = REDIS_PORT }
+ }
+ local ok = request.limit (config)
+ if not ok then
+ logger.err('Rate limit exceeded. Sending 429')
+ ngx.exit(429)
+ end
end
+_M.limit = limit
+
+return _M
\ No newline at end of file
diff --git a/api-gateway-config/scripts/lua/routing.lua b/api-gateway-config/scripts/lua/routing.lua
index f4969d3..9cead45 100644
--- a/api-gateway-config/scripts/lua/routing.lua
+++ b/api-gateway-config/scripts/lua/routing.lua
@@ -26,6 +26,7 @@
local mapping = require "policies/mapping"
local rateLimit = require "policies/rateLimit"
local logger = require "lib/logger"
+local utils = require "lib/utils"
local url = require "url"
local cjson = require "cjson"
@@ -45,26 +46,26 @@
local found = false
for k, v in pairs(obj) do
if k == verb then
- logger.debug( 'found verb: ' .. k)
- logger.debug( 'found backendUrl: ' .. v.backendUrl)
+ logger.debug(utils.concatStrings({'found verb: ', k}))
+ logger.debug(utils.concatStrings({'found backendUrl: ', v.backendUrl}))
local u = url.parse(v.backendUrl)
ngx.req.set_uri(u.path)
- ngx.var.upstream = u.scheme .. '://' .. u.host
- logger.debug('upstream: ' .. ngx.var.upstream)
+ ngx.var.upstream = utils.concatStrings({u.scheme, '://', u.host})
+ logger.debug(utils.concatStrings({'upstream: ', ngx.var.upstream}))
if v.backendMethod ~= nil then
- logger.debug('Setting a backend method: ' .. v.backendMethod)
+ logger.debug(utils.concatStrings({'Setting a backend method: ', v.backendMethod}))
setVerb(v.backendMethod)
end
parsePolicies(v.policies)
found = true
break
else
- logger.debug( 'verb not found: ' .. k)
+ logger.debug(utils.concatStrings({'verb not found: ', k}))
end
end
if found == false then
- logger.debug( 'Finished loop without finding.')
- ngx.say("Whoops. Verb not supported.")
+ logger.debug('Finished loop without finding.')
+ ngx.say('Whoops. Verb not supported.')
ngx.exit(404)
end
end
@@ -86,11 +87,11 @@
--- Given a verb, transforms the backend request to use that method
-- @param v Verb to set on the backend request
function setVerb(v)
- if (string.lower(v) == "post") then
+ if (string.lower(v) == 'post') then
ngx.req.set_method(ngx.HTTP_POST)
- elseif (string.lower(v) == "put") then
+ elseif (string.lower(v) == 'put') then
ngx.req.set_method(ngx.HTTP_PUT)
- elseif (string.lower(v) == "delete") then
+ elseif (string.lower(v) == 'delete') then
ngx.req.set_method(ngx.HTTP_DELETE)
else
ngx.req.set_method(ngx.HTTP_GET)