From ac5504b672c95f2540fdae459528786b013eadd2 Mon Sep 17 00:00:00 2001 From: thefosk Date: Tue, 7 Jul 2015 23:04:03 -0700 Subject: [PATCH] Closes #379 --- .luacheckrc | 5 + kong-0.4.0-1.rockspec | 7 + kong.yml | 1 + kong/dao/cassandra/base_dao.lua | 2 +- kong/dao/schemas_validation.lua | 2 +- kong/plugins/ip-restriction/access.lua | 30 + kong/plugins/ip-restriction/handler.lua | 23 + kong/plugins/ip-restriction/init_worker.lua | 9 + kong/plugins/ip-restriction/schema.lua | 26 + .../resty-lrucache/lib/resty/lrucache.lua | 229 ++++++++ .../lib/resty/lrucache/pureffi.lua | 534 ++++++++++++++++++ spec/plugins/ip-restriction/access_spec.lua | 97 ++++ spec/unit/statics_spec.lua | 1 + 13 files changed, 964 insertions(+), 2 deletions(-) create mode 100644 kong/plugins/ip-restriction/access.lua create mode 100644 kong/plugins/ip-restriction/handler.lua create mode 100644 kong/plugins/ip-restriction/init_worker.lua create mode 100644 kong/plugins/ip-restriction/schema.lua create mode 100644 kong/vendor/resty-lrucache/lib/resty/lrucache.lua create mode 100644 kong/vendor/resty-lrucache/lib/resty/lrucache/pureffi.lua create mode 100644 spec/plugins/ip-restriction/access_spec.lua diff --git a/.luacheckrc b/.luacheckrc index b5652c1fca3d..237740702a71 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -19,6 +19,11 @@ files["kong/vendor/resty_http.lua"] = { unused = false } +files["kong/vendor/resty-lrucache/lib/resty/"] = { + global = false, + unused = false +} + files["spec/"] = { globals = {"describe", "it", "before_each", "setup", "after_each", "teardown", "stub", "mock", "spy", "finally", "pending"} } diff --git a/kong-0.4.0-1.rockspec b/kong-0.4.0-1.rockspec index d343ae997a23..5930ef098eeb 100644 --- a/kong-0.4.0-1.rockspec +++ b/kong-0.4.0-1.rockspec @@ -25,6 +25,7 @@ dependencies = { "lua-cjson ~> 2.1.0-1", "ansicolors ~> 1.0.2-3", "lbase64 ~> 20120820-1", + "lua-resty-iputils ~> 0.1.0-1", "luasocket ~> 2.0.2-5", "lrexlib-pcre ~> 2.7.2-1", @@ -39,6 +40,7 @@ build = { ["lapp"] = "kong/vendor/lapp.lua", ["ngx.ssl"] = "kong/vendor/ssl.lua", ["resty_http"] = "kong/vendor/resty_http.lua", + ["resty.lrucache"] = "kong/vendor/resty-lrucache/lib/resty/lrucache.lua", ["kong.constants"] = "kong/constants.lua", @@ -160,6 +162,11 @@ build = { ["kong.plugins.ssl.ssl_util"] = "kong/plugins/ssl/ssl_util.lua", ["kong.plugins.ssl.schema"] = "kong/plugins/ssl/schema.lua", + ["kong.plugins.ip-restriction.handler"] = "kong/plugins/ip-restriction/handler.lua", + ["kong.plugins.ip-restriction.init_worker"] = "kong/plugins/ip-restriction/init_worker.lua", + ["kong.plugins.ip-restriction.access"] = "kong/plugins/ip-restriction/access.lua", + ["kong.plugins.ip-restriction.schema"] = "kong/plugins/ip-restriction/schema.lua", + ["kong.api.app"] = "kong/api/app.lua", ["kong.api.crud_helpers"] = "kong/api/crud_helpers.lua", ["kong.api.routes.kong"] = "kong/api/routes/kong.lua", diff --git a/kong.yml b/kong.yml index 03bb331d249a..12a553752643 100644 --- a/kong.yml +++ b/kong.yml @@ -14,6 +14,7 @@ plugins_available: - response_transformer - requestsizelimiting - analytics + - ip-restriction ## The Kong working directory ## (Make sure you have read and write permissions) diff --git a/kong/dao/cassandra/base_dao.lua b/kong/dao/cassandra/base_dao.lua index 6a40c4f38b4f..27898a2cd670 100644 --- a/kong/dao/cassandra/base_dao.lua +++ b/kong/dao/cassandra/base_dao.lua @@ -415,7 +415,7 @@ end -- the other should be preserved. Of course this only applies in partial update. local function fix_tables(t, old_t, schema) for k, v in pairs(schema.fields) do - if v.schema then + if t[k] ~= nil and v.schema then local s = type(v.schema) == "function" and v.schema(t) or v.schema for s_k, s_v in pairs(s.fields) do if not t[k][s_k] and old_t[k] then diff --git a/kong/dao/schemas_validation.lua b/kong/dao/schemas_validation.lua index 3caebaceafa9..1486760dbc27 100644 --- a/kong/dao/schemas_validation.lua +++ b/kong/dao/schemas_validation.lua @@ -83,7 +83,7 @@ function _M.validate_entity(t, schema, options) is_valid_type = bool == "true" or bool == "false" t[column] = bool == "true" elseif v.type == "array" then - t[column] = stringy.split(t[column], ",") + t[column] = stringy.strip(t[column]) == "" and {} or stringy.split(t[column], ",") -- Handling empty arrays for arr_k, arr_v in ipairs(t[column]) do t[column][arr_k] = stringy.strip(arr_v) end diff --git a/kong/plugins/ip-restriction/access.lua b/kong/plugins/ip-restriction/access.lua new file mode 100644 index 000000000000..cb86f5eebcdb --- /dev/null +++ b/kong/plugins/ip-restriction/access.lua @@ -0,0 +1,30 @@ +local iputils = require "resty.iputils" +local responses = require "kong.tools.responses" +local utils = require "kong.tools.utils" + +local _M = {} + +function _M.execute(conf) + local block = false + + if conf.blacklist and utils.table_size(conf.blacklist) > 0 then + if iputils.ip_in_cidrs(ngx.var.remote_addr, conf._blacklist_cache) then + block = true + end + end + + if conf.whitelist and utils.table_size(conf.whitelist) > 0 then + if iputils.ip_in_cidrs(ngx.var.remote_addr, conf._whitelist_cache) then + block = false + else + block = true + end + end + + if block then + ngx.ctx.stop_phases = true -- interrupt other phases of this request + return responses.send_HTTP_FORBIDDEN("Your IP address is not allowed") + end +end + +return _M diff --git a/kong/plugins/ip-restriction/handler.lua b/kong/plugins/ip-restriction/handler.lua new file mode 100644 index 000000000000..2735d360fe23 --- /dev/null +++ b/kong/plugins/ip-restriction/handler.lua @@ -0,0 +1,23 @@ +local BasePlugin = require "kong.plugins.base_plugin" +local init_worker = require "kong.plugins.ip-restriction.init_worker" +local access = require "kong.plugins.ip-restriction.access" + +local IpRestrictionHandler = BasePlugin:extend() + +function IpRestrictionHandler:new() + IpRestrictionHandler.super.new(self, "ip-restriction") +end + +function IpRestrictionHandler:init_worker() + IpRestrictionHandler.super.init_worker(self) + init_worker.execute() +end + +function IpRestrictionHandler:access(conf) + IpRestrictionHandler.super.access(self) + access.execute(conf) +end + +IpRestrictionHandler.PRIORITY = 990 + +return IpRestrictionHandler diff --git a/kong/plugins/ip-restriction/init_worker.lua b/kong/plugins/ip-restriction/init_worker.lua new file mode 100644 index 000000000000..5ad15199e129 --- /dev/null +++ b/kong/plugins/ip-restriction/init_worker.lua @@ -0,0 +1,9 @@ +local iputils = require "resty.iputils" + +local _M = {} + +function _M.execute() + iputils.enable_lrucache() +end + +return _M diff --git a/kong/plugins/ip-restriction/schema.lua b/kong/plugins/ip-restriction/schema.lua new file mode 100644 index 000000000000..7c4a1bb49138 --- /dev/null +++ b/kong/plugins/ip-restriction/schema.lua @@ -0,0 +1,26 @@ +local iputils = require "resty.iputils" + +local function validate_ips(v, t, column) + local new_fields + if v and type(v) == "table" then + for _, ip in ipairs(v) do + local _, err = iputils.parse_cidr(ip) + if type(err) == "string" then -- It's an error only if the second variable is a string + return false, err + end + end + new_fields = { ["_"..column.."_cache"] = iputils.parse_cidrs(v) } + end + return true, nil, new_fields +end + +return { + fields = { + whitelist = { type = "array", func = validate_ips }, + blacklist = { type = "array", func = validate_ips }, + + -- Internal use + _whitelist_cache = { type = "array" }, + _blacklist_cache = { type = "array" } + } +} diff --git a/kong/vendor/resty-lrucache/lib/resty/lrucache.lua b/kong/vendor/resty-lrucache/lib/resty/lrucache.lua new file mode 100644 index 000000000000..fdaccde8de4d --- /dev/null +++ b/kong/vendor/resty-lrucache/lib/resty/lrucache.lua @@ -0,0 +1,229 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local ffi_new = ffi.new +local ffi_sizeof = ffi.sizeof +local ffi_cast = ffi.cast +local ffi_fill = ffi.fill +local ngx_now = ngx.now +local uintptr_t = ffi.typeof("uintptr_t") +local setmetatable = setmetatable +local tonumber = tonumber + + +-- queue data types +-- +-- this queue is a double-ended queue and the first node +-- is reserved for the queue itself. +-- the implementation is mostly borrowed from nginx's ngx_queue_t data +-- structure. + +ffi.cdef[[ + typedef struct lrucache_queue_s lrucache_queue_t; + struct lrucache_queue_s { + double expire; /* in seconds */ + lrucache_queue_t *prev; + lrucache_queue_t *next; + }; +]] + +local queue_arr_type = ffi.typeof("lrucache_queue_t[?]") +local queue_ptr_type = ffi.typeof("lrucache_queue_t*") +local queue_type = ffi.typeof("lrucache_queue_t") +local NULL = ffi.null + + +-- queue utility functions + +local function queue_insert_tail(h, x) + local last = h[0].prev + x.prev = last + last.next = x + x.next = h + h[0].prev = x +end + + +local function queue_init(size) + if not size then + size = 0 + end + local q = ffi_new(queue_arr_type, size + 1) + ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) + + if size == 0 then + q[0].prev = q + q[0].next = q + + else + local prev = q[0] + for i = 1, size do + local e = q[i] + prev.next = e + e.prev = prev + prev = e + end + + local last = q[size] + last.next = q + q[0].prev = last + end + + return q +end + + +local function queue_is_empty(q) + -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) + return q == q[0].prev +end + + +local function queue_remove(x) + local prev = x.prev + local next = x.next + + next.prev = prev + prev.next = next + + -- for debugging purpose only: + x.prev = NULL + x.next = NULL +end + + +local function queue_insert_head(h, x) + x.next = h[0].next + x.next.prev = x + x.prev = h + h[0].next = x +end + + +local function queue_last(h) + return h[0].prev +end + + +local function queue_head(h) + return h[0].next +end + + +-- true module stuffs + +local _M = { + _VERSION = '0.04' +} +local mt = { __index = _M } + + +local function ptr2num(ptr) + return tonumber(ffi_cast(uintptr_t, ptr)) +end + + +function _M.new(size) + if size < 1 then + return nil, "size too small" + end + + local self = { + keys = {}, + hasht = {}, + free_queue = queue_init(size), + cache_queue = queue_init(), + key2node = {}, + node2key = {}, + } + return setmetatable(self, mt) +end + + +function _M.get(self, key) + local hasht = self.hasht + local val = hasht[key] + if not val then + return nil + end + + local node = self.key2node[key] + + -- print(key, ": moving node ", tostring(node), " to cache queue head") + local cache_queue = self.cache_queue + queue_remove(node) + queue_insert_head(cache_queue, node) + + if node.expire >= 0 and node.expire < ngx_now() then + -- print("expired: ", node.expire, " > ", ngx_now()) + return nil, val + end + return val +end + + +function _M.delete(self, key) + self.hasht[key] = nil + + local key2node = self.key2node + local node = key2node[key] + + if not node then + return false + end + + key2node[key] = nil + self.node2key[ptr2num(node)] = nil + + queue_remove(node) + queue_insert_tail(self.free_queue, node) + return true +end + + +function _M.set(self, key, value, ttl) + local hasht = self.hasht + hasht[key] = value + + local key2node = self.key2node + local node = key2node[key] + if not node then + local free_queue = self.free_queue + local node2key = self.node2key + + if queue_is_empty(free_queue) then + -- evict the least recently used key + -- assert(not queue_is_empty(self.cache_queue)) + node = queue_last(self.cache_queue) + + local oldkey = node2key[ptr2num(node)] + -- print(key, ": evicting oldkey: ", oldkey, ", oldnode: ", + -- tostring(node)) + if oldkey then + hasht[oldkey] = nil + key2node[oldkey] = nil + end + + else + -- take a free queue node + node = queue_head(free_queue) + -- print(key, ": get a new free node: ", tostring(node)) + end + + node2key[ptr2num(node)] = key + key2node[key] = node + end + + queue_remove(node) + queue_insert_head(self.cache_queue, node) + + if ttl then + node.expire = ngx_now() + ttl + else + node.expire = -1 + end +end + + +return _M diff --git a/kong/vendor/resty-lrucache/lib/resty/lrucache/pureffi.lua b/kong/vendor/resty-lrucache/lib/resty/lrucache/pureffi.lua new file mode 100644 index 000000000000..6f82ed5999ad --- /dev/null +++ b/kong/vendor/resty-lrucache/lib/resty/lrucache/pureffi.lua @@ -0,0 +1,534 @@ +-- Copyright (C) Yichun Zhang (agentzh) +-- Copyright (C) Shuxin Yang + +--[[ + This module implements a key/value cache store. We adopt LRU as our +replace/evict policy. Each key/value pair is tagged with a Time-to-Live (TTL); +from user's perspective, stale pairs are automatically removed from the cache. + +Why FFI +------- + In Lua, expression "table[key] = nil" does not *PHYSICALLY* remove the value +associated with the key; it just set the value to be nil! So the table will +keep growing with large number of the key/nil pairs which will be purged until +resize() operator is called. + + This "feature" is terribly ill-suited to what we need. Therefore we have to +rely on FFI to build a hash-table where any entry can be physically deleted +immediately. + +Under the hood: +-------------- + In concept, we introduce three data structures to implement the cache store: + 1. key/value vector for storing keys and values. + 2. a queue to mimic the LRU. + 3. hash-table for looking up the value for a given key. + + Unfortunately, efficiency and clarity usually come at each other cost. The +data strucutres we are using are slightly more complicated than what we +described above. + + o. Lua does not have efficient way to store a vector of pair. So, we use + two vectors for key/value pair: one for keys and the other for values + (_M.key_v and _M.val_v, respectively), and i-th key corresponds to + i-th value. + + A key/value pair is identified by the "id" field in a "node" (we shall + discuss node later) + + o. The queue is nothing more than a doubly-linked list of "node" linked via + lrucache_pureffi_queue_s::{next|prev} fields. + + o. The hash-table has two parts: + - the _M.bucket_v[] a vector of bucket, indiced by hash-value, and + - a bucket is a singly-linked list of "node" via the + lrucache_pureffi_queue_s::conflict field. + + A key must be a string, and the hash value of a key is evaluated by: + crc32(key-cast-to-pointer) % size(_M.bucket_v). + We mandate size(_M.bucket_v) being a power-of-two in order to avoid + expensive modulo operation. + + At the heart of the module is an array of "node" (of type + lrucache_pureffi_queue_s). A node: + - keeps the meta-data of its corresponding key/value pair + (embodied by the "id", and "expire" field); + - is a part of LRU queue (embodied by "prev" and "next" fields); + - is a part of hash-table (embodied by the "conflict" field). +]] + +local ffi = require "ffi" +local bit = require "bit" + + +local ffi_new = ffi.new +local ffi_sizeof = ffi.sizeof +local ffi_cast = ffi.cast +local ffi_fill = ffi.fill +local ngx_now = ngx.now +local uintptr_t = ffi.typeof("uintptr_t") +local c_str_t = ffi.typeof("const char*") +local int_t = ffi.typeof("int") +local int_array_t = ffi.typeof("int[?]") + + +local crc_tab = ffi.new("const unsigned int[256]", { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }); + +local setmetatable = setmetatable +local tonumber = tonumber + +local brshift = bit.rshift +local bxor = bit.bxor +local band = bit.band + +local ok, tab_new = pcall(require, "table.new") +if not ok then + tab_new = function (narr, nrec) return {} end +end + +-- queue data types +-- +-- this queue is a double-ended queue and the first node +-- is reserved for the queue itself. +-- the implementation is mostly borrowed from nginx's ngx_queue_t data +-- structure. + +ffi.cdef[[ + /* A lrucache_pureffi_queue_s node hook together three data structures: + * o. the key/value store as embodied by the "id" (which is in essence the + * indentifier of key/pair pair) and the "expire" (which is a metadata + * of the corresponding key/pair pair). + * o. The LRU queue via the prev/next fields. + * o. The hash-tabble as embodied by the "conflict" field. + */ + typedef struct lrucache_pureffi_queue_s lrucache_pureffi_queue_t; + struct lrucache_pureffi_queue_s { + /* Each node is assigned a unique ID at construction time, and the + * ID remain immutatble, regardless the node is in active-list or + * free-list. The queue header is assigned ID 0. Since queue-header + * is a sentinel node, 0 denodes "invalid ID". + * + * Intuitively, we can view the "id" as the identifier of key/value + * pair. + */ + int id; + + /* The bucket of the hash-table is implemented as a singly-linked list. + * The "conflict" refers to the ID of the next node in the bucket. + */ + int conflict; + + double expire; /* in seconds */ + + lrucache_pureffi_queue_t *prev; + lrucache_pureffi_queue_t *next; + }; +]] + +local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]") +local queue_ptr_type = ffi.typeof("lrucache_pureffi_queue_t*") +local queue_type = ffi.typeof("lrucache_pureffi_queue_t") +local NULL = ffi.null + + +--======================================================================== +-- +-- Queue utility functions +-- +--======================================================================== + +-- Append the element "x" to the given queue "h". +local function queue_insert_tail(h, x) + local last = h[0].prev + x.prev = last + last.next = x + x.next = h + h[0].prev = x +end + + +--[[ +Allocate a queue with size + 1 elements. Elements are linked together in a +circular way, i.e. the last element's "next" points to the first element, +while the first element's "prev" element points to the last element. +]] +local function queue_init(size) + if not size then + size = 0 + end + local q = ffi_new(queue_arr_type, size + 1) + ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) + + if size == 0 then + q[0].prev = q + q[0].next = q + + else + local prev = q[0] + for i = 1, size do + local e = q[i] + e.id = i + prev.next = e + e.prev = prev + prev = e + end + + local last = q[size] + last.next = q + q[0].prev = last + end + + return q +end + + +local function queue_is_empty(q) + -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) + return q == q[0].prev +end + + +local function queue_remove(x) + local prev = x.prev + local next = x.next + + next.prev = prev + prev.next = next + + -- for debugging purpose only: + x.prev = NULL + x.next = NULL +end + + +-- Insert the element "x" the to the given queue "h" +local function queue_insert_head(h, x) + x.next = h[0].next + x.next.prev = x + x.prev = h + h[0].next = x +end + + +local function queue_last(h) + return h[0].prev +end + + +local function queue_head(h) + return h[0].next +end + + +--======================================================================== +-- +-- Miscellaneous Utility Functions +-- +--======================================================================== + +local function ptr2num(ptr) + return tonumber(ffi_cast(uintptr_t, ptr)) +end + + +local function crc32_ptr(ptr) + local crc32 = 0; + + local p = brshift(ptr2num(ptr), 3) + local b = band(p, 255) + crc32 = crc_tab[b] + + b = band(brshift(p, 8), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + b = band(brshift(p, 16), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + --b = band(brshift(p, 24), 255) + --crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + return crc32 +end + + +--======================================================================== +-- +-- Implementation of "export" functions +-- +--======================================================================== + +local _M = { + _VERSION = '0.04' +} +local mt = { __index = _M } + + +-- "size" specifies the maximum number of entries in the LRU queue, and the +-- "load_factor" designates the 'load factor' of the hash-table we are using +-- internally. The default value of load-factor is 0.5 (i.e. 50%); if the +-- load-factor is specified, it will be clamped to the range of [0.1, 1](i.e. +-- if load-factor is greater than 1, it will be saturated to 1, likewise, +-- if load-factor is smaller than 0.1, it will be clamped to 0.1). +function _M.new(size, load_factor) + if size < 1 then + return nil, "size too small" + end + + -- Determine bucket size, which must be power of two. + local load_f = load_factor + if not load_factor then + load_f = 0.5 + elseif load_factor > 1 then + load_f = 1 + elseif load_factor < 0.1 then + load_f = 0.1 + end + + local bs_min = size / load_f + -- The bucket_sz *MUST* be a power-of-two. See the hash_string(). + local bucket_sz = 1 + repeat + bucket_sz = bucket_sz * 2 + until bucket_sz >= bs_min + + local self = { + size = size, + bucket_sz = bucket_sz, + free_queue = queue_init(size), + cache_queue = queue_init(0), + node_v = nil, + key_v = tab_new(size, 0), + val_v = tab_new(size, 0), + bucket_v = ffi_new(int_array_t, bucket_sz) + } + -- "note_v" is an array of all the nodes used in the LRU queue. Exprpession + -- node_v[i] evaluates to the element of ID "i". + self.node_v = self.free_queue + + -- Allocate the array-part of the key_v, val_v, bucket_v. + local key_v = self.key_v + local val_v = self.val_v + local bucket_v = self.bucket_v + ffi_fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0) + + return setmetatable(self, mt) +end + + +local function hash_string(self, str) + local c_str = ffi_cast(c_str_t, str) + + local hv = crc32_ptr(c_str) + hv = band(hv, self.bucket_sz - 1) + -- Hint: bucket is 0-based + return hv +end + + +-- Search the node associated with the key in the bucket, if found returns +-- the the id of the node, and the id of its previous node in the conflict list. +-- The "bucket_hdr_id" is the ID of the first node in the bucket +local function _find_node_in_bucket(key, key_v, node_v, bucket_hdr_id) + if bucket_hdr_id ~= 0 then + local prev = 0 + local cur = bucket_hdr_id + + while cur ~= 0 and key_v[cur] ~= key do + prev = cur + cur = node_v[cur].conflict + end + + if cur ~= 0 then + return cur, prev + end + end +end + + +-- Return the node corresponding to the key/val. +local function find_key(self, key) + local key_hash = hash_string(self, key) + return _find_node_in_bucket(key, self.key_v, self.node_v, + self.bucket_v[key_hash]) +end + + +--[[ This function tries to + 1. Remove the given key and the associated value from the key/value store, + 2. Remove the entry associated with the key from the hash-table. + + NOTE: all queues remain intact. + + If there was a node bound to the key/val, return that node; otherwise, + nil is returned. +]] +local function remove_key(self, key) + local key_v = self.key_v + local val_v = self.val_v + local node_v = self.node_v + local bucket_v = self.bucket_v + + local key_hash = hash_string(self, key) + local cur, prev = + _find_node_in_bucket(key, key_v, node_v, bucket_v[key_hash]) + + if cur then + -- In an attempt to make key and val dead. + key_v[cur] = nil + val_v[cur] = nil + + -- Remove the node from the hash table + local next_node = node_v[cur].conflict + if prev ~= 0 then + node_v[prev].conflict = next_node + else + bucket_v[key_hash] = next_node + end + node_v[cur].conflict = 0 + + return cur + end +end + + +--[[ Bind the key/val with the given node, and insert the node into the Hashtab. + NOTE: this function does not touch any queue +]] +local function insert_key(self, key, val, node) + -- Bind the key/val with the node + local node_id = node.id + self.key_v[node_id] = key + self.val_v[node_id] = val + + -- Insert the node into the hash-table + local key_hash = hash_string(self, key) + local bucket_v = self.bucket_v + node.conflict = bucket_v[key_hash] + bucket_v[key_hash] = node_id +end + + +function _M.get(self, key) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = find_key(self, key) + if not node_id then + return nil + end + + -- print(key, ": moving node ", tostring(node), " to cache queue head") + local cache_queue = self.cache_queue + local node = self.node_v + node_id + queue_remove(node) + queue_insert_head(cache_queue, node) + + local expire = node.expire + if expire >= 0 and expire < ngx_now() then + -- print("expired: ", node.expire, " > ", ngx_now()) + return nil, self.val_v[node_id] + end + + return self.val_v[node_id] +end + + +function _M.delete(self, key) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = remove_key(self, key); + if not node_id then + return false + end + + local node = self.node_v + node_id + queue_remove(node) + queue_insert_tail(self.free_queue, node) + return true +end + + +function _M.set(self, key, value, ttl) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = find_key(self, key) + local node + if not node_id then + local free_queue = self.free_queue + if queue_is_empty(free_queue) then + -- evict the least recently used key + -- assert(not queue_is_empty(self.cache_queue)) + node = queue_last(self.cache_queue) + remove_key(self, self.key_v[node.id]) + else + -- take a free queue node + node = queue_head(free_queue) + -- print(key, ": get a new free node: ", tostring(node)) + end + + -- insert the key + insert_key(self, key, value, node) + else + node = self.node_v + node_id + self.val_v[node_id] = value + end + + queue_remove(node) + queue_insert_head(self.cache_queue, node) + + if ttl then + node.expire = ngx_now() + ttl + else + node.expire = -1 + end +end + + +return _M diff --git a/spec/plugins/ip-restriction/access_spec.lua b/spec/plugins/ip-restriction/access_spec.lua new file mode 100644 index 000000000000..2564eafc477d --- /dev/null +++ b/spec/plugins/ip-restriction/access_spec.lua @@ -0,0 +1,97 @@ +local spec_helper = require "spec.spec_helpers" +local http_client = require "kong.tools.http_client" +local cjson = require "cjson" + +local STUB_GET_URL = spec_helper.STUB_GET_URL + +describe("IP Restriction", function() + + setup(function() + spec_helper.prepare_db() + spec_helper.insert_fixtures { + api = { + { name = "iprestriction1", public_dns = "test1.com", target_url = "http://mockbin.com" }, + { name = "iprestriction2", public_dns = "test2.com", target_url = "http://mockbin.com" }, + { name = "iprestriction3", public_dns = "test3.com", target_url = "http://mockbin.com" }, + { name = "iprestriction4", public_dns = "test4.com", target_url = "http://mockbin.com" }, + { name = "iprestriction5", public_dns = "test5.com", target_url = "http://mockbin.com" }, + { name = "iprestriction6", public_dns = "test6.com", target_url = "http://mockbin.com" }, + { name = "iprestriction7", public_dns = "test7.com", target_url = "http://mockbin.com" }, + { name = "iprestriction8", public_dns = "test8.com", target_url = "http://mockbin.com" } + }, + plugin_configuration = { + { name = "ip-restriction", value = { blacklist = { "127.0.0.1" }}, __api = 1 }, + { name = "ip-restriction", value = { blacklist = { "127.0.0.2" }}, __api = 2 }, + { name = "ip-restriction", value = { whitelist = { "127.0.0.2" }}, __api = 3 }, + { name = "ip-restriction", value = { whitelist = { "127.0.0.1" }}, __api = 4 }, + { name = "ip-restriction", value = { blacklist = { "127.0.0.1" }, whitelist = { "127.0.0.1" }}, __api = 5 }, + { name = "ip-restriction", value = { blacklist = { "127.0.0.0/24" }, whitelist = { "127.0.0.1" }}, __api = 6 }, + { name = "ip-restriction", value = { blacklist = { "127.0.0.0/24" }}, __api = 7 }, + { name = "ip-restriction", value = { whitelist = { "127.0.0.1", "127.0.0.2" }}, __api = 8 }, + } + } + + spec_helper.start_kong() + end) + + teardown(function() + spec_helper.stop_kong() + end) + + it("should block request when IP is in blacklist", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test1.com"}) + local body = cjson.decode(response) + assert.are.equal(403, status) + assert.are.equal("Your IP address is not allowed", body.message) + end) + + it("should allow request when IP is not in blacklist", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test2.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equal("127.0.0.1", body.clientIPAddress) + end) + + it("should block request when IP is not in whitelist", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test3.com"}) + local body = cjson.decode(response) + assert.are.equal(403, status) + assert.are.equal("Your IP address is not allowed", body.message) + end) + + it("should allow request when IP is in whitelist", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test4.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equal("127.0.0.1", body.clientIPAddress) + end) + + it("should allow request when IP is in whitelist after being blacklisted", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test5.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equal("127.0.0.1", body.clientIPAddress) + end) + + it("should allow request when IP is in whitelist after being blacklisted with CIDR", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test6.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equal("127.0.0.1", body.clientIPAddress) + end) + + it("should block request when IP is blacklisted with CIDR", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test7.com"}) + local body = cjson.decode(response) + assert.are.equal(403, status) + assert.are.equal("Your IP address is not allowed", body.message) + end) + + it("should allow request when IP is in whitelist with another IP", function() + local response, status = http_client.get(STUB_GET_URL, {}, {host = "test8.com"}) + local body = cjson.decode(response) + assert.are.equal(200, status) + assert.are.equal("127.0.0.1", body.clientIPAddress) + end) + +end) diff --git a/spec/unit/statics_spec.lua b/spec/unit/statics_spec.lua index 5ee92338f140..55d80f8f5d79 100644 --- a/spec/unit/statics_spec.lua +++ b/spec/unit/statics_spec.lua @@ -54,6 +54,7 @@ plugins_available: - response_transformer - requestsizelimiting - analytics + - ip-restriction ## The Kong working directory ## (Make sure you have read and write permissions)