feat(apisix): add Cloudron package
- 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>
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
--
|
||||
-- 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
|
Reference in New Issue
Block a user