- 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>
391 lines
11 KiB
Lua
391 lines
11 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 expr = require("resty.expr.v1")
|
|
local re_compile = require("resty.core.regex").re_match_compile
|
|
local plugin_name = "response-rewrite"
|
|
local ngx = ngx
|
|
local ngx_header = ngx.header
|
|
local re_match = ngx.re.match
|
|
local re_sub = ngx.re.sub
|
|
local re_gsub = ngx.re.gsub
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local pcall = pcall
|
|
local content_decode = require("apisix.utils.content-decode")
|
|
|
|
|
|
local lrucache = core.lrucache.new({
|
|
type = "plugin",
|
|
})
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
headers = {
|
|
description = "new headers for response",
|
|
anyOf = {
|
|
{
|
|
type = "object",
|
|
minProperties = 1,
|
|
patternProperties = {
|
|
["^[^:]+$"] = {
|
|
oneOf = {
|
|
{type = "string"},
|
|
{type = "number"},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
properties = {
|
|
add = {
|
|
type = "array",
|
|
minItems = 1,
|
|
items = {
|
|
type = "string",
|
|
-- "Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>"
|
|
pattern = "^[^:]+:[^:]*[^/]$"
|
|
}
|
|
},
|
|
set = {
|
|
type = "object",
|
|
minProperties = 1,
|
|
patternProperties = {
|
|
["^[^:]+$"] = {
|
|
oneOf = {
|
|
{type = "string"},
|
|
{type = "number"},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
remove = {
|
|
type = "array",
|
|
minItems = 1,
|
|
items = {
|
|
type = "string",
|
|
-- "Set-Cookie"
|
|
pattern = "^[^:]+$"
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
body = {
|
|
description = "new body for response",
|
|
type = "string",
|
|
},
|
|
body_base64 = {
|
|
description = "whether new body for response need base64 decode before return",
|
|
type = "boolean",
|
|
default = false,
|
|
},
|
|
status_code = {
|
|
description = "new status code for response",
|
|
type = "integer",
|
|
minimum = 200,
|
|
maximum = 598,
|
|
},
|
|
vars = {
|
|
type = "array",
|
|
},
|
|
filters = {
|
|
description = "a group of filters that modify response body" ..
|
|
"by replacing one specified string by another",
|
|
type = "array",
|
|
minItems = 1,
|
|
items = {
|
|
description = "filter that modifies response body",
|
|
type = "object",
|
|
required = {"regex", "replace"},
|
|
properties = {
|
|
regex = {
|
|
description = "match pattern on response body",
|
|
type = "string",
|
|
minLength = 1,
|
|
},
|
|
scope = {
|
|
description = "regex substitution range",
|
|
type = "string",
|
|
enum = {"once", "global"},
|
|
default = "once",
|
|
},
|
|
replace = {
|
|
description = "regex substitution content",
|
|
type = "string",
|
|
},
|
|
options = {
|
|
description = "regex options",
|
|
type = "string",
|
|
default = "jo",
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dependencies = {
|
|
body = {
|
|
["not"] = {required = {"filters"}}
|
|
},
|
|
filters = {
|
|
["not"] = {required = {"body"}}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 899,
|
|
name = plugin_name,
|
|
schema = schema,
|
|
}
|
|
|
|
local function vars_matched(conf, ctx)
|
|
if not conf.vars then
|
|
return true
|
|
end
|
|
|
|
if not conf.response_expr then
|
|
local response_expr, _ = expr.new(conf.vars)
|
|
conf.response_expr = response_expr
|
|
end
|
|
|
|
local match_result = conf.response_expr:eval(ctx.var)
|
|
|
|
return match_result
|
|
end
|
|
|
|
|
|
local function is_new_headers_conf(headers)
|
|
return
|
|
(headers.add and type(headers.add) == "table") or
|
|
(headers.set and type(headers.set) == "table") or
|
|
(headers.remove and type(headers.remove) == "table")
|
|
end
|
|
|
|
|
|
local function check_set_headers(headers)
|
|
for field, value in pairs(headers) do
|
|
if type(field) ~= 'string' then
|
|
return false, 'invalid type as header field'
|
|
end
|
|
|
|
if type(value) ~= 'string' and type(value) ~= 'number' then
|
|
return false, 'invalid type as header value'
|
|
end
|
|
|
|
if #field == 0 then
|
|
return false, 'invalid field length in header'
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function _M.check_schema(conf)
|
|
local ok, err = core.schema.check(schema, conf)
|
|
if not ok then
|
|
return false, err
|
|
end
|
|
|
|
if conf.headers then
|
|
if not is_new_headers_conf(conf.headers) then
|
|
ok, err = check_set_headers(conf.headers)
|
|
if not ok then
|
|
return false, err
|
|
end
|
|
end
|
|
end
|
|
|
|
if conf.body_base64 then
|
|
if not conf.body or #conf.body == 0 then
|
|
return false, 'invalid base64 content'
|
|
end
|
|
local body = ngx.decode_base64(conf.body)
|
|
if not body then
|
|
return false, 'invalid base64 content'
|
|
end
|
|
end
|
|
|
|
if conf.vars then
|
|
local ok, err = expr.new(conf.vars)
|
|
if not ok then
|
|
return false, "failed to validate the 'vars' expression: " .. err
|
|
end
|
|
end
|
|
|
|
if conf.filters then
|
|
for _, filter in ipairs(conf.filters) do
|
|
local ok, err = pcall(re_compile, filter.regex, filter.options)
|
|
if not ok then
|
|
return false, "regex \"" .. filter.regex ..
|
|
"\" validation failed: " .. err
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
do
|
|
|
|
function _M.body_filter(conf, ctx)
|
|
if not ctx.response_rewrite_matched then
|
|
return
|
|
end
|
|
|
|
if conf.filters then
|
|
|
|
local body = core.response.hold_body_chunk(ctx)
|
|
if not body then
|
|
return
|
|
end
|
|
|
|
local err
|
|
if ctx.response_encoding ~= nil then
|
|
local decoder = content_decode.dispatch_decoder(ctx.response_encoding)
|
|
if not decoder then
|
|
core.log.error("filters may not work as expected ",
|
|
"due to unsupported compression encoding type: ",
|
|
ctx.response_encoding)
|
|
return
|
|
end
|
|
body, err = decoder(body)
|
|
if err ~= nil then
|
|
core.log.error("filters may not work as expected: ", err)
|
|
return
|
|
end
|
|
end
|
|
|
|
for _, filter in ipairs(conf.filters) do
|
|
if filter.scope == "once" then
|
|
body, _, err = re_sub(body, filter.regex, filter.replace, filter.options)
|
|
else
|
|
body, _, err = re_gsub(body, filter.regex, filter.replace, filter.options)
|
|
end
|
|
if err ~= nil then
|
|
core.log.error("regex \"" .. filter.regex .. "\" substitutes failed:" .. err)
|
|
end
|
|
end
|
|
|
|
ngx.arg[1] = body
|
|
return
|
|
end
|
|
|
|
if conf.body then
|
|
ngx.arg[2] = true
|
|
if conf.body_base64 then
|
|
ngx.arg[1] = ngx.decode_base64(conf.body)
|
|
else
|
|
ngx.arg[1] = conf.body
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function create_header_operation(hdr_conf)
|
|
local set = {}
|
|
local add = {}
|
|
if is_new_headers_conf(hdr_conf) then
|
|
if hdr_conf.add then
|
|
for _, value in ipairs(hdr_conf.add) do
|
|
local m, err = re_match(value, [[^([^:\s]+)\s*:\s*([^:]+)$]], "jo")
|
|
if not m then
|
|
return nil, err
|
|
end
|
|
core.table.insert_tail(add, m[1], m[2])
|
|
end
|
|
end
|
|
|
|
if hdr_conf.set then
|
|
for field, value in pairs(hdr_conf.set) do
|
|
--reform header from object into array, so can avoid use pairs, which is NYI
|
|
core.table.insert_tail(set, field, value)
|
|
end
|
|
end
|
|
|
|
else
|
|
for field, value in pairs(hdr_conf) do
|
|
core.table.insert_tail(set, field, value)
|
|
end
|
|
end
|
|
|
|
return {
|
|
add = add,
|
|
set = set,
|
|
remove = hdr_conf.remove or {},
|
|
}
|
|
end
|
|
|
|
|
|
function _M.header_filter(conf, ctx)
|
|
ctx.response_rewrite_matched = vars_matched(conf, ctx)
|
|
if not ctx.response_rewrite_matched then
|
|
return
|
|
end
|
|
|
|
if conf.status_code then
|
|
ngx.status = conf.status_code
|
|
end
|
|
|
|
-- if filters have no any match, response body won't be modified.
|
|
if conf.filters or conf.body then
|
|
local response_encoding = ngx_header["Content-Encoding"]
|
|
core.response.clear_header_as_body_modified()
|
|
ctx.response_encoding = response_encoding
|
|
end
|
|
|
|
if not conf.headers then
|
|
return
|
|
end
|
|
|
|
local hdr_op, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,
|
|
create_header_operation, conf.headers)
|
|
if not hdr_op then
|
|
core.log.error("failed to create header operation: ", err)
|
|
return
|
|
end
|
|
|
|
local field_cnt = #hdr_op.add
|
|
for i = 1, field_cnt, 2 do
|
|
local val = core.utils.resolve_var(hdr_op.add[i+1], ctx.var)
|
|
core.response.add_header(hdr_op.add[i], val)
|
|
end
|
|
|
|
local field_cnt = #hdr_op.set
|
|
for i = 1, field_cnt, 2 do
|
|
local val = core.utils.resolve_var(hdr_op.set[i+1], ctx.var)
|
|
core.response.set_header(hdr_op.set[i], val)
|
|
end
|
|
|
|
local field_cnt = #hdr_op.remove
|
|
for i = 1, field_cnt do
|
|
core.response.set_header(hdr_op.remove[i], nil)
|
|
end
|
|
end
|
|
|
|
end -- do
|
|
|
|
|
|
return _M
|