2026-09-15 13:00:32 +02:00
|
|
|
import { Transform } from 'node:stream';
|
2020-02-29 23:01:03 +01:00
|
|
|
/**
|
|
|
|
|
* MessageParser instance is a transform stream that separates message headers
|
|
|
|
|
* from the rest of the body. Headers are emitted with the 'headers' event. Message
|
|
|
|
|
* body is passed on as the resulting stream.
|
|
|
|
|
*/
|
2026-09-15 13:00:32 +02:00
|
|
|
export default class MessageParser extends Transform {
|
2020-02-29 23:01:03 +01:00
|
|
|
constructor(options) {
|
|
|
|
|
super(options);
|
|
|
|
|
this.lastBytes = Buffer.alloc(4);
|
|
|
|
|
this.headersParsed = false;
|
|
|
|
|
this.headerBytes = 0;
|
|
|
|
|
this.headerChunks = [];
|
|
|
|
|
this.rawHeaders = false;
|
|
|
|
|
this.bodySize = 0;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
|
|
|
|
|
*
|
2026-09-15 13:00:32 +02:00
|
|
|
* @param data Next data chunk from the stream
|
2020-02-29 23:01:03 +01:00
|
|
|
*/
|
|
|
|
|
updateLastBytes(data) {
|
2026-04-28 12:50:45 +02:00
|
|
|
const lblen = this.lastBytes.length;
|
|
|
|
|
const nblen = Math.min(data.length, lblen);
|
2020-02-29 23:01:03 +01:00
|
|
|
// shift existing bytes
|
|
|
|
|
for (let i = 0, len = lblen - nblen; i < len; i++) {
|
|
|
|
|
this.lastBytes[i] = this.lastBytes[i + nblen];
|
|
|
|
|
}
|
|
|
|
|
// add new bytes
|
|
|
|
|
for (let i = 1; i <= nblen; i++) {
|
|
|
|
|
this.lastBytes[lblen - i] = data[data.length - i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Finds and removes message headers from the remaining body. We want to keep
|
|
|
|
|
* headers separated until final delivery to be able to modify these
|
|
|
|
|
*
|
2026-09-15 13:00:32 +02:00
|
|
|
* @param data Next chunk of data
|
|
|
|
|
* @return Returns true if headers are already found or false otherwise
|
2020-02-29 23:01:03 +01:00
|
|
|
*/
|
|
|
|
|
checkHeaders(data) {
|
|
|
|
|
if (this.headersParsed) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-28 12:50:45 +02:00
|
|
|
const lblen = this.lastBytes.length;
|
2020-02-29 23:01:03 +01:00
|
|
|
let headerPos = 0;
|
|
|
|
|
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
|
|
|
|
|
let chr;
|
|
|
|
|
if (i < lblen) {
|
|
|
|
|
chr = this.lastBytes[i];
|
2026-09-15 13:00:32 +02:00
|
|
|
}
|
|
|
|
|
else {
|
2020-02-29 23:01:03 +01:00
|
|
|
chr = data[i - lblen];
|
|
|
|
|
}
|
|
|
|
|
if (chr === 0x0a && i) {
|
2026-04-28 12:50:45 +02:00
|
|
|
const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
|
|
|
|
|
const pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
|
2020-02-29 23:01:03 +01:00
|
|
|
if (pr1 === 0x0a) {
|
|
|
|
|
this.headersParsed = true;
|
|
|
|
|
headerPos = i - lblen + 1;
|
|
|
|
|
this.headerBytes += headerPos;
|
|
|
|
|
break;
|
2026-09-15 13:00:32 +02:00
|
|
|
}
|
|
|
|
|
else if (pr1 === 0x0d && pr2 === 0x0a) {
|
2020-02-29 23:01:03 +01:00
|
|
|
this.headersParsed = true;
|
|
|
|
|
headerPos = i - lblen + 1;
|
|
|
|
|
this.headerBytes += headerPos;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this.headersParsed) {
|
|
|
|
|
this.headerChunks.push(data.slice(0, headerPos));
|
|
|
|
|
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
|
|
|
|
|
this.headerChunks = null;
|
|
|
|
|
this.emit('headers', this.parseHeaders());
|
2026-09-15 13:00:32 +02:00
|
|
|
if (data.length > headerPos) {
|
2026-04-28 12:50:45 +02:00
|
|
|
const chunk = data.slice(headerPos);
|
2020-02-29 23:01:03 +01:00
|
|
|
this.bodySize += chunk.length;
|
|
|
|
|
// this would be the first chunk of data sent downstream
|
|
|
|
|
setImmediate(() => this.push(chunk));
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-28 12:50:45 +02:00
|
|
|
this.headerBytes += data.length;
|
|
|
|
|
this.headerChunks.push(data);
|
2020-02-29 23:01:03 +01:00
|
|
|
// store last 4 bytes to catch header break
|
|
|
|
|
this.updateLastBytes(data);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-09-15 13:00:32 +02:00
|
|
|
/** @internal */
|
2020-02-29 23:01:03 +01:00
|
|
|
_transform(chunk, encoding, callback) {
|
|
|
|
|
if (!chunk || !chunk.length) {
|
|
|
|
|
return callback();
|
|
|
|
|
}
|
|
|
|
|
if (typeof chunk === 'string') {
|
|
|
|
|
chunk = Buffer.from(chunk, encoding);
|
|
|
|
|
}
|
|
|
|
|
let headersFound;
|
|
|
|
|
try {
|
|
|
|
|
headersFound = this.checkHeaders(chunk);
|
2026-09-15 13:00:32 +02:00
|
|
|
}
|
|
|
|
|
catch (E) {
|
2020-02-29 23:01:03 +01:00
|
|
|
return callback(E);
|
|
|
|
|
}
|
|
|
|
|
if (headersFound) {
|
|
|
|
|
this.bodySize += chunk.length;
|
|
|
|
|
this.push(chunk);
|
|
|
|
|
}
|
|
|
|
|
setImmediate(callback);
|
|
|
|
|
}
|
2026-09-15 13:00:32 +02:00
|
|
|
/** @internal */
|
2020-02-29 23:01:03 +01:00
|
|
|
_flush(callback) {
|
|
|
|
|
if (this.headerChunks) {
|
2026-09-15 13:00:32 +02:00
|
|
|
// no empty line was seen, so the message consists of headers only
|
|
|
|
|
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
|
2020-02-29 23:01:03 +01:00
|
|
|
this.headerChunks = null;
|
2026-09-15 13:00:32 +02:00
|
|
|
this.emit('headers', this.parseHeaders());
|
2020-02-29 23:01:03 +01:00
|
|
|
}
|
|
|
|
|
callback();
|
|
|
|
|
}
|
|
|
|
|
parseHeaders() {
|
2026-09-15 13:00:32 +02:00
|
|
|
// the header bytes are kept as they are, one character per byte, so the
|
|
|
|
|
// signature covers exactly the bytes the receiving side canonicalizes
|
|
|
|
|
// Only SP and HTAB fold a line, and only they are trimmed from the field name, the
|
|
|
|
|
// same whitespace the relaxed canonicalization in sign.ts works with
|
|
|
|
|
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
2020-02-29 23:01:03 +01:00
|
|
|
for (let i = lines.length - 1; i > 0; i--) {
|
2026-09-15 13:00:32 +02:00
|
|
|
if (/^[ \t]/.test(lines[i])) {
|
2020-02-29 23:01:03 +01:00
|
|
|
lines[i - 1] += '\n' + lines[i];
|
|
|
|
|
lines.splice(i, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return lines
|
2026-09-15 13:00:32 +02:00
|
|
|
.filter(line => /[^ \t\r]/.test(line))
|
2020-02-29 23:01:03 +01:00
|
|
|
.map(line => ({
|
2026-09-15 13:00:32 +02:00
|
|
|
key: line
|
|
|
|
|
.substr(0, line.indexOf(':'))
|
|
|
|
|
.replace(/^[ \t]+|[ \t]+$/g, '')
|
|
|
|
|
.toLowerCase(),
|
|
|
|
|
line
|
|
|
|
|
}));
|
2020-02-29 23:01:03 +01:00
|
|
|
}
|
|
|
|
|
}
|