Skip to content

Commit 306b68d

Browse files
feat(HTTP Request Node): Option to provide SSL Certificates in Http Request Node (#9125)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <[email protected]>
1 parent 2cb62fa commit 306b68d

File tree

9 files changed

+226
-3
lines changed

9 files changed

+226
-3
lines changed

packages/core/src/NodeExecuteFunctions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,14 +497,15 @@ export async function parseRequestObject(requestObject: IRequestOptions) {
497497
}
498498

499499
const host = getHostFromRequestObject(requestObject);
500-
const agentOptions: AgentOptions = {};
500+
const agentOptions: AgentOptions = { ...requestObject.agentOptions };
501501
if (host) {
502502
agentOptions.servername = host;
503503
}
504504
if (requestObject.rejectUnauthorized === false) {
505505
agentOptions.rejectUnauthorized = false;
506506
agentOptions.secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT;
507507
}
508+
508509
axiosConfig.httpsAgent = new Agent(agentOptions);
509510

510511
axiosConfig.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosConfig);

packages/core/test/NodeExecuteFunctions.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SecureContextOptions } from 'tls';
12
import {
23
cleanupParameterData,
34
copyInputItems,
@@ -387,6 +388,42 @@ describe('NodeExecuteFunctions', () => {
387388
expect((axiosOptions.httpsAgent as Agent).options.servername).toEqual('example.de');
388389
});
389390

391+
describe('should set SSL certificates', () => {
392+
const agentOptions: SecureContextOptions = {
393+
ca: '-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----',
394+
};
395+
const requestObject: IRequestOptions = {
396+
method: 'GET',
397+
uri: 'https://example.de',
398+
agentOptions,
399+
};
400+
401+
test('on regular requests', async () => {
402+
const axiosOptions = await parseRequestObject(requestObject);
403+
expect((axiosOptions.httpsAgent as Agent).options).toEqual({
404+
servername: 'example.de',
405+
...agentOptions,
406+
noDelay: true,
407+
path: null,
408+
});
409+
});
410+
411+
test('on redirected requests', async () => {
412+
const axiosOptions = await parseRequestObject(requestObject);
413+
expect(axiosOptions.beforeRedirect).toBeDefined;
414+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
415+
const redirectOptions: Record<string, any> = { agents: {}, hostname: 'example.de' };
416+
axiosOptions.beforeRedirect!(redirectOptions, mock());
417+
expect(redirectOptions.agent).toEqual(redirectOptions.agents.https);
418+
expect((redirectOptions.agent as Agent).options).toEqual({
419+
servername: 'example.de',
420+
...agentOptions,
421+
noDelay: true,
422+
path: null,
423+
});
424+
});
425+
});
426+
390427
describe('when followRedirect is true', () => {
391428
test.each(['GET', 'HEAD'] as IHttpRequestMethods[])(
392429
'should set maxRedirects on %s ',
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* eslint-disable n8n-nodes-base/cred-class-name-unsuffixed */
2+
/* eslint-disable n8n-nodes-base/cred-class-field-name-unsuffixed */
3+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
4+
5+
export class HttpSslAuth implements ICredentialType {
6+
name = 'httpSslAuth';
7+
8+
displayName = 'SSL Certificates';
9+
10+
documentationUrl = 'httpRequest';
11+
12+
icon = 'node:n8n-nodes-base.httpRequest';
13+
14+
properties: INodeProperties[] = [
15+
{
16+
displayName: 'CA',
17+
name: 'ca',
18+
type: 'string',
19+
description: 'Certificate Authority certificate',
20+
typeOptions: {
21+
password: true,
22+
},
23+
default: '',
24+
},
25+
{
26+
displayName: 'Certificate',
27+
name: 'cert',
28+
type: 'string',
29+
typeOptions: {
30+
password: true,
31+
},
32+
default: '',
33+
},
34+
{
35+
displayName: 'Private Key',
36+
name: 'key',
37+
type: 'string',
38+
typeOptions: {
39+
password: true,
40+
},
41+
default: '',
42+
},
43+
{
44+
displayName: 'Passphrase',
45+
name: 'passphrase',
46+
type: 'string',
47+
description: 'Optional passphrase for the private key, if the private key is encrypted',
48+
typeOptions: {
49+
password: true,
50+
},
51+
default: '',
52+
},
53+
];
54+
}

packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SecureContextOptions } from 'tls';
12
import type {
23
IDataObject,
34
INodeExecutionData,
@@ -8,6 +9,8 @@ import type {
89
import set from 'lodash/set';
910

1011
import FormData from 'form-data';
12+
import type { HttpSslAuthCredentials } from './interfaces';
13+
import { formatPrivateKey } from '../../utils/utilities';
1114

1215
export type BodyParameter = {
1316
name: string;
@@ -194,3 +197,18 @@ export const prepareRequestBody = async (
194197
return await reduceAsync(parameters, defaultReducer);
195198
}
196199
};
200+
201+
export const setAgentOptions = (
202+
requestOptions: IRequestOptions,
203+
sslCertificates: HttpSslAuthCredentials | undefined,
204+
) => {
205+
if (sslCertificates) {
206+
const agentOptions: SecureContextOptions = {};
207+
if (sslCertificates.ca) agentOptions.ca = formatPrivateKey(sslCertificates.ca);
208+
if (sslCertificates.cert) agentOptions.cert = formatPrivateKey(sslCertificates.cert);
209+
if (sslCertificates.key) agentOptions.key = formatPrivateKey(sslCertificates.key);
210+
if (sslCertificates.passphrase)
211+
agentOptions.passphrase = formatPrivateKey(sslCertificates.passphrase);
212+
requestOptions.agentOptions = agentOptions;
213+
}
214+
};

packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import {
3333
reduceAsync,
3434
replaceNullValues,
3535
sanitizeUiMessage,
36+
setAgentOptions,
3637
} from '../GenericFunctions';
3738
import { keysToLowercase } from '@utils/utilities';
39+
import type { HttpSslAuthCredentials } from '../interfaces';
3840

3941
function toText<T>(data: T) {
4042
if (typeof data === 'object' && data !== null) {
@@ -56,7 +58,17 @@ export class HttpRequestV3 implements INodeType {
5658
},
5759
inputs: ['main'],
5860
outputs: ['main'],
59-
credentials: [],
61+
credentials: [
62+
{
63+
name: 'httpSslAuth',
64+
required: true,
65+
displayOptions: {
66+
show: {
67+
provideSslCertificates: [true],
68+
},
69+
},
70+
},
71+
],
6072
properties: [
6173
{
6274
displayName: '',
@@ -173,6 +185,36 @@ export class HttpRequestV3 implements INodeType {
173185
},
174186
},
175187
},
188+
{
189+
displayName: 'SSL Certificates',
190+
name: 'provideSslCertificates',
191+
type: 'boolean',
192+
default: false,
193+
isNodeSetting: true,
194+
},
195+
{
196+
displayName: "Provide certificates in node's 'Credential for SSL Certificates' parameter",
197+
name: 'provideSslCertificatesNotice',
198+
type: 'notice',
199+
default: '',
200+
isNodeSetting: true,
201+
displayOptions: {
202+
show: {
203+
provideSslCertificates: [true],
204+
},
205+
},
206+
},
207+
{
208+
displayName: 'SSL Certificate',
209+
name: 'sslCertificate',
210+
type: 'credentials',
211+
default: '',
212+
displayOptions: {
213+
show: {
214+
provideSslCertificates: [true],
215+
},
216+
},
217+
},
176218
{
177219
displayName: 'Send Query Parameters',
178220
name: 'sendQuery',
@@ -1221,6 +1263,7 @@ export class HttpRequestV3 implements INodeType {
12211263
let httpCustomAuth;
12221264
let oAuth1Api;
12231265
let oAuth2Api;
1266+
let sslCertificates;
12241267
let nodeCredentialType: string | undefined;
12251268
let genericCredentialType: string | undefined;
12261269

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

1326+
const provideSslCertificates = this.getNodeParameter(
1327+
'provideSslCertificates',
1328+
itemIndex,
1329+
false,
1330+
);
1331+
1332+
if (provideSslCertificates) {
1333+
sslCertificates = (await this.getCredentials(
1334+
'httpSslAuth',
1335+
itemIndex,
1336+
)) as HttpSslAuthCredentials;
1337+
}
1338+
12831339
const requestMethod = this.getNodeParameter('method', itemIndex) as IHttpRequestMethods;
12841340

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

15761632
const authDataKeys: IAuthDataSanitizeKeys = {};
15771633

1634+
// Add SSL certificates if any are set
1635+
setAgentOptions(requestOptions, sslCertificates);
1636+
if (requestOptions.agentOptions) {
1637+
authDataKeys.agentOptions = Object.keys(requestOptions.agentOptions);
1638+
}
1639+
15781640
// Add credentials if any are set
15791641
if (httpBasicAuth !== undefined) {
15801642
requestOptions.auth = {
@@ -1594,6 +1656,7 @@ export class HttpRequestV3 implements INodeType {
15941656
requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value;
15951657
authDataKeys.qs = [httpQueryAuth.name as string];
15961658
}
1659+
15971660
if (httpDigestAuth !== undefined) {
15981661
requestOptions.auth = {
15991662
user: httpDigestAuth.user as string,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type HttpSslAuthCredentials = {
2+
ca?: string;
3+
cert?: string;
4+
key?: string;
5+
passphrase?: string;
6+
};

packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { prepareRequestBody } from '../../GenericFunctions';
1+
import type { IRequestOptions } from 'n8n-workflow';
2+
import { prepareRequestBody, setAgentOptions } from '../../GenericFunctions';
23
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';
34

45
describe('HTTP Node Utils, prepareRequestBody', () => {
@@ -33,3 +34,42 @@ describe('HTTP Node Utils, prepareRequestBody', () => {
3334
expect(result).toEqual({ foo: { bar: { spam: 'baz' } } });
3435
});
3536
});
37+
38+
describe('HTTP Node Utils, setAgentOptions', () => {
39+
it("should not have agentOptions as it's undefined", async () => {
40+
const requestOptions: IRequestOptions = {
41+
method: 'GET',
42+
uri: 'https://example.com',
43+
};
44+
45+
const sslCertificates = undefined;
46+
47+
setAgentOptions(requestOptions, sslCertificates);
48+
49+
expect(requestOptions).toEqual({
50+
method: 'GET',
51+
uri: 'https://example.com',
52+
});
53+
});
54+
55+
it('should have agentOptions set', async () => {
56+
const requestOptions: IRequestOptions = {
57+
method: 'GET',
58+
uri: 'https://example.com',
59+
};
60+
61+
const sslCertificates = {
62+
ca: 'mock-ca',
63+
};
64+
65+
setAgentOptions(requestOptions, sslCertificates);
66+
67+
expect(requestOptions).toStrictEqual({
68+
method: 'GET',
69+
uri: 'https://example.com',
70+
agentOptions: {
71+
ca: 'mock-ca',
72+
},
73+
});
74+
});
75+
});

packages/nodes-base/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
"dist/credentials/HttpHeaderAuth.credentials.js",
169169
"dist/credentials/HttpCustomAuth.credentials.js",
170170
"dist/credentials/HttpQueryAuth.credentials.js",
171+
"dist/credentials/HttpSslAuth.credentials.js",
171172
"dist/credentials/HubspotApi.credentials.js",
172173
"dist/credentials/HubspotAppToken.credentials.js",
173174
"dist/credentials/HubspotDeveloperApi.credentials.js",

packages/workflow/src/Interfaces.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type * as express from 'express';
44
import type FormData from 'form-data';
55
import type { PathLike } from 'fs';
66
import type { IncomingHttpHeaders } from 'http';
7+
import type { SecureContextOptions } from 'tls';
78
import type { Readable } from 'stream';
89
import type { URLSearchParams } from 'url';
910

@@ -547,6 +548,8 @@ export interface IRequestOptions {
547548

548549
/** Max number of redirects to follow @default 21 */
549550
maxRedirects?: number;
551+
552+
agentOptions?: SecureContextOptions;
550553
}
551554

552555
export interface PaginationOptions {

0 commit comments

Comments
 (0)