mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 20:18:14 +00:00
119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
import crypto from 'crypto';
|
|
|
|
export function checkCooldown(cooldowns, key, windowMs, now = Date.now()) {
|
|
const lastAllowedAt = cooldowns.get(key) || 0;
|
|
if (now - lastAllowedAt < windowMs) {
|
|
return false;
|
|
}
|
|
cooldowns.set(key, now);
|
|
return true;
|
|
}
|
|
|
|
export function getCachedPayload(cache, key, ttlMs, buildPayload, now = Date.now()) {
|
|
const cached = cache.get(key);
|
|
if (cached && now - cached.createdAt < ttlMs) {
|
|
return cached.payload;
|
|
}
|
|
|
|
const payload = buildPayload();
|
|
cache.set(key, { createdAt: now, payload });
|
|
return payload;
|
|
}
|
|
|
|
export function isAdminMetricsAuthorized(authHeader, adminToken) {
|
|
if (!adminToken || typeof adminToken !== 'string') return false;
|
|
if (!authHeader || typeof authHeader !== 'string') return false;
|
|
|
|
const prefix = 'Bearer ';
|
|
if (!authHeader.startsWith(prefix)) return false;
|
|
|
|
const provided = authHeader.slice(prefix.length);
|
|
if (!provided) return false;
|
|
|
|
const expectedBuffer = Buffer.from(adminToken);
|
|
const providedBuffer = Buffer.from(provided);
|
|
|
|
// Always run timingSafeEqual to prevent length-based timing leaks.
|
|
// timingSafeEqual throws on different-length buffers, so when lengths
|
|
// differ we compare against a zeroed buffer of the provided length
|
|
// (guaranteed mismatch, constant time).
|
|
// NOTE: timingSafeEqual must be evaluated eagerly (assigned to const)
|
|
// before the && short-circuit, otherwise it's skipped on length mismatch
|
|
// and the timing leak remains.
|
|
const sameLength = expectedBuffer.length === providedBuffer.length;
|
|
const compareBuf = sameLength ? expectedBuffer : Buffer.alloc(providedBuffer.length);
|
|
const equal = crypto.timingSafeEqual(compareBuf, providedBuffer);
|
|
return sameLength && equal;
|
|
}
|
|
|
|
export function isAdminMetricsTokenStrong(adminToken, minLength = 32) {
|
|
return !adminToken || (typeof adminToken === 'string' && adminToken.length >= minLength);
|
|
}
|
|
|
|
export function buildHealthPayload({
|
|
rooms,
|
|
connections,
|
|
includeMetrics = false,
|
|
now = Date.now(),
|
|
uptime = 0,
|
|
memoryUsage = () => process.memoryUsage(),
|
|
rateLimitSizes = {},
|
|
rateLimitDenied = {}
|
|
}) {
|
|
const payload = {
|
|
status: 'ok',
|
|
uptime,
|
|
rooms: rooms.size,
|
|
connections,
|
|
timestamp: now
|
|
};
|
|
|
|
if (!includeMetrics) return payload;
|
|
|
|
let peers = 0;
|
|
let maxPeersInRoom = 0;
|
|
let roomsWithLobby = 0;
|
|
for (const room of rooms.values()) {
|
|
const size = room.peers?.size || 0;
|
|
peers += size;
|
|
if (size > maxPeersInRoom) maxPeersInRoom = size;
|
|
if (room.activeLobby) roomsWithLobby++;
|
|
}
|
|
const avgPeersPerRoom = rooms.size > 0
|
|
? Math.round((peers / rooms.size) * 100) / 100
|
|
: 0;
|
|
const mem = memoryUsage();
|
|
|
|
return {
|
|
...payload,
|
|
peers,
|
|
roomsWithLobby,
|
|
avgPeersPerRoom,
|
|
maxPeersInRoom,
|
|
rateLimits: {
|
|
trackedClients: {
|
|
connections: rateLimitSizes.connections || 0,
|
|
events: rateLimitSizes.events || 0,
|
|
health: rateLimitSizes.health || 0,
|
|
adminMetricsAuth: rateLimitSizes.adminMetricsAuth || 0,
|
|
authFailures: rateLimitSizes.authFailures || 0,
|
|
roomList: rateLimitSizes.roomList || 0,
|
|
leaveRoom: rateLimitSizes.leaveRoom || 0
|
|
},
|
|
denied: {
|
|
connections: rateLimitDenied.connections || 0,
|
|
events: rateLimitDenied.events || 0,
|
|
health: rateLimitDenied.health || 0,
|
|
adminMetricsAuth: rateLimitDenied.adminMetricsAuth || 0,
|
|
roomList: rateLimitDenied.roomList || 0,
|
|
leaveRoom: rateLimitDenied.leaveRoom || 0
|
|
}
|
|
},
|
|
memory: {
|
|
rss: mem.rss,
|
|
heapUsed: mem.heapUsed,
|
|
heapTotal: mem.heapTotal
|
|
}
|
|
};
|
|
}
|