363 lines
10 KiB
Lua
363 lines
10 KiB
Lua
local kong = kong
|
|
local ngx_encode_base64 = ngx.encode_base64
|
|
local ngx_decode_base64 = ngx.decode_base64
|
|
local null = ngx.null
|
|
local cjson = require "cjson.safe"
|
|
|
|
local date = require("date")
|
|
local get_request_id = require("kong.observability.tracing.request_id").get
|
|
|
|
local EMPTY = {}
|
|
|
|
local isempty = require "table.isempty"
|
|
local split_once = require("kong.tools.string").split_once
|
|
local ngx_req_get_headers = ngx.req.get_headers
|
|
local ngx_req_get_uri_args = ngx.req.get_uri_args
|
|
local ngx_get_http_version = ngx.req.http_version
|
|
local ngx_req_start_time = ngx.req.start_time
|
|
|
|
|
|
local raw_content_types = {
|
|
["text/plain"] = true,
|
|
["text/html"] = true,
|
|
["application/xml"] = true,
|
|
["text/xml"] = true,
|
|
["application/soap+xml"] = true,
|
|
}
|
|
|
|
|
|
local function read_request_body(skip_large_bodies)
|
|
ngx.req.read_body()
|
|
local body = ngx.req.get_body_data()
|
|
|
|
if not body then
|
|
-- see if body was buffered to tmp file, payload could have exceeded client_body_buffer_size
|
|
local body_filepath = ngx.req.get_body_file()
|
|
if body_filepath then
|
|
if skip_large_bodies then
|
|
ngx.log(ngx.ERR, "request body was buffered to disk, too large")
|
|
else
|
|
local file = io.open(body_filepath, "rb")
|
|
body = file:read("*all")
|
|
file:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
return body
|
|
end
|
|
|
|
|
|
local function validate_http_status_code(status_code)
|
|
if not status_code then
|
|
return false
|
|
end
|
|
|
|
if type(status_code) == "string" then
|
|
status_code = tonumber(status_code)
|
|
|
|
if not status_code then
|
|
return false
|
|
end
|
|
end
|
|
|
|
if status_code >= 100 and status_code <= 599 then
|
|
return status_code
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
--[[
|
|
Response format should be
|
|
{
|
|
"statusCode": httpStatusCode,
|
|
"headers": { "headerName": "headerValue", ... },
|
|
"body": "..."
|
|
}
|
|
--]]
|
|
local function validate_custom_response(response)
|
|
if not validate_http_status_code(response.statusCode) then
|
|
return nil, "statusCode validation failed"
|
|
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
|
|
|
|
if response.isBase64Encoded ~= nil and type(response.isBase64Encoded) ~= "boolean" then
|
|
return nil, "isBase64Encoded must be a boolean"
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function extract_proxy_response(content)
|
|
local serialized_content, err
|
|
if type(content) == "string" then
|
|
serialized_content, err = cjson.decode(content)
|
|
if not serialized_content then
|
|
return nil, err
|
|
end
|
|
|
|
elseif type(content) == "table" then
|
|
serialized_content = content
|
|
|
|
else
|
|
return nil, "proxy response must be json format"
|
|
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 ""
|
|
local isBase64Encoded = serialized_content.isBase64Encoded
|
|
if isBase64Encoded == true then
|
|
body = ngx_decode_base64(body)
|
|
end
|
|
|
|
local multiValueHeaders = serialized_content.multiValueHeaders
|
|
if multiValueHeaders and multiValueHeaders ~= null then
|
|
for header, values in pairs(multiValueHeaders) do
|
|
headers[header] = values
|
|
end
|
|
end
|
|
|
|
headers["Content-Length"] = #body
|
|
|
|
return {
|
|
status_code = tonumber(serialized_content.statusCode),
|
|
body = body,
|
|
headers = headers,
|
|
}
|
|
end
|
|
|
|
|
|
local function aws_serializer(ctx, config)
|
|
ctx = ctx or ngx.ctx
|
|
local var = ngx.var
|
|
|
|
-- prepare headers
|
|
local headers = ngx_req_get_headers()
|
|
local multiValueHeaders = {}
|
|
for hname, hvalue in pairs(headers) do
|
|
if type(hvalue) == "table" then
|
|
-- multi value
|
|
multiValueHeaders[hname] = hvalue
|
|
headers[hname] = hvalue[1]
|
|
|
|
else
|
|
-- single value
|
|
multiValueHeaders[hname] = { hvalue }
|
|
end
|
|
end
|
|
|
|
-- prepare url-captures/path-parameters
|
|
local pathParameters = {}
|
|
for name, value in pairs(ctx.router_matches.uri_captures or EMPTY) do
|
|
if type(name) == "string" then -- skip numerical indices, only named
|
|
pathParameters[name] = value
|
|
end
|
|
end
|
|
|
|
-- query parameters
|
|
local queryStringParameters = ngx_req_get_uri_args()
|
|
local multiValueQueryStringParameters = {}
|
|
for qname, qvalue in pairs(queryStringParameters) do
|
|
if type(qvalue) == "table" then
|
|
-- multi value
|
|
multiValueQueryStringParameters[qname] = qvalue
|
|
queryStringParameters[qname] = qvalue[1]
|
|
|
|
else
|
|
-- single value
|
|
multiValueQueryStringParameters[qname] = { qvalue }
|
|
end
|
|
end
|
|
|
|
-- prepare body
|
|
local body, isBase64Encoded
|
|
local skip_large_bodies = true
|
|
local base64_encode_body = true
|
|
|
|
if config then
|
|
if config.skip_large_bodies ~= nil then
|
|
skip_large_bodies = config.skip_large_bodies
|
|
end
|
|
|
|
if config.base64_encode_body ~= nil then
|
|
base64_encode_body = config.base64_encode_body
|
|
end
|
|
end
|
|
|
|
do
|
|
body = read_request_body(skip_large_bodies)
|
|
if body ~= "" and base64_encode_body then
|
|
body = ngx_encode_base64(body)
|
|
isBase64Encoded = true
|
|
else
|
|
isBase64Encoded = false
|
|
end
|
|
end
|
|
|
|
-- prepare path
|
|
local uri = var.upstream_uri or var.request_uri
|
|
local path = uri:match("^([^%?]+)") -- strip any query args
|
|
|
|
local requestContext
|
|
do
|
|
local http_version = ngx_get_http_version()
|
|
local protocol = http_version and 'HTTP/'..http_version or nil
|
|
local httpMethod = var.request_method
|
|
local domainName = var.host
|
|
local domainPrefix = split_once(domainName, ".")
|
|
local identity = {
|
|
sourceIp = var.realip_remote_addr or var.remote_addr,
|
|
userAgent = headers["user-agent"],
|
|
}
|
|
local requestId = get_request_id()
|
|
local start_time = ngx_req_start_time()
|
|
-- The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm).
|
|
local requestTime = date(start_time):fmt("%d/%b/%Y:%H:%M:%S %z")
|
|
local requestTimeEpoch = start_time * 1000
|
|
|
|
-- Kong does not have the concept of stage, so we just let resource path be the same as path
|
|
local resourcePath = path
|
|
|
|
requestContext = {
|
|
path = path,
|
|
protocol = protocol,
|
|
httpMethod = httpMethod,
|
|
domainName = domainName,
|
|
domainPrefix = domainPrefix,
|
|
identity = identity,
|
|
requestId = requestId,
|
|
requestTime = requestTime,
|
|
requestTimeEpoch = requestTimeEpoch,
|
|
resourcePath = resourcePath,
|
|
}
|
|
end
|
|
|
|
local request = {
|
|
version = "1.0",
|
|
resource = ctx.router_matches.uri,
|
|
path = path,
|
|
httpMethod = var.request_method,
|
|
headers = headers,
|
|
multiValueHeaders = multiValueHeaders,
|
|
pathParameters = pathParameters,
|
|
queryStringParameters = queryStringParameters,
|
|
multiValueQueryStringParameters = multiValueQueryStringParameters,
|
|
body = body,
|
|
isBase64Encoded = isBase64Encoded,
|
|
requestContext = requestContext,
|
|
}
|
|
|
|
return request
|
|
end
|
|
|
|
|
|
-- Build the JSON blob that you want to provide to your Lambda function as input.
|
|
local function build_request_payload(conf)
|
|
local upstream_body = kong.table.new(0, 6)
|
|
local ctx = ngx.ctx
|
|
|
|
if conf.awsgateway_compatible then
|
|
upstream_body = aws_serializer(ctx, conf)
|
|
|
|
elseif conf.forward_request_body or
|
|
conf.forward_request_headers or
|
|
conf.forward_request_method or
|
|
conf.forward_request_uri then
|
|
|
|
-- new behavior to forward request method, body, uri and their args
|
|
if conf.forward_request_method then
|
|
upstream_body.request_method = kong.request.get_method()
|
|
end
|
|
|
|
if conf.forward_request_headers then
|
|
upstream_body.request_headers = kong.request.get_headers()
|
|
end
|
|
|
|
if conf.forward_request_uri then
|
|
upstream_body.request_uri = kong.request.get_path_with_query()
|
|
upstream_body.request_uri_args = kong.request.get_query()
|
|
end
|
|
|
|
if conf.forward_request_body then
|
|
local content_type = kong.request.get_header("content-type")
|
|
local body_raw = read_request_body(conf.skip_large_bodies)
|
|
local body_args, err = kong.request.get_body()
|
|
if err and err:match("content type") then
|
|
body_args = {}
|
|
if not raw_content_types[content_type] and conf.base64_encode_body then
|
|
-- don't know what this body MIME type is, base64 it just in case
|
|
body_raw = ngx_encode_base64(body_raw)
|
|
upstream_body.request_body_base64 = true
|
|
end
|
|
end
|
|
|
|
upstream_body.request_body = body_raw
|
|
upstream_body.request_body_args = body_args
|
|
end
|
|
|
|
else
|
|
-- backwards compatible upstream body for configurations not specifying
|
|
-- `forward_request_*` values
|
|
local body_args = kong.request.get_body()
|
|
upstream_body = kong.table.merge(kong.request.get_query(), body_args)
|
|
end
|
|
|
|
local upstream_body_json, err = cjson.encode(upstream_body)
|
|
if not upstream_body_json then
|
|
kong.log.err("could not JSON encode upstream body",
|
|
" to forward request values: ", err)
|
|
end
|
|
|
|
return upstream_body_json
|
|
end
|
|
|
|
|
|
-- TODO: remove this in the next major release
|
|
-- function to remove array_mt metatables from empty tables
|
|
-- This is just a backward compatibility code to keep a
|
|
-- long-lived behavior that Kong responsed JSON objects
|
|
-- instead of JSON arrays for empty arrays.
|
|
local function remove_array_mt_for_empty_table(tbl)
|
|
if type(tbl) ~= "table" then
|
|
return tbl
|
|
end
|
|
|
|
-- Check if the current table(array) is empty and has a array_mt metatable, and remove it
|
|
if isempty(tbl) and getmetatable(tbl) == cjson.array_mt then
|
|
setmetatable(tbl, nil)
|
|
end
|
|
|
|
for _, value in pairs(tbl) do
|
|
if type(value) == "table" then
|
|
remove_array_mt_for_empty_table(value)
|
|
end
|
|
end
|
|
|
|
return tbl
|
|
end
|
|
|
|
|
|
return {
|
|
aws_serializer = aws_serializer,
|
|
validate_http_status_code = validate_http_status_code,
|
|
validate_custom_response = validate_custom_response,
|
|
build_request_payload = build_request_payload,
|
|
extract_proxy_response = extract_proxy_response,
|
|
remove_array_mt_for_empty_table = remove_array_mt_for_empty_table,
|
|
}
|