561 lines
16 KiB
Lua
561 lines
16 KiB
Lua
local multipart = require "multipart"
|
|
local cjson = require("cjson.safe").new()
|
|
local pl_template = require "pl.template"
|
|
local sandbox = require "kong.tools.sandbox"
|
|
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
|
|
|
|
local table_insert = table.insert
|
|
local get_uri_args = kong.request.get_query
|
|
local set_uri_args = kong.service.request.set_query
|
|
local clear_header = kong.service.request.clear_header
|
|
local get_header = kong.request.get_header
|
|
local set_header = kong.service.request.set_header
|
|
local get_headers = kong.request.get_headers
|
|
local set_headers = kong.service.request.set_headers
|
|
local set_method = kong.service.request.set_method
|
|
local set_path = kong.service.request.set_path
|
|
local get_raw_body = kong.request.get_raw_body
|
|
local set_raw_body = kong.service.request.set_raw_body
|
|
local encode_args = ngx.encode_args
|
|
local ngx_decode_args = ngx.decode_args
|
|
local type = type
|
|
local str_find = string.find
|
|
local pairs = pairs
|
|
local error = error
|
|
local rawset = rawset
|
|
local lua_enabled = sandbox.configuration.enabled
|
|
local sandbox_enabled = sandbox.configuration.sandbox_enabled
|
|
|
|
local _M = {}
|
|
local template_cache = setmetatable( {}, { __mode = "k" })
|
|
|
|
local DEBUG = ngx.DEBUG
|
|
local CONTENT_LENGTH = "content-length"
|
|
local CONTENT_TYPE = "content-type"
|
|
local HOST = "host"
|
|
local JSON, MULTI, ENCODED = "json", "multi_part", "form_encoded"
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
|
|
|
|
local compile_opts = {
|
|
escape = "\xff", -- disable '#' as a valid template escape
|
|
}
|
|
|
|
|
|
cjson.decode_array_with_array_mt(true)
|
|
|
|
|
|
local function parse_json(body)
|
|
if body then
|
|
return cjson.decode(body)
|
|
end
|
|
end
|
|
|
|
local function decode_args(body)
|
|
if body then
|
|
return ngx_decode_args(body)
|
|
end
|
|
return {}
|
|
end
|
|
|
|
local function get_content_type(content_type)
|
|
if content_type == nil then
|
|
return
|
|
end
|
|
if str_find(content_type:lower(), "application/json", nil, true) then
|
|
return JSON
|
|
elseif str_find(content_type:lower(), "multipart/form-data", nil, true) then
|
|
return MULTI
|
|
elseif str_find(content_type:lower(), "application/x-www-form-urlencoded", nil, true) then
|
|
return ENCODED
|
|
end
|
|
end
|
|
|
|
local function param_value(source_template, config_array, template_env)
|
|
if not source_template or source_template == "" then
|
|
return nil
|
|
end
|
|
|
|
if not lua_enabled then
|
|
-- Detect expressions in the source template
|
|
local expr = str_find(source_template, "%$%(.*%)")
|
|
if expr then
|
|
return nil, "loading of untrusted Lua code disabled because " ..
|
|
"'untrusted_lua' config option is set to 'off'"
|
|
end
|
|
-- Lua is disabled, no need to render the template
|
|
return source_template
|
|
end
|
|
|
|
-- find compiled templates for this plugin-configuration array
|
|
local compiled_templates = template_cache[config_array]
|
|
if not compiled_templates then
|
|
compiled_templates = {}
|
|
-- store it by `config_array` which is part of the plugin `conf` table
|
|
-- it will be GC'ed at the same time as `conf` and hence invalidate the
|
|
-- compiled templates here as well as the cache-table has weak-keys
|
|
template_cache[config_array] = compiled_templates
|
|
end
|
|
|
|
-- Find or compile the specific template
|
|
local compiled_template = compiled_templates[source_template]
|
|
if not compiled_template then
|
|
compiled_template = assert(pl_template.compile(source_template, compile_opts))
|
|
compiled_templates[source_template] = compiled_template
|
|
end
|
|
|
|
return compiled_template:render(template_env)
|
|
end
|
|
|
|
local function iter(config_array, template_env)
|
|
return function(config_array, i, previous_name, previous_value)
|
|
i = i + 1
|
|
local current_pair = config_array[i]
|
|
if current_pair == nil then -- n + 1
|
|
return nil
|
|
end
|
|
|
|
local current_name, current_value = current_pair:match("^([^:]+):*(.-)$")
|
|
|
|
if current_value == "" then
|
|
return i, current_name
|
|
end
|
|
|
|
local res, err = param_value(current_value, config_array, template_env)
|
|
if err then
|
|
return error("[request-transformer] failed to render the template " ..
|
|
current_value .. ", error:" .. err)
|
|
end
|
|
|
|
kong.log.debug("[request-transformer] template `", current_value,
|
|
"` rendered to `", res, "`")
|
|
|
|
return i, current_name, res
|
|
end, config_array, 0
|
|
end
|
|
|
|
local function append_value(current_value, value)
|
|
local current_value_type = type(current_value)
|
|
|
|
if current_value_type == "string" then
|
|
return { current_value, value }
|
|
elseif current_value_type == "table" then
|
|
table_insert(current_value, value)
|
|
return current_value
|
|
else
|
|
return { value }
|
|
end
|
|
end
|
|
|
|
local function rename(tbl, old_name, new_name)
|
|
if old_name == new_name then
|
|
return
|
|
end
|
|
|
|
local value = tbl[old_name]
|
|
if value then
|
|
tbl[old_name] = nil
|
|
tbl[new_name] = value
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function transform_headers(conf, template_env)
|
|
local headers = get_headers()
|
|
local headers_to_remove = {}
|
|
|
|
headers.host = nil
|
|
|
|
-- Remove header(s)
|
|
for _, name, value in iter(conf.remove.headers, template_env) do
|
|
name = name:lower()
|
|
if headers[name] then
|
|
headers[name] = nil
|
|
headers_to_remove[name] = true
|
|
end
|
|
end
|
|
|
|
-- Rename headers(s)
|
|
for _, old_name, new_name in iter(conf.rename.headers, template_env) do
|
|
local lower_old_name, lower_new_name = old_name:lower(), new_name:lower()
|
|
-- headers by default are case-insensitive
|
|
-- but if we have a case change, we need to handle it as a special case
|
|
local need_remove
|
|
if lower_old_name == lower_new_name then
|
|
need_remove = rename(headers, old_name, new_name)
|
|
else
|
|
need_remove = rename(headers, lower_old_name, lower_new_name)
|
|
end
|
|
|
|
if need_remove then
|
|
headers_to_remove[old_name] = true
|
|
end
|
|
end
|
|
|
|
-- Replace header(s)
|
|
for _, name, value in iter(conf.replace.headers, template_env) do
|
|
name = name:lower()
|
|
if headers[name] or name == HOST then
|
|
headers[name] = value
|
|
end
|
|
end
|
|
|
|
-- Add header(s)
|
|
for _, name, value in iter(conf.add.headers, template_env) do
|
|
if not headers[name] and name:lower() ~= HOST then
|
|
headers[name] = value
|
|
end
|
|
end
|
|
|
|
-- Append header(s)
|
|
for _, name, value in iter(conf.append.headers, template_env) do
|
|
local name_lc = name:lower()
|
|
|
|
if name_lc ~= HOST and name ~= name_lc and headers[name] ~= nil then
|
|
-- keep original content, use configd case
|
|
-- note: the __index method of table returned by ngx.req.get_header
|
|
-- is overwritten to check for lower case as well, see documentation
|
|
-- for ngx.req.get_header to get more information
|
|
-- effectively, it does this: headers[name] = headers[name] or headers[name_lc]
|
|
headers[name] = headers[name]
|
|
headers[name_lc] = nil
|
|
end
|
|
|
|
headers[name] = append_value(headers[name], value)
|
|
end
|
|
|
|
for name, _ in pairs(headers_to_remove) do
|
|
clear_header(name)
|
|
end
|
|
|
|
set_headers(headers)
|
|
end
|
|
|
|
local function transform_querystrings(conf, template_env)
|
|
|
|
if not (#conf.remove.querystring > 0 or #conf.rename.querystring > 0 or
|
|
#conf.replace.querystring > 0 or #conf.add.querystring > 0 or
|
|
#conf.append.querystring > 0) then
|
|
return
|
|
end
|
|
|
|
local querystring = cycle_aware_deep_copy(template_env.query_params)
|
|
|
|
-- Remove querystring(s)
|
|
for _, name, value in iter(conf.remove.querystring, template_env) do
|
|
querystring[name] = nil
|
|
end
|
|
|
|
-- Rename querystring(s)
|
|
for _, old_name, new_name in iter(conf.rename.querystring, template_env) do
|
|
rename(querystring, old_name, new_name)
|
|
end
|
|
|
|
for _, name, value in iter(conf.replace.querystring, template_env) do
|
|
if querystring[name] then
|
|
querystring[name] = value
|
|
end
|
|
end
|
|
|
|
-- Add querystring(s)
|
|
for _, name, value in iter(conf.add.querystring, template_env) do
|
|
if not querystring[name] then
|
|
querystring[name] = value
|
|
end
|
|
end
|
|
|
|
-- Append querystring(s)
|
|
for _, name, value in iter(conf.append.querystring, template_env) do
|
|
querystring[name] = append_value(querystring[name], value)
|
|
end
|
|
set_uri_args(querystring)
|
|
end
|
|
|
|
local function transform_json_body(conf, body, content_length, template_env)
|
|
local removed, renamed, replaced, added, appended = false, false, false, false, false
|
|
local content_length = (body and #body) or 0
|
|
local parameters = parse_json(body)
|
|
if parameters == nil then
|
|
if content_length > 0 then
|
|
return false, nil
|
|
end
|
|
parameters = {}
|
|
end
|
|
|
|
if content_length > 0 and #conf.remove.body > 0 then
|
|
for _, name, value in iter(conf.remove.body, template_env) do
|
|
parameters[name] = nil
|
|
removed = true
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.rename.body > 0 then
|
|
for _, old_name, new_name in iter(conf.rename.body, template_env) do
|
|
renamed = rename(parameters, old_name, new_name) or renamed
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.replace.body > 0 then
|
|
for _, name, value in iter(conf.replace.body, template_env) do
|
|
if parameters[name] then
|
|
parameters[name] = value
|
|
replaced = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if #conf.add.body > 0 then
|
|
for _, name, value in iter(conf.add.body, template_env) do
|
|
if not parameters[name] then
|
|
parameters[name] = value
|
|
added = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if #conf.append.body > 0 then
|
|
for _, name, value in iter(conf.append.body, template_env) do
|
|
local old_value = parameters[name]
|
|
parameters[name] = append_value(old_value, value)
|
|
appended = true
|
|
end
|
|
end
|
|
|
|
if removed or renamed or replaced or added or appended then
|
|
return true, assert(cjson.encode(parameters))
|
|
end
|
|
end
|
|
|
|
local function transform_url_encoded_body(conf, body, content_length, template_env)
|
|
local renamed, removed, replaced, added, appended = false, false, false, false, false
|
|
local parameters = decode_args(body)
|
|
|
|
if content_length > 0 and #conf.remove.body > 0 then
|
|
for _, name, value in iter(conf.remove.body, template_env) do
|
|
parameters[name] = nil
|
|
removed = true
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.rename.body > 0 then
|
|
for _, old_name, new_name in iter(conf.rename.body, template_env) do
|
|
renamed = rename(parameters, old_name, new_name) or renamed
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.replace.body > 0 then
|
|
for _, name, value in iter(conf.replace.body, template_env) do
|
|
if parameters[name] then
|
|
parameters[name] = value
|
|
replaced = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if #conf.add.body > 0 then
|
|
for _, name, value in iter(conf.add.body, template_env) do
|
|
if parameters[name] == nil then
|
|
parameters[name] = value
|
|
added = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if #conf.append.body > 0 then
|
|
for _, name, value in iter(conf.append.body, template_env) do
|
|
local old_value = parameters[name]
|
|
parameters[name] = append_value(old_value, value)
|
|
appended = true
|
|
end
|
|
end
|
|
|
|
if removed or renamed or replaced or added or appended then
|
|
return true, encode_args(parameters)
|
|
end
|
|
end
|
|
|
|
local function transform_multipart_body(conf, body, content_length, content_type_value, template_env)
|
|
local removed, renamed, replaced, added, appended = false, false, false, false, false
|
|
local parameters = multipart(body and body or "", content_type_value)
|
|
|
|
if content_length > 0 and #conf.rename.body > 0 then
|
|
for _, old_name, new_name in iter(conf.rename.body, template_env) do
|
|
local para = parameters:get(old_name)
|
|
if para and old_name ~= new_name then
|
|
local value = para.value
|
|
parameters:set_simple(new_name, value)
|
|
parameters:delete(old_name)
|
|
renamed = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.remove.body > 0 then
|
|
for _, name, value in iter(conf.remove.body, template_env) do
|
|
parameters:delete(name)
|
|
removed = true
|
|
end
|
|
end
|
|
|
|
if content_length > 0 and #conf.replace.body > 0 then
|
|
for _, name, value in iter(conf.replace.body, template_env) do
|
|
if parameters:get(name) then
|
|
parameters:delete(name)
|
|
parameters:set_simple(name, value)
|
|
replaced = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if #conf.add.body > 0 then
|
|
for _, name, value in iter(conf.add.body, template_env) do
|
|
if not parameters:get(name) then
|
|
parameters:set_simple(name, value)
|
|
added = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if removed or renamed or replaced or added or appended then
|
|
return true, parameters:tostring()
|
|
end
|
|
end
|
|
|
|
local function transform_body(conf, template_env)
|
|
local content_type_value = get_header(CONTENT_TYPE)
|
|
local content_type = get_content_type(content_type_value)
|
|
if content_type == nil or #conf.rename.body < 1 and
|
|
#conf.remove.body < 1 and #conf.replace.body < 1 and
|
|
#conf.add.body < 1 and #conf.append.body < 1 then
|
|
return
|
|
end
|
|
|
|
-- Call req_read_body to read the request body first
|
|
local body, err = get_raw_body()
|
|
if err then
|
|
kong.log.warn(err)
|
|
end
|
|
local is_body_transformed = false
|
|
local content_length = (body and #body) or 0
|
|
|
|
if content_type == ENCODED then
|
|
is_body_transformed, body = transform_url_encoded_body(conf, body, content_length, template_env)
|
|
elseif content_type == MULTI then
|
|
is_body_transformed, body = transform_multipart_body(conf, body, content_length, content_type_value, template_env)
|
|
elseif content_type == JSON then
|
|
is_body_transformed, body = transform_json_body(conf, body, content_length, template_env)
|
|
end
|
|
|
|
if is_body_transformed then
|
|
set_raw_body(body)
|
|
set_header(CONTENT_LENGTH, #body)
|
|
end
|
|
end
|
|
|
|
local function transform_method(conf)
|
|
if conf.http_method then
|
|
set_method(conf.http_method:upper())
|
|
if conf.http_method == "GET" or conf.http_method == "HEAD" or conf.http_method == "TRACE" then
|
|
local content_type_value = get_header(CONTENT_TYPE)
|
|
local content_type = get_content_type(content_type_value)
|
|
if content_type == ENCODED then
|
|
-- Also put the body into querystring
|
|
local body = get_raw_body()
|
|
local parameters = decode_args(body)
|
|
|
|
-- Append to querystring
|
|
if type(parameters) == "table" and next(parameters) then
|
|
local querystring = get_uri_args()
|
|
for name, value in pairs(parameters) do
|
|
if querystring[name] then
|
|
if type(querystring[name]) == "table" then
|
|
append_value(querystring[name], value)
|
|
else
|
|
querystring[name] = { querystring[name], value }
|
|
end
|
|
else
|
|
querystring[name] = value
|
|
end
|
|
end
|
|
|
|
set_uri_args(querystring)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function transform_uri(conf, template_env)
|
|
if conf.replace.uri then
|
|
|
|
local res, err = param_value(conf.replace.uri, conf.replace, template_env)
|
|
if err then
|
|
error("[request-transformer] failed to render the template " ..
|
|
tostring(conf.replace.uri) .. ", error:" .. err)
|
|
end
|
|
|
|
kong.log.debug(DEBUG, "[request-transformer] template `", conf.replace.uri,
|
|
"` rendered to `", res, "`")
|
|
|
|
if res then
|
|
set_path(res)
|
|
end
|
|
end
|
|
end
|
|
|
|
function _M.execute(conf)
|
|
-- meta table for the sandbox, exposing lazily loaded values
|
|
local __meta_environment = {
|
|
__index = function(self, key)
|
|
local lazy_loaders = {
|
|
headers = function(self)
|
|
return get_headers() or EMPTY
|
|
end,
|
|
query_params = function(self)
|
|
return get_uri_args() or EMPTY
|
|
end,
|
|
uri_captures = function(self)
|
|
return (ngx.ctx.router_matches or EMPTY).uri_captures or EMPTY
|
|
end,
|
|
shared = function(self)
|
|
return ((kong or EMPTY).ctx or EMPTY).shared or EMPTY
|
|
end,
|
|
}
|
|
local loader = lazy_loaders[key]
|
|
if not loader then
|
|
if lua_enabled and not sandbox_enabled then
|
|
return _G[key]
|
|
end
|
|
return
|
|
end
|
|
-- set the result on the table to not load again
|
|
local value = loader()
|
|
rawset(self, key, value)
|
|
return value
|
|
end,
|
|
__newindex = function(self)
|
|
error("This environment is read-only.")
|
|
end,
|
|
}
|
|
|
|
local template_env = {}
|
|
if lua_enabled and sandbox_enabled then
|
|
-- load the sandbox environment to be used to render the template
|
|
template_env = cycle_aware_deep_copy(sandbox.configuration.environment)
|
|
-- here we can optionally add functions to expose to the sandbox, eg:
|
|
-- tostring = tostring,
|
|
-- because headers may contain array elements such as duplicated headers
|
|
-- type is a useful function in these cases. See issue #25.
|
|
template_env.type = type
|
|
end
|
|
setmetatable(template_env, __meta_environment)
|
|
|
|
transform_uri(conf, template_env)
|
|
transform_method(conf)
|
|
transform_headers(conf, template_env)
|
|
transform_body(conf, template_env)
|
|
transform_querystrings(conf, template_env)
|
|
end
|
|
|
|
return _M
|