Files
2026-07-13 12:32:21 +08:00

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,
}