项目文件夹

文件
2026-07-13 12:32:21 +08:00

411 行
11 KiB
Lua

local constants = require "kong.constants"
local DAO = require "kong.db.dao"
local plugin_loader = require "kong.db.schema.plugin_loader"
local reports = require "kong.reports"
local plugin_servers = require "kong.runloop.plugin_servers"
local wasm_plugins = require "kong.runloop.wasm.plugins"
local version = require "version"
local load_module_if_exists = require "kong.tools.module".load_module_if_exists
local Plugins = {}
local fmt = string.format
local type = type
local null = ngx.null
local pairs = pairs
local tostring = tostring
local ngx_log = ngx.log
local ngx_WARN = ngx.WARN
local ngx_DEBUG = ngx.DEBUG
local function has_a_common_protocol_with_route(plugin, route)
local plugin_prot = plugin.protocols
local route_prot = route.protocols
-- plugin.protocols and route.protocols are both sets provided by the schema
-- this means that they can be iterated as over an array, and queried as a hash
for i = 1, #plugin_prot do
if route_prot[plugin_prot[i]] then
return true
end
end
end
local function has_common_protocol_with_service(self, plugin, service_pk)
local had_at_least_one_route = false
for route, err, err_t in self.db.routes:each_for_service(service_pk) do
if not route then
return nil, err, err_t
end
had_at_least_one_route = true
if has_a_common_protocol_with_route(plugin, route) then
return true
end
end
return not had_at_least_one_route
end
local function check_protocols_match(self, plugin)
if type(plugin.protocols) ~= "table" then
return true
end
if type(plugin.route) == "table" then
local route = self.db.routes:select(plugin.route) -- ignore error
if route and not has_a_common_protocol_with_route(plugin, route) then
local err_t = self.errors:schema_violation({
protocols = "must match the associated route's protocols",
})
return nil, tostring(err_t), err_t
end
elseif type(plugin.service) == "table" then
if not has_common_protocol_with_service(self, plugin, plugin.service) then
local err_t = self.errors:schema_violation({
protocols = "must match the protocols of at least one route " ..
"pointing to this Plugin's service",
})
return nil, tostring(err_t), err_t
end
end
return true
end
function Plugins:insert(entity, options)
local ok, err, err_t = check_protocols_match(self, entity)
if not ok then
return nil, err, err_t
end
return self.super.insert(self, entity, options)
end
function Plugins:update(primary_key, entity, options)
if entity.protocols or entity.service or entity.route then
if (entity.protocols and not entity.route)
or (entity.service and not entity.protocols)
or (entity.route and not entity.protocols)
then
local rbw_entity = self.super.select(self, primary_key, options)
if rbw_entity then
entity.protocols = entity.protocols or rbw_entity.protocols
entity.service = entity.service or rbw_entity.service
entity.route = entity.route or rbw_entity.route
end
end
local ok, err, err_t = check_protocols_match(self, entity)
if not ok then
return nil, err, err_t
end
end
return self.super.update(self, primary_key, entity, options)
end
function Plugins:upsert(primary_key, entity, options)
local ok, err, err_t = check_protocols_match(self, entity)
if not ok then
return nil, err, err_t
end
return self.super.upsert(self, primary_key, entity, options)
end
local function implements(plugin, method)
if type(plugin) ~= "table" then
return false
end
local m = plugin[method]
return type(m) == "function"
end
local load_plugin_handler do
local function valid_priority(prio)
if type(prio) ~= "number" or
prio ~= prio or -- NaN
math.abs(prio) == math.huge or
math.floor(prio) ~= prio then
return false
end
return true
end
-- Returns the cleaned version string, only x.y.z part
local function valid_version(v)
if type(v) ~= "string" then
return false
end
local vparsed = version(v)
if not vparsed or vparsed[4] ~= nil then
return false
end
return tostring(vparsed)
end
function load_plugin_handler(plugin)
-- NOTE: no version _G.kong (nor PDK) in plugins main chunk
local plugin_handler = "kong.plugins." .. plugin .. ".handler"
local ok, handler = load_module_if_exists(plugin_handler)
if not ok then
ok, handler = wasm_plugins.load_plugin(plugin)
if type(handler) == "table" then
handler._wasm = true
end
end
if not ok then
ok, handler = plugin_servers.load_plugin(plugin)
if type(handler) == "table" then
handler._go = true
end
end
if not ok then
return nil, plugin .. " plugin is enabled but not installed;\n" .. handler
end
if type(handler) == "table" then
if not valid_priority(handler.PRIORITY) then
return nil, fmt(
"Plugin %q cannot be loaded because its PRIORITY field is not " ..
"a valid integer number, got: %q.\n", plugin, tostring(handler.PRIORITY))
end
local v = valid_version(handler.VERSION)
if v then
handler.VERSION = v -- update to cleaned version string
else
return nil, fmt(
"Plugin %q cannot be loaded because its VERSION field does not " ..
"follow the \"x.y.z\" format, got: %q.\n", plugin, tostring(handler.VERSION))
end
end
if implements(handler, "response") and
(implements(handler, "header_filter") or implements(handler, "body_filter"))
then
return nil, fmt(
"Plugin %q can't be loaded because it implements both `response` " ..
"and `header_filter` or `body_filter` methods.\n", plugin)
end
return handler
end
end
local function load_plugin_entity_strategy(schema, db, plugin)
local Strategy = require(fmt("kong.db.strategies.%s", db.strategy))
local strategy, err = Strategy.new(db.connector, schema, db.errors)
if not strategy then
return nil, err
end
local custom_strat = fmt("kong.plugins.%s.strategies.%s.%s",
plugin, db.strategy, schema.name)
local exists, mod = load_module_if_exists(custom_strat)
if exists and mod then
local parent_mt = getmetatable(strategy)
local mt = {
__index = function(t, k)
-- explicit parent
if k == "super" then
return parent_mt
end
-- override
local f = mod[k]
if f then
return f
end
-- parent fallback
return parent_mt[k]
end
}
setmetatable(strategy, mt)
end
db.strategies[schema.name] = strategy
local dao, err = DAO.new(db, schema, strategy, db.errors)
if not dao then
return nil, err
end
db.daos[schema.name] = dao
end
local function plugin_entity_loader(db)
return function(plugin, schema_def)
ngx_log(ngx_DEBUG, fmt("Loading custom plugin entity: '%s.%s'", plugin, schema_def.name))
local schema, err = plugin_loader.load_entity_schema(plugin, schema_def, db.errors)
if not schema then
return nil, err
end
load_plugin_entity_strategy(schema, db, plugin)
end
end
local function load_plugin(self, plugin)
local db = self.db
if constants.DEPRECATED_PLUGINS[plugin] then
ngx_log(ngx_WARN, "plugin '", plugin, "' has been deprecated")
end
local handler, err = load_plugin_handler(plugin)
if not handler then
return nil, err
end
local schema, err = plugin_loader.load_subschema(self.schema, plugin, db.errors)
if err then
return nil, err
end
for _, field in ipairs(schema.fields) do
if field.consumer and field.consumer.eq == null then
handler.no_consumer = true
end
if field.route and field.route.eq == null then
handler.no_route = true
end
if field.service and field.service.eq == null then
handler.no_service = true
end
end
ngx_log(ngx_DEBUG, "Loading plugin: ", plugin)
if db.strategy then -- skip during tests
local _, err = plugin_loader.load_entities(plugin, db.errors,
plugin_entity_loader(db))
if err then
return nil, err
end
end
return handler
end
--- Load subschemas for all configured plugins into the Plugins entity. It has two side effects:
-- * It makes the Plugin sub-schemas available for the rest of the application
-- * It initializes the Plugin.
-- @param plugin_set a set of plugin names.
-- @return true if success, or nil and an error message.
function Plugins:load_plugin_schemas(plugin_set)
self.handlers = nil
local go_plugins_cnt = 0
local handlers = {}
local errs
-- load installed plugins
for plugin in pairs(plugin_set) do
local handler, err = load_plugin(self, plugin)
if handler then
if handler._go then
go_plugins_cnt = go_plugins_cnt + 1
end
handlers[plugin] = handler
else
errs = errs or {}
table.insert(errs, "on plugin '" .. plugin .. "': " .. tostring(err))
end
end
if errs then
return nil, "error loading plugin schemas: " .. table.concat(errs, "; ")
end
reports.add_immutable_value("go_plugins_cnt", go_plugins_cnt)
self.handlers = handlers
return true
end
---
-- Sort by handler priority and check for collisions. In case of a collision
-- sorting will be applied based on the plugin's name.
-- @tparam table plugin table containing `handler` table and a `name` string
-- @tparam table plugin table containing `handler` table and a `name` string
-- @treturn boolean outcome of sorting
local sort_by_handler_priority = function (a, b)
local prio_a = a.handler.PRIORITY or 0
local prio_b = b.handler.PRIORITY or 0
if prio_a == prio_b and not
(prio_a == 0 or prio_b == 0) then
return a.name > b.name
end
return prio_a > prio_b
end
-- Requires Plugins:load_plugin_schemas to be loaded first
-- @return an array where each element has the format { name = "keyauth", handler = function() .. end }. Or nil, error
function Plugins:get_handlers()
if not self.handlers then
return nil, "Please invoke Plugins:load_plugin_schemas() before invoking Plugins:get_plugin_handlers"
end
local list = {}
local len = 0
for name, handler in pairs(self.handlers) do
len = len + 1
list[len] = { name = name, handler = handler }
end
table.sort(list, sort_by_handler_priority)
return list
end
-- @ca_id: the id of ca certificate to be searched
-- @limit: the maximum number of entities to return (must >= 0)
-- @plugin_names: the plugin names to filter the entities (must be of type table, string or nil)
-- @return an array of the plugin entity
function Plugins:select_by_ca_certificate(ca_id, limit, plugin_names)
local param_type = type(plugin_names)
if param_type ~= "table" and param_type ~= "string" and param_type ~= "nil" then
return nil, "parameter `plugin_names` must be of type table, string, or nil"
end
local plugins, err = self.strategy:select_by_ca_certificate(ca_id, limit, plugin_names)
if err then
return nil, err
end
return self:rows_to_entities(plugins), nil
end
return Plugins