Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(HTTP Request Node): Option to provide SSL Certificates in Http Request Node #9125

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
837e308
:zap: setup
michael-radency Apr 11, 2024
fe52c1c
:zap: set undefined not empty strings
michael-radency Apr 11, 2024
e886bcf
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 11, 2024
caaa904
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 13, 2024
125804c
:zap: separate option to provide ssl certificates
michael-radency Apr 13, 2024
b95fa09
:zap: ui update
michael-radency Apr 15, 2024
3ca6d15
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 15, 2024
14dbf3d
:zap: ui update
michael-radency Apr 15, 2024
3ea9b3d
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 16, 2024
106b6a6
:zap: setAgentOptions utility and tests
michael-radency Apr 16, 2024
a3689f8
:zap: parseRequestObject test case
michael-radency Apr 16, 2024
a3b6dd8
Merge remote-tracking branch 'origin/master' into node-1153-http-node…
netroy Apr 18, 2024
17e19cb
make it work with redirects
netroy Apr 18, 2024
20245dc
don't use actual certs in tests. they trigger secret-scanning tools
netroy Apr 18, 2024
cbba2a8
lintfix
netroy Apr 18, 2024
2ee9060
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 23, 2024
3867deb
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1153…
michael-radency Apr 24, 2024
96a0ff4
:zap: moved toggle into node settings
michael-radency Apr 24, 2024
916d38f
Merge branch 'node-1153-http-node-add-client-certificate-support' of …
michael-radency Apr 24, 2024
b192fdc
:zap: notice parameter renamed
michael-radency Apr 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,14 +497,15 @@ export async function parseRequestObject(requestObject: IRequestOptions) {
}

const host = getHostFromRequestObject(requestObject);
const agentOptions: AgentOptions = {};
const agentOptions: AgentOptions = { ...requestObject.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);

axiosConfig.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosConfig);
Expand Down
37 changes: 37 additions & 0 deletions packages/core/test/NodeExecuteFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SecureContextOptions } from 'tls';
import {
cleanupParameterData,
copyInputItems,
Expand Down Expand Up @@ -387,6 +388,42 @@ describe('NodeExecuteFunctions', () => {
expect((axiosOptions.httpsAgent as Agent).options.servername).toEqual('example.de');
});

describe('should set SSL certificates', () => {
const agentOptions: SecureContextOptions = {
ca: '-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----',
};
const requestObject: IRequestOptions = {
method: 'GET',
uri: 'https://example.de',
agentOptions,
};

test('on regular requests', async () => {
const axiosOptions = await parseRequestObject(requestObject);
expect((axiosOptions.httpsAgent as Agent).options).toEqual({
servername: 'example.de',
...agentOptions,
noDelay: true,
path: null,
});
});

test('on redirected requests', async () => {
const axiosOptions = await parseRequestObject(requestObject);
expect(axiosOptions.beforeRedirect).toBeDefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const redirectOptions: Record<string, any> = { agents: {}, hostname: 'example.de' };
axiosOptions.beforeRedirect!(redirectOptions, mock());
expect(redirectOptions.agent).toEqual(redirectOptions.agents.https);
expect((redirectOptions.agent as Agent).options).toEqual({
servername: 'example.de',
...agentOptions,
noDelay: true,
path: null,
});
});
});

describe('when followRedirect is true', () => {
test.each(['GET', 'HEAD'] as IHttpRequestMethods[])(
'should set maxRedirects on %s ',
Expand Down
54 changes: 54 additions & 0 deletions packages/nodes-base/credentials/HttpSslAuth.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable n8n-nodes-base/cred-class-name-unsuffixed */
/* eslint-disable n8n-nodes-base/cred-class-field-name-unsuffixed */
import type { ICredentialType, INodeProperties } from 'n8n-workflow';

export class HttpSslAuth implements ICredentialType {
name = 'httpSslAuth';

displayName = 'SSL Certificates';

documentationUrl = 'httpRequest';

icon = 'node:n8n-nodes-base.httpRequest';

properties: INodeProperties[] = [
{
displayName: 'CA',
name: 'ca',
type: 'string',
description: 'Certificate Authority certificate',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Certificate',
name: 'cert',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Private Key',
name: 'key',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Passphrase',
name: 'passphrase',
type: 'string',
description: 'Optional passphrase for the private key, if the private key is encrypted',
typeOptions: {
password: true,
},
default: '',
},
];
}
18 changes: 18 additions & 0 deletions packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SecureContextOptions } from 'tls';
import type {
IDataObject,
INodeExecutionData,
Expand All @@ -8,6 +9,8 @@ import type {
import set from 'lodash/set';

import FormData from 'form-data';
import type { HttpSslAuthCredentials } from './interfaces';
import { formatPrivateKey } from '../../utils/utilities';

export type BodyParameter = {
name: string;
Expand Down Expand Up @@ -194,3 +197,18 @@ export const prepareRequestBody = async (
return await reduceAsync(parameters, defaultReducer);
}
};

export const setAgentOptions = (
requestOptions: IRequestOptions,
sslCertificates: HttpSslAuthCredentials | undefined,
) => {
if (sslCertificates) {
const agentOptions: SecureContextOptions = {};
if (sslCertificates.ca) agentOptions.ca = formatPrivateKey(sslCertificates.ca);
if (sslCertificates.cert) agentOptions.cert = formatPrivateKey(sslCertificates.cert);
if (sslCertificates.key) agentOptions.key = formatPrivateKey(sslCertificates.key);
if (sslCertificates.passphrase)
agentOptions.passphrase = formatPrivateKey(sslCertificates.passphrase);
requestOptions.agentOptions = agentOptions;
}
};
65 changes: 64 additions & 1 deletion packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import {
reduceAsync,
replaceNullValues,
sanitizeUiMessage,
setAgentOptions,
} from '../GenericFunctions';
import { keysToLowercase } from '@utils/utilities';
import type { HttpSslAuthCredentials } from '../interfaces';

function toText<T>(data: T) {
if (typeof data === 'object' && data !== null) {
Expand All @@ -56,7 +58,17 @@ export class HttpRequestV3 implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
credentials: [],
credentials: [
{
name: 'httpSslAuth',
required: true,
displayOptions: {
show: {
provideSslCertificates: [true],
},
},
},
],
properties: [
{
displayName: '',
Expand Down Expand Up @@ -173,6 +185,36 @@ export class HttpRequestV3 implements INodeType {
},
},
},
{
displayName: 'SSL Certificates',
name: 'provideSslCertificates',
type: 'boolean',
default: false,
isNodeSetting: true,
},
{
displayName: "Provide certificates in node's 'Credential for SSL Certificates' parameter",
name: 'provideSslCertificatesNotice',
type: 'notice',
default: '',
isNodeSetting: true,
displayOptions: {
show: {
provideSslCertificates: [true],
},
},
},
{
displayName: 'SSL Certificate',
name: 'sslCertificate',
type: 'credentials',
default: '',
displayOptions: {
show: {
provideSslCertificates: [true],
},
},
},
{
displayName: 'Send Query Parameters',
name: 'sendQuery',
Expand Down Expand Up @@ -1221,6 +1263,7 @@ export class HttpRequestV3 implements INodeType {
let httpCustomAuth;
let oAuth1Api;
let oAuth2Api;
let sslCertificates;
let nodeCredentialType: string | undefined;
let genericCredentialType: string | undefined;

Expand Down Expand Up @@ -1280,6 +1323,19 @@ export class HttpRequestV3 implements INodeType {
nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex) as string;
}

const provideSslCertificates = this.getNodeParameter(
'provideSslCertificates',
itemIndex,
false,
);

if (provideSslCertificates) {
sslCertificates = (await this.getCredentials(
'httpSslAuth',
itemIndex,
)) as HttpSslAuthCredentials;
}

const requestMethod = this.getNodeParameter('method', itemIndex) as IHttpRequestMethods;

const sendQuery = this.getNodeParameter('sendQuery', itemIndex, false) as boolean;
Expand Down Expand Up @@ -1575,6 +1631,12 @@ export class HttpRequestV3 implements INodeType {

const authDataKeys: IAuthDataSanitizeKeys = {};

// Add SSL certificates if any are set
setAgentOptions(requestOptions, sslCertificates);
if (requestOptions.agentOptions) {
authDataKeys.agentOptions = Object.keys(requestOptions.agentOptions);
}

// Add credentials if any are set
if (httpBasicAuth !== undefined) {
requestOptions.auth = {
Expand All @@ -1594,6 +1656,7 @@ export class HttpRequestV3 implements INodeType {
requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value;
authDataKeys.qs = [httpQueryAuth.name as string];
}

if (httpDigestAuth !== undefined) {
requestOptions.auth = {
user: httpDigestAuth.user as string,
Expand Down
6 changes: 6 additions & 0 deletions packages/nodes-base/nodes/HttpRequest/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type HttpSslAuthCredentials = {
ca?: string;
cert?: string;
key?: string;
passphrase?: string;
};
42 changes: 41 additions & 1 deletion packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prepareRequestBody } from '../../GenericFunctions';
import type { IRequestOptions } from 'n8n-workflow';
import { prepareRequestBody, setAgentOptions } from '../../GenericFunctions';
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';

describe('HTTP Node Utils, prepareRequestBody', () => {
Expand Down Expand Up @@ -33,3 +34,42 @@ describe('HTTP Node Utils, prepareRequestBody', () => {
expect(result).toEqual({ foo: { bar: { spam: 'baz' } } });
});
});

describe('HTTP Node Utils, setAgentOptions', () => {
it("should not have agentOptions as it's undefined", async () => {
const requestOptions: IRequestOptions = {
method: 'GET',
uri: 'https://example.com',
};

const sslCertificates = undefined;

setAgentOptions(requestOptions, sslCertificates);

expect(requestOptions).toEqual({
method: 'GET',
uri: 'https://example.com',
});
});

it('should have agentOptions set', async () => {
const requestOptions: IRequestOptions = {
method: 'GET',
uri: 'https://example.com',
};

const sslCertificates = {
ca: 'mock-ca',
};

setAgentOptions(requestOptions, sslCertificates);

expect(requestOptions).toStrictEqual({
method: 'GET',
uri: 'https://example.com',
agentOptions: {
ca: 'mock-ca',
},
});
});
});
1 change: 1 addition & 0 deletions packages/nodes-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"dist/credentials/HttpHeaderAuth.credentials.js",
"dist/credentials/HttpCustomAuth.credentials.js",
"dist/credentials/HttpQueryAuth.credentials.js",
"dist/credentials/HttpSslAuth.credentials.js",
"dist/credentials/HubspotApi.credentials.js",
"dist/credentials/HubspotAppToken.credentials.js",
"dist/credentials/HubspotDeveloperApi.credentials.js",
Expand Down
3 changes: 3 additions & 0 deletions packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type * as express from 'express';
import type FormData from 'form-data';
import type { PathLike } from 'fs';
import type { IncomingHttpHeaders } from 'http';
import type { SecureContextOptions } from 'tls';
import type { Readable } from 'stream';
import type { URLSearchParams } from 'url';

Expand Down Expand Up @@ -547,6 +548,8 @@ export interface IRequestOptions {

/** Max number of redirects to follow @default 21 */
maxRedirects?: number;

agentOptions?: SecureContextOptions;
}

export interface PaginationOptions {
Expand Down
Loading