From 680cbb3357a1d751d664a7e54d27ed6dde11f120 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 27 May 2015 19:58:42 -0700 Subject: [PATCH] Response Transformer plugin Former-commit-id: 5e4e0c9ca4489e56678805fb4d5e48bf9fd3f7ef --- kong-0.3.0-1.rockspec | 5 + kong.yml | 1 + kong/dao/cassandra/apis.lua | 2 +- kong/plugins/request_transformer/access.lua | 12 +-- .../response_transformer/body_filter.lua | 92 +++++++++++++++++++ kong/plugins/response_transformer/handler.lua | 23 +++++ .../response_transformer/header_filter.lua | 66 +++++++++++++ kong/plugins/response_transformer/schema.lua | 12 +++ .../request_transformer/access_spec.lua | 1 + spec/plugins/response_transformer.lua | 84 +++++++++++++++++ spec/unit/statics_spec.lua | 1 + 11 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 kong/plugins/response_transformer/body_filter.lua create mode 100644 kong/plugins/response_transformer/handler.lua create mode 100644 kong/plugins/response_transformer/header_filter.lua create mode 100644 kong/plugins/response_transformer/schema.lua create mode 100644 spec/plugins/response_transformer.lua diff --git a/kong-0.3.0-1.rockspec b/kong-0.3.0-1.rockspec index 471e2ded02b4..cfaafd1fb28a 100644 --- a/kong-0.3.0-1.rockspec +++ b/kong-0.3.0-1.rockspec @@ -123,6 +123,11 @@ build = { ["kong.plugins.request_transformer.access"] = "kong/plugins/request_transformer/access.lua", ["kong.plugins.request_transformer.schema"] = "kong/plugins/request_transformer/schema.lua", + ["kong.plugins.response_transformer.handler"] = "kong/plugins/response_transformer/handler.lua", + ["kong.plugins.response_transformer.body_filter"] = "kong/plugins/response_transformer/body_filter.lua", + ["kong.plugins.response_transformer.header_filter"] = "kong/plugins/response_transformer/header_filter.lua", + ["kong.plugins.response_transformer.schema"] = "kong/plugins/response_transformer/schema.lua", + ["kong.plugins.cors.handler"] = "kong/plugins/cors/handler.lua", ["kong.plugins.cors.access"] = "kong/plugins/cors/access.lua", ["kong.plugins.cors.schema"] = "kong/plugins/cors/schema.lua", diff --git a/kong.yml b/kong.yml index 7c229db3ee9f..5dd56c76bc77 100644 --- a/kong.yml +++ b/kong.yml @@ -10,6 +10,7 @@ plugins_available: - httplog - cors - request_transformer + - response_transformer ## The Kong working directory ## (Make sure you have read and write permissions) diff --git a/kong/dao/cassandra/apis.lua b/kong/dao/cassandra/apis.lua index f8d0618d73a5..986421fbf928 100644 --- a/kong/dao/cassandra/apis.lua +++ b/kong/dao/cassandra/apis.lua @@ -94,4 +94,4 @@ function Apis:delete(api_id) return ok end -return Apis +return Apis \ No newline at end of file diff --git a/kong/plugins/request_transformer/access.lua b/kong/plugins/request_transformer/access.lua index a9f92ca0b1ab..b29b589b20f5 100644 --- a/kong/plugins/request_transformer/access.lua +++ b/kong/plugins/request_transformer/access.lua @@ -18,10 +18,10 @@ local function iterate_and_exec(val, cb) end end -local function get_content_type(request) +local function get_content_type() local header_value = ngx.req.get_headers()[CONTENT_TYPE] if header_value then - return stringy.strip(header_value) + return stringy.strip(header_value):lower() end end @@ -47,7 +47,7 @@ function _M.execute(conf) end if conf.add.form then - local content_type = get_content_type(ngx.req) + local content_type = get_content_type() if content_type and stringy.startswith(content_type, FORM_URLENCODED) then -- Call ngx.req.read_body to read the request body first -- or turn on the lua_need_request_body directive to avoid errors. @@ -80,7 +80,7 @@ function _M.execute(conf) if conf.remove then - -- Add headers + -- Remove headers if conf.remove.headers then iterate_and_exec(conf.remove.headers, function(name, value) ngx.req.clear_header(name) @@ -96,7 +96,7 @@ function _M.execute(conf) end if conf.remove.form then - local content_type = get_content_type(ngx.req) + local content_type = get_content_type() if content_type and stringy.startswith(content_type, FORM_URLENCODED) then local parameters = ngx.req.get_post_args() @@ -108,7 +108,7 @@ function _M.execute(conf) ngx.req.set_header(CONTENT_LENGTH, string.len(encoded_args)) ngx.req.set_body_data(encoded_args) elseif content_type and stringy.startswith(content_type, MULTIPART_DATA) then - -- Call ngx.req.read_body to read the request body first + -- Call ngx.req.read_body to read the request body first -- or turn on the lua_need_request_body directive to avoid errors. ngx.req.read_body() diff --git a/kong/plugins/response_transformer/body_filter.lua b/kong/plugins/response_transformer/body_filter.lua new file mode 100644 index 000000000000..8cc0cdb69728 --- /dev/null +++ b/kong/plugins/response_transformer/body_filter.lua @@ -0,0 +1,92 @@ +local utils = require "kong.tools.utils" +local stringy = require "stringy" +local cjson = require "cjson" + +local _M = {} + +local APPLICATION_JSON = "application/json" +local CONTENT_TYPE = "content-type" + +local function get_content_type() + local header_value = ngx.header[CONTENT_TYPE] + if header_value then + return stringy.strip(header_value):lower() + end + return nil +end + +local function read_response_body() + local chunk, eof = ngx.arg[1], ngx.arg[2] + local buffered = ngx.ctx.buffered + if not buffered then + buffered = {} + ngx.ctx.buffered = buffered + end + if chunk ~= "" then + buffered[#buffered + 1] = chunk + ngx.arg[1] = nil + end + if eof then + local response_body = table.concat(buffered) + return response_body + end + return nil +end + +local function read_json_body() + local body = read_response_body() + if body then + local status, res = pcall(cjson.decode, body) + if status then + return res + end + end + return nil +end + +local function set_json_body(json) + local body = cjson.encode(json) + ngx.arg[1] = body +end + +local function iterate_and_exec(val, cb) + if utils.table_size(val) > 0 then + for _, entry in ipairs(val) do + local parts = stringy.split(entry, ":") + cb(parts[1], utils.table_size(parts) == 2 and parts[2] or nil) + end + end +end + +function _M.execute(conf) + if not conf then return end + + local is_json_body = get_content_type() == APPLICATION_JSON + + if (conf.add.json or conf.remove.json) and is_json_body then + local json_body = read_json_body() + if json_body then + + if conf.add.json then + iterate_and_exec(conf.add.json, function(name, value) + local v = cjson.encode(value) + if stringy.startswith(v, "\"") and stringy.endswith(v, "\"") then + v = v:sub(2, v:len() - 1):gsub("\\\"", "\"") -- To prevent having double encoded quotes + end + json_body[name] = v + end) + end + + if conf.remove.json then + iterate_and_exec(conf.remove.json, function(name) + json_body[name] = nil + end) + end + + set_json_body(json_body) + end + end + +end + +return _M diff --git a/kong/plugins/response_transformer/handler.lua b/kong/plugins/response_transformer/handler.lua new file mode 100644 index 000000000000..52712f242f9f --- /dev/null +++ b/kong/plugins/response_transformer/handler.lua @@ -0,0 +1,23 @@ +local BasePlugin = require "kong.plugins.base_plugin" +local body_filter = require "kong.plugins.response_transformer.body_filter" +local header_filter = require "kong.plugins.response_transformer.header_filter" + +local ResponseTransformerHandler = BasePlugin:extend() + +function ResponseTransformerHandler:new() + ResponseTransformerHandler.super.new(self, "response_transformer") +end + +function ResponseTransformerHandler:header_filter(conf) + ResponseTransformerHandler.super.header_filter(self) + header_filter.execute(conf) +end + +function ResponseTransformerHandler:body_filter(conf) + ResponseTransformerHandler.super.body_filter(self) + body_filter.execute(conf) +end + +ResponseTransformerHandler.PRIORITY = 800 + +return ResponseTransformerHandler diff --git a/kong/plugins/response_transformer/header_filter.lua b/kong/plugins/response_transformer/header_filter.lua new file mode 100644 index 000000000000..b94537de5a2b --- /dev/null +++ b/kong/plugins/response_transformer/header_filter.lua @@ -0,0 +1,66 @@ +local utils = require "kong.tools.utils" +local stringy = require "stringy" + +local _M = {} + +local APPLICATION_JSON = "application/json" +local CONTENT_TYPE = "content-type" +local CONTENT_LENGTH = "content-length" + +local function get_content_type() + local header_value = ngx.header[CONTENT_TYPE] + if header_value then + return stringy.strip(header_value):lower() + end + return nil +end + +local function iterate_and_exec(val, cb) + if utils.table_size(val) > 0 then + for _, entry in ipairs(val) do + local parts = stringy.split(entry, ":") + cb(parts[1], utils.table_size(parts) == 2 and parts[2] or nil) + end + end +end + +function _M.execute(conf) + if not conf then return end + + local is_json_body = get_content_type() == APPLICATION_JSON + + if conf.add then + + -- Add headers + if conf.add.headers then + iterate_and_exec(conf.add.headers, function(name, value) + ngx.header[name] = value + end) + end + + -- Removing the header because the body is going to change + if conf.add.json and is_json_body then + ngx.header[CONTENT_LENGTH] = nil + end + + end + + if conf.remove then + + -- Remove headers + if conf.remove.headers then + iterate_and_exec(conf.remove.headers, function(name, value) + ngx.header[name] = nil + end) + end + + -- Removing the header because the body is going to change + if conf.remove.json and is_json_body then + ngx.header[CONTENT_LENGTH] = nil + end + + end + +end + +return _M diff --git a/kong/plugins/response_transformer/schema.lua b/kong/plugins/response_transformer/schema.lua new file mode 100644 index 000000000000..9c110d148196 --- /dev/null +++ b/kong/plugins/response_transformer/schema.lua @@ -0,0 +1,12 @@ +return { + add = { type = "table", schema = { + json = { type = "array" }, + headers = { type = "array" } + } + }, + remove = { type = "table", schema = { + json = { type = "array" }, + headers = { type = "array" } + } + } +} diff --git a/spec/plugins/request_transformer/access_spec.lua b/spec/plugins/request_transformer/access_spec.lua index bb88ab541fe1..e37fcaf28b61 100644 --- a/spec/plugins/request_transformer/access_spec.lua +++ b/spec/plugins/request_transformer/access_spec.lua @@ -123,4 +123,5 @@ describe("Request Transformer", function() end) end) + end) diff --git a/spec/plugins/response_transformer.lua b/spec/plugins/response_transformer.lua new file mode 100644 index 000000000000..e1159bd1245d --- /dev/null +++ b/spec/plugins/response_transformer.lua @@ -0,0 +1,84 @@ +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.PROXY_URL.."/get" +local STUB_HEADERS_URL = spec_helper.PROXY_URL.."/response-headers" +local STUB_POST_URL = spec_helper.PROXY_URL.."/post" + +describe("Response Transformer Plugin #proxy", function() + + setup(function() + spec_helper.prepare_db() + spec_helper.insert_fixtures { + api = { + { name = "tests response_transformer", public_dns = "response.com", target_url = "http://httpbin.org" }, + }, + plugin_configuration = { + { + name = "response_transformer", + value = { + add = { + headers = {"x-added:true", "x-added2:true" }, + json = {"newjsonparam:newvalue"} + }, + remove = { + headers = { "x-to-remove" }, + json = { "origin" } + } + }, + __api = 1 + } + } + } + + spec_helper.start_kong() + end) + + teardown(function() + spec_helper.stop_kong() + end) + + describe("Test adding parameters", function() + + it("should add new headers", function() + local body, status, headers = http_client.get(STUB_GET_URL, {}, {host = "response.com"}) + assert.are.equal(200, status) + assert.are.equal("true", headers["x-added"]) + assert.are.equal("true", headers["x-added2"]) + end) + + it("should add new parameters on GET", function() + local response, status, headers = http_client.get("http://127.0.0.1:8100/get", {}, {host = "response.com"}) + assert.are.equal(200, status) + local body = cjson.decode(response) + assert.are.equal("newvalue", body["newjsonparam"]) + end) + + it("should add new parameters on POST", function() + local response, status, headers = http_client.post("http://127.0.0.1:8100/post", {}, {host = "response.com"}) + assert.are.equal(200, status) + local body = cjson.decode(response) + assert.are.equal("newvalue", body["newjsonparam"]) + end) + + end) + + describe("Test removing parameters", function() + + it("should remove a header", function() + local _, status, headers = http_client.get(STUB_HEADERS_URL, { ["x-to-remove"] = "true"}, {host = "response.com"}) + assert.are.equal(200, status) + assert.falsy(headers["x-to-remove"]) + end) + + it("should remove a parameter on GET", function() + local response, status, headers = http_client.get("http://127.0.0.1:8100/get", {}, {host = "response.com"}) + assert.are.equal(200, status) + local body = cjson.decode(response) + assert.falsy(body.origin) + end) + + end) + +end) diff --git a/spec/unit/statics_spec.lua b/spec/unit/statics_spec.lua index c26c61d14c0e..0f698d6f6006 100644 --- a/spec/unit/statics_spec.lua +++ b/spec/unit/statics_spec.lua @@ -50,6 +50,7 @@ plugins_available: - httplog - cors - request_transformer + - response_transformer ## The Kong working directory ## (Make sure you have read and write permissions)