fix(core): Improve header parameter parsing on http client responses (#11953)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-11-28 15:53:18 +01:00 committed by GitHub
parent 439a1cc4f3
commit 41e9e39b5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 178 additions and 23 deletions

View file

@ -675,14 +675,18 @@ function parseHeaderParameters(parameters: string[]): Record<string, string> {
return parameters.reduce( return parameters.reduce(
(acc, param) => { (acc, param) => {
const [key, value] = param.split('='); const [key, value] = param.split('=');
acc[key.toLowerCase().trim()] = decodeURIComponent(value); let decodedValue = decodeURIComponent(value).trim();
if (decodedValue.startsWith('"') && decodedValue.endsWith('"')) {
decodedValue = decodedValue.slice(1, -1);
}
acc[key.toLowerCase().trim()] = decodedValue;
return acc; return acc;
}, },
{} as Record<string, string>, {} as Record<string, string>,
); );
} }
function parseContentType(contentType?: string): IContentType | null { export function parseContentType(contentType?: string): IContentType | null {
if (!contentType) { if (!contentType) {
return null; return null;
} }
@ -695,22 +699,7 @@ function parseContentType(contentType?: string): IContentType | null {
}; };
} }
function parseFileName(filename?: string): string | undefined { export function parseContentDisposition(contentDisposition?: string): IContentDisposition | null {
if (filename?.startsWith('"') && filename?.endsWith('"')) {
return filename.slice(1, -1);
}
return filename;
}
// https://datatracker.ietf.org/doc/html/rfc5987
function parseFileNameStar(filename?: string): string | undefined {
const [_encoding, _locale, content] = parseFileName(filename)?.split("'") ?? [];
return content;
}
function parseContentDisposition(contentDisposition?: string): IContentDisposition | null {
if (!contentDisposition) { if (!contentDisposition) {
return null; return null;
} }
@ -725,11 +714,15 @@ function parseContentDisposition(contentDisposition?: string): IContentDispositi
const parsedParameters = parseHeaderParameters(parameters); const parsedParameters = parseHeaderParameters(parameters);
return { let { filename } = parsedParameters;
type, const wildcard = parsedParameters['filename*'];
filename: if (wildcard) {
parseFileNameStar(parsedParameters['filename*']) ?? parseFileName(parsedParameters.filename), // https://datatracker.ietf.org/doc/html/rfc5987
}; const [_encoding, _locale, content] = wildcard?.split("'") ?? [];
filename = content;
}
return { type, filename };
} }
export function parseIncomingMessage(message: IncomingMessage) { export function parseIncomingMessage(message: IncomingMessage) {

View file

@ -27,6 +27,8 @@ import {
copyInputItems, copyInputItems,
getBinaryDataBuffer, getBinaryDataBuffer,
isFilePathBlocked, isFilePathBlocked,
parseContentDisposition,
parseContentType,
parseIncomingMessage, parseIncomingMessage,
parseRequestObject, parseRequestObject,
proxyRequestToAxios, proxyRequestToAxios,
@ -150,6 +152,152 @@ describe('NodeExecuteFunctions', () => {
}); });
}); });
describe('parseContentType', () => {
const testCases = [
{
input: 'text/plain',
expected: {
type: 'text/plain',
parameters: {
charset: 'utf-8',
},
},
description: 'should parse basic content type',
},
{
input: 'TEXT/PLAIN',
expected: {
type: 'text/plain',
parameters: {
charset: 'utf-8',
},
},
description: 'should convert type to lowercase',
},
{
input: 'text/html; charset=iso-8859-1',
expected: {
type: 'text/html',
parameters: {
charset: 'iso-8859-1',
},
},
description: 'should parse content type with charset',
},
{
input: 'application/json; charset=utf-8; boundary=---123',
expected: {
type: 'application/json',
parameters: {
charset: 'utf-8',
boundary: '---123',
},
},
description: 'should parse content type with multiple parameters',
},
{
input: 'text/plain; charset="utf-8"; filename="test.txt"',
expected: {
type: 'text/plain',
parameters: {
charset: 'utf-8',
filename: 'test.txt',
},
},
description: 'should handle quoted parameter values',
},
{
input: 'text/plain; filename=%22test%20file.txt%22',
expected: {
type: 'text/plain',
parameters: {
charset: 'utf-8',
filename: 'test file.txt',
},
},
description: 'should handle encoded parameter values',
},
{
input: undefined,
expected: null,
description: 'should return null for undefined input',
},
{
input: '',
expected: null,
description: 'should return null for empty string',
},
];
test.each(testCases)('$description', ({ input, expected }) => {
expect(parseContentType(input)).toEqual(expected);
});
});
describe('parseContentDisposition', () => {
const testCases = [
{
input: 'attachment; filename="file.txt"',
expected: { type: 'attachment', filename: 'file.txt' },
description: 'should parse basic content disposition',
},
{
input: 'attachment; filename=file.txt',
expected: { type: 'attachment', filename: 'file.txt' },
description: 'should parse filename without quotes',
},
{
input: 'inline; filename="image.jpg"',
expected: { type: 'inline', filename: 'image.jpg' },
description: 'should parse inline disposition',
},
{
input: 'attachment; filename="my file.pdf"',
expected: { type: 'attachment', filename: 'my file.pdf' },
description: 'should parse filename with spaces',
},
{
input: "attachment; filename*=UTF-8''my%20file.txt",
expected: { type: 'attachment', filename: 'my file.txt' },
description: 'should parse filename* parameter (RFC 5987)',
},
{
input: 'filename="test.txt"',
expected: { type: 'attachment', filename: 'test.txt' },
description: 'should handle invalid syntax but with filename',
},
{
input: 'filename=test.txt',
expected: { type: 'attachment', filename: 'test.txt' },
description: 'should handle invalid syntax with only filename parameter',
},
{
input: undefined,
expected: null,
description: 'should return null for undefined input',
},
{
input: '',
expected: null,
description: 'should return null for empty string',
},
{
input: 'attachment; filename="%F0%9F%98%80.txt"',
expected: { type: 'attachment', filename: '😀.txt' },
description: 'should handle encoded filenames',
},
{
input: 'attachment; size=123; filename="test.txt"; creation-date="Thu, 1 Jan 2020"',
expected: { type: 'attachment', filename: 'test.txt' },
description: 'should handle multiple parameters',
},
];
test.each(testCases)('$description', ({ input, expected }) => {
expect(parseContentDisposition(input)).toEqual(expected);
});
});
describe('parseIncomingMessage', () => { describe('parseIncomingMessage', () => {
it('parses valid content-type header', () => { it('parses valid content-type header', () => {
const message = mock<IncomingMessage>({ const message = mock<IncomingMessage>({
@ -170,6 +318,20 @@ describe('NodeExecuteFunctions', () => {
parseIncomingMessage(message); parseIncomingMessage(message);
expect(message.contentType).toEqual('application/json'); expect(message.contentType).toEqual('application/json');
expect(message.encoding).toEqual('utf-8');
});
it('parses valid content-type header with encoding wrapped in quotes', () => {
const message = mock<IncomingMessage>({
headers: {
'content-type': 'application/json; charset="utf-8"',
'content-disposition': undefined,
},
});
parseIncomingMessage(message);
expect(message.contentType).toEqual('application/json');
expect(message.encoding).toEqual('utf-8');
}); });
it('parses valid content-disposition header with filename*', () => { it('parses valid content-disposition header with filename*', () => {