Comparing version 2.3.3 to 2.3.4
@@ -0,7 +1,12 @@ | ||
/* eslint no-control-regex: 0 */ | ||
'use strict'; | ||
// Calculates relaxed body hash for a message body stream | ||
const crypto = require('crypto'); | ||
const CHAR_CR = 0x0d; | ||
const CHAR_LF = 0x0a; | ||
const CHAR_SPACE = 0x20; | ||
const CHAR_TAB = 0x09; | ||
/** | ||
@@ -19,6 +24,7 @@ * Class for calculating body hash of an email message body stream | ||
constructor(algorithm, maxBodyLength) { | ||
algorithm = (algorithm || 'sha256').split('-').pop(); | ||
algorithm = (algorithm || 'sha256').split('-').pop().toLowerCase(); | ||
this.bodyHash = crypto.createHash(algorithm); | ||
this.remainder = ''; | ||
this.remainder = false; | ||
this.byteLength = 0; | ||
@@ -28,6 +34,14 @@ | ||
this.maxBodyLength = maxBodyLength; | ||
this.maxSizeReached = false; | ||
this.emptyLinesQueue = []; | ||
} | ||
_updateBodyHash(chunk) { | ||
// the following is needed for l= option | ||
if (this.maxSizeReached) { | ||
return; | ||
} | ||
// the following is needed for the l= option | ||
if ( | ||
@@ -39,2 +53,3 @@ typeof this.maxBodyLength === 'number' && | ||
) { | ||
this.maxSizeReached = true; | ||
if (this.bodyHashedBytes >= this.maxBodyLength) { | ||
@@ -44,4 +59,5 @@ // nothing to do here, skip entire chunk | ||
} | ||
// only use allowed size of bytes | ||
chunk = chunk.slice(0, this.maxBodyLength - this.bodyHashedBytes); | ||
chunk = chunk.subarray(0, this.maxBodyLength - this.bodyHashedBytes); | ||
} | ||
@@ -51,82 +67,164 @@ | ||
this.bodyHash.update(chunk); | ||
//process.stdout.write(chunk); | ||
} | ||
update(chunk) { | ||
this.byteLength += chunk.length; | ||
_drainPendingEmptyLines() { | ||
if (this.emptyLinesQueue.length) { | ||
for (let emptyLine of this.emptyLinesQueue) { | ||
this._updateBodyHash(emptyLine); | ||
} | ||
this.emptyLinesQueue = []; | ||
} | ||
} | ||
let bodyStr; | ||
_pushBodyHash(chunk) { | ||
if (!chunk || !chunk.length) { | ||
return; | ||
} | ||
// find next remainder | ||
let nextRemainder = ''; | ||
// remove line endings | ||
let foundNonLn = false; | ||
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line | ||
// If we get another chunk that does not match this description then we can restore the previously processed data | ||
let state = 'file'; | ||
// buffer line endings and empty lines | ||
for (let i = chunk.length - 1; i >= 0; i--) { | ||
let c = chunk[i]; | ||
if (state === 'file' && (c === 0x0a || c === 0x0d)) { | ||
// do nothing, found \n or \r at the end of chunk, stil end of file | ||
} else if (state === 'file' && (c === 0x09 || c === 0x20)) { | ||
// switch to line ending mode, this is the last non-empty line | ||
state = 'line'; | ||
} else if (state === 'line' && (c === 0x09 || c === 0x20)) { | ||
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line | ||
} else if (state === 'file' || state === 'line') { | ||
// non line/file ending character found, switch to body mode | ||
state = 'body'; | ||
if (i === chunk.length - 1) { | ||
// final char is not part of line end or file end, so do nothing | ||
break; | ||
if (chunk[i] !== CHAR_LF && chunk[i] !== CHAR_CR) { | ||
this._drainPendingEmptyLines(); | ||
if (i < chunk.length - 1) { | ||
this.emptyLinesQueue.push(chunk.subarray(i + 1)); | ||
chunk = chunk.subarray(0, i + 1); | ||
} | ||
foundNonLn = true; | ||
break; | ||
} | ||
} | ||
if (i === 0) { | ||
// reached to the beginning of the chunk, check if it is still about the ending | ||
// and if the remainder also matches | ||
if ( | ||
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) || | ||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder))) | ||
) { | ||
// keep everything | ||
this.remainder += chunk.toString('binary'); | ||
return; | ||
} else if (state === 'line' || state === 'file') { | ||
// process existing remainder as normal line but store the current chunk | ||
nextRemainder = chunk.toString('binary'); | ||
chunk = false; | ||
break; | ||
if (!foundNonLn) { | ||
this.emptyLinesQueue.push(chunk); | ||
return; | ||
} | ||
this._updateBodyHash(chunk); | ||
} | ||
fixLineBuffer(line) { | ||
let resultLine = []; | ||
let nonWspFound = false; | ||
let prevWsp = false; | ||
for (let i = line.length - 1; i >= 0; i--) { | ||
if (line[i] === CHAR_LF) { | ||
resultLine.unshift(line[i]); | ||
if (i === 0 || line[i - 1] !== CHAR_CR) { | ||
// add missing carriage return | ||
resultLine.unshift(CHAR_CR); | ||
} | ||
continue; | ||
} | ||
if (state !== 'body') { | ||
if (line[i] === CHAR_CR) { | ||
resultLine.unshift(line[i]); | ||
continue; | ||
} | ||
// reached first non ending byte | ||
nextRemainder = chunk.slice(i + 1).toString('binary'); | ||
chunk = chunk.slice(0, i + 1); | ||
break; | ||
if (line[i] === CHAR_SPACE || line[i] === CHAR_TAB) { | ||
if (nonWspFound) { | ||
prevWsp = true; | ||
} | ||
continue; | ||
} | ||
if (prevWsp) { | ||
resultLine.unshift(CHAR_SPACE); | ||
prevWsp = false; | ||
} | ||
nonWspFound = true; | ||
resultLine.unshift(line[i]); | ||
} | ||
let needsFixing = !!this.remainder; | ||
if (chunk && !needsFixing) { | ||
// check if we even need to change anything | ||
for (let i = 0, len = chunk.length; i < len; i++) { | ||
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) { | ||
// missing \r before \n | ||
needsFixing = true; | ||
break; | ||
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) { | ||
// trailing WSP found | ||
needsFixing = true; | ||
break; | ||
} else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) { | ||
// multiple spaces found, needs to be replaced with just one | ||
needsFixing = true; | ||
break; | ||
} else if (chunk[i] === 0x09) { | ||
// TAB found, needs to be replaced with a space | ||
needsFixing = true; | ||
break; | ||
if (prevWsp && nonWspFound) { | ||
resultLine.unshift(CHAR_SPACE); | ||
} | ||
return Buffer.from(resultLine); | ||
} | ||
update(chunk, final) { | ||
this.byteLength += (chunk && chunk.length) || 0; | ||
if (this.maxSizeReached) { | ||
return; | ||
} | ||
// Canonicalize content by applying a and b in order: | ||
// a.1. Ignore all whitespace at the end of lines. | ||
// a.2. Reduce all sequences of WSP within a line to a single SP character. | ||
// b.1. Ignore all empty lines at the end of the message body. | ||
// b.2. If the body is non-empty but does not end with a CRLF, a CRLF is added. | ||
let lineEndPos = -1; | ||
let lineNeedsFixing = false; | ||
let cursorPos = 0; | ||
if (this.remainder && this.remainder.length) { | ||
if (chunk) { | ||
// concatting chunks might be bad for performance :S | ||
chunk = Buffer.concat([this.remainder, chunk]); | ||
} else { | ||
chunk = this.remainder; | ||
} | ||
this.remainder = false; | ||
} | ||
if (chunk && chunk.length) { | ||
for (let pos = 0; pos < chunk.length; pos++) { | ||
switch (chunk[pos]) { | ||
case CHAR_LF: | ||
if ( | ||
!lineNeedsFixing && | ||
// previous character is not <CR> | ||
((pos >= 1 && chunk[pos - 1] !== CHAR_CR) || | ||
// LF is the first byte on the line | ||
pos === 0 || | ||
// there's a space before line break | ||
(pos >= 2 && chunk[pos - 1] === CHAR_CR && chunk[pos - 2] === CHAR_SPACE)) | ||
) { | ||
lineNeedsFixing = true; | ||
} | ||
// line break | ||
if (lineNeedsFixing) { | ||
// emit pending bytes up to the last line break before current line | ||
if (lineEndPos >= 0 && lineEndPos >= cursorPos) { | ||
let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1); | ||
this._pushBodyHash(chunkPart); | ||
} | ||
let line = chunk.subarray(lineEndPos + 1, pos + 1); | ||
this._pushBodyHash(this.fixLineBuffer(line)); | ||
lineNeedsFixing = false; | ||
// move cursor to the start of next line | ||
cursorPos = pos + 1; | ||
} | ||
lineEndPos = pos; | ||
break; | ||
case CHAR_SPACE: | ||
if (!lineNeedsFixing && pos && chunk[pos - 1] === CHAR_SPACE) { | ||
lineNeedsFixing = true; | ||
} | ||
break; | ||
case CHAR_TAB: | ||
// non-space WSP always needs replacing | ||
lineNeedsFixing = true; | ||
break; | ||
default: | ||
} | ||
@@ -136,23 +234,35 @@ } | ||
if (needsFixing) { | ||
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : ''); | ||
this.remainder = nextRemainder; | ||
bodyStr = bodyStr | ||
.replace(/\r?\n/g, '\n') // use js line endings | ||
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim | ||
.replace(/[ \t]+/gm, ' ') // single spaces | ||
.replace(/\n/g, '\r\n'); // restore rfc822 line endings | ||
chunk = Buffer.from(bodyStr, 'binary'); | ||
} else if (nextRemainder) { | ||
this.remainder = nextRemainder; | ||
if (chunk && cursorPos < chunk.length && cursorPos !== lineEndPos) { | ||
// emit data from chunk | ||
let chunkPart = chunk.subarray(cursorPos, lineEndPos + 1); | ||
if (chunkPart.length) { | ||
this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart); | ||
lineNeedsFixing = false; | ||
} | ||
cursorPos = lineEndPos + 1; | ||
} | ||
this._updateBodyHash(chunk); | ||
if (chunk && !final && cursorPos < chunk.length) { | ||
this.remainder = chunk.subarray(cursorPos); | ||
} | ||
if (final) { | ||
let chunkPart = (cursorPos && chunk && chunk.subarray(cursorPos)) || chunk; | ||
if (chunkPart && chunkPart.length) { | ||
this._pushBodyHash(lineNeedsFixing ? this.fixLineBuffer(chunkPart) : chunkPart); | ||
lineNeedsFixing = false; | ||
} | ||
if (this.bodyHashedBytes) { | ||
// terminating line break for non-empty messages | ||
this._updateBodyHash(Buffer.from([CHAR_CR, CHAR_LF])); | ||
} | ||
} | ||
} | ||
digest(encoding) { | ||
if (/[\r\n]$/.test(this.remainder) && this.bodyHashedBytes > 0) { | ||
// add terminating line end | ||
this._updateBodyHash(Buffer.from('\r\n')); | ||
} | ||
this.update(null, true); | ||
@@ -165,1 +275,25 @@ // finalize | ||
module.exports = { RelaxedHash }; | ||
/* | ||
let fs = require('fs'); | ||
const getBody = message => { | ||
message = message.toString('binary'); | ||
let match = message.match(/\r?\n\r?\n/); | ||
if (match) { | ||
message = message.substr(match.index + match[0].length); | ||
} | ||
return Buffer.from(message, 'binary'); | ||
}; | ||
let s = fs.readFileSync(process.argv[2]); | ||
let k = new RelaxedHash('rsa-sha256', -1); | ||
for (let byte of getBody(s)) { | ||
k.update(Buffer.from([byte])); | ||
} | ||
console.error(k.digest('base64')); | ||
console.error(k.byteLength, k.bodyHashedBytes); | ||
*/ |
@@ -25,2 +25,4 @@ 'use strict'; | ||
this.maxBodyLength = maxBodyLength; | ||
this.lastNewline = false; | ||
} | ||
@@ -46,2 +48,4 @@ | ||
this.bodyHash.update(chunk); | ||
//process.stdout.write(chunk); | ||
} | ||
@@ -86,6 +90,7 @@ | ||
this._updateBodyHash(chunk); | ||
this.lastNewline = chunk[chunk.length - 1] === 0x0a; | ||
} | ||
digest(encoding) { | ||
if (this.remainder.length || !this.bodyHashedBytes) { | ||
if (!this.lastNewline || !this.bodyHashedBytes) { | ||
// emit empty line buffer to keep the stream flowing | ||
@@ -92,0 +97,0 @@ this._updateBodyHash(Buffer.from('\r\n')); |
@@ -6,7 +6,7 @@ name license type link installed version author | ||
joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0 | ||
libmime MIT git://github.com/andris9/libmime.git 5.0.0 Andris Reinman andris@kreata.ee | ||
libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman andris@kreata.ee | ||
node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forge.git 1.3.1 Digital Bazaar, Inc. support@digitalbazaar.com http://digitalbazaar.com/ | ||
nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.3 Andris Reinman | ||
nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.5 Andris Reinman | ||
psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.8.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/ | ||
punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/ | ||
yargs MIT git+https://github.com/yargs/yargs.git 17.4.1 | ||
yargs MIT git+https://github.com/yargs/yargs.git 17.5.0 |
{ | ||
"name": "mailauth", | ||
"version": "2.3.3", | ||
"version": "2.3.4", | ||
"description": "Email authentication library for Node.js", | ||
@@ -36,3 +36,3 @@ "main": "lib/mailauth.js", | ||
"chai": "4.3.6", | ||
"eslint": "8.15.0", | ||
"eslint": "8.17.0", | ||
"eslint-config-nodemailer": "1.2.0", | ||
@@ -46,3 +46,3 @@ "eslint-config-prettier": "8.5.0", | ||
"mocha": "10.0.0", | ||
"pkg": "5.6.0" | ||
"pkg": "5.7.0" | ||
}, | ||
@@ -58,3 +58,3 @@ "dependencies": { | ||
"punycode": "2.1.1", | ||
"yargs": "17.5.0" | ||
"yargs": "17.5.1" | ||
}, | ||
@@ -61,0 +61,0 @@ "engines": { |
Sorry, the diff of this file is not supported yet
239485
4618
+ Addedyargs@17.5.1(transitive)
- Removedyargs@17.5.0(transitive)
Updatedyargs@17.5.1