From 7531f343861d91df075b8f3220f5bce8858b117f Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 6 Feb 2024 19:38:36 +0100 Subject: [PATCH] fix(core): Use hostname from URL instead of Host header for SNI (#8562) --- packages/core/src/NodeExecuteFunctions.ts | 51 ++++++++++++++----- .../core/test/NodeExecuteFunctions.test.ts | 12 +++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 97b5cf62d7545..9ae33f6b963e3 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,22 @@ async function generateContentLengthHeader(config: AxiosRequestConfig) { } } -async function parseRequestObject(requestObject: IDataObject) { +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: IDataObject) { // This function is a temporary implementation // That translates all http requests done via // the request library to axios directly @@ -468,6 +483,17 @@ async function parseRequestObject(requestObject: IDataObject) { }); } + const host = getHostFromRequestObject(requestObject); + const agentOptions: AgentOptions = {}; + if (host) { + agentOptions.servername = host; + } + 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; } @@ -732,14 +758,11 @@ export async function proxyRequestToAxios( 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: ConfigObject & { uri?: string }; + if (typeof uriOrObject === 'string') { + configObject = { uri: uriOrObject, ...options }; } else { - configObject = options || {}; + configObject = uriOrObject ?? {}; } axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject)); @@ -859,11 +882,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) => { 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(