"use strict"; // streams through a message body and calculates relaxed body hash var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_stream_1 = require("node:stream"); const node_crypto_1 = __importDefault(require("node:crypto")); const CHAR_CR = 0x0d; const CHAR_LF = 0x0a; const CHAR_SPACE = 0x20; const CHAR_TAB = 0x09; const CRLF = Buffer.from('\r\n'); // a run of empty lines is hashed from this buffer in slices const EMPTY_LINES = Buffer.alloc(4096, CRLF); /** * Passes the message body through unchanged and hashes its relaxed * canonicalization (RFC 6376 section 3.4.4) on the side: whitespace at the end * of a line is dropped, runs of whitespace within a line become a single space, * every line ends with CRLF, empty lines at the end of the body are ignored and * a non-empty body always ends with CRLF. Bytes are canonicalized as they arrive, * so a line of any length costs constant memory. */ class RelaxedBody extends node_stream_1.Transform { constructor(options) { super(); options = options || {}; this.bodyHash = node_crypto_1.default.createHash(options.hashAlgo || 'sha256'); this.byteLength = 0; this.debug = options.debug; this._debugBody = options.debug ? [] : false; this._lineHasContent = false; this._pendingWsp = false; this._pendingCr = false; this._pendingEmptyLines = 0; } /** @internal */ _hashCanonical(data) { if (!data.length) { return; } this.bodyHash.update(data); if (this._debugBody) { this._debugBody.push(Buffer.from(data)); } } /** @internal */ _hashEmptyLines() { while (this._pendingEmptyLines > 0) { const count = Math.min(this._pendingEmptyLines, EMPTY_LINES.length / 2); this._hashCanonical(EMPTY_LINES.subarray(0, count * 2)); this._pendingEmptyLines -= count; } } /** * Writes a content byte, with the space a pending run of whitespace collapses to, * into the output buffer and returns the new write position. Kept a method rather * than a closure so the write position stays a plain local in the byte loop * @internal */ _emitContent(out, outPos, c) { if (!this._lineHasContent) { if (this._pendingEmptyLines) { // the first content byte of a line is where the empty lines before it // become part of the body, so hash what is in the buffer before them this._hashCanonical(out.subarray(0, outPos)); outPos = 0; this._hashEmptyLines(); } this._lineHasContent = true; } if (this._pendingWsp) { out[outPos++] = CHAR_SPACE; this._pendingWsp = false; } out[outPos++] = c; return outPos; } updateHash(chunk, final) { // every byte contributes itself at most once, plus a CR for a bare LF // and, once per chunk, a pending space and CR carried over from before const out = Buffer.allocUnsafe(chunk.length * 2 + 2); let outPos = 0; for (let i = 0; i < chunk.length; i++) { const c = chunk[i]; if (c === CHAR_LF) { // end of line, a CR right before it and any trailing whitespace are dropped if (this._lineHasContent) { out[outPos++] = CHAR_CR; out[outPos++] = CHAR_LF; this._lineHasContent = false; } else { this._pendingEmptyLines++; } this._pendingWsp = false; this._pendingCr = false; continue; } if (this._pendingCr) { // not followed by LF, so the CR is content outPos = this._emitContent(out, outPos, CHAR_CR); this._pendingCr = false; } if (c === CHAR_CR) { this._pendingCr = true; } else if (c === CHAR_SPACE || c === CHAR_TAB) { this._pendingWsp = true; } else { outPos = this._emitContent(out, outPos, c); } } if (final && this._pendingCr) { // a CR at the very end of the body is content outPos = this._emitContent(out, outPos, CHAR_CR); this._pendingCr = false; } this._hashCanonical(out.subarray(0, outPos)); } /** @internal */ _transform(chunk, encoding, callback) { if (!chunk || !chunk.length) { return callback(); } if (typeof chunk === 'string') { chunk = Buffer.from(chunk, encoding); } this.updateHash(chunk); this.byteLength += chunk.length; this.push(chunk); callback(); } /** @internal */ _flush(callback) { this.updateHash(Buffer.alloc(0), true); if (this._lineHasContent) { // the body does not end with a line break, add one this._hashCanonical(CRLF); } this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false); callback(); } } exports.default = RelaxedBody; module.exports = exports.default; Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });