Add core modules for RCON Data Bridge: audit log, Base64 codec, JSON parser, config management, logging, and options registry

This commit is contained in:
2026-08-26 14:43:16 +02:00
parent 26001a22fc
commit 96b8e6263e
8 changed files with 691 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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