Skip to content

Commit

Permalink
feat(HTTP Request Node): Option to provide SSL Certificates in Http R…
Browse files Browse the repository at this point in the history
…equest Node (#9125)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
  • Loading branch information
michael-radency and netroy committed Apr 24, 2024
1 parent 2cb62fa commit 306b68d
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 3 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/NodeExecuteFunctions.ts
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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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
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
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

0 comments on commit 306b68d

Please sign in to comment.