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,421 @@
|
||||
--
|
||||
-- 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 require = require
|
||||
local core = require("apisix.core")
|
||||
local rr_balancer = require("apisix.balancer.roundrobin")
|
||||
local plugin = require("apisix.plugin")
|
||||
local t1k = require "resty.t1k"
|
||||
local expr = require("resty.expr.v1")
|
||||
|
||||
local ngx = ngx
|
||||
local ngx_now = ngx.now
|
||||
local string = string
|
||||
local fmt = string.format
|
||||
local tostring = tostring
|
||||
local tonumber = tonumber
|
||||
local ipairs = ipairs
|
||||
|
||||
local plugin_name = "chaitin-waf"
|
||||
|
||||
local vars_schema = {
|
||||
type = "array",
|
||||
}
|
||||
|
||||
local lrucache = core.lrucache.new({
|
||||
ttl = 300, count = 1024
|
||||
})
|
||||
|
||||
local match_schema = {
|
||||
type = "array",
|
||||
items = {
|
||||
type = "object",
|
||||
properties = {
|
||||
vars = vars_schema
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
local plugin_schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
mode = {
|
||||
type = "string",
|
||||
enum = { "off", "monitor", "block", nil },
|
||||
default = nil,
|
||||
},
|
||||
match = match_schema,
|
||||
append_waf_resp_header = {
|
||||
type = "boolean",
|
||||
default = true
|
||||
},
|
||||
append_waf_debug_header = {
|
||||
type = "boolean",
|
||||
default = false
|
||||
},
|
||||
config = {
|
||||
type = "object",
|
||||
properties = {
|
||||
connect_timeout = {
|
||||
type = "integer",
|
||||
},
|
||||
send_timeout = {
|
||||
type = "integer",
|
||||
},
|
||||
read_timeout = {
|
||||
type = "integer",
|
||||
},
|
||||
req_body_size = {
|
||||
type = "integer",
|
||||
},
|
||||
keepalive_size = {
|
||||
type = "integer",
|
||||
},
|
||||
keepalive_timeout = {
|
||||
type = "integer",
|
||||
},
|
||||
real_client_ip = {
|
||||
type = "boolean"
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local metadata_schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
mode = {
|
||||
type = "string",
|
||||
enum = { "off", "monitor", "block", nil },
|
||||
default = nil,
|
||||
},
|
||||
nodes = {
|
||||
type = "array",
|
||||
items = {
|
||||
type = "object",
|
||||
properties = {
|
||||
host = {
|
||||
type = "string",
|
||||
pattern = "^\\*?[0-9a-zA-Z-._\\[\\]:/]+$"
|
||||
},
|
||||
port = {
|
||||
type = "integer",
|
||||
minimum = 1,
|
||||
default = 80
|
||||
},
|
||||
},
|
||||
required = { "host" }
|
||||
},
|
||||
minItems = 1,
|
||||
},
|
||||
config = {
|
||||
type = "object",
|
||||
properties = {
|
||||
connect_timeout = {
|
||||
type = "integer",
|
||||
default = 1000 -- milliseconds
|
||||
},
|
||||
send_timeout = {
|
||||
type = "integer",
|
||||
default = 1000 -- milliseconds
|
||||
},
|
||||
read_timeout = {
|
||||
type = "integer",
|
||||
default = 1000 -- milliseconds
|
||||
},
|
||||
req_body_size = {
|
||||
type = "integer",
|
||||
default = 1024 -- milliseconds
|
||||
},
|
||||
-- maximum concurrent idle connections to
|
||||
-- the SafeLine WAF detection service
|
||||
keepalive_size = {
|
||||
type = "integer",
|
||||
default = 256
|
||||
},
|
||||
keepalive_timeout = {
|
||||
type = "integer",
|
||||
default = 60000 -- milliseconds
|
||||
},
|
||||
real_client_ip = {
|
||||
type = "boolean",
|
||||
default = true
|
||||
}
|
||||
},
|
||||
default = {},
|
||||
},
|
||||
},
|
||||
required = { "nodes" },
|
||||
}
|
||||
|
||||
local _M = {
|
||||
version = 0.1,
|
||||
priority = 2700,
|
||||
name = plugin_name,
|
||||
schema = plugin_schema,
|
||||
metadata_schema = metadata_schema
|
||||
}
|
||||
|
||||
local global_server_picker
|
||||
|
||||
local HEADER_CHAITIN_WAF = "X-APISIX-CHAITIN-WAF"
|
||||
local HEADER_CHAITIN_WAF_ERROR = "X-APISIX-CHAITIN-WAF-ERROR"
|
||||
local HEADER_CHAITIN_WAF_TIME = "X-APISIX-CHAITIN-WAF-TIME"
|
||||
local HEADER_CHAITIN_WAF_STATUS = "X-APISIX-CHAITIN-WAF-STATUS"
|
||||
local HEADER_CHAITIN_WAF_ACTION = "X-APISIX-CHAITIN-WAF-ACTION"
|
||||
local HEADER_CHAITIN_WAF_SERVER = "X-APISIX-CHAITIN-WAF-SERVER"
|
||||
local blocked_message = [[{"code": %s, "success":false, ]] ..
|
||||
[["message": "blocked by Chaitin SafeLine Web Application Firewall", "event_id": "%s"}]]
|
||||
local warning_message = "chaitin-waf monitor mode: request would have been rejected, event_id: "
|
||||
|
||||
|
||||
function _M.check_schema(conf, schema_type)
|
||||
if schema_type == core.schema.TYPE_METADATA then
|
||||
return core.schema.check(metadata_schema, conf)
|
||||
end
|
||||
|
||||
local ok, err = core.schema.check(plugin_schema, conf)
|
||||
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
|
||||
if conf.match then
|
||||
for _, m in ipairs(conf.match) do
|
||||
local ok, err = expr.new(m.vars)
|
||||
if not ok then
|
||||
return false, "failed to validate the 'vars' expression: " .. err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function get_healthy_chaitin_server_nodes(metadata, checker)
|
||||
local nodes = metadata.nodes
|
||||
local new_nodes = core.table.new(0, #nodes)
|
||||
|
||||
for i = 1, #nodes do
|
||||
local host, port = nodes[i].host, nodes[i].port
|
||||
new_nodes[host .. ":" .. tostring(port)] = 1
|
||||
end
|
||||
|
||||
return new_nodes
|
||||
end
|
||||
|
||||
|
||||
local function get_chaitin_server(metadata, ctx)
|
||||
if not global_server_picker or global_server_picker.upstream ~= metadata.value.nodes then
|
||||
local up_nodes = get_healthy_chaitin_server_nodes(metadata.value)
|
||||
if core.table.nkeys(up_nodes) == 0 then
|
||||
return nil, nil, "no healthy nodes"
|
||||
end
|
||||
core.log.info("chaitin-waf nodes: ", core.json.delay_encode(up_nodes))
|
||||
|
||||
global_server_picker = rr_balancer.new(up_nodes, metadata.value.nodes)
|
||||
end
|
||||
|
||||
local server = global_server_picker.get(ctx)
|
||||
local host, port, err = core.utils.parse_addr(server)
|
||||
if err then
|
||||
return nil, nil, err
|
||||
end
|
||||
|
||||
return host, port, nil
|
||||
end
|
||||
|
||||
|
||||
local function check_match(conf, ctx)
|
||||
if not conf.match or #conf.match == 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
for _, match in ipairs(conf.match) do
|
||||
local cache_key = tostring(match.vars)
|
||||
|
||||
local exp, err = lrucache(cache_key, nil, function(vars)
|
||||
return expr.new(vars)
|
||||
end, match.vars)
|
||||
|
||||
if not exp then
|
||||
local msg = "failed to create match expression for " ..
|
||||
tostring(match.vars) .. ", err: " .. tostring(err)
|
||||
return false, msg
|
||||
end
|
||||
|
||||
local matched = exp:eval(ctx.var)
|
||||
if matched then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function get_conf(conf, metadata)
|
||||
local t = {
|
||||
mode = "block",
|
||||
real_client_ip = true,
|
||||
}
|
||||
|
||||
if metadata.config then
|
||||
t.connect_timeout = metadata.config.connect_timeout
|
||||
t.send_timeout = metadata.config.send_timeout
|
||||
t.read_timeout = metadata.config.read_timeout
|
||||
t.req_body_size = metadata.config.req_body_size
|
||||
t.keepalive_size = metadata.config.keepalive_size
|
||||
t.keepalive_timeout = metadata.config.keepalive_timeout
|
||||
t.real_client_ip = metadata.config.real_client_ip or t.real_client_ip
|
||||
end
|
||||
|
||||
if conf.config then
|
||||
t.connect_timeout = conf.config.connect_timeout
|
||||
t.send_timeout = conf.config.send_timeout
|
||||
t.read_timeout = conf.config.read_timeout
|
||||
t.req_body_size = conf.config.req_body_size
|
||||
t.keepalive_size = conf.config.keepalive_size
|
||||
t.keepalive_timeout = conf.config.keepalive_timeout
|
||||
t.real_client_ip = conf.config.real_client_ip or t.real_client_ip
|
||||
end
|
||||
|
||||
t.mode = conf.mode or metadata.mode or t.mode
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
local function do_access(conf, ctx)
|
||||
local extra_headers = {}
|
||||
|
||||
local metadata = plugin.plugin_metadata(plugin_name)
|
||||
if not core.table.try_read_attr(metadata, "value", "nodes") then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "err"
|
||||
extra_headers[HEADER_CHAITIN_WAF_ERROR] = "missing metadata"
|
||||
return 500, nil, extra_headers
|
||||
end
|
||||
|
||||
local host, port, err = get_chaitin_server(metadata, ctx)
|
||||
if err then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "unhealthy"
|
||||
extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)
|
||||
|
||||
return 500, nil, extra_headers
|
||||
end
|
||||
|
||||
core.log.info("picked chaitin-waf server: ", host, ":", port)
|
||||
local t = get_conf(conf, metadata.value)
|
||||
t.host = host
|
||||
t.port = port
|
||||
|
||||
extra_headers[HEADER_CHAITIN_WAF_SERVER] = host
|
||||
|
||||
local mode = t.mode or "block"
|
||||
if mode == "off" then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "off"
|
||||
return nil, nil, extra_headers
|
||||
end
|
||||
|
||||
local match, err = check_match(conf, ctx)
|
||||
if not match then
|
||||
if err then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "err"
|
||||
extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)
|
||||
return 500, nil, extra_headers
|
||||
else
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "no"
|
||||
return nil, nil, extra_headers
|
||||
end
|
||||
end
|
||||
|
||||
if t.real_client_ip then
|
||||
t.client_ip = ctx.var.http_x_forwarded_for or ctx.var.remote_addr
|
||||
else
|
||||
t.client_ip = ctx.var.remote_addr
|
||||
end
|
||||
|
||||
local start_time = ngx_now() * 1000
|
||||
local ok, err, result = t1k.do_access(t, false)
|
||||
|
||||
extra_headers[HEADER_CHAITIN_WAF_TIME] = ngx_now() * 1000 - start_time
|
||||
|
||||
if not ok then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "waf-err"
|
||||
local err_msg = tostring(err)
|
||||
if core.string.find(err_msg, "timeout") then
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "timeout"
|
||||
end
|
||||
extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)
|
||||
|
||||
if mode == "monitor" then
|
||||
core.log.warn("chaitin-waf monitor mode: detected waf error - ", err_msg)
|
||||
return nil, nil, extra_headers
|
||||
end
|
||||
|
||||
return 500, nil, extra_headers
|
||||
else
|
||||
extra_headers[HEADER_CHAITIN_WAF] = "yes"
|
||||
extra_headers[HEADER_CHAITIN_WAF_ACTION] = "pass"
|
||||
end
|
||||
|
||||
local code = 200
|
||||
extra_headers[HEADER_CHAITIN_WAF_STATUS] = code
|
||||
|
||||
if result and result.status and result.status ~= 200 and result.event_id then
|
||||
extra_headers[HEADER_CHAITIN_WAF_STATUS] = result.status
|
||||
extra_headers[HEADER_CHAITIN_WAF_ACTION] = "reject"
|
||||
|
||||
if mode == "monitor" then
|
||||
core.log.warn(warning_message, result.event_id)
|
||||
return nil, nil, extra_headers
|
||||
end
|
||||
|
||||
core.log.error("request rejected by chaitin-waf, event_id: " .. result.event_id)
|
||||
|
||||
return tonumber(result.status),
|
||||
fmt(blocked_message, result.status, result.event_id) .. "\n",
|
||||
extra_headers
|
||||
end
|
||||
|
||||
return nil, nil, extra_headers
|
||||
end
|
||||
|
||||
|
||||
function _M.access(conf, ctx)
|
||||
local code, msg, extra_headers = do_access(conf, ctx)
|
||||
|
||||
if not conf.append_waf_debug_header then
|
||||
extra_headers[HEADER_CHAITIN_WAF_ERROR] = nil
|
||||
extra_headers[HEADER_CHAITIN_WAF_SERVER] = nil
|
||||
end
|
||||
|
||||
if conf.append_waf_resp_header then
|
||||
core.response.set_header(extra_headers)
|
||||
end
|
||||
|
||||
return code, msg
|
||||
end
|
||||
|
||||
|
||||
function _M.header_filter(conf, ctx)
|
||||
t1k.do_header_filter()
|
||||
end
|
||||
|
||||
|
||||
return _M
|
Reference in New Issue
Block a user