Add core modules for RCON Data Bridge: audit log, Base64 codec, JSON parser, config management, logging, and options registry
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
-- Structured audit log: every remote action is logged with its request id,
|
||||
-- operation, result, and timestamp, including rejected requests
|
||||
-- (bad payload, unknown op, rate-limited, etc). Persisted as a bounded
|
||||
-- ring buffer via ModData, plus a console line for live tailing.
|
||||
|
||||
require("RconDataBridge.RDB_Log")
|
||||
|
||||
RDB_AuditLog = {}
|
||||
|
||||
local TAG = "RconDataBridge_AuditLog"
|
||||
local MAX_ENTRIES = 500
|
||||
|
||||
local store
|
||||
|
||||
local function ensureStore()
|
||||
if not store then
|
||||
store = ModData.getOrCreate(TAG)
|
||||
if store.entries == nil then
|
||||
store.entries = {}
|
||||
store.nextIndex = 1
|
||||
end
|
||||
end
|
||||
|
||||
return store
|
||||
end
|
||||
|
||||
-- entry: { id, op, ok, code, ts }
|
||||
function RDB_AuditLog.record(entry)
|
||||
local s = ensureStore()
|
||||
entry.ts = entry.ts or getTimestampMs()
|
||||
if #s.entries < MAX_ENTRIES then
|
||||
s.entries[#s.entries + 1] = entry
|
||||
else
|
||||
s.entries[s.nextIndex] = entry
|
||||
s.nextIndex = (s.nextIndex % MAX_ENTRIES) + 1
|
||||
end
|
||||
|
||||
local resultText = entry.ok and "ok" or ("error:" .. tostring(entry.code))
|
||||
RDB_Log.info(
|
||||
"audit id=" .. tostring(entry.id) ..
|
||||
" op=" .. tostring(entry.op) ..
|
||||
" result=" .. resultText
|
||||
)
|
||||
end
|
||||
|
||||
function RDB_AuditLog.getEntries()
|
||||
return ensureStore().entries
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
---@diagnostic disable: need-check-nil
|
||||
|
||||
-- ModData-backed bridge configuration: rate limits, payload cap, sensitive
|
||||
-- field policy. Defaults are baked in and only written to the ModData table
|
||||
-- the first time it's created.
|
||||
|
||||
require("RconDataBridge.RDB_Constants")
|
||||
|
||||
RDB_Config = {}
|
||||
|
||||
local TAG = "RconDataBridge_Config"
|
||||
|
||||
local DEFAULTS = {
|
||||
maxPayloadBytes = RDB_Constants.MAX_PAYLOAD_BYTES,
|
||||
rateLimitMaxRequests = 10,
|
||||
rateLimitWindowSeconds = 10,
|
||||
includeSensitiveFields = false,
|
||||
}
|
||||
|
||||
local store
|
||||
|
||||
function RDB_Config.init()
|
||||
store = ModData.getOrCreate(TAG)
|
||||
for key, value in pairs(DEFAULTS) do
|
||||
if store[key] == nil then
|
||||
store[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RDB_Config.get(key)
|
||||
if not store then
|
||||
RDB_Config.init()
|
||||
end
|
||||
|
||||
return store[key]
|
||||
end
|
||||
|
||||
function RDB_Config.set(key, value)
|
||||
if not store then
|
||||
RDB_Config.init()
|
||||
end
|
||||
|
||||
store[key] = value
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Thin wrapper around zombie.network.ServerOptions.
|
||||
--
|
||||
-- Registration must do two things for an option to be both settable (via
|
||||
-- changeoption) and visible (via showoptions):
|
||||
-- 1. ServerOptions.instance:addOption(...) -- makes it settable/gettable
|
||||
-- 2. ServerOptions.instance:getPublicOptions():add(name) -- makes it visible
|
||||
-- addOption alone is not enough: showoptions reads a separate publicOptions
|
||||
-- list that addOption never touches.
|
||||
--
|
||||
-- Registration is idempotent: Events.OnServerStarted fires more than once per
|
||||
-- boot. A getOptionByName pre-check alone isn't sufficient to suppress the
|
||||
-- resulting duplicate-name IllegalArgumentException from addOption (observed
|
||||
-- in practice even with the pre-check in place, likely a timing/ordering
|
||||
-- quirk between the two OnServerStarted firings); addOption is wrapped in
|
||||
-- pcall as a second, authoritative line of defense. Runtime state ends up
|
||||
-- correct either way.
|
||||
|
||||
require("RconDataBridge.RDB_Log")
|
||||
|
||||
RDB_OptionsRegistry = {}
|
||||
|
||||
function RDB_OptionsRegistry.register(name, defaultValue, maxLength)
|
||||
local so = ServerOptions.instance
|
||||
if so:getOptionByName(name) ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local opt = TextServerOption.new(so, name, defaultValue, maxLength)
|
||||
local ok = pcall(function() so:addOption(opt) end)
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
|
||||
so:getPublicOptions():add(name)
|
||||
RDB_Log.info("registered option " .. name)
|
||||
end
|
||||
|
||||
function RDB_OptionsRegistry.get(name)
|
||||
return ServerOptions.instance:getOption(name)
|
||||
end
|
||||
|
||||
-- Uses putOption (in-memory only), not changeOption: changeOption also does a
|
||||
-- full saveServerTextFile disk write on every call, which is fine for a real
|
||||
-- RCON-initiated config change but wasteful for our own frequent internal
|
||||
-- publishes (Response, WorldStats, etc. are runtime state, not persisted config).
|
||||
function RDB_OptionsRegistry.set(name, value)
|
||||
ServerOptions.instance:putOption(name, value)
|
||||
end
|
||||
Reference in New Issue
Block a user