@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
111 lines (110 loc) • 4.42 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.connectToSSE = connectToSSE;
const http_1 = require("http");
const https_1 = require("https");
const promises_1 = require("node:timers/promises");
const parse_1 = require("../../helpers/eventsource-parser/parse");
const isDebuggingSSE_1 = require("../../helpers/isDebuggingSSE");
const getRealtimeURL_1 = require("./getRealtimeURL");
const INITIAL_RECONNECT_MS = 5000;
const MAX_RECONNECT_MS = 60 * 1000;
const STABLE_CONNECTION_MS = 30 * 1000;
const READ_TIMEOUT_MS = 70 * 1000;
function connect({ token, onEvent, readTimeoutMs, logDebug, }) {
return new Promise((resolve) => {
let resolved = false;
function resolveOnce(result) {
if (resolved) {
return;
}
resolved = true;
resolve(result);
}
const url = new URL(`${(0, getRealtimeURL_1.getRealtimeURL)().toString()}api/runtime/stream`);
logDebug(`SSE connecting to ${url.toString()}`);
const requestFn = url.protocol === "https:" ? https_1.request : http_1.request;
const req = requestFn(url.toString(), {
method: "GET",
headers: {
Authorization: token.asString(),
Accept: "text/event-stream",
"Cache-Control": "no-cache",
},
}, (response) => {
const statusCode = response.statusCode;
if (statusCode !== 200) {
response.destroy();
resolveOnce({ outcome: "disconnected", statusCode });
return;
}
logDebug("SSE connected successfully");
const parser = (0, parse_1.createParser)({
onEvent,
});
response.setEncoding("utf-8");
response.on("data", (chunk) => {
logDebug(`SSE received chunk: ${chunk.trimEnd()}`);
parser.feed(chunk);
});
response.on("end", () => {
logDebug("SSE connection closed by server");
parser.reset();
resolveOnce({ outcome: "disconnected", statusCode });
});
response.on("error", (error) => {
logDebug(`SSE stream error: ${error.message}`);
parser.reset();
resolveOnce({ outcome: "disconnected", statusCode });
});
});
req.on("socket", (socket) => {
socket.setTimeout(readTimeoutMs, () => {
if (socket.destroyed) {
return;
}
logDebug("SSE read timeout");
resolveOnce({ outcome: "error" });
req.destroy();
});
socket.unref();
});
req.on("error", (error) => {
logDebug(`SSE connection error: ${error.message}`);
resolveOnce({ outcome: "error" });
});
req.end();
});
}
function connectToSSE({ token, logger, onEvent, initialReconnectMs = INITIAL_RECONNECT_MS, readTimeoutMs = READ_TIMEOUT_MS, }) {
let reconnectMs = initialReconnectMs;
const debugSSE = (0, isDebuggingSSE_1.isDebuggingSSE)();
function logDebug(msg) {
if (debugSSE) {
logger.log(msg);
}
}
async function loop() {
while (true) {
const start = Date.now();
const result = await connect({ token, onEvent, readTimeoutMs, logDebug });
if (result.outcome === "disconnected" &&
(result.statusCode === 401 || result.statusCode === 403)) {
logger.log(`SSE connection rejected with status ${result.statusCode}, stopping`);
return;
}
if (Date.now() - start >= STABLE_CONNECTION_MS) {
reconnectMs = initialReconnectMs;
}
const jitter = Math.random() * (reconnectMs / 2);
const delayMs = reconnectMs + jitter;
logDebug(`SSE scheduling reconnect in ${Math.round(delayMs)}ms`);
reconnectMs = Math.min(reconnectMs * 2, MAX_RECONNECT_MS);
// ref: false so the timer doesn't keep the process alive
await (0, promises_1.setTimeout)(delayMs, undefined, { ref: false });
}
}
loop().catch((error) => {
logger.log(`SSE loop error: ${error.message}`);
});
}