relaxed-body.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. 'use strict';
  2. // streams through a message body and calculates relaxed body hash
  3. const Transform = require('stream').Transform;
  4. const crypto = require('crypto');
  5. class RelaxedBody extends Transform {
  6. constructor(options) {
  7. super();
  8. options = options || {};
  9. this.chunkBuffer = [];
  10. this.chunkBufferLen = 0;
  11. this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
  12. this.remainder = '';
  13. this.byteLength = 0;
  14. this.debug = options.debug;
  15. this._debugBody = options.debug ? [] : false;
  16. }
  17. updateHash(chunk) {
  18. let bodyStr;
  19. // find next remainder
  20. let nextRemainder = '';
  21. // This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
  22. // If we get another chunk that does not match this description then we can restore the previously processed data
  23. let state = 'file';
  24. for (let i = chunk.length - 1; i >= 0; i--) {
  25. let c = chunk[i];
  26. if (state === 'file' && (c === 0x0a || c === 0x0d)) {
  27. // do nothing, found \n or \r at the end of chunk, stil end of file
  28. } else if (state === 'file' && (c === 0x09 || c === 0x20)) {
  29. // switch to line ending mode, this is the last non-empty line
  30. state = 'line';
  31. } else if (state === 'line' && (c === 0x09 || c === 0x20)) {
  32. // do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
  33. } else if (state === 'file' || state === 'line') {
  34. // non line/file ending character found, switch to body mode
  35. state = 'body';
  36. if (i === chunk.length - 1) {
  37. // final char is not part of line end or file end, so do nothing
  38. break;
  39. }
  40. }
  41. if (i === 0) {
  42. // reached to the beginning of the chunk, check if it is still about the ending
  43. // and if the remainder also matches
  44. if (
  45. (state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
  46. (state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
  47. ) {
  48. // keep everything
  49. this.remainder += chunk.toString('binary');
  50. return;
  51. } else if (state === 'line' || state === 'file') {
  52. // process existing remainder as normal line but store the current chunk
  53. nextRemainder = chunk.toString('binary');
  54. chunk = false;
  55. break;
  56. }
  57. }
  58. if (state !== 'body') {
  59. continue;
  60. }
  61. // reached first non ending byte
  62. nextRemainder = chunk.slice(i + 1).toString('binary');
  63. chunk = chunk.slice(0, i + 1);
  64. break;
  65. }
  66. let needsFixing = !!this.remainder;
  67. if (chunk && !needsFixing) {
  68. // check if we even need to change anything
  69. for (let i = 0, len = chunk.length; i < len; i++) {
  70. if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
  71. // missing \r before \n
  72. needsFixing = true;
  73. break;
  74. } else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
  75. // trailing WSP found
  76. needsFixing = true;
  77. break;
  78. } else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
  79. // multiple spaces found, needs to be replaced with just one
  80. needsFixing = true;
  81. break;
  82. } else if (chunk[i] === 0x09) {
  83. // TAB found, needs to be replaced with a space
  84. needsFixing = true;
  85. break;
  86. }
  87. }
  88. }
  89. if (needsFixing) {
  90. bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
  91. this.remainder = nextRemainder;
  92. bodyStr = bodyStr
  93. .replace(/\r?\n/g, '\n') // use js line endings
  94. .replace(/[ \t]*$/gm, '') // remove line endings, rtrim
  95. .replace(/[ \t]+/gm, ' ') // single spaces
  96. .replace(/\n/g, '\r\n'); // restore rfc822 line endings
  97. chunk = Buffer.from(bodyStr, 'binary');
  98. } else if (nextRemainder) {
  99. this.remainder = nextRemainder;
  100. }
  101. if (this.debug) {
  102. this._debugBody.push(chunk);
  103. }
  104. this.bodyHash.update(chunk);
  105. }
  106. _transform(chunk, encoding, callback) {
  107. if (!chunk || !chunk.length) {
  108. return callback();
  109. }
  110. if (typeof chunk === 'string') {
  111. chunk = Buffer.from(chunk, encoding);
  112. }
  113. this.updateHash(chunk);
  114. this.byteLength += chunk.length;
  115. this.push(chunk);
  116. callback();
  117. }
  118. _flush(callback) {
  119. // generate final hash and emit it
  120. if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) {
  121. // add terminating line end
  122. this.bodyHash.update(Buffer.from('\r\n'));
  123. }
  124. if (!this.byteLength) {
  125. // emit empty line buffer to keep the stream flowing
  126. this.push(Buffer.from('\r\n'));
  127. // this.bodyHash.update(Buffer.from('\r\n'));
  128. }
  129. this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
  130. callback();
  131. }
  132. }
  133. module.exports = RelaxedBody;