diff --git a/kong/plugins/liamp/handler.lua b/kong/plugins/liamp/handler.lua index cb64692..69b4c67 100644 --- a/kong/plugins/liamp/handler.lua +++ b/kong/plugins/liamp/handler.lua @@ -26,6 +26,7 @@ do end local tostring = tostring +local tonumber = tonumber local pairs = pairs local type = type local fmt = string.format @@ -54,6 +55,7 @@ end local server_header_value local server_header_name +local response_bad_gateway local AWS_PORT = 443 @@ -63,6 +65,54 @@ local function get_now() end +--[[ + Response format should be + { + "statusCode": httpStatusCode, + "headers": { "headerName": "headerValue", ... }, + "body": "..." + } +--]] +local function validate_custom_response(response) + if type(response.statusCode) ~= "number" then + return nil, "statusCode must be a number" + end + + if response.headers ~= nil and type(response.headers) ~= "table" then + return nil, "headers must be a table" + end + + if response.body ~= nil and type(response.body) ~= "string" then + return nil, "body must be a string" + end + + return true +end + + +local function extract_proxy_response(content) + local serialized_content, err = cjson.decode(content) + if not serialized_content then + return nil, err + end + + local ok, err = validate_custom_response(serialized_content) + if not ok then + return nil, err + end + + local headers = serialized_content.headers or {} + local body = serialized_content.body or "" + headers["Content-Length"] = #body + + return { + status_code = tonumber(serialized_content.statusCode), + body = body, + headers = headers, + } +end + + local function send(status, content, headers) ngx.status = status @@ -76,9 +126,6 @@ local function send(status, content, headers) ngx.header["Content-Length"] = #content end --- if singletons.configuration.enabled_headers[constants.HEADERS.VIA] then --- ngx.header[constants.HEADERS.VIA] = server_header --- end if server_header_value then ngx.header[server_header_name] = server_header_value end @@ -126,6 +173,16 @@ function AWSLambdaHandler:init_worker() server_header_name = nil end end + + + -- response for BAD_GATEWAY was added in 0.14x + response_bad_gateway = responses.send_HTTP_BAD_GATEWAY + if not response_bad_gateway then + response_bad_gateway = function(msg) + ngx.log(ngx.ERR, LOG_PREFIX, msg) + return responses.send(502, "Bad Gateway") + end + end end @@ -279,15 +336,30 @@ function AWSLambdaHandler:access(conf) end local status - if conf.unhandled_status - and headers["X-Amz-Function-Error"] == "Unhandled" - then - status = conf.unhandled_status + if conf.is_proxy_integration then + local proxy_response, err = extract_proxy_response(content) + if not proxy_response then + return response_bad_gateway("could not JSON decode Lambda function " .. + "response: " .. tostring(err)) + end - else - status = res.status + status = proxy_response.status_code + headers = utils.table_merge(headers, proxy_response.headers) + content = proxy_response.body + end + + if not status then + if conf.unhandled_status + and headers["X-Amz-Function-Error"] == "Unhandled" + then + status = conf.unhandled_status + + else + status = res.status + end end + local ctx = ngx.ctx if ctx.delay_response and not ctx.delayed_response then ctx.delayed_response = { diff --git a/kong/plugins/liamp/schema.lua b/kong/plugins/liamp/schema.lua index bffd053..35ca092 100644 --- a/kong/plugins/liamp/schema.lua +++ b/kong/plugins/liamp/schema.lua @@ -96,6 +96,10 @@ return { type = "boolean", default = false, }, + is_proxy_integration = { + type = "boolean", + default = false, + }, proxy_scheme = { type = "string", enum = { diff --git a/spec/plugins/liamp/99-access_spec.lua b/spec/plugins/liamp/99-access_spec.lua index 8bca633..f050810 100644 --- a/spec/plugins/liamp/99-access_spec.lua +++ b/spec/plugins/liamp/99-access_spec.lua @@ -1,3 +1,4 @@ +local cjson = require "cjson" local helpers = require "spec.helpers" local meta = require "kong.meta" @@ -67,6 +68,36 @@ for _, strategy in helpers.each_strategy() do service = service10 } + local service11 = bp.services:insert({ + protocol = "http", + host = "httpbin.org", + port = 80, + }) + + local route11 = bp.routes:insert { + hosts = { "lambda11.com" }, + protocols = { "http", "https" }, + service = service11 + } + + local service12 = bp.services:insert({ + protocol = "http", + host = "httpbin.org", + port = 80, + }) + + local route12 = bp.routes:insert { + hosts = { "lambda12.com" }, + protocols = { "http", "https" }, + service = service12 + } + + local route13 = bp.routes:insert { + hosts = { "lambda13.com" }, + protocols = { "http", "https" }, + service = service12, + } + bp.plugins:insert { name = "liamp", route_id = route1.id, @@ -201,6 +232,45 @@ for _, strategy in helpers.each_strategy() do } } + bp.plugins:insert { + name = "liamp", + route_id = route11.id, + config = { + port = 10001, + aws_key = "mock-key", + aws_secret = "mock-secret", + aws_region = "us-east-1", + function_name = "kongLambdaTest", + is_proxy_integration = true, + } + } + + bp.plugins:insert { + name = "liamp", + route_id = route12.id, + config = { + port = 10001, + aws_key = "mock-key", + aws_secret = "mock-secret", + aws_region = "us-east-1", + function_name = "functionWithBadJSON", + is_proxy_integration = true, + } + } + + bp.plugins:insert { + name = "liamp", + route_id = route13.id, + config = { + port = 10001, + aws_key = "mock-key", + aws_secret = "mock-secret", + aws_region = "us-east-1", + function_name = "functionWithNoResponse", + is_proxy_integration = true, + } + } + assert(helpers.start_kong{ database = strategy, custom_plugins = "liamp", @@ -534,5 +604,173 @@ for _, strategy in helpers.each_strategy() do assert.equal(65, tonumber(res.headers["Content-Length"])) end) + describe("config.is_proxy_integration = true", function() + + +-- here's where we miss the changes to the custom nginx template, to be able to +-- run the tests against older versions (0.13.x) of Kong. Add those manually +-- and the tests pass. +-- see: https://github.com/Kong/kong/commit/c6f9e4558b5a654e78ca96b2ba4309e527053403#diff-9d13d8efc852de84b07e71bf419a2c4d + + it("sets proper status code on custom response from Lambda", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json" + }, + body = { + statusCode = 201, + } + }) + local body = assert.res_status(201, res) + assert.equal(0, tonumber(res.headers["Content-Length"])) + assert.equal(nil, res.headers["X-Custom-Header"]) + assert.equal("", body) + end) + + it("sets proper status code/headers/body on custom response from Lambda", function() + -- the lambda function must return a string + -- for the custom response "body" property + local body = cjson.encode({ + key1 = "some_value_post1", + key2 = "some_value_post2", + key3 = "some_value_post3", + }) + + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json", + }, + body = { + statusCode = 201, + body = body, + headers = { + ["X-Custom-Header"] = "Hello world!" + } + } + }) + + local res_body = assert.res_status(201, res) + assert.equal(79, tonumber(res.headers["Content-Length"])) + assert.equal("Hello world!", res.headers["X-Custom-Header"]) + assert.equal(body, res_body) + end) + + it("override duplicated headers with value from the custom response from Lambda", function() + -- the default "x-amzn-RequestId" returned is "foo" + -- let's check it is overriden with a custom value + local headers = { + ["x-amzn-RequestId"] = "bar", + } + + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json", + }, + body = { + statusCode = 201, + headers = headers, + } + }) + + assert.res_status(201, res) + assert.equal("bar", res.headers["x-amzn-RequestId"]) + end) + + it("returns HTTP 502 when 'status' property of custom response is not a number", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json", + }, + body = { + statusCode = "hello", + } + }) + + assert.res_status(502, res) + local b = assert.response(res).has.jsonbody() + assert.equal("Bad Gateway", b.message) + end) + + it("returns HTTP 502 when 'headers' property of custom response is not a table", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json", + }, + body = { + headers = "hello", + } + }) + + assert.res_status(502, res) + local b = assert.response(res).has.jsonbody() + assert.equal("Bad Gateway", b.message) + end) + + it("returns HTTP 502 when 'body' property of custom response is not a string", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda11.com", + ["Content-Type"] = "application/json", + }, + body = { + statusCode = 201, + body = 1234, + } + }) + + assert.res_status(502, res) + local b = assert.response(res).has.jsonbody() + assert.equal("Bad Gateway", b.message) + end) + + it("returns HTTP 502 with when response from lambda is not valid JSON", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda12.com", + } + }) + + assert.res_status(502, res) + local b = assert.response(res).has.jsonbody() + assert.equal("Bad Gateway", b.message) + end) + + it("returns HTTP 502 on empty response from Lambda", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/post", + headers = { + ["Host"] = "lambda13.com", + } + }) + + assert.res_status(502, res) + + local b = assert.response(res).has.jsonbody() + assert.equal("Bad Gateway", b.message) + end) + + end) + end) end +