diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpAuditLogGet.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpAuditLogGet.lua new file mode 100644 index 0000000..39fb5a5 --- /dev/null +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpAuditLogGet.lua @@ -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 diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua new file mode 100644 index 0000000..66a21e8 --- /dev/null +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua @@ -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 diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Bootstrap.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Bootstrap.lua index 02aec97..576a4b9 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Bootstrap.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Bootstrap.lua @@ -12,6 +12,16 @@ require("RconDataBridge.Ops.RDB_OpWorldGetStats") require("RconDataBridge.Ops.RDB_OpPlayerGet") require("RconDataBridge.Ops.RDB_OpPlayerList") 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() if not RDB_Constants.DEBUG then @@ -47,6 +57,8 @@ local function registerOps() RDB_OpPlayerGet.register() RDB_OpPlayerList.register() RDB_OpServerSave.register() + RDB_OpBridgeStatus.register() + RDB_OpAuditLogGet.register() end local function publishProtocolVersion() @@ -59,6 +71,9 @@ local function publishProtocolVersion() end local function onServerStarted() + if not bootTimestamp then + bootTimestamp = getTimestampMs() + end runSelfTests() RDB_Config.init() registerBaseOptions() diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Config.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Config.lua index a8b2cd8..29c542f 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Config.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Config.lua @@ -43,3 +43,16 @@ function RDB_Config.set(key, value) store[key] = value 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 diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua index 8470122..834e778 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua @@ -92,7 +92,12 @@ local function processRequest(rawBase64) if RDB_Idempotency.has(id) then local cached = RDB_Idempotency.get(id) 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 end @@ -114,7 +119,12 @@ local function processRequest(rawBase64) RDB_Idempotency.put(id, 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 local function onTick() diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Security.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Security.lua index d60f00e..ba1ec76 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Security.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Security.lua @@ -41,10 +41,12 @@ end 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 windowMs = RDB_Config.get("rateLimitWindowSeconds") * 1000 - local maxRequests = RDB_Config.get("rateLimitMaxRequests") local kept = {} for _, ts in ipairs(requestTimestamps) do @@ -54,10 +56,24 @@ function RDB_Security.checkRateLimit() end 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" end - requestTimestamps[#requestTimestamps + 1] = now + requestTimestamps[#requestTimestamps + 1] = getTimestampMs() return true end + +function RDB_Security.getRateLimitStatus() + return { + currentWindowRequests = pruneAndCount(), + maxRequests = RDB_Config.get("rateLimitMaxRequests"), + windowSeconds = RDB_Config.get("rateLimitWindowSeconds"), + } +end