@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.
140 lines (139 loc) • 4.51 kB
JavaScript
;
// Based on https://github.com/rexxars/eventsource-parser
// MIT License - Copyright (c) 2025 Espen Hovlandsdal
Object.defineProperty(exports, "__esModule", { value: true });
exports.createParser = createParser;
const errors_1 = require("./errors");
function noop(_arg) {
// intentional noop
}
function createParser(callbacks) {
const { onEvent = noop, onError = noop, onRetry = noop, onComment, } = callbacks;
let incompleteLine = "";
let isFirstChunk = true;
let id;
let data = "";
let eventType = "";
function feed(newChunk) {
// Strip any UTF8 byte order mark (BOM) at the start of the stream
const chunk = isFirstChunk
? newChunk.replace(/^\xEF\xBB\xBF/, "")
: newChunk;
const [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`);
for (const line of complete) {
parseLine(line);
}
incompleteLine = incomplete;
isFirstChunk = false;
}
function parseLine(line) {
if (line === "") {
dispatchEvent();
return;
}
if (line.startsWith(":")) {
if (onComment) {
onComment(line.slice(line.startsWith(": ") ? 2 : 1));
}
return;
}
const fieldSeparatorIndex = line.indexOf(":");
if (fieldSeparatorIndex !== -1) {
const field = line.slice(0, fieldSeparatorIndex);
const offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1;
const value = line.slice(fieldSeparatorIndex + offset);
processField(field, value, line);
return;
}
processField(line, "", line);
}
function processField(field, value, line) {
switch (field) {
case "event":
eventType = value;
break;
case "data":
data = `${data}${value}\n`;
break;
case "id":
id = value.includes("\0") ? undefined : value;
break;
case "retry":
if (/^\d+$/.test(value)) {
onRetry(parseInt(value, 10));
}
else {
onError(new errors_1.ParseError(`Invalid \`retry\` value: "${value}"`, {
type: "invalid-retry",
value,
line,
}));
}
break;
default:
onError(new errors_1.ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}…` : field}"`, { type: "unknown-field", field, value, line }));
break;
}
}
function dispatchEvent() {
const shouldDispatch = data.length > 0;
if (shouldDispatch) {
onEvent({
id,
event: eventType || undefined,
data: data.endsWith("\n") ? data.slice(0, -1) : data,
});
}
id = undefined;
data = "";
eventType = "";
}
function reset(options = {}) {
if (incompleteLine && options.consume) {
parseLine(incompleteLine);
}
isFirstChunk = true;
id = undefined;
data = "";
eventType = "";
incompleteLine = "";
}
return { feed, reset };
}
function splitLines(chunk) {
const lines = [];
let incompleteLine = "";
let searchIndex = 0;
while (searchIndex < chunk.length) {
const crIndex = chunk.indexOf("\r", searchIndex);
const lfIndex = chunk.indexOf("\n", searchIndex);
let lineEnd = -1;
if (crIndex !== -1 && lfIndex !== -1) {
lineEnd = Math.min(crIndex, lfIndex);
}
else if (crIndex !== -1) {
if (crIndex === chunk.length - 1) {
lineEnd = -1;
}
else {
lineEnd = crIndex;
}
}
else if (lfIndex !== -1) {
lineEnd = lfIndex;
}
if (lineEnd === -1) {
incompleteLine = chunk.slice(searchIndex);
break;
}
else {
const line = chunk.slice(searchIndex, lineEnd);
lines.push(line);
searchIndex = lineEnd + 1;
if (chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === "\n") {
searchIndex++;
}
}
}
return [lines, incompleteLine];
}