Files
2026-07-13 12:32:21 +08:00

127 lines
4.2 KiB
Lua

--- This plugin serves as a bridge for debugging RPC calls, allowing to incercept and manipulate the calls
-- debugging is supported by a set of RPC calls,
-- CP side:
-- 1. kong.rpc.debug.register: registers self as the debugger of the CP
-- params: nil
-- returns: true
-- 2. kong.rpc.debug.call: let CP to call a method on a node
-- returns: { result = "result", error = "error" }
-- params: { node_id = "node_id", method = "method", args = { ... } }
-- 3. kong.rpc.debug.lua_code: let CP to execute lua code on a node
-- params: "lua code"
-- returns: the return value of the lua code
--
--- debugger side:
-- 1. kong.rpc.debug.call_handler: the debugger will receive a call from the CP when a hooked API is called
-- params: { call_seq = "call_seq", method = "method", node_id = "node_id", payload = { ... } }
-- the debugger can return 2 types of responses:
-- a. { mock = true, result = "result", error = "error" }, if the API is mocked
-- b. { prehook = true/false, posthook = true/false, args = { ... }/nil }, boolean to be true if a prehook/posthook is present, and args to be the manipulated args
-- 2. kong.rpc.debug.call_handler_post: the debugger will receive a call from the CP when a posthook is called
-- params: { call_seq = "call_seq", method = "method", node_id = "node_id", payload = { result = "result", error = "error" } }
-- return: { result = "result", error = "error" }
local kong_meta = require("kong.meta")
local shallow_copy = require("kong.tools.table").shallow_copy
local debugger_prefix = "kong.rpc.debug."
local _M = {
PRIORITY = 1000,
VERSION = kong_meta.version,
}
local function hook(debugger_node_id)
local original_callbacks = shallow_copy(kong.rpc.callbacks.callbacks)
local next_call_seq = 0
for api, cb in pairs(original_callbacks) do
if api:sub(1, #debugger_prefix) == "kong.rpc.debug." then
goto skip
end
kong.log.info("hooking registering RPC proxy API: ", api)
-- re-register
kong.rpc.callbacks.callbacks[api] = nil
kong.rpc.callbacks:register(api, function(node_id, payload)
local call_seq = next_call_seq
next_call_seq = next_call_seq + 1
kong.log.info("hooked proxy API ", api, " called by node: ", node_id)
kong.log.info("forwarding to node: ", node_id)
local res, err = kong.rpc:call(debugger_node_id, "kong.rpc.debug.call_handler", { call_seq = call_seq, method = api, node_id = node_id, payload = payload })
if not res then
return nil, "Failed to call debugger(" .. debugger_node_id .. "): " .. err
end
if res.error then
return nil, res.error
end
-- no prehook/posthook, directly return mock result
if res.mock then
return res.result, res.error
end
if res.prehook then
payload = res.args
end
local call_res, call_err = cb(node_id, payload)
if res.posthook then
res, err = kong.rpc:call(debugger_node_id, "kong.rpc.debug.call_handler_post",
{ call_seq = call_seq, method = api, node_id = node_id, payload = { result = call_res, error = call_err } })
if not res then
return nil, "Failed to call debugger post hook(" .. debugger_node_id .. "): " .. err
end
call_res, call_err = res.result, res.error
end
return call_res, call_err
end)
::skip::
end
end
function _M.init_worker()
local registered
kong.rpc.callbacks:register("kong.rpc.debug.register", function(node_id, register_payload)
if registered then
return nil, "already registered: " .. registered
else
registered = node_id
end
hook(node_id)
return true
end)
kong.rpc.callbacks:register("kong.rpc.debug.call", function(node_id, payload)
if node_id ~= registered then
return nil, "not authorized"
end
local res, err = kong.rpc:call(payload.node_id, payload.method, payload.args)
return {
result = res,
error = err,
}
end)
kong.rpc.callbacks:register("kong.rpc.debug.lua_code", function(node_id, payload)
if node_id ~= registered then
return nil, "not authorized"
end
local code = assert(loadstring(payload))
return code()
end)
end
return _M