local kong_global = require "kong.global" local cjson = require "cjson.safe" local grpc_tools = require "kong.tools.grpc" local pb = require "pb" local lpack = require "lua_pack" local rpc_util = require "kong.runloop.plugin_servers.rpc.util" local ngx = ngx local kong = kong local cjson_encode = cjson.encode local t_unpack = table.unpack -- luacheck: ignore table local st_pack = lpack.pack local st_unpack = lpack.unpack local str_find = string.find local proto_fname = "kong/pluginsocket.proto" local Rpc = {} Rpc.__index = Rpc local pb_unwrap do local structpb_value, structpb_list, structpb_struct function structpb_value(v) if type(v) ~= "table" then return v end if v.list_value then return structpb_list(v.list_value) end if v.struct_value then return structpb_struct(v.struct_value) end return v.bool_value or v.string_value or v.number_value or v.null_value end function structpb_list(l) local out = {} if type(l) == "table" then for i, v in ipairs(l.values or l) do out[i] = structpb_value(v) end end return out end function structpb_struct(struct) if type(struct) ~= "table" then return struct end local out = {} for k, v in pairs(struct.fields or struct) do out[k] = structpb_value(v) end return out end local function unwrap_val(d) return d.v end pb_unwrap = { [".google.protobuf.Empty"] = function() end, [".google.protobuf.Value"] = structpb_value, [".google.protobuf.ListValue"] = function(v) return t_unpack(structpb_list(v)) end, [".google.protobuf.Struct"] = structpb_struct, [".kong_plugin_protocol.Bool"] = unwrap_val, [".kong_plugin_protocol.Number"] = unwrap_val, [".kong_plugin_protocol.Int"] = unwrap_val, [".kong_plugin_protocol.String"] = unwrap_val, [".kong_plugin_protocol.ByteString"] = unwrap_val, [".kong_plugin_protocol.KV"] = function(d) return d.k, structpb_value(d.v) end, [".kong_plugin_protocol.Target"] = function(d) return d.host, d.port end, [".kong_plugin_protocol.ExitArgs"] = function (d) return d.status, d.body, structpb_struct(d.headers) end, [".kong_plugin_protocol.ConsumerSpec"] = function (d) return d.id, d.by_username end, [".kong_plugin_protocol.AuthenticateArgs"] = function (d) return d.consumer, d.credential end, } end local pb_wrap do local structpb_value, structpb_list, structpb_struct function structpb_value(v) local t = type(v) local bool_v = nil if t == "boolean" then bool_v = v end local list_v = nil local struct_v = nil if t == "table" then if v[1] ~= nil then list_v = structpb_list(v) else struct_v = structpb_struct(v) end end return { null_value = t == "nil" and 1 or nil, bool_value = bool_v, number_value = t == "number" and v or nil, string_value = t == "string" and v or nil, list_value = list_v, struct_value = struct_v, } end function structpb_list(l) local out = {} for i, v in ipairs(l) do out[i] = structpb_value(v) end return { values = out } end function structpb_struct(d) local out = {} for k, v in pairs(d) do out[k] = structpb_value(v) end return { fields = out } end local function wrap_val(v) return { v = v } end pb_wrap = { [".google.protobuf.Value"] = structpb_value, [".google.protobuf.ListValue"] = structpb_list, [".google.protobuf.Struct"] = structpb_struct, [".kong_plugin_protocol.Bool"] = wrap_val, [".kong_plugin_protocol.Number"] = wrap_val, [".kong_plugin_protocol.Int"] = wrap_val, [".kong_plugin_protocol.String"] = wrap_val, [".kong_plugin_protocol.ByteString"] = wrap_val, [".kong_plugin_protocol.RawBodyResult"] = function(v, err) if type(v) == "string" then return { content = v } end local path = ngx.req.get_body_file() if path then return { body_filepath = path } end return { error = err or "Can't read request body" } end, --[".kong_plugin_protocol.MemoryStats"] = - function(v) -- return { -- lua_shared_dicts = { -- -- } -- } --end } end local function load_service(pdk) local p = grpc_tools.new() local protoc_instance = p.protoc_instance protoc_instance:loadfile(proto_fname) local parsed = protoc_instance:parsefile(proto_fname) local service = {} for i, s in ipairs(parsed.service) do for j, m in ipairs(s.method) do local method_name = s.name .. '.' .. m.name local lower_name = m.options and m.options.MethodName or method_name :gsub('_', '.') :gsub('([a-z]%d*)([A-Z])', '%1_%2') :lower() service[lower_name] = { method_name = method_name, method = rpc_util.index_table(pdk, lower_name), input_type = m.input_type, output_type = m.output_type, } --print(("service[%q] = %s"):format(lower_name, pp(service[lower_name]))) end end return service end local rpc_service local function identity_function(x) return x end local function call_pdk(pdk, method_name, arg) local method = rpc_service[method_name] if not method then return nil, ("method %q not found"):format(method_name) end local saved = pdk.get_saved_req_data() if saved and saved.plugin_name then kong_global.set_namespaced_log(kong, saved.plugin_name) end arg = assert(pb.decode(method.input_type, arg)) local unwrap = pb_unwrap[method.input_type] or identity_function local wrap = pb_wrap[method.output_type] or identity_function local reply = wrap(method.method(unwrap(arg))) if reply == nil then --kong.log.debug("no reply") return "" end reply = assert(pb.encode(method.output_type, reply)) return reply end local function read_frame(c) --kong.log.debug("reading frame...") local msg, err = c:receive(4) -- uint32 if not msg then return nil, err end local _, msg_len = st_unpack(msg, "I") --kong.log.debug("len: ", msg_len) msg, err = c:receive(msg_len) if not msg then return nil, err end --kong.log.debug(("data: %q"):format(msg)) return msg, nil end local function write_frame(c, msg) assert (c:send(st_pack("I", #msg))) assert (c:send(msg)) end function Rpc:call(method, data, do_bridge_loop) self.msg_id = self.msg_id + 1 local msg_id = self.msg_id local c, err = ngx.socket.connect("unix:" .. self.plugin.server_def.socket) if not c then kong.log.err("trying to connect: ", err) return nil, err end msg_id = msg_id + 1 --kong.log.debug("will encode: ", pp{sequence = msg_id, [method] = data}) local msg, err = assert(pb.encode(".kong_plugin_protocol.RpcCall", { -- luacheck: ignore err sequence = msg_id, [method] = data, })) --kong.log.debug("encoded len: ", #msg) assert (c:send(st_pack("I", #msg))) assert (c:send(msg)) while do_bridge_loop do local method_name method_name, err = read_frame(c) if not method_name then return nil, err end if method_name == "" then break end --kong.log.debug(("pdk method: %q (%d)"):format(method_name, #method_name)) local args args, err = read_frame(c) if not args then return nil, err end local reply reply, err = call_pdk(self.plugin.exposed_api, method_name, args) if not reply then return nil, err end err = write_frame(c, reply) if err then return nil, err end end msg, err = read_frame(c) if not msg then return nil, err end c:setkeepalive() msg = assert(pb.decode(".kong_plugin_protocol.RpcReturn", msg)) --kong.log.debug("decoded: "..pp(msg)) assert(msg.sequence == msg_id) return msg end function Rpc:call_start_instance(plugin_name, conf) local status, err = self:call("cmd_start_instance", { name = plugin_name, config = cjson_encode(conf) }) if status == nil then return nil, err end kong.log.debug("started plugin server: seq ", conf.__seq__, ", worker ", ngx.worker.id(), ", instance id ", status.instance_status.instance_id) return { id = status.instance_status.instance_id, conf = conf, seq = conf.__seq__, Config = status.instance_status.config, rpc = self, } end function Rpc:call_close_instance(instance_id) return self:call("cmd_close_instance", { instance_id = instance_id, }) end function Rpc:handle_event(conf, phase) local plugin_name = self.plugin.name local instance_id, err = self.plugin.instance_callbacks.get_instance_id(self.plugin, conf) local res if not err then res, err = self:call("cmd_handle_event", { instance_id = instance_id, event_name = phase, }, true) end if not res or res == "" then local err_lowered = err and err:lower() or "unknown error" if str_find(err_lowered, "no plugin instance", nil, true) or str_find(err_lowered, "closed", nil, true) then self.plugin.instance_callbacks.reset_instance(plugin_name, conf) kong.log.warn(err) return self:handle_event(conf, phase) else kong.log.err("pluginserver error: ", err or "unknown error") kong.response.error(500) end end end local function new(plugin) if not rpc_service then rpc_service = load_service(plugin.exposed_api) end local self = setmetatable({ msg_id = 0, plugin = plugin, }, Rpc) return self end return { new = new, }