parse-multipart-data
Version:
A javascript/nodejs multipart/form-data parser which operates on raw data.
194 lines • 7.43 kB
JavaScript
;
/**
* Multipart Parser (Finite State Machine)
* usage:
* const multipart = require('./multipart.js');
* const body = multipart.DemoData(); // raw body
* const body = Buffer.from(event['body-json'].toString(),'base64'); // AWS case
* const boundary = multipart.getBoundary(event.params.header['content-type']);
* const parts = multipart.Parse(body,boundary);
* each part is:
* { filename: 'A.txt', type: 'text/plain', data: <Buffer 41 41 41 41 42 42 42 42> }
* or { name: 'key', data: <Buffer 41 41 41 41 42 42 42 42> }
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DemoData = exports.getBoundary = exports.parse = void 0;
var ParsingState;
(function (ParsingState) {
ParsingState[ParsingState["INIT"] = 0] = "INIT";
ParsingState[ParsingState["READING_HEADERS"] = 1] = "READING_HEADERS";
ParsingState[ParsingState["READING_DATA"] = 2] = "READING_DATA";
ParsingState[ParsingState["READING_PART_SEPARATOR"] = 3] = "READING_PART_SEPARATOR";
})(ParsingState || (ParsingState = {}));
function parse(multipartBodyBuffer, boundary) {
var lastline = '';
var contentDispositionHeader = '';
var contentTypeHeader = '';
var state = ParsingState.INIT;
var buffer = [];
var allParts = [];
var currentPartHeaders = [];
for (var i = 0; i < multipartBodyBuffer.length; i++) {
var oneByte = multipartBodyBuffer[i];
var prevByte = i > 0 ? multipartBodyBuffer[i - 1] : null;
// 0x0a => \n
// 0x0d => \r
var newLineDetected = oneByte === 0x0a && prevByte === 0x0d;
var newLineChar = oneByte === 0x0a || oneByte === 0x0d;
if (!newLineChar)
lastline += String.fromCharCode(oneByte);
if (ParsingState.INIT === state && newLineDetected) {
// searching for boundary
if ('--' + boundary === lastline) {
state = ParsingState.READING_HEADERS; // found boundary. start reading headers
}
lastline = '';
}
else if (ParsingState.READING_HEADERS === state && newLineDetected) {
// parsing headers. Headers are separated by an empty line from the content. Stop reading headers when the line is empty
if (lastline.length) {
currentPartHeaders.push(lastline);
}
else {
// found empty line. search for the headers we want and set the values
for (var _i = 0, currentPartHeaders_1 = currentPartHeaders; _i < currentPartHeaders_1.length; _i++) {
var h = currentPartHeaders_1[_i];
if (h.toLowerCase().startsWith('content-disposition:')) {
contentDispositionHeader = h;
}
else if (h.toLowerCase().startsWith('content-type:')) {
contentTypeHeader = h;
}
}
state = ParsingState.READING_DATA;
buffer = [];
}
lastline = '';
}
else if (ParsingState.READING_DATA === state) {
// parsing data
if (lastline.length > boundary.length + 4) {
lastline = ''; // mem save
}
if ('--' + boundary === lastline) {
var j = buffer.length - lastline.length;
var part = buffer.slice(0, j - 1);
allParts.push(process({ contentDispositionHeader: contentDispositionHeader, contentTypeHeader: contentTypeHeader, part: part }));
buffer = [];
currentPartHeaders = [];
lastline = '';
state = ParsingState.READING_PART_SEPARATOR;
contentDispositionHeader = '';
contentTypeHeader = '';
}
else {
buffer.push(oneByte);
}
if (newLineDetected) {
lastline = '';
}
}
else if (ParsingState.READING_PART_SEPARATOR === state) {
if (newLineDetected) {
state = ParsingState.READING_HEADERS;
}
}
}
return allParts;
}
exports.parse = parse;
// read the boundary from the content-type header sent by the http client
// this value may be similar to:
// 'multipart/form-data; boundary=----WebKitFormBoundaryvm5A9tzU1ONaGP5B',
function getBoundary(header) {
var items = header.split(';');
if (items) {
for (var i = 0; i < items.length; i++) {
var item = new String(items[i]).trim();
if (item.indexOf('boundary') >= 0) {
var k = item.split('=');
return new String(k[1]).trim().replace(/^["']|["']$/g, '');
}
}
}
return '';
}
exports.getBoundary = getBoundary;
function DemoData() {
var body = 'trash1\r\n';
body += '------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n';
body += 'Content-Type: text/plain\r\n';
body +=
'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"\r\n';
body += '\r\n';
body += '@11X';
body += '111Y\r\n';
body += '111Z\rCCCC\nCCCC\r\nCCCCC@\r\n\r\n';
body += '------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n';
body += 'Content-Type: text/plain\r\n';
body +=
'Content-Disposition: form-data; name="uploads[]"; filename="B.txt"\r\n';
body += '\r\n';
body += '@22X';
body += '222Y\r\n';
body += '222Z\r222W\n2220\r\n666@\r\n';
body += '------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n';
body += 'Content-Disposition: form-data; name="input1"\r\n';
body += '\r\n';
body += 'value1\r\n';
body += '------WebKitFormBoundaryvef1fLxmoUdYZWXp--\r\n';
return {
body: Buffer.from(body),
boundary: '----WebKitFormBoundaryvef1fLxmoUdYZWXp'
};
}
exports.DemoData = DemoData;
function process(part) {
// will transform this object:
// { header: 'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"',
// info: 'Content-Type: text/plain',
// part: 'AAAABBBB' }
// into this one:
// { filename: 'A.txt', type: 'text/plain', data: <Buffer 41 41 41 41 42 42 42 42> }
var obj = function (str) {
var k = str.split('=');
var a = k[0].trim();
var b = JSON.parse(k[1].trim());
var o = {};
Object.defineProperty(o, a, {
value: b,
writable: true,
enumerable: true,
configurable: true
});
return o;
};
var header = part.contentDispositionHeader.split(';');
var filenameData = header[2];
var input = {};
if (filenameData) {
input = obj(filenameData);
var contentType = part.contentTypeHeader.split(':')[1].trim();
Object.defineProperty(input, 'type', {
value: contentType,
writable: true,
enumerable: true,
configurable: true
});
}
// always process the name field
Object.defineProperty(input, 'name', {
value: header[1].split('=')[1].replace(/"/g, ''),
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(input, 'data', {
value: Buffer.from(part.part),
writable: true,
enumerable: true,
configurable: true
});
return input;
}
//# sourceMappingURL=multipart.js.map