From d82e87979dc0fe47e264b0e7bb46c325157d9603 Mon Sep 17 00:00:00 2001 From: agobrech <45268029+agobrech@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:29:15 +0200 Subject: [PATCH] feature: add database and non http credentials test Add credential testing to Postgres, MySQL, MicrosoftSQL, Redis, FTP, SFTP, IMAP, RabbitMQ and MQTT Co-authored-by: Omar Ajoue --- .../nodes/EmailReadImap/EmailReadImap.node.ts | 50 +++++++++++ packages/nodes-base/nodes/Ftp/Ftp.node.ts | 62 +++++++++++++ packages/nodes-base/nodes/MQTT/Mqtt.node.ts | 89 ++++++++++++++++++- .../nodes/Microsoft/Sql/MicrosoftSql.node.ts | 43 +++++++++ packages/nodes-base/nodes/MySql/MySql.node.ts | 47 +++++++++- .../nodes/Postgres/Postgres.node.ts | 46 ++++++++++ .../nodes/RabbitMQ/RabbitMQ.node.ts | 53 ++++++++++- packages/nodes-base/nodes/Redis/Redis.node.ts | 51 +++++++++++ 8 files changed, 438 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts index 06886b8cad..508390c893 100644 --- a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts @@ -3,8 +3,12 @@ import { createDeferredPromise, IBinaryData, IBinaryKeyData, + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, IDeferredPromise, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -43,6 +47,7 @@ export class EmailReadImap implements INodeType { { name: 'imap', required: true, + testedBy: 'imapConnectionTest', }, ], properties: [ @@ -171,6 +176,51 @@ export class EmailReadImap implements INodeType { ], }; + methods = { + credentialTest: { + async imapConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + const config: ImapSimpleOptions = { + imap: { + user: credentials.user as string, + password: credentials.password as string, + host: credentials.host as string, + port: credentials.port as number, + tls: credentials.secure as boolean, + authTimeout: 20000, + }, + }; + const tlsOptions: IDataObject = {}; + + if (credentials.secure) { + tlsOptions.servername = credentials.host as string; + } + if (!_.isEmpty(tlsOptions)) { + config.imap.tlsOptions = tlsOptions; + } + const conn = imapConnect(config).then(async (conn) => { + return conn; + }); + (await conn).getBoxes((err, boxes) => {}); + } catch (error) { + console.log(error); + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async trigger(this: ITriggerFunctions): Promise { const credentials = await this.getCredentials('imap'); diff --git a/packages/nodes-base/nodes/Ftp/Ftp.node.ts b/packages/nodes-base/nodes/Ftp/Ftp.node.ts index c64c756f11..d0bc12b2fe 100644 --- a/packages/nodes-base/nodes/Ftp/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp/Ftp.node.ts @@ -1,7 +1,10 @@ import { IExecuteFunctions } from 'n8n-core'; import { ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -56,6 +59,7 @@ export class Ftp implements INodeType { protocol: ['ftp'], }, }, + testedBy: 'ftpConnectionTest', }, { // nodelinter-ignore-next-line @@ -66,6 +70,7 @@ export class Ftp implements INodeType { protocol: ['sftp'], }, }, + testedBy: 'sftpConnectionTest', }, ], properties: [ @@ -348,6 +353,63 @@ export class Ftp implements INodeType { ], }; + methods = { + credentialTest: { + async ftpConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + let ftp: ftpClient; + ftp = new ftpClient(); + await ftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + user: credentials.username as string, + password: credentials.password as string, + }); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + async sftpConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + let sftp: sftpClient; + sftp = new sftpClient(); + await sftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + username: credentials.username as string, + password: credentials.password as string, + privateKey: credentials.privateKey as string | undefined, + passphrase: credentials.passphrase as string | undefined, + }); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); // const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts index 98959481d8..48eccda6c1 100644 --- a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts @@ -1,6 +1,15 @@ import { IExecuteFunctions } from 'n8n-core'; -import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow'; +import { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + INodeCredentialTestResult, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; import mqtt from 'mqtt'; @@ -23,6 +32,7 @@ export class Mqtt implements INodeType { { name: 'mqtt', required: true, + testedBy: 'mqttConnectionTest', }, ], properties: [ @@ -96,6 +106,83 @@ export class Mqtt implements INodeType { ], }; + methods = { + credentialTest: { + async mqttConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + const protocol = (credentials.protocol as string) || 'mqtt'; + const host = credentials.host as string; + const brokerUrl = `${protocol}://${host}`; + const port = (credentials.port as number) || 1883; + const clientId = + (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + const clean = credentials.clean as boolean; + const ssl = credentials.ssl as boolean; + const ca = credentials.ca as string; + const cert = credentials.cert as string; + const key = credentials.key as string; + const rejectUnauthorized = credentials.rejectUnauthorized as boolean; + + let client: mqtt.MqttClient; + + if (ssl === false) { + const clientOptions: IClientOptions = { + port, + clean, + clientId, + }; + + if (credentials.username && credentials.password) { + clientOptions.username = credentials.username as string; + clientOptions.password = credentials.password as string; + } + client = mqtt.connect(brokerUrl, clientOptions); + } else { + const clientOptions: IClientOptions = { + port, + clean, + clientId, + ca, + cert, + key, + rejectUnauthorized, + }; + if (credentials.username && credentials.password) { + clientOptions.username = credentials.username as string; + clientOptions.password = credentials.password as string; + } + + client = mqtt.connect(brokerUrl, clientOptions); + } + // tslint:disable-next-line: no-any + await new Promise((resolve, reject): any => { + client.on('connect', (test) => { + resolve(test); + client.end(); + }); + client.on('error', (error) => { + client.end(); + reject(error); + }); + }); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const length = items.length; diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index fe0bebb7e0..dc158d8ca8 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -1,7 +1,11 @@ import { IExecuteFunctions } from 'n8n-core'; import { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -42,6 +46,7 @@ export class MicrosoftSql implements INodeType { { name: 'microsoftSql', required: true, + testedBy: 'microsoftSqlConnectionTest', }, ], properties: [ @@ -213,6 +218,44 @@ export class MicrosoftSql implements INodeType { ], }; + methods = { + credentialTest: { + async microsoftSqlConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + const config = { + server: credentials.server as string, + port: credentials.port as number, + database: credentials.database as string, + user: credentials.user as string, + password: credentials.password as string, + domain: credentials.domain ? (credentials.domain as string) : undefined, + connectionTimeout: credentials.connectTimeout as number, + requestTimeout: credentials.requestTimeout as number, + options: { + encrypt: credentials.tls as boolean, + enableArithAbort: false, + }, + }; + const pool = new mssql.ConnectionPool(config); + await pool.connect(); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const credentials = await this.getCredentials('microsoftSql'); diff --git a/packages/nodes-base/nodes/MySql/MySql.node.ts b/packages/nodes-base/nodes/MySql/MySql.node.ts index 75885d598e..f9e793201f 100644 --- a/packages/nodes-base/nodes/MySql/MySql.node.ts +++ b/packages/nodes-base/nodes/MySql/MySql.node.ts @@ -1,6 +1,9 @@ -import { IExecuteFunctions } from 'n8n-core'; import { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -10,6 +13,7 @@ import { import mysql2 from 'mysql2/promise'; import { copyInputItems } from './GenericFunctions'; +import { IExecuteFunctions } from 'n8n-core'; export class MySql implements INodeType { description: INodeTypeDescription = { @@ -28,6 +32,7 @@ export class MySql implements INodeType { { name: 'mySql', required: true, + testedBy: 'mysqlConnectionTest', }, ], properties: [ @@ -204,6 +209,46 @@ export class MySql implements INodeType { ], }; + methods = { + credentialTest: { + async mysqlConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + try { + const { ssl, caCertificate, clientCertificate, clientPrivateKey, ...baseCredentials } = + credentials; + + if (ssl) { + baseCredentials.ssl = {}; + + if (caCertificate) { + baseCredentials.ssl.ca = caCertificate; + } + + if (clientCertificate || clientPrivateKey) { + baseCredentials.ssl.cert = clientCertificate; + baseCredentials.ssl.key = clientPrivateKey; + } + } + + const connection = await mysql2.createConnection(baseCredentials); + connection.end(); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const credentials = await this.getCredentials('mySql'); diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index 20a9ad51fd..7f69650c17 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -1,6 +1,9 @@ import { IExecuteFunctions } from 'n8n-core'; import { + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -28,6 +31,7 @@ export class Postgres implements INodeType { { name: 'postgres', required: true, + testedBy: 'postgresConnectionTest', }, ], properties: [ @@ -273,6 +277,48 @@ export class Postgres implements INodeType { }, ], }; + methods = { + credentialTest: { + async postgresConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as IDataObject; + try { + const pgp = pgPromise(); + const config: IDataObject = { + host: credentials.host as string, + port: credentials.port as number, + database: credentials.database as string, + user: credentials.user as string, + password: credentials.password as string, + }; + + if (credentials.allowUnauthorizedCerts === true) { + config.ssl = { + rejectUnauthorized: false, + }; + } else { + config.ssl = !['disable', undefined].includes(credentials.ssl as string | undefined); + config.sslmode = (credentials.ssl as string) || 'disable'; + } + + const db = pgp(config); + await db.connect(); + await pgp.end(); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; async execute(this: IExecuteFunctions): Promise { const credentials = await this.getCredentials('postgres'); diff --git a/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts b/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts index 58c09c0e75..f46030bdaf 100644 --- a/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts +++ b/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts @@ -1,8 +1,11 @@ /* eslint-disable n8n-nodes-base/node-filename-against-convention */ import { IExecuteFunctions } from 'n8n-core'; - +import * as amqplib from 'amqplib'; import { + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -31,6 +34,7 @@ export class RabbitMQ implements INodeType { { name: 'rabbitmq', required: true, + testedBy: 'rabbitmqConnectionTest', }, ], properties: [ @@ -273,6 +277,53 @@ export class RabbitMQ implements INodeType { ], }; + methods = { + credentialTest: { + async rabbitmqConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as IDataObject; + try { + const credentialKeys = ['hostname', 'port', 'username', 'password', 'vhost']; + + const credentialData: IDataObject = {}; + credentialKeys.forEach((key) => { + credentialData[key] = credentials[key] === '' ? undefined : credentials[key]; + }); + + const optsData: IDataObject = {}; + if (credentials.ssl === true) { + credentialData.protocol = 'amqps'; + + optsData.ca = + credentials.ca === '' ? undefined : [Buffer.from(credentials.ca as string)]; + if (credentials.passwordless === true) { + optsData.cert = + credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string); + optsData.key = + credentials.key === '' ? undefined : Buffer.from(credentials.key as string); + optsData.passphrase = + credentials.passphrase === '' ? undefined : credentials.passphrase; + optsData.credentials = amqplib.credentials.external(); + } + } + const connection = await amqplib.connect(credentialData, optsData); + await connection.close(); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { let channel, options: IDataObject; try { diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index 3de58a66e7..b246bee865 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -1,7 +1,11 @@ import { IExecuteFunctions } from 'n8n-core'; import { GenericValue, + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -30,6 +34,7 @@ export class Redis implements INodeType { { name: 'redis', required: true, + testedBy: 'redisConnectionTest', }, ], properties: [ @@ -489,6 +494,52 @@ export class Redis implements INodeType { ], }; + methods = { + credentialTest: { + async redisConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as ICredentialDataDecryptedObject; + const redisOptions: redis.ClientOpts = { + host: credentials.host as string, + port: credentials.port as number, + db: credentials.database as number, + }; + + if (credentials.password) { + redisOptions.password = credentials.password as string; + } + try { + const client = await redis.createClient(redisOptions); + // tslint:disable-next-line: no-any + const data = await new Promise((resolve, reject): any => { + client.on('connect', async () => { + client.ping('ping', (error, pong) => { + if (error) reject(error); + resolve(pong); + client.quit(); + }); + }); + client.on('error', async (err) => { + client.quit(); + reject(err); + }); + }); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + execute(this: IExecuteFunctions): Promise { // Parses the given value in a number if it is one else returns a string function getParsedValue(value: string): string | number {