347 lines
9.5 KiB
Lua
347 lines
9.5 KiB
Lua
local cjson = require "cjson.safe"
|
|
local ngx_ssl = require "ngx.ssl"
|
|
local clone = require "table.clone"
|
|
local rpc = require "kong.runloop.plugin_servers.rpc"
|
|
|
|
local type = type
|
|
local ngx_sleep = ngx.sleep
|
|
local ngx_var = ngx.var
|
|
local cjson_encode = cjson.encode
|
|
local ipairs = ipairs
|
|
local coroutine_running = coroutine.running
|
|
local get_ctx_table = require("resty.core.ctx").get_ctx_table
|
|
local native_timer_at = _G.native_timer_at or ngx.timer.at
|
|
|
|
--- currently running plugin instances
|
|
local running_instances = {}
|
|
|
|
local req_start_time
|
|
local req_get_headers
|
|
local resp_get_headers
|
|
|
|
if ngx.config.subsystem == "http" then
|
|
req_start_time = ngx.req.start_time
|
|
req_get_headers = ngx.req.get_headers
|
|
resp_get_headers = ngx.resp.get_headers
|
|
|
|
else
|
|
local NOOP = function() end
|
|
|
|
req_start_time = NOOP
|
|
req_get_headers = NOOP
|
|
resp_get_headers = NOOP
|
|
end
|
|
|
|
--- keep request data a bit longer, into the log timer
|
|
local req_data = {}
|
|
|
|
local function get_saved_req_data()
|
|
return req_data[coroutine_running()]
|
|
end
|
|
|
|
local exposed_api = {
|
|
kong = kong,
|
|
|
|
get_saved_req_data = get_saved_req_data,
|
|
|
|
["kong.log.serialize"] = function()
|
|
local saved = get_saved_req_data()
|
|
return cjson_encode(saved and saved.serialize_data or kong.log.serialize())
|
|
end,
|
|
|
|
["kong.nginx.get_var"] = function(v)
|
|
return ngx_var[v]
|
|
end,
|
|
|
|
["kong.nginx.get_tls1_version_str"] = ngx_ssl.get_tls1_version_str,
|
|
|
|
["kong.nginx.get_ctx"] = function(k)
|
|
local saved = get_saved_req_data()
|
|
local ngx_ctx = saved and saved.ngx_ctx or ngx.ctx
|
|
return ngx_ctx[k]
|
|
end,
|
|
|
|
["kong.nginx.set_ctx"] = function(k, v)
|
|
local saved = get_saved_req_data()
|
|
local ngx_ctx = saved and saved.ngx_ctx or ngx.ctx
|
|
ngx_ctx[k] = v
|
|
end,
|
|
|
|
["kong.ctx.shared.get"] = function(k)
|
|
local saved = get_saved_req_data()
|
|
local ctx_shared = saved and saved.ctx_shared or kong.ctx.shared
|
|
return ctx_shared[k]
|
|
end,
|
|
|
|
["kong.ctx.shared.set"] = function(k, v)
|
|
local saved = get_saved_req_data()
|
|
local ctx_shared = saved and saved.ctx_shared or kong.ctx.shared
|
|
ctx_shared[k] = v
|
|
end,
|
|
|
|
["kong.request.get_headers"] = function(max)
|
|
local saved = get_saved_req_data()
|
|
return saved and saved.request_headers or kong.request.get_headers(max)
|
|
end,
|
|
|
|
["kong.request.get_header"] = function(name)
|
|
local saved = get_saved_req_data()
|
|
if not saved then
|
|
return kong.request.get_header(name)
|
|
end
|
|
|
|
local header_value = saved.request_headers[name]
|
|
if type(header_value) == "table" then
|
|
header_value = header_value[1]
|
|
end
|
|
|
|
return header_value
|
|
end,
|
|
|
|
["kong.request.get_uri_captures"] = function()
|
|
local saved = get_saved_req_data()
|
|
local ngx_ctx = saved and saved.ngx_ctx or ngx.ctx
|
|
return kong.request.get_uri_captures(ngx_ctx)
|
|
end,
|
|
|
|
["kong.response.get_status"] = function()
|
|
local saved = get_saved_req_data()
|
|
return saved and saved.response_status or kong.response.get_status()
|
|
end,
|
|
|
|
["kong.response.get_headers"] = function(max)
|
|
local saved = get_saved_req_data()
|
|
return saved and saved.response_headers or kong.response.get_headers(max)
|
|
end,
|
|
|
|
["kong.response.get_header"] = function(name)
|
|
local saved = get_saved_req_data()
|
|
if not saved then
|
|
return kong.response.get_header(name)
|
|
end
|
|
|
|
local header_value = saved.response_headers and saved.response_headers[name]
|
|
if type(header_value) == "table" then
|
|
header_value = header_value[1]
|
|
end
|
|
|
|
return header_value
|
|
end,
|
|
|
|
["kong.response.get_source"] = function()
|
|
local saved = get_saved_req_data()
|
|
return kong.response.get_source(saved and saved.ngx_ctx or nil)
|
|
end,
|
|
|
|
["kong.nginx.req_start_time"] = function()
|
|
local saved = get_saved_req_data()
|
|
return saved and saved.req_start_time or req_start_time()
|
|
end,
|
|
}
|
|
|
|
|
|
--- Phase closures
|
|
local function build_phases(plugin)
|
|
if not plugin then
|
|
return
|
|
end
|
|
|
|
for _, phase in ipairs(plugin.phases) do
|
|
if phase == "log" then
|
|
plugin[phase] = function(self, conf)
|
|
native_timer_at(0, function(premature, saved)
|
|
if premature then
|
|
return
|
|
end
|
|
get_ctx_table(saved.ngx_ctx)
|
|
local co = coroutine_running()
|
|
req_data[co] = saved
|
|
plugin.rpc:handle_event(conf, phase)
|
|
req_data[co] = nil
|
|
end, {
|
|
plugin_name = self.name,
|
|
serialize_data = kong.log.serialize(),
|
|
ngx_ctx = clone(ngx.ctx),
|
|
ctx_shared = kong.ctx.shared,
|
|
request_headers = req_get_headers(),
|
|
response_headers = resp_get_headers(),
|
|
response_status = ngx.status,
|
|
req_start_time = req_start_time(),
|
|
})
|
|
end
|
|
|
|
else
|
|
plugin[phase] = function(self, conf)
|
|
plugin.rpc:handle_event(conf, phase)
|
|
end
|
|
end
|
|
end
|
|
|
|
return plugin
|
|
end
|
|
|
|
--- handle notifications from pluginservers
|
|
local rpc_notifications = {}
|
|
|
|
--- serverPid notification sent by the pluginserver. if it changes,
|
|
--- all instances tied to this RPC socket should be restarted.
|
|
function rpc_notifications:serverPid(n)
|
|
n = tonumber(n)
|
|
if self.pluginserver_pid and n ~= self.pluginserver_pid then
|
|
for key, instance in pairs(running_instances) do
|
|
if instance.rpc == self then
|
|
running_instances[key] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
self.pluginserver_pid = n
|
|
end
|
|
|
|
local function reset_instances_for_plugin(plugin_name)
|
|
for k, instance in pairs(running_instances) do
|
|
if instance.plugin_name == plugin_name then
|
|
running_instances[k] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
--- reset_instance: removes an instance from the table.
|
|
local function reset_instance(plugin_name, conf)
|
|
--
|
|
-- the same plugin (which acts as a plugin server) is shared among
|
|
-- instances of the plugin; for example, the same plugin can be applied
|
|
-- to many routes
|
|
-- `reset_instance` is called when (but not only) the plugin server died;
|
|
-- in such case, all associated instances must be removed, not only the current
|
|
--
|
|
reset_instances_for_plugin(plugin_name)
|
|
|
|
local ok, err = kong.worker_events.post("plugin_server", "reset_instances", { plugin_name = plugin_name })
|
|
if not ok then
|
|
kong.log.err("failed to post plugin_server reset_instances event: ", err)
|
|
end
|
|
end
|
|
|
|
local get_instance_id
|
|
|
|
do
|
|
local SLEEP_STEP = 0.1
|
|
local WAIT_TIME = 10
|
|
local MAX_WAIT_STEPS = WAIT_TIME / SLEEP_STEP
|
|
|
|
--- get_instance_id: gets an ID to reference a plugin instance running in the
|
|
--- pluginserver; each configuration of a plugin is handled by a different
|
|
--- instance. Biggest complexity here is due to the remote (and thus non-atomic
|
|
--- and fallible) operation of starting the instance at the server.
|
|
function get_instance_id(plugin, conf)
|
|
local plugin_name = plugin.name
|
|
|
|
local key = kong.plugin.get_id()
|
|
local instance_info = running_instances[key]
|
|
|
|
local wait_count = 0
|
|
while instance_info and not instance_info.id do
|
|
-- some other thread is already starting an instance
|
|
-- prevent busy-waiting
|
|
ngx_sleep(SLEEP_STEP)
|
|
|
|
-- to prevent a potential dead loop when someone failed to release the ID
|
|
wait_count = wait_count + 1
|
|
if wait_count > MAX_WAIT_STEPS then
|
|
running_instances[key] = nil
|
|
return nil, "Could not claim instance_id for " .. plugin_name .. " (key: " .. key .. ")"
|
|
end
|
|
instance_info = running_instances[key]
|
|
end
|
|
|
|
if instance_info
|
|
and instance_info.id
|
|
and instance_info.seq == conf.__seq__
|
|
and instance_info.conf and instance_info.conf.__plugin_id == key
|
|
then
|
|
-- exact match, return it
|
|
return instance_info.id
|
|
end
|
|
|
|
local old_instance_id = instance_info and instance_info.id
|
|
if not instance_info then
|
|
-- we're the first, put something to claim
|
|
instance_info = {
|
|
conf = conf,
|
|
seq = conf.__seq__,
|
|
}
|
|
running_instances[key] = instance_info
|
|
else
|
|
|
|
-- there already was something, make it evident that we're changing it
|
|
instance_info.id = nil
|
|
end
|
|
|
|
local new_instance_info, err = plugin.rpc:call_start_instance(plugin_name, conf)
|
|
if new_instance_info == nil then
|
|
kong.log.err("starting instance: ", err)
|
|
-- remove claim, some other thread might succeed
|
|
running_instances[key] = nil
|
|
error(err)
|
|
end
|
|
|
|
instance_info.id = new_instance_info.id
|
|
instance_info.plugin_name = plugin_name
|
|
instance_info.conf = new_instance_info.conf
|
|
instance_info.seq = new_instance_info.seq
|
|
instance_info.Config = new_instance_info.Config
|
|
instance_info.rpc = new_instance_info.rpc
|
|
|
|
if old_instance_id then
|
|
-- there was a previous instance with same key, close it
|
|
plugin.rpc:call_close_instance(old_instance_id)
|
|
-- don't care if there's an error, maybe other thread closed it first.
|
|
end
|
|
|
|
return instance_info.id
|
|
end
|
|
end
|
|
|
|
--
|
|
-- instance callbacks manage the state of a plugin instance
|
|
-- - get_instance_id (which also starts and instance)
|
|
-- - reset_instance, which removes an instance from the local cache
|
|
--
|
|
local instance_callbacks = {
|
|
reset_instance = reset_instance,
|
|
get_instance_id = get_instance_id,
|
|
}
|
|
|
|
local function new(plugin_info)
|
|
--
|
|
-- plugin_info
|
|
-- * name
|
|
-- * priority
|
|
-- * version
|
|
-- * schema
|
|
-- * phases
|
|
-- * server_def
|
|
--
|
|
|
|
local self = build_phases(plugin_info)
|
|
self.instance_callbacks = instance_callbacks
|
|
self.exposed_api = exposed_api
|
|
self.rpc_notifications = rpc_notifications
|
|
|
|
local plugin_rpc, err = rpc.new(self)
|
|
if not rpc then
|
|
return nil, err
|
|
end
|
|
|
|
self.rpc = plugin_rpc
|
|
|
|
return self
|
|
end
|
|
|
|
|
|
return {
|
|
new = new,
|
|
reset_instances_for_plugin = reset_instances_for_plugin,
|
|
}
|