UNPKG

serverless-offline-msk

Version:

A serverless offline plugin that enables AWS MSK events

366 lines (268 loc) 10.4 kB
'use strict'; const Stream = require('stream'); const B64 = require('@hapi/b64'); const Boom = require('@hapi/boom'); const Content = require('@hapi/content'); const Hoek = require('@hapi/hoek'); const Nigel = require('@hapi/nigel'); const internals = {}; /* RFC 2046 (http://tools.ietf.org/html/rfc2046) multipart-body = [preamble CRLF] dash-boundary *( SPACE / HTAB ) CRLF body-part *( CRLF dash-boundary *( SPACE / HTAB ) CRLF body-part ) CRLF dash-boundary "--" *( SPACE / HTAB ) [CRLF epilogue] boundary = 0*69<bchars> bcharsnospace bchars = bcharsnospace / " " bcharsnospace = DIGIT / ALPHA / "'" / "(" / ")" / "+" / "_" / "," / "-" / "." / "/" / ":" / "=" / "?" dash-boundary = "--" boundary preamble = discard-text epilogue = discard-text discard-text = *(*text CRLF) *text body-part = MIME-part-headers [CRLF *OCTET] OCTET = <any 0-255 octet value> SPACE = 32 HTAB = 9 CRLF = 13 10 */ internals.state = { preamble: 0, // Until the first boundary is received boundary: 1, // After a boundary, waiting for first line with optional linear-whitespace header: 2, // Receiving part headers payload: 3, // Receiving part payload epilogue: 4 }; internals.defaults = { maxBytes: Infinity }; exports.Dispenser = internals.Dispenser = class extends Stream.Writable { constructor(options) { super({ autoDestroy: false }); Hoek.assert(options !== null && typeof options === 'object', 'options must be an object'); const settings = Hoek.applyToDefaults(internals.defaults, options); this._boundary = settings.boundary; this._state = internals.state.preamble; this._held = ''; this._stream = null; this._headers = {}; this._name = ''; this._pendingHeader = ''; this._error = null; this._bytes = 0; this._maxBytes = settings.maxBytes; this._parts = new Nigel.Stream(Buffer.from('--' + settings.boundary)); this._lines = new Nigel.Stream(Buffer.from('\r\n')); this._parts.on('needle', () => this._onPartEnd()); this._parts.on('haystack', (chunk) => this._onPart(chunk)); this._lines.on('needle', () => this._onLineEnd()); this._lines.on('haystack', (chunk) => this._onLine(chunk)); this.once('finish', () => this._parts.end()); this._parts.once('close', () => this._lines.end()); let piper = null; let finish = (err) => { if (piper) { piper.removeListener('data', onReqData); piper.removeListener('error', finish); piper.removeListener('aborted', onReqAborted); } if (err) { return this._abort(err); } this._emit('close'); }; finish = Hoek.once(finish); this._lines.once('close', () => { if (this._state === internals.state.epilogue) { if (this._held) { this._emit('epilogue', this._held); this._held = ''; } } else if (this._state === internals.state.boundary) { if (!this._held) { this._abort(Boom.badRequest('Missing end boundary')); } else if (this._held !== '--') { this._abort(Boom.badRequest('Only white space allowed after boundary at end')); } } else { this._abort(Boom.badRequest('Incomplete multipart payload')); } setImmediate(finish); // Give pending events a chance to fire }); const onReqAborted = () => { finish(Boom.badRequest('Client request aborted')); }; const onReqData = (data) => { this._bytes += Buffer.byteLength(data); if (this._bytes > this._maxBytes) { finish(Boom.entityTooLarge('Maximum size exceeded')); } }; this.once('pipe', (req) => { piper = req; req.on('data', onReqData); req.once('error', finish); req.once('aborted', onReqAborted); }); } _write(buffer, encoding, next) { if (this._error) { return next(); } this._parts.write(buffer); return next(); } _emit(...args) { if (this._error) { return; } this.emit(...args); } _abort(err) { this._emit('error', err); this._error = err; } _onPartEnd() { this._lines.flush(); if (this._state === internals.state.preamble) { if (this._held) { const last = this._held.length - 1; if (this._held[last] !== '\n' || this._held[last - 1] !== '\r') { return this._abort(Boom.badRequest('Preamble missing CRLF terminator')); } this._emit('preamble', this._held.slice(0, -2)); this._held = ''; } this._parts.needle(Buffer.from('\r\n--' + this._boundary)); // CRLF no longer optional } this._state = internals.state.boundary; if (this._stream) { this._stream.end(); this._stream = null; } else if (this._name) { this._emit('field', this._name, this._held); this._name = ''; this._held = ''; } } _onPart(chunk) { if (this._state === internals.state.preamble) { this._held = this._held + chunk.toString(); } else if (this._state === internals.state.payload) { if (this._stream) { this._stream.write(chunk); // Stream payload } else { this._held = this._held + chunk.toString(); } } else { this._lines.write(chunk); // Look for boundary } } _onLineEnd() { // Boundary whitespace if (this._state === internals.state.boundary) { if (this._held) { this._held = this._held.replace(/[\t ]/g, ''); // trim() removes new lines if (this._held) { if (this._held === '--') { this._state = internals.state.epilogue; this._held = ''; return; } return this._abort(Boom.badRequest('Only white space allowed after boundary')); } } this._state = internals.state.header; return; } // Part headers if (this._state === internals.state.header) { // Header if (this._held) { // Header continuation if (this._held[0] === ' ' || this._held[0] === '\t') { if (!this._pendingHeader) { return this._abort(Boom.badRequest('Invalid header continuation without valid declaration on previous line')); } this._pendingHeader = this._pendingHeader + ' ' + this._held.slice(1); // Drop tab this._held = ''; return; } // Start of new header this._flushHeader(); this._pendingHeader = this._held; this._held = ''; return; } // End of headers this._flushHeader(); this._state = internals.state.payload; let disposition; try { disposition = Content.disposition(this._headers['content-disposition']); } catch (err) { return this._abort(err); } if (disposition.filename !== undefined) { const stream = new Stream.PassThrough(); const transferEncoding = this._headers['content-transfer-encoding']; if (transferEncoding && transferEncoding.toLowerCase() === 'base64') { this._stream = new B64.Decoder(); this._stream.pipe(stream); } else { this._stream = stream; } stream.name = disposition.name; stream.filename = disposition.filename; stream.headers = this._headers; this._headers = {}; this._emit('part', stream); } else { this._name = disposition.name; } this._lines.flush(); return; } // Epilogue this._held = this._held + '\r\n'; // Put the new line back } _onLine(chunk) { if (this._stream) { this._stream.write(chunk); // Stream payload } else { this._held = this._held + chunk.toString(); // Reading header or field } } _flushHeader() { if (!this._pendingHeader) { return; } const sep = this._pendingHeader.indexOf(':'); if (sep === -1) { return this._abort(Boom.badRequest('Invalid header missing colon separator')); } if (!sep) { return this._abort(Boom.badRequest('Invalid header missing field name')); } const name = this._pendingHeader.slice(0, sep).toLowerCase(); if (name === '__proto__') { return this._abort(Boom.badRequest('Invalid header')); } this._headers[name] = this._pendingHeader.slice(sep + 1).trim(); this._pendingHeader = ''; } };