From 506eb2fc15589e6c14069f94cf459d1008b855e8 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 6 Feb 2024 15:50:03 +0100 Subject: [PATCH 1/3] Use hostname from URL instead of Host header for SNI --- packages/core/src/NodeExecuteFunctions.ts | 127 +++++++++++++----- .../core/test/NodeExecuteFunctions.test.ts | 12 ++ 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 97b5cf62d7545..d90b460c9af6f 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -31,7 +31,7 @@ import FormData from 'form-data'; import { createReadStream } from 'fs'; import { access as fsAccess, writeFile as fsWriteFile } from 'fs/promises'; import { IncomingMessage, type IncomingHttpHeaders } from 'http'; -import { Agent } from 'https'; +import { Agent, type AgentOptions } from 'https'; import get from 'lodash/get'; import pick from 'lodash/pick'; import { extension, lookup } from 'mime-types'; @@ -229,7 +229,54 @@ async function generateContentLengthHeader(config: AxiosRequestConfig) { } } -async function parseRequestObject(requestObject: IDataObject) { +type RequestObject = Partial<{ + baseURL: string; + uri: string; + url: string; + method: Method; + qs: Record; + qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' }; + useQuerystring: boolean; + headers: Record; + auth: Partial<{ + sendImmediately: boolean; + bearer: string; + user: string; + username: string; + password: string; + pass: string; + }>; + body: unknown; + formData: FormData; + form: FormData; + json: boolean; + useStream: boolean; + encoding: string; + followRedirect: boolean; + followAllRedirects: boolean; + timeout: number; + rejectUnauthorized: boolean; + proxy: string | AxiosProxyConfig; + simple: boolean; + resolveWithFullResponse: boolean; +}>; + +const getHostFromRequestObject = ( + requestObject: Partial<{ + url: string; + uri: string; + baseURL: string; + }>, +): string | null => { + try { + const url = (requestObject.url ?? requestObject.uri) as string; + return new URL(url, requestObject.baseURL).hostname; + } catch (error) { + return null; + } +}; + +export async function parseRequestObject(requestObject: RequestObject) { // This function is a temporary implementation // That translates all http requests done via // the request library to axios directly @@ -343,28 +390,28 @@ async function parseRequestObject(requestObject: IDataObject) { } if (requestObject.uri !== undefined) { - axiosConfig.url = requestObject.uri?.toString() as string; + axiosConfig.url = requestObject.uri?.toString(); } if (requestObject.url !== undefined) { - axiosConfig.url = requestObject.url?.toString() as string; + axiosConfig.url = requestObject.url?.toString(); } if (requestObject.baseURL !== undefined) { - axiosConfig.baseURL = requestObject.baseURL?.toString() as string; + axiosConfig.baseURL = requestObject.baseURL?.toString(); } if (requestObject.method !== undefined) { - axiosConfig.method = requestObject.method as Method; + axiosConfig.method = requestObject.method; } if (requestObject.qs !== undefined && Object.keys(requestObject.qs as object).length > 0) { - axiosConfig.params = requestObject.qs as IDataObject; + axiosConfig.params = requestObject.qs; } function hasArrayFormatOptions( - arg: IDataObject, - ): arg is IDataObject & { qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' } } { + arg: RequestObject, + ): arg is RequestObject & { qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' } } { if ( typeof arg.qsStringifyOptions === 'object' && arg.qsStringifyOptions !== null && @@ -402,13 +449,13 @@ async function parseRequestObject(requestObject: IDataObject) { if (requestObject.auth !== undefined) { // Check support for sendImmediately - if ((requestObject.auth as IDataObject).bearer !== undefined) { + if (requestObject.auth.bearer !== undefined) { axiosConfig.headers = Object.assign(axiosConfig.headers || {}, { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Authorization: `Bearer ${(requestObject.auth as IDataObject).bearer}`, + Authorization: `Bearer ${requestObject.auth.bearer}`, }); } else { - const authObj = requestObject.auth as IDataObject; + const authObj = requestObject.auth; // Request accepts both user/username and pass/password axiosConfig.auth = { username: (authObj.user || authObj.username) as string, @@ -452,6 +499,7 @@ async function parseRequestObject(requestObject: IDataObject) { axiosConfig.maxRedirects = 0; } +<<<<<<< Updated upstream axiosConfig.beforeRedirect = (redirectedRequest) => { if (axiosConfig.headers?.Authorization) { redirectedRequest.headers.Authorization = axiosConfig.headers.Authorization; @@ -466,10 +514,21 @@ async function parseRequestObject(requestObject: IDataObject) { rejectUnauthorized: false, secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, }); +======= + const host = getHostFromRequestObject(requestObject); + const agentOptions: AgentOptions = {}; + if (host) { + agentOptions.servername = host; +>>>>>>> Stashed changes + } + if (requestObject.rejectUnauthorized === false) { + agentOptions.rejectUnauthorized = false; + agentOptions.secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT; } + axiosConfig.httpsAgent = new Agent(agentOptions); if (requestObject.timeout !== undefined) { - axiosConfig.timeout = requestObject.timeout as number; + axiosConfig.timeout = requestObject.timeout; } if (requestObject.proxy !== undefined) { @@ -528,7 +587,7 @@ async function parseRequestObject(requestObject: IDataObject) { } } } else { - axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig; + axiosConfig.proxy = requestObject.proxy; } } @@ -627,12 +686,6 @@ function digestAuthAxiosConfig( return axiosConfig; } -type ConfigObject = { - auth?: { sendImmediately: boolean }; - resolveWithFullResponse?: boolean; - simple?: boolean; -}; - interface IContentType { type: string; parameters: { @@ -725,21 +778,18 @@ export async function proxyRequestToAxios( workflow: Workflow | undefined, additionalData: IWorkflowExecuteAdditionalData | undefined, node: INode | undefined, - uriOrObject: string | object, - options?: object, + uriOrObject: string | RequestObject, + options?: RequestObject, ): Promise { let axiosConfig: AxiosRequestConfig = { maxBodyLength: Infinity, maxContentLength: Infinity, }; - let configObject: ConfigObject; - if (uriOrObject !== undefined && typeof uriOrObject === 'string') { - axiosConfig.url = uriOrObject; - } - if (uriOrObject !== undefined && typeof uriOrObject === 'object') { - configObject = uriOrObject; + let configObject: RequestObject; + if (typeof uriOrObject === 'string') { + configObject = { uri: uriOrObject, ...options }; } else { - configObject = options || {}; + configObject = uriOrObject ?? {}; } axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject)); @@ -859,11 +909,15 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest axiosRequest.responseType = n8nRequest.encoding; } + const host = getHostFromRequestObject(n8nRequest); + const agentOptions: AgentOptions = {}; + if (host) { + agentOptions.servername = host; + } if (n8nRequest.skipSslCertificateValidation === true) { - axiosRequest.httpsAgent = new Agent({ - rejectUnauthorized: false, - }); + agentOptions.rejectUnauthorized = false; } + axiosRequest.httpsAgent = new Agent(agentOptions); if (n8nRequest.arrayFormat !== undefined) { axiosRequest.paramsSerializer = (params) => { @@ -1797,7 +1851,12 @@ export async function requestWithAuthentication( workflow, node, ); - return await proxyRequestToAxios(workflow, additionalData, node, requestOptions as IDataObject); + return await proxyRequestToAxios( + workflow, + additionalData, + node, + requestOptions as RequestObject, + ); } catch (error) { try { if (credentialsDecrypted !== undefined) { @@ -1826,7 +1885,7 @@ export async function requestWithAuthentication( workflow, additionalData, node, - requestOptions as IDataObject, + requestOptions as RequestObject, ); } } diff --git a/packages/core/test/NodeExecuteFunctions.test.ts b/packages/core/test/NodeExecuteFunctions.test.ts index 76c9ff8c89f03..7f8fe4296c5a0 100644 --- a/packages/core/test/NodeExecuteFunctions.test.ts +++ b/packages/core/test/NodeExecuteFunctions.test.ts @@ -2,6 +2,7 @@ import { copyInputItems, getBinaryDataBuffer, parseIncomingMessage, + parseRequestObject, proxyRequestToAxios, setBinaryDataBuffer, } from '@/NodeExecuteFunctions'; @@ -21,6 +22,7 @@ import nock from 'nock'; import { tmpdir } from 'os'; import { join } from 'path'; import Container from 'typedi'; +import type { Agent } from 'https'; const temporaryDir = mkdtempSync(join(tmpdir(), 'n8n')); @@ -358,6 +360,16 @@ describe('NodeExecuteFunctions', () => { }); }); + describe('parseRequestObject', () => { + test('should not use Host header for SNI', async () => { + const axiosOptions = await parseRequestObject({ + url: 'https://example.de/foo/bar', + headers: { Host: 'other.host.com' }, + }); + expect((axiosOptions.httpsAgent as Agent).options.servername).toEqual('example.de'); + }); + }); + describe('copyInputItems', () => { it('should pick only selected properties', () => { const output = copyInputItems( From dd0f5d7b64bbe8f25069faaec93311cf0ca50ff5 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 6 Feb 2024 16:07:38 +0100 Subject: [PATCH 2/3] Fix conflict --- packages/core/src/NodeExecuteFunctions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index d90b460c9af6f..01291ccea914d 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -499,7 +499,6 @@ export async function parseRequestObject(requestObject: RequestObject) { axiosConfig.maxRedirects = 0; } -<<<<<<< Updated upstream axiosConfig.beforeRedirect = (redirectedRequest) => { if (axiosConfig.headers?.Authorization) { redirectedRequest.headers.Authorization = axiosConfig.headers.Authorization; @@ -514,12 +513,12 @@ export async function parseRequestObject(requestObject: RequestObject) { rejectUnauthorized: false, secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, }); -======= + } + const host = getHostFromRequestObject(requestObject); const agentOptions: AgentOptions = {}; if (host) { agentOptions.servername = host; ->>>>>>> Stashed changes } if (requestObject.rejectUnauthorized === false) { agentOptions.rejectUnauthorized = false; From 0bed864643418b0340044e28c7bd8d27ce4aefd7 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 6 Feb 2024 16:13:26 +0100 Subject: [PATCH 3/3] Remove type changes --- packages/core/src/NodeExecuteFunctions.ts | 79 +++++++---------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 01291ccea914d..9ae33f6b963e3 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -229,38 +229,6 @@ async function generateContentLengthHeader(config: AxiosRequestConfig) { } } -type RequestObject = Partial<{ - baseURL: string; - uri: string; - url: string; - method: Method; - qs: Record; - qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' }; - useQuerystring: boolean; - headers: Record; - auth: Partial<{ - sendImmediately: boolean; - bearer: string; - user: string; - username: string; - password: string; - pass: string; - }>; - body: unknown; - formData: FormData; - form: FormData; - json: boolean; - useStream: boolean; - encoding: string; - followRedirect: boolean; - followAllRedirects: boolean; - timeout: number; - rejectUnauthorized: boolean; - proxy: string | AxiosProxyConfig; - simple: boolean; - resolveWithFullResponse: boolean; -}>; - const getHostFromRequestObject = ( requestObject: Partial<{ url: string; @@ -276,7 +244,7 @@ const getHostFromRequestObject = ( } }; -export async function parseRequestObject(requestObject: RequestObject) { +export async function parseRequestObject(requestObject: IDataObject) { // This function is a temporary implementation // That translates all http requests done via // the request library to axios directly @@ -390,28 +358,28 @@ export async function parseRequestObject(requestObject: RequestObject) { } if (requestObject.uri !== undefined) { - axiosConfig.url = requestObject.uri?.toString(); + axiosConfig.url = requestObject.uri?.toString() as string; } if (requestObject.url !== undefined) { - axiosConfig.url = requestObject.url?.toString(); + axiosConfig.url = requestObject.url?.toString() as string; } if (requestObject.baseURL !== undefined) { - axiosConfig.baseURL = requestObject.baseURL?.toString(); + axiosConfig.baseURL = requestObject.baseURL?.toString() as string; } if (requestObject.method !== undefined) { - axiosConfig.method = requestObject.method; + axiosConfig.method = requestObject.method as Method; } if (requestObject.qs !== undefined && Object.keys(requestObject.qs as object).length > 0) { - axiosConfig.params = requestObject.qs; + axiosConfig.params = requestObject.qs as IDataObject; } function hasArrayFormatOptions( - arg: RequestObject, - ): arg is RequestObject & { qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' } } { + arg: IDataObject, + ): arg is IDataObject & { qsStringifyOptions: { arrayFormat: 'repeat' | 'brackets' } } { if ( typeof arg.qsStringifyOptions === 'object' && arg.qsStringifyOptions !== null && @@ -449,13 +417,13 @@ export async function parseRequestObject(requestObject: RequestObject) { if (requestObject.auth !== undefined) { // Check support for sendImmediately - if (requestObject.auth.bearer !== undefined) { + if ((requestObject.auth as IDataObject).bearer !== undefined) { axiosConfig.headers = Object.assign(axiosConfig.headers || {}, { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Authorization: `Bearer ${requestObject.auth.bearer}`, + Authorization: `Bearer ${(requestObject.auth as IDataObject).bearer}`, }); } else { - const authObj = requestObject.auth; + const authObj = requestObject.auth as IDataObject; // Request accepts both user/username and pass/password axiosConfig.auth = { username: (authObj.user || authObj.username) as string, @@ -527,7 +495,7 @@ export async function parseRequestObject(requestObject: RequestObject) { axiosConfig.httpsAgent = new Agent(agentOptions); if (requestObject.timeout !== undefined) { - axiosConfig.timeout = requestObject.timeout; + axiosConfig.timeout = requestObject.timeout as number; } if (requestObject.proxy !== undefined) { @@ -586,7 +554,7 @@ export async function parseRequestObject(requestObject: RequestObject) { } } } else { - axiosConfig.proxy = requestObject.proxy; + axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig; } } @@ -685,6 +653,12 @@ function digestAuthAxiosConfig( return axiosConfig; } +type ConfigObject = { + auth?: { sendImmediately: boolean }; + resolveWithFullResponse?: boolean; + simple?: boolean; +}; + interface IContentType { type: string; parameters: { @@ -777,14 +751,14 @@ export async function proxyRequestToAxios( workflow: Workflow | undefined, additionalData: IWorkflowExecuteAdditionalData | undefined, node: INode | undefined, - uriOrObject: string | RequestObject, - options?: RequestObject, + uriOrObject: string | object, + options?: object, ): Promise { let axiosConfig: AxiosRequestConfig = { maxBodyLength: Infinity, maxContentLength: Infinity, }; - let configObject: RequestObject; + let configObject: ConfigObject & { uri?: string }; if (typeof uriOrObject === 'string') { configObject = { uri: uriOrObject, ...options }; } else { @@ -1850,12 +1824,7 @@ export async function requestWithAuthentication( workflow, node, ); - return await proxyRequestToAxios( - workflow, - additionalData, - node, - requestOptions as RequestObject, - ); + return await proxyRequestToAxios(workflow, additionalData, node, requestOptions as IDataObject); } catch (error) { try { if (credentialsDecrypted !== undefined) { @@ -1884,7 +1853,7 @@ export async function requestWithAuthentication( workflow, additionalData, node, - requestOptions as RequestObject, + requestOptions as IDataObject, ); } }