Files
f0ckm/src/inc/multipart.mjs

105 lines
3.8 KiB
JavaScript

/**
* Shared Multi-part Form Data Parsing Utilities
*/
/**
* Native multipart form data parser
* @param {Buffer} buffer The raw request body buffer
* @param {string} boundary The boundary string from the Content-Type header
* @returns {Object} Parsed form parts
*/
export const parseMultipart = (buffer, boundary) => {
const parts = {};
const boundaryBuffer = Buffer.from(`--${boundary}`);
const segments = [];
let start = 0;
let idx;
while ((idx = buffer.indexOf(boundaryBuffer, start)) !== -1) {
if (start !== 0) {
segments.push(buffer.slice(start, idx - 2)); // -2 for \r\n before boundary
}
start = idx + boundaryBuffer.length + 2; // +2 for \r\n after boundary
}
for (const segment of segments) {
const headerEnd = segment.indexOf('\r\n\r\n');
if (headerEnd === -1) continue;
const headers = segment.slice(0, headerEnd).toString();
const body = segment.slice(headerEnd + 4);
const nameMatch = headers.match(/name="([^"]+)"/);
// Robust filename extraction:
// 1. RFC 5987: filename*=UTF-8''encoded-name (used by modern browsers for non-ASCII)
// 2. Standard: filename="name" (ASCII)
// 3. Fallback: filename=name (unquoted)
let extractedFilename = null;
const filenameStarMatch = headers.match(/filename\*\s*=\s*[Uu][Tt][Ff]-8''([^\r\n;]+)/i);
if (filenameStarMatch) {
try { extractedFilename = decodeURIComponent(filenameStarMatch[1].trim()); } catch (e) { extractedFilename = filenameStarMatch[1].trim(); }
} else {
const filenameQuotedMatch = headers.match(/filename="((?:[^"\\]|\\.)*)"/);
if (filenameQuotedMatch) {
extractedFilename = filenameQuotedMatch[1].replace(/\\(.)/g, '$1');
} else {
const filenameUnquotedMatch = headers.match(/filename=([^\r\n;]+)/);
if (filenameUnquotedMatch) extractedFilename = filenameUnquotedMatch[1].trim();
}
}
const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i);
if (nameMatch) {
const name = nameMatch[1];
if (extractedFilename !== null) {
parts[name] = {
filename: extractedFilename,
contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream',
data: body
};
} else {
parts[name] = body.toString().trim();
}
}
}
return parts;
};
/**
* Collects the raw request body buffer
* @param {IncomingMessage} req
* @param {number} maxBytes
* @returns {Promise<Buffer>}
*/
export const collectBody = (req, maxBytes = 157286400) => { // Default to 150MB (matching maxfilesize)
return new Promise((resolve, reject) => {
const chunks = [];
let received = 0;
// Fast-path: reject immediately if Content-Length header already exceeds limit
const declaredLength = parseInt(req.headers['content-length'] || '0', 10);
if (declaredLength > maxBytes) {
console.error(`[BODY_TOO_LARGE] Rejection via Content-Length: ${declaredLength} > ${maxBytes}`);
req.resume();
return reject(Object.assign(new Error('Request body too large'), { code: 'BODY_TOO_LARGE' }));
}
req.on('data', chunk => {
received += chunk.length;
if (received > maxBytes) {
req.destroy();
return reject(Object.assign(new Error('Request body too large'), { code: 'BODY_TOO_LARGE' }));
}
chunks.push(chunk);
});
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
if (req.isPaused()) req.resume();
});
};