mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 20:18:14 +00:00
dd8eefe3f9
The previous fix had a subtle bug: JavaScript && short-circuits, so crypto.timingSafeEqual() was skipped when buffer lengths differed (sameLength was false). The dummy buffer was allocated but never compared, leaving the original length-based timing leak intact. Now timingSafeEqual is eagerly assigned to a const before the && guard, guaranteeing it runs in constant time on every auth attempt regardless of whether the length guess was correct.
117 lines
3.8 KiB
JavaScript
117 lines
3.8 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
|
|
},
|
|
denied: {
|
|
connections: rateLimitDenied.connections || 0,
|
|
events: rateLimitDenied.events || 0,
|
|
health: rateLimitDenied.health || 0,
|
|
adminMetricsAuth: rateLimitDenied.adminMetricsAuth || 0,
|
|
roomList: rateLimitDenied.roomList || 0
|
|
}
|
|
},
|
|
memory: {
|
|
rss: mem.rss,
|
|
heapUsed: mem.heapUsed,
|
|
heapTotal: mem.heapTotal
|
|
}
|
|
};
|
|
}
|