Files
2026-07-02 00:14:55 +02:00

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
}
};
}