mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(HTTP Request Node): Option to provide SSL Certificates in Http Request Node (#9125)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
2cb62faf2f
commit
306b68da6b
|
@ -497,7 +497,7 @@ export async function parseRequestObject(requestObject: IRequestOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = getHostFromRequestObject(requestObject);
|
const host = getHostFromRequestObject(requestObject);
|
||||||
const agentOptions: AgentOptions = {};
|
const agentOptions: AgentOptions = { ...requestObject.agentOptions };
|
||||||
if (host) {
|
if (host) {
|
||||||
agentOptions.servername = host;
|
agentOptions.servername = host;
|
||||||
}
|
}
|
||||||
|
@ -505,6 +505,7 @@ export async function parseRequestObject(requestObject: IRequestOptions) {
|
||||||
agentOptions.rejectUnauthorized = false;
|
agentOptions.rejectUnauthorized = false;
|
||||||
agentOptions.secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT;
|
agentOptions.secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
axiosConfig.httpsAgent = new Agent(agentOptions);
|
axiosConfig.httpsAgent = new Agent(agentOptions);
|
||||||
|
|
||||||
axiosConfig.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosConfig);
|
axiosConfig.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosConfig);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { SecureContextOptions } from 'tls';
|
||||||
import {
|
import {
|
||||||
cleanupParameterData,
|
cleanupParameterData,
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
|
@ -387,6 +388,42 @@ describe('NodeExecuteFunctions', () => {
|
||||||
expect((axiosOptions.httpsAgent as Agent).options.servername).toEqual('example.de');
|
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', () => {
|
describe('when followRedirect is true', () => {
|
||||||
test.each(['GET', 'HEAD'] as IHttpRequestMethods[])(
|
test.each(['GET', 'HEAD'] as IHttpRequestMethods[])(
|
||||||
'should set maxRedirects on %s ',
|
'should set maxRedirects on %s ',
|
||||||
|
|
54
packages/nodes-base/credentials/HttpSslAuth.credentials.ts
Normal file
54
packages/nodes-base/credentials/HttpSslAuth.credentials.ts
Normal file
|
@ -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: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { SecureContextOptions } from 'tls';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
@ -8,6 +9,8 @@ import type {
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
|
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
|
import type { HttpSslAuthCredentials } from './interfaces';
|
||||||
|
import { formatPrivateKey } from '../../utils/utilities';
|
||||||
|
|
||||||
export type BodyParameter = {
|
export type BodyParameter = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -194,3 +197,18 @@ export const prepareRequestBody = async (
|
||||||
return await reduceAsync(parameters, defaultReducer);
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -33,8 +33,10 @@ import {
|
||||||
reduceAsync,
|
reduceAsync,
|
||||||
replaceNullValues,
|
replaceNullValues,
|
||||||
sanitizeUiMessage,
|
sanitizeUiMessage,
|
||||||
|
setAgentOptions,
|
||||||
} from '../GenericFunctions';
|
} from '../GenericFunctions';
|
||||||
import { keysToLowercase } from '@utils/utilities';
|
import { keysToLowercase } from '@utils/utilities';
|
||||||
|
import type { HttpSslAuthCredentials } from '../interfaces';
|
||||||
|
|
||||||
function toText<T>(data: T) {
|
function toText<T>(data: T) {
|
||||||
if (typeof data === 'object' && data !== null) {
|
if (typeof data === 'object' && data !== null) {
|
||||||
|
@ -56,7 +58,17 @@ export class HttpRequestV3 implements INodeType {
|
||||||
},
|
},
|
||||||
inputs: ['main'],
|
inputs: ['main'],
|
||||||
outputs: ['main'],
|
outputs: ['main'],
|
||||||
credentials: [],
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'httpSslAuth',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
provideSslCertificates: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
displayName: '',
|
displayName: '',
|
||||||
|
@ -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',
|
displayName: 'Send Query Parameters',
|
||||||
name: 'sendQuery',
|
name: 'sendQuery',
|
||||||
|
@ -1221,6 +1263,7 @@ export class HttpRequestV3 implements INodeType {
|
||||||
let httpCustomAuth;
|
let httpCustomAuth;
|
||||||
let oAuth1Api;
|
let oAuth1Api;
|
||||||
let oAuth2Api;
|
let oAuth2Api;
|
||||||
|
let sslCertificates;
|
||||||
let nodeCredentialType: string | undefined;
|
let nodeCredentialType: string | undefined;
|
||||||
let genericCredentialType: string | undefined;
|
let genericCredentialType: string | undefined;
|
||||||
|
|
||||||
|
@ -1280,6 +1323,19 @@ export class HttpRequestV3 implements INodeType {
|
||||||
nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex) as string;
|
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 requestMethod = this.getNodeParameter('method', itemIndex) as IHttpRequestMethods;
|
||||||
|
|
||||||
const sendQuery = this.getNodeParameter('sendQuery', itemIndex, false) as boolean;
|
const sendQuery = this.getNodeParameter('sendQuery', itemIndex, false) as boolean;
|
||||||
|
@ -1575,6 +1631,12 @@ export class HttpRequestV3 implements INodeType {
|
||||||
|
|
||||||
const authDataKeys: IAuthDataSanitizeKeys = {};
|
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
|
// Add credentials if any are set
|
||||||
if (httpBasicAuth !== undefined) {
|
if (httpBasicAuth !== undefined) {
|
||||||
requestOptions.auth = {
|
requestOptions.auth = {
|
||||||
|
@ -1594,6 +1656,7 @@ export class HttpRequestV3 implements INodeType {
|
||||||
requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value;
|
requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value;
|
||||||
authDataKeys.qs = [httpQueryAuth.name as string];
|
authDataKeys.qs = [httpQueryAuth.name as string];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (httpDigestAuth !== undefined) {
|
if (httpDigestAuth !== undefined) {
|
||||||
requestOptions.auth = {
|
requestOptions.auth = {
|
||||||
user: httpDigestAuth.user as string,
|
user: httpDigestAuth.user as string,
|
||||||
|
|
6
packages/nodes-base/nodes/HttpRequest/interfaces.ts
Normal file
6
packages/nodes-base/nodes/HttpRequest/interfaces.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export type HttpSslAuthCredentials = {
|
||||||
|
ca?: string;
|
||||||
|
cert?: string;
|
||||||
|
key?: string;
|
||||||
|
passphrase?: string;
|
||||||
|
};
|
|
@ -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';
|
import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions';
|
||||||
|
|
||||||
describe('HTTP Node Utils, prepareRequestBody', () => {
|
describe('HTTP Node Utils, prepareRequestBody', () => {
|
||||||
|
@ -33,3 +34,42 @@ describe('HTTP Node Utils, prepareRequestBody', () => {
|
||||||
expect(result).toEqual({ foo: { bar: { spam: 'baz' } } });
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -168,6 +168,7 @@
|
||||||
"dist/credentials/HttpHeaderAuth.credentials.js",
|
"dist/credentials/HttpHeaderAuth.credentials.js",
|
||||||
"dist/credentials/HttpCustomAuth.credentials.js",
|
"dist/credentials/HttpCustomAuth.credentials.js",
|
||||||
"dist/credentials/HttpQueryAuth.credentials.js",
|
"dist/credentials/HttpQueryAuth.credentials.js",
|
||||||
|
"dist/credentials/HttpSslAuth.credentials.js",
|
||||||
"dist/credentials/HubspotApi.credentials.js",
|
"dist/credentials/HubspotApi.credentials.js",
|
||||||
"dist/credentials/HubspotAppToken.credentials.js",
|
"dist/credentials/HubspotAppToken.credentials.js",
|
||||||
"dist/credentials/HubspotDeveloperApi.credentials.js",
|
"dist/credentials/HubspotDeveloperApi.credentials.js",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type * as express from 'express';
|
||||||
import type FormData from 'form-data';
|
import type FormData from 'form-data';
|
||||||
import type { PathLike } from 'fs';
|
import type { PathLike } from 'fs';
|
||||||
import type { IncomingHttpHeaders } from 'http';
|
import type { IncomingHttpHeaders } from 'http';
|
||||||
|
import type { SecureContextOptions } from 'tls';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import type { URLSearchParams } from 'url';
|
import type { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
@ -547,6 +548,8 @@ export interface IRequestOptions {
|
||||||
|
|
||||||
/** Max number of redirects to follow @default 21 */
|
/** Max number of redirects to follow @default 21 */
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
agentOptions?: SecureContextOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationOptions {
|
export interface PaginationOptions {
|
||||||
|
|
Loading…
Reference in a new issue