- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
262 lines
7.3 KiB
Lua
262 lines
7.3 KiB
Lua
--
|
|
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
|
-- contributor license agreements. See the NOTICE file distributed with
|
|
-- this work for additional information regarding copyright ownership.
|
|
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
|
-- (the "License"); you may not use this file except in compliance with
|
|
-- the License. You may obtain a copy of the License at
|
|
--
|
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
--
|
|
-- Unless required by applicable law or agreed to in writing, software
|
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
-- See the License for the specific language governing permissions and
|
|
-- limitations under the License.
|
|
--
|
|
local core = require("apisix.core")
|
|
local xml2lua = require("xml2lua")
|
|
local xmlhandler = require("xmlhandler.tree")
|
|
local template = require("resty.template")
|
|
local ngx = ngx
|
|
local decode_base64 = ngx.decode_base64
|
|
local req_set_body_data = ngx.req.set_body_data
|
|
local req_get_uri_args = ngx.req.get_uri_args
|
|
local str_format = string.format
|
|
local decode_args = ngx.decode_args
|
|
local str_find = core.string.find
|
|
local type = type
|
|
local pcall = pcall
|
|
local pairs = pairs
|
|
local next = next
|
|
local multipart = require("multipart")
|
|
local setmetatable = setmetatable
|
|
|
|
local transform_schema = {
|
|
type = "object",
|
|
properties = {
|
|
input_format = { type = "string",
|
|
enum = {"xml", "json", "encoded", "args", "plain", "multipart",}},
|
|
template = { type = "string" },
|
|
template_is_base64 = { type = "boolean" },
|
|
},
|
|
required = {"template"},
|
|
}
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
request = transform_schema,
|
|
response = transform_schema,
|
|
},
|
|
anyOf = {
|
|
{required = {"request"}},
|
|
{required = {"response"}},
|
|
{required = {"request", "response"}},
|
|
},
|
|
}
|
|
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 1080,
|
|
name = "body-transformer",
|
|
schema = schema,
|
|
}
|
|
|
|
|
|
local function escape_xml(s)
|
|
return s:gsub("&", "&")
|
|
:gsub("<", "<")
|
|
:gsub(">", ">")
|
|
:gsub("'", "'")
|
|
:gsub('"', """)
|
|
end
|
|
|
|
|
|
local function escape_json(s)
|
|
return core.json.encode(s)
|
|
end
|
|
|
|
|
|
local function remove_namespace(tbl)
|
|
for k, v in pairs(tbl) do
|
|
if type(v) == "table" and next(v) == nil then
|
|
v = ""
|
|
tbl[k] = v
|
|
end
|
|
if type(k) == "string" then
|
|
local newk = k:match(".*:(.*)")
|
|
if newk then
|
|
tbl[newk] = v
|
|
tbl[k] = nil
|
|
end
|
|
if type(v) == "table" then
|
|
remove_namespace(v)
|
|
end
|
|
end
|
|
end
|
|
return tbl
|
|
end
|
|
|
|
|
|
local decoders = {
|
|
xml = function(data)
|
|
local handler = xmlhandler:new()
|
|
local parser = xml2lua.parser(handler)
|
|
local ok, err = pcall(parser.parse, parser, data)
|
|
if ok then
|
|
return remove_namespace(handler.root)
|
|
else
|
|
return nil, err
|
|
end
|
|
end,
|
|
json = function(data)
|
|
return core.json.decode(data)
|
|
end,
|
|
encoded = function(data)
|
|
return decode_args(data)
|
|
end,
|
|
args = function()
|
|
return req_get_uri_args()
|
|
end,
|
|
multipart = function (data, content_type_header)
|
|
local res = multipart(data, content_type_header)
|
|
return res
|
|
end
|
|
}
|
|
|
|
|
|
function _M.check_schema(conf)
|
|
return core.schema.check(schema, conf)
|
|
end
|
|
|
|
|
|
local function transform(conf, body, typ, ctx, request_method)
|
|
local out = {}
|
|
local _multipart
|
|
local format = conf[typ].input_format
|
|
local ct = ctx.var.http_content_type
|
|
if typ == "response" then
|
|
ct = ngx.header.content_type
|
|
end
|
|
if (body or request_method == "GET") and format ~= "plain" then
|
|
local err
|
|
if format then
|
|
out, err = decoders[format](body, ct)
|
|
if format == "multipart" then
|
|
_multipart = out
|
|
out = out:get_all_with_arrays()
|
|
end
|
|
if not out then
|
|
err = str_format("%s body decode: %s", typ, err)
|
|
core.log.error(err, ", body=", body)
|
|
return nil, 400, err
|
|
end
|
|
else
|
|
core.log.warn("no input format to parse ", typ, " body")
|
|
end
|
|
end
|
|
|
|
local text = conf[typ].template
|
|
if (conf[typ].template_is_base64 or (format and format ~= "encoded" and format ~= "args")) then
|
|
text = decode_base64(text) or text
|
|
end
|
|
local ok, render = pcall(template.compile, text)
|
|
if not ok then
|
|
local err = render
|
|
err = str_format("%s template compile: %s", typ, err)
|
|
core.log.error(err)
|
|
return nil, 503, err
|
|
end
|
|
|
|
setmetatable(out, {__index = {
|
|
_ctx = ctx,
|
|
_body = body,
|
|
_escape_xml = escape_xml,
|
|
_escape_json = escape_json,
|
|
_multipart = _multipart
|
|
}})
|
|
|
|
local ok, render_out = pcall(render, out)
|
|
if not ok then
|
|
local err = str_format("%s template rendering: %s", typ, render_out)
|
|
core.log.error(err)
|
|
return nil, 503, err
|
|
end
|
|
|
|
core.log.info(typ, " body transform output=", render_out)
|
|
return render_out
|
|
end
|
|
|
|
|
|
local function set_input_format(conf, typ, ct, method)
|
|
if method == "GET" then
|
|
conf[typ].input_format = "args"
|
|
end
|
|
if conf[typ].input_format == nil and ct then
|
|
if ct:find("text/xml") then
|
|
conf[typ].input_format = "xml"
|
|
elseif ct:find("application/json") then
|
|
conf[typ].input_format = "json"
|
|
elseif str_find(ct:lower(), "application/x-www-form-urlencoded", nil, true) then
|
|
conf[typ].input_format = "encoded"
|
|
elseif str_find(ct:lower(), "multipart/", nil, true) then
|
|
conf[typ].input_format = "multipart"
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function _M.rewrite(conf, ctx)
|
|
if conf.request then
|
|
local request_method = ngx.var.request_method
|
|
conf = core.table.deepcopy(conf)
|
|
ctx.body_transformer_conf = conf
|
|
local body = core.request.get_body()
|
|
set_input_format(conf, "request", ctx.var.http_content_type, request_method)
|
|
local out, status, err = transform(conf, body, "request", ctx, request_method)
|
|
if not out then
|
|
return status, { message = err }
|
|
end
|
|
req_set_body_data(out)
|
|
end
|
|
end
|
|
|
|
|
|
function _M.header_filter(conf, ctx)
|
|
if conf.response then
|
|
if not ctx.body_transformer_conf then
|
|
conf = core.table.deepcopy(conf)
|
|
ctx.body_transformer_conf = conf
|
|
end
|
|
set_input_format(conf, "response", ngx.header.content_type)
|
|
core.response.clear_header_as_body_modified()
|
|
end
|
|
end
|
|
|
|
|
|
function _M.body_filter(_, ctx)
|
|
local conf = ctx.body_transformer_conf
|
|
if not conf then
|
|
return
|
|
end
|
|
if conf.response then
|
|
local body = core.response.hold_body_chunk(ctx)
|
|
if ngx.arg[2] == false and not body then
|
|
return
|
|
end
|
|
|
|
local out = transform(conf, body, "response", ctx)
|
|
if not out then
|
|
core.log.error("failed to transform response body: ", body)
|
|
return
|
|
end
|
|
|
|
ngx.arg[1] = out
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|