Initial commit
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
const { Writable } = require('stream');
|
||||
|
||||
const StreamSearch = require('streamsearch');
|
||||
|
||||
const PartStream = require('./PartStream');
|
||||
const HeaderParser = require('./HeaderParser');
|
||||
|
||||
const DASH = 45;
|
||||
const B_ONEDASH = Buffer.from('-');
|
||||
const B_CRLF = Buffer.from('\r\n');
|
||||
const EMPTY_FN = () => {};
|
||||
|
||||
class Dicer extends Writable {
|
||||
constructor(cfg) {
|
||||
super(cfg);
|
||||
|
||||
if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string'))
|
||||
throw new TypeError('Boundary required');
|
||||
|
||||
if (typeof cfg.boundary === 'string')
|
||||
this.setBoundary(cfg.boundary);
|
||||
else
|
||||
this._bparser = undefined;
|
||||
|
||||
this._headerFirst = cfg.headerFirst;
|
||||
|
||||
this._dashes = 0;
|
||||
this._parts = 0;
|
||||
this._finished = false;
|
||||
this._realFinish = false;
|
||||
this._isPreamble = true;
|
||||
this._justMatched = false;
|
||||
this._firstWrite = true;
|
||||
this._inHeader = true;
|
||||
this._part = undefined;
|
||||
this._cb = undefined;
|
||||
this._ignoreData = false;
|
||||
this._partOpts = (typeof cfg.partHwm === 'number'
|
||||
? { highWaterMark: cfg.partHwm }
|
||||
: {});
|
||||
this._pause = false;
|
||||
|
||||
this._hparser = new HeaderParser(cfg);
|
||||
this._hparser.on('header', (header) => {
|
||||
this._inHeader = false;
|
||||
this._part.emit('header', header);
|
||||
});
|
||||
this._hparser.on('error', (err) => {
|
||||
if (this._part && !this._ignoreData) {
|
||||
this._part.emit('error', err);
|
||||
this._part.push(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
emit(ev) {
|
||||
if (ev !== 'finish' || this._realFinish) {
|
||||
Writable.prototype.emit.apply(this, arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._finished)
|
||||
return;
|
||||
|
||||
process.nextTick(() => {
|
||||
this.emit('error', new Error('Unexpected end of multipart data'));
|
||||
|
||||
if (this._part && !this._ignoreData) {
|
||||
const type = (this._isPreamble ? 'Preamble' : 'Part');
|
||||
this._part.emit(
|
||||
'error',
|
||||
new Error(`${type} terminated early due to `
|
||||
+ 'unexpected end of multipart data')
|
||||
);
|
||||
this._part.push(null);
|
||||
process.nextTick(() => {
|
||||
this._realFinish = true;
|
||||
this.emit('finish');
|
||||
this._realFinish = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._realFinish = true;
|
||||
this.emit('finish');
|
||||
this._realFinish = false;
|
||||
});
|
||||
}
|
||||
|
||||
_write(data, encoding, cb) {
|
||||
// Ignore unexpected data (e.g. extra trailer data after finished)
|
||||
if (!this._hparser && !this._bparser)
|
||||
return cb();
|
||||
|
||||
if (this._headerFirst && this._isPreamble) {
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts);
|
||||
if (this._events.preamble)
|
||||
this.emit('preamble', this._part);
|
||||
else
|
||||
ignore(this);
|
||||
}
|
||||
const r = this._hparser.push(data);
|
||||
if (!this._inHeader && r !== undefined && r < data.length)
|
||||
data = data.slice(r);
|
||||
else
|
||||
return cb();
|
||||
}
|
||||
|
||||
// Allows for "easier" testing
|
||||
if (this._firstWrite) {
|
||||
this._bparser.push(B_CRLF);
|
||||
this._firstWrite = false;
|
||||
}
|
||||
|
||||
this._bparser.push(data);
|
||||
|
||||
if (this._pause)
|
||||
this._cb = cb;
|
||||
else
|
||||
cb();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._part = undefined;
|
||||
this._bparser = undefined;
|
||||
this._hparser = undefined;
|
||||
}
|
||||
|
||||
setBoundary(boundary) {
|
||||
this._bparser = new StreamSearch(`\r\n--${boundary}`, onInfo.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
function onInfo(isMatch, data, start, end) {
|
||||
let buf;
|
||||
let i = 0;
|
||||
let r;
|
||||
let ev;
|
||||
let shouldWriteMore = true;
|
||||
|
||||
if (!this._part && this._justMatched && data) {
|
||||
while (this._dashes < 2 && (start + i) < end) {
|
||||
if (data[start + i] === DASH) {
|
||||
++i;
|
||||
++this._dashes;
|
||||
} else {
|
||||
if (this._dashes)
|
||||
buf = B_ONEDASH;
|
||||
this._dashes = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this._dashes === 2) {
|
||||
if ((start + i) < end && this._events.trailer)
|
||||
this.emit('trailer', data.slice(start + i, end));
|
||||
this.reset();
|
||||
this._finished = true;
|
||||
// No more parts will be added
|
||||
if (this._parts === 0) {
|
||||
this._realFinish = true;
|
||||
this.emit('finish');
|
||||
this._realFinish = false;
|
||||
}
|
||||
}
|
||||
if (this._dashes)
|
||||
return;
|
||||
}
|
||||
if (this._justMatched)
|
||||
this._justMatched = false;
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts);
|
||||
this._part._read = (n) => {
|
||||
unpause(this);
|
||||
};
|
||||
ev = this._isPreamble ? 'preamble' : 'part';
|
||||
if (this._events[ev])
|
||||
this.emit(ev, this._part);
|
||||
else
|
||||
ignore(this);
|
||||
if (!this._isPreamble)
|
||||
this._inHeader = true;
|
||||
}
|
||||
if (data && start < end && !this._ignoreData) {
|
||||
if (this._isPreamble || !this._inHeader) {
|
||||
if (buf)
|
||||
shouldWriteMore = this._part.push(buf);
|
||||
shouldWriteMore = this._part.push(data.slice(start, end));
|
||||
if (!shouldWriteMore)
|
||||
this._pause = true;
|
||||
} else if (!this._isPreamble && this._inHeader) {
|
||||
if (buf)
|
||||
this._hparser.push(buf);
|
||||
r = this._hparser.push(data.slice(start, end));
|
||||
if (!this._inHeader && r !== undefined && r < end)
|
||||
onInfo.call(this, false, data, start + r, end);
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
this._hparser.reset();
|
||||
if (this._isPreamble) {
|
||||
this._isPreamble = false;
|
||||
} else {
|
||||
++this._parts;
|
||||
this._part.on('end', () => {
|
||||
if (--this._parts === 0) {
|
||||
if (this._finished) {
|
||||
this._realFinish = true;
|
||||
this.emit('finish');
|
||||
this._realFinish = false;
|
||||
} else {
|
||||
unpause(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this._part.push(null);
|
||||
this._part = undefined;
|
||||
this._ignoreData = false;
|
||||
this._justMatched = true;
|
||||
this._dashes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function ignore(self) {
|
||||
if (self._part && !self._ignoreData) {
|
||||
self._ignoreData = true;
|
||||
self._part.on('error', EMPTY_FN);
|
||||
// We must perform some kind of read on the stream even though we are
|
||||
// ignoring the data, otherwise node's Readable stream will not emit 'end'
|
||||
// after pushing null to the stream
|
||||
self._part.resume();
|
||||
}
|
||||
}
|
||||
|
||||
function unpause(self) {
|
||||
if (!self._pause)
|
||||
return;
|
||||
|
||||
self._pause = false;
|
||||
if (self._cb) {
|
||||
const cb = self._cb;
|
||||
self._cb = undefined;
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dicer;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const StreamSearch = require('streamsearch');
|
||||
|
||||
const B_DCRLF = Buffer.from('\r\n\r\n');
|
||||
const RE_CRLF = /\r\n/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/;
|
||||
const MAX_HEADER_PAIRS = 2000; // From node's http.js
|
||||
const MAX_HEADER_SIZE = 80 * 1024; // From node's http_parser
|
||||
|
||||
class HeaderParser extends EventEmitter {
|
||||
constructor(cfg) {
|
||||
super();
|
||||
|
||||
this.nread = 0;
|
||||
this.maxed = false;
|
||||
this.npairs = 0;
|
||||
this.maxHeaderPairs = (cfg && typeof cfg.maxHeaderPairs === 'number'
|
||||
? cfg.maxHeaderPairs
|
||||
: MAX_HEADER_PAIRS);
|
||||
this.buffer = '';
|
||||
this.header = {};
|
||||
this.finished = false;
|
||||
this.ss = new StreamSearch(B_DCRLF, (isMatch, data, start, end) => {
|
||||
if (data && !this.maxed) {
|
||||
if (this.nread + (end - start) > MAX_HEADER_SIZE) {
|
||||
end = (MAX_HEADER_SIZE - this.nread);
|
||||
this.nread = MAX_HEADER_SIZE;
|
||||
} else {
|
||||
this.nread += (end - start);
|
||||
}
|
||||
|
||||
if (this.nread === MAX_HEADER_SIZE)
|
||||
this.maxed = true;
|
||||
|
||||
this.buffer += data.toString('latin1', start, end);
|
||||
}
|
||||
if (isMatch)
|
||||
this._finish();
|
||||
});
|
||||
}
|
||||
|
||||
push(data) {
|
||||
const r = this.ss.push(data);
|
||||
if (this.finished)
|
||||
return r;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.finished = false;
|
||||
this.buffer = '';
|
||||
this.header = {};
|
||||
this.ss.reset();
|
||||
}
|
||||
|
||||
_finish() {
|
||||
let hadError = false;
|
||||
if (this.buffer)
|
||||
hadError = !parseHeader(this);
|
||||
this.ss.matches = this.ss.maxMatches;
|
||||
const header = this.header;
|
||||
this.header = {};
|
||||
this.buffer = '';
|
||||
this.finished = true;
|
||||
this.nread = this.npairs = 0;
|
||||
this.maxed = false;
|
||||
if (!hadError)
|
||||
this.emit('header', header);
|
||||
}
|
||||
}
|
||||
|
||||
function parseHeader(self) {
|
||||
if (self.npairs === self.maxHeaderPairs)
|
||||
return true;
|
||||
|
||||
const lines = self.buffer.split(RE_CRLF);
|
||||
const len = lines.length;
|
||||
let m;
|
||||
let h;
|
||||
let modded = false;
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
if (lines[i].length === 0)
|
||||
continue;
|
||||
|
||||
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
|
||||
// Folded header content
|
||||
// RFC2822 says to just remove the CRLF and not the whitespace following
|
||||
// it, so we follow the RFC and include the leading whitespace ...
|
||||
if (!h) {
|
||||
self.emit('error', new Error('Unexpected folded header value'));
|
||||
return false;
|
||||
}
|
||||
self.header[h][self.header[h].length - 1] += lines[i];
|
||||
} else {
|
||||
m = RE_HDR.exec(lines[i]);
|
||||
if (m) {
|
||||
h = m[1].toLowerCase();
|
||||
if (m[2]) {
|
||||
if (self.header[h] === undefined)
|
||||
self.header[h] = [m[2]];
|
||||
else
|
||||
self.header[h].push(m[2]);
|
||||
} else {
|
||||
self.header[h] = [''];
|
||||
}
|
||||
if (++self.npairs === self.maxHeaderPairs)
|
||||
break;
|
||||
} else {
|
||||
self.buffer = lines[i];
|
||||
modded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!modded)
|
||||
self.buffer = '';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = HeaderParser;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const { Readable } = require('stream');
|
||||
|
||||
class PartStream extends Readable {
|
||||
_read(n) {}
|
||||
}
|
||||
|
||||
module.exports = PartStream;
|
||||
Reference in New Issue
Block a user