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