feat(cli): Load all nodes and credentials code in isolation - N8N-4362 (#3906)

[N8N-4362] Load all nodes and credentials code in isolation

Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2022-09-09 18:08:08 +02:00
committed by GitHub
parent 9267e8fb12
commit b450e977a3
3 changed files with 52 additions and 94 deletions

View File

@@ -4,7 +4,7 @@
import { promisify } from 'util'; import { promisify } from 'util';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
import { createContext, Script } from 'vm';
import axios from 'axios'; import axios from 'axios';
import { UserSettings } from 'n8n-core'; import { UserSettings } from 'n8n-core';
import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow'; import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow';
@@ -235,3 +235,10 @@ export const isClientError = (error: Error): boolean => {
export function isNpmError(error: unknown): error is { code: number; stdout: string } { export function isNpmError(error: unknown): error is { code: number; stdout: string } {
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error; return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
} }
const context = createContext({ require });
export const loadClassInIsolation = (filePath: string, className: string) => {
const script = new Script(`new (require('${filePath}').${className})()`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return script.runInContext(context);
};

View File

@@ -37,7 +37,7 @@ import config from '../config';
import { NodeTypes } from '.'; import { NodeTypes } from '.';
import { InstalledPackages } from './databases/entities/InstalledPackages'; import { InstalledPackages } from './databases/entities/InstalledPackages';
import { InstalledNodes } from './databases/entities/InstalledNodes'; import { InstalledNodes } from './databases/entities/InstalledNodes';
import { executeCommand } from './CommunityNodes/helpers'; import { executeCommand, loadClassInIsolation } from './CommunityNodes/helpers';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { RESPONSE_ERROR_MESSAGES } from './constants';
import { import {
persistInstalledPackageData, persistInstalledPackageData,
@@ -46,6 +46,14 @@ import {
const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
function toJSON() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
...this,
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
};
}
class LoadNodesAndCredentialsClass { class LoadNodesAndCredentialsClass {
nodeTypes: INodeTypeData = {}; nodeTypes: INodeTypeData = {};
@@ -104,10 +112,8 @@ class LoadNodesAndCredentialsClass {
await fsAccess(checkPath); await fsAccess(checkPath);
// Folder exists, so use it. // Folder exists, so use it.
return path.dirname(checkPath); return path.dirname(checkPath);
} catch (error) { } catch (_) {
// Folder does not exist so get next one // Folder does not exist so get next one
// eslint-disable-next-line no-continue
continue;
} }
} }
throw new Error('Could not find "node_modules" folder!'); throw new Error('Could not find "node_modules" folder!');
@@ -144,8 +150,7 @@ class LoadNodesAndCredentialsClass {
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) { if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';'); const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
// eslint-disable-next-line prefer-spread customDirectories.push(...customExtensionFolders);
customDirectories.push.apply(customDirectories, customExtensionFolders);
} }
for (const directory of customDirectories) { for (const directory of customDirectories) {
@@ -192,26 +197,16 @@ class LoadNodesAndCredentialsClass {
* @param {string} filePath The file to read credentials from * @param {string} filePath The file to read credentials from
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> { loadCredentialsFromFile(credentialName: string, filePath: string): void {
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
let tempCredential: ICredentialType; let tempCredential: ICredentialType;
try { try {
tempCredential = loadClassInIsolation(filePath, credentialName);
// Add serializer method "toJSON" to the class so that authenticate method (if defined) // Add serializer method "toJSON" to the class so that authenticate method (if defined)
// gets mapped to the authenticate attribute before it is sent to the client. // gets mapped to the authenticate attribute before it is sent to the client.
// The authenticate property is used by the client to decide whether or not to // The authenticate property is used by the client to decide whether or not to
// include the credential type in the predefined credentials (HTTP node) // include the credential type in the predefined credentials (HTTP node)
// eslint-disable-next-line func-names Object.assign(tempCredential, { toJSON });
tempModule[credentialName].prototype.toJSON = function () {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
...this,
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
};
};
tempCredential = new tempModule[credentialName]() as ICredentialType;
if (tempCredential.icon && tempCredential.icon.startsWith('file:')) { if (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
// If a file icon gets used add the full path // If a file icon gets used add the full path
@@ -353,19 +348,16 @@ class LoadNodesAndCredentialsClass {
* @param {string} filePath The file to read node from * @param {string} filePath The file to read node from
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadNodeFromFile( loadNodeFromFile(
packageName: string, packageName: string,
nodeName: string, nodeName: string,
filePath: string, filePath: string,
): Promise<INodeTypeNameVersion | undefined> { ): INodeTypeNameVersion | undefined {
let tempNode: INodeType | INodeVersionedType; let tempNode: INodeType | INodeVersionedType;
let fullNodeName: string;
let nodeVersion = 1; let nodeVersion = 1;
try { try {
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires tempNode = loadClassInIsolation(filePath, nodeName);
const tempModule = require(filePath);
tempNode = new tempModule[nodeName]();
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' }); this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions // eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
@@ -373,8 +365,7 @@ class LoadNodesAndCredentialsClass {
throw error; throw error;
} }
// eslint-disable-next-line prefer-const const fullNodeName = `${packageName}.${tempNode.description.name}`;
fullNodeName = `${packageName}.${tempNode.description.name}`;
tempNode.description.name = fullNodeName; tempNode.description.name = fullNodeName;
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) { if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
@@ -385,13 +376,6 @@ class LoadNodesAndCredentialsClass {
)}`; )}`;
} }
if (tempNode.hasOwnProperty('executeSingle')) {
this.logger.warn(
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
{ filePath },
);
}
if (tempNode.hasOwnProperty('nodeVersions')) { if (tempNode.hasOwnProperty('nodeVersions')) {
const versionedNodeType = (tempNode as INodeVersionedType).getNodeType(); const versionedNodeType = (tempNode as INodeVersionedType).getNodeType();
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' }); this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
@@ -491,8 +475,7 @@ class LoadNodesAndCredentialsClass {
node.description.codex = codex; node.description.codex = codex;
} catch (_) { } catch (_) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions this.logger.debug(`No codex available for: ${filePath.split('/').pop() ?? ''}`);
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
if (isCustom) { if (isCustom) {
node.description.codex = { node.description.codex = {
@@ -512,22 +495,15 @@ class LoadNodesAndCredentialsClass {
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> { async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
const files = await glob(path.join(directory, '**/*.@(node|credentials).js')); const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
let fileName: string;
let type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadPromises: any[] = [];
for (const filePath of files) { for (const filePath of files) {
[fileName, type] = path.parse(filePath).name.split('.'); const [fileName, type] = path.parse(filePath).name.split('.');
if (type === 'node') { if (type === 'node') {
loadPromises.push(this.loadNodeFromFile(setPackageName, fileName, filePath)); this.loadNodeFromFile(setPackageName, fileName, filePath);
} else if (type === 'credentials') { } else if (type === 'credentials') {
loadPromises.push(this.loadCredentialsFromFile(fileName, filePath)); this.loadCredentialsFromFile(fileName, filePath);
} }
} }
await Promise.all(loadPromises);
} }
async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> { async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> {
@@ -545,26 +521,20 @@ class LoadNodesAndCredentialsClass {
async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> { async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> {
// Get the absolute path of the package // Get the absolute path of the package
const packageFile = await this.readPackageJson(packagePath); const packageFile = await this.readPackageJson(packagePath);
// if (!packageFile.hasOwnProperty('n8n')) {
if (!packageFile.n8n) { if (!packageFile.n8n) {
return []; return [];
} }
const packageName = packageFile.name; const packageName = packageFile.name;
const { nodes, credentials } = packageFile.n8n;
let tempPath: string;
let filePath: string;
const returnData: INodeTypeNameVersion[] = []; const returnData: INodeTypeNameVersion[] = [];
// Read all node types // Read all node types
let fileName: string; if (Array.isArray(nodes)) {
let type: string; for (const filePath of nodes) {
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) { const tempPath = path.join(packagePath, filePath);
for (filePath of packageFile.n8n.nodes) { const [fileName] = path.parse(filePath).name.split('.');
tempPath = path.join(packagePath, filePath); const loadData = this.loadNodeFromFile(packageName, fileName, tempPath);
[fileName, type] = path.parse(filePath).name.split('.');
const loadData = await this.loadNodeFromFile(packageName, fileName, tempPath);
if (loadData) { if (loadData) {
returnData.push(loadData); returnData.push(loadData);
} }
@@ -572,15 +542,10 @@ class LoadNodesAndCredentialsClass {
} }
// Read all credential types // Read all credential types
if ( if (Array.isArray(credentials)) {
packageFile.n8n.hasOwnProperty('credentials') && for (const filePath of credentials) {
Array.isArray(packageFile.n8n.credentials) const tempPath = path.join(packagePath, filePath);
) { const [fileName] = path.parse(filePath).name.split('.');
for (filePath of packageFile.n8n.credentials) {
tempPath = path.join(packagePath, filePath);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[fileName, type] = path.parse(filePath).name.split('.');
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadCredentialsFromFile(fileName, tempPath); this.loadCredentialsFromFile(fileName, tempPath);
} }
} }

View File

@@ -49,6 +49,7 @@ import { getLogger } from './Logger';
import config from '../config'; import config from '../config';
import { InternalHooksManager } from './InternalHooksManager'; import { InternalHooksManager } from './InternalHooksManager';
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper'; import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
import { loadClassInIsolation } from './CommunityNodes/helpers';
export class WorkflowRunnerProcess { export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined; data: IWorkflowExecutionDataProcessWithExecution | undefined;
@@ -92,41 +93,30 @@ export class WorkflowRunnerProcess {
workflowId: this.data.workflowData.id, workflowId: this.data.workflowData.id,
}); });
let className: string;
let tempNode: INodeType;
let tempCredential: ICredentialType;
let filePath: string;
this.startedAt = new Date(); this.startedAt = new Date();
// Load the required nodes // Load the required nodes
const nodeTypesData: INodeTypeData = {}; const nodeTypesData: INodeTypeData = {};
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) { for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
className = this.data.nodeTypeData[nodeTypeName].className; let tempNode: INodeType;
const { className, sourcePath } = this.data.nodeTypeData[nodeTypeName];
filePath = this.data.nodeTypeData[nodeTypeName].sourcePath;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call const nodeObject = loadClassInIsolation(sourcePath, className);
const nodeObject = new tempModule[className]();
if (nodeObject.getNodeType !== undefined) { if (nodeObject.getNodeType !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
tempNode = nodeObject.getNodeType(); tempNode = nodeObject.getNodeType();
} else { } else {
tempNode = nodeObject; tempNode = nodeObject;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
tempNode = new tempModule[className]() as INodeType;
} catch (error) { } catch (error) {
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`); throw new Error(`Error loading node "${nodeTypeName}" from: "${sourcePath}"`);
} }
nodeTypesData[nodeTypeName] = { nodeTypesData[nodeTypeName] = {
type: tempNode, type: tempNode,
sourcePath: filePath, sourcePath,
}; };
} }
@@ -137,22 +127,18 @@ export class WorkflowRunnerProcess {
const credentialsTypeData: ICredentialTypeData = {}; const credentialsTypeData: ICredentialTypeData = {};
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) { for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) {
className = this.data.credentialsTypeData[credentialTypeName].className; let tempCredential: ICredentialType;
const { className, sourcePath } = this.data.credentialsTypeData[credentialTypeName];
filePath = this.data.credentialsTypeData[credentialTypeName].sourcePath;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call tempCredential = loadClassInIsolation(sourcePath, className);
tempCredential = new tempModule[className]() as ICredentialType;
} catch (error) { } catch (error) {
throw new Error(`Error loading credential "${credentialTypeName}" from: "${filePath}"`); throw new Error(`Error loading credential "${credentialTypeName}" from: "${sourcePath}"`);
} }
credentialsTypeData[credentialTypeName] = { credentialsTypeData[credentialTypeName] = {
type: tempCredential, type: tempCredential,
sourcePath: filePath, sourcePath,
}; };
} }