Add new RCON operations for audit log retrieval and bridge status reporting, extend rate limit tracking, and improve response handling

This commit is contained in:
2026-08-27 10:28:52 +02:00
parent 854c19dca7
commit 439105ec60
6 changed files with 116 additions and 6 deletions

View File

@@ -0,0 +1,35 @@
-- Returns the most recent audit entries, newest first. RDB_AuditLog.getEntries()
-- is a ring buffer in slot order, not chronological order once it's wrapped
-- past its capacity, so entries are sorted by ts before slicing.
--
-- Registration deferred to register(), called from RDB_Bootstrap's
-- onServerStarted, see RDB_OpWorldGetStats.lua.
RDB_OpAuditLogGet = {}
local DEFAULT_LIMIT = 50
local MAX_LIMIT = 200
function RDB_OpAuditLogGet.register()
RDB_OpRegistry.register("get_audit_log", {
handler = function(args)
local limit = DEFAULT_LIMIT
if type(args.limit) == "number" then
limit = math.min(math.max(1, math.floor(args.limit)), MAX_LIMIT)
end
local entries = {}
for _, entry in ipairs(RDB_AuditLog.getEntries()) do
entries[#entries + 1] = entry
end
table.sort(entries, function(a, b) return (a.ts or 0) > (b.ts or 0) end)
local page = {}
for i = 1, math.min(limit, #entries) do
page[#page + 1] = entries[i]
end
return true, { entries = page }
end,
})
end

View File

@@ -0,0 +1,21 @@
-- Registration deferred to register(), called from RDB_Bootstrap's
-- onServerStarted, see RDB_OpWorldGetStats.lua.
RDB_OpBridgeStatus = {}
function RDB_OpBridgeStatus.register()
RDB_OpRegistry.register("get_bridge_status", {
handler = function(_)
local bootedAt = RDB_Bootstrap.getBootTimestamp()
local now = getTimestampMs()
return true, {
version = RDB_Constants.PROTOCOL_VERSION,
bootedAt = bootedAt,
uptimeSeconds = (now - bootedAt) / 1000,
rateLimit = RDB_Security.getRateLimitStatus(),
config = RDB_Config.getAll(),
}
end,
})
end

View File

@@ -12,6 +12,16 @@ require("RconDataBridge.Ops.RDB_OpWorldGetStats")
require("RconDataBridge.Ops.RDB_OpPlayerGet") require("RconDataBridge.Ops.RDB_OpPlayerGet")
require("RconDataBridge.Ops.RDB_OpPlayerList") require("RconDataBridge.Ops.RDB_OpPlayerList")
require("RconDataBridge.Ops.RDB_OpServerSave") require("RconDataBridge.Ops.RDB_OpServerSave")
require("RconDataBridge.Ops.RDB_OpBridgeStatus")
require("RconDataBridge.Ops.RDB_OpAuditLogGet")
RDB_Bootstrap = {}
local bootTimestamp
function RDB_Bootstrap.getBootTimestamp()
return bootTimestamp
end
local function runSelfTests() local function runSelfTests()
if not RDB_Constants.DEBUG then if not RDB_Constants.DEBUG then
@@ -47,6 +57,8 @@ local function registerOps()
RDB_OpPlayerGet.register() RDB_OpPlayerGet.register()
RDB_OpPlayerList.register() RDB_OpPlayerList.register()
RDB_OpServerSave.register() RDB_OpServerSave.register()
RDB_OpBridgeStatus.register()
RDB_OpAuditLogGet.register()
end end
local function publishProtocolVersion() local function publishProtocolVersion()
@@ -59,6 +71,9 @@ local function publishProtocolVersion()
end end
local function onServerStarted() local function onServerStarted()
if not bootTimestamp then
bootTimestamp = getTimestampMs()
end
runSelfTests() runSelfTests()
RDB_Config.init() RDB_Config.init()
registerBaseOptions() registerBaseOptions()

View File

@@ -43,3 +43,16 @@ function RDB_Config.set(key, value)
store[key] = value store[key] = value
end end
-- Shallow copy, so a caller can't mutate live config through the returned table.
function RDB_Config.getAll()
if not store then
RDB_Config.init()
end
local copy = {}
for key, value in pairs(store) do
copy[key] = value
end
return copy
end

View File

@@ -92,7 +92,12 @@ local function processRequest(rawBase64)
if RDB_Idempotency.has(id) then if RDB_Idempotency.has(id) then
local cached = RDB_Idempotency.get(id) local cached = RDB_Idempotency.get(id)
publishResponse(cached) publishResponse(cached)
audit(id, op, cached.ok, cached.ok and nil or (cached.error and cached.error.code))
local cachedErrCode = nil
if not cached.ok then
cachedErrCode = cached.error and cached.error.code
end
audit(id, op, cached.ok, cachedErrCode)
return return
end end
@@ -114,7 +119,12 @@ local function processRequest(rawBase64)
RDB_Idempotency.put(id, response) RDB_Idempotency.put(id, response)
publishResponse(response) publishResponse(response)
audit(id, op, dispatchOk, dispatchOk and nil or dataOrCode)
local errCode = nil
if not dispatchOk then
errCode = dataOrCode
end
audit(id, op, dispatchOk, errCode)
end end
local function onTick() local function onTick()

View File

@@ -41,10 +41,12 @@ end
local requestTimestamps = {} local requestTimestamps = {}
function RDB_Security.checkRateLimit() -- Drops timestamps that have aged out of the current window and returns how
-- many remain. Shared by checkRateLimit (which then appends the new request)
-- and getRateLimitStatus (which only observes).
local function pruneAndCount()
local now = getTimestampMs() local now = getTimestampMs()
local windowMs = RDB_Config.get("rateLimitWindowSeconds") * 1000 local windowMs = RDB_Config.get("rateLimitWindowSeconds") * 1000
local maxRequests = RDB_Config.get("rateLimitMaxRequests")
local kept = {} local kept = {}
for _, ts in ipairs(requestTimestamps) do for _, ts in ipairs(requestTimestamps) do
@@ -54,10 +56,24 @@ function RDB_Security.checkRateLimit()
end end
requestTimestamps = kept requestTimestamps = kept
if #requestTimestamps >= maxRequests then return #requestTimestamps
end
function RDB_Security.checkRateLimit()
local maxRequests = RDB_Config.get("rateLimitMaxRequests")
if pruneAndCount() >= maxRequests then
return false, RDB_Constants.ERROR_CODES.RATE_LIMITED, "Too many requests, slow down" return false, RDB_Constants.ERROR_CODES.RATE_LIMITED, "Too many requests, slow down"
end end
requestTimestamps[#requestTimestamps + 1] = now requestTimestamps[#requestTimestamps + 1] = getTimestampMs()
return true return true
end end
function RDB_Security.getRateLimitStatus()
return {
currentWindowRequests = pruneAndCount(),
maxRequests = RDB_Config.get("rateLimitMaxRequests"),
windowSeconds = RDB_Config.get("rateLimitWindowSeconds"),
}
end