n8n/packages/nodes-base/nodes/Ssh/Ssh.node.ts

430 lines
9.9 KiB
TypeScript
Raw Normal View History

import { IExecuteFunctions } from 'n8n-core';
import {
IBinaryData,
INodeExecutionData,
INodeType,
INodeTypeDescription,
refactor: Apply more `eslint-plugin-n8n-nodes-base` rules (#3624) * :arrow_up: Upgrade `eslint-plugin-n8n-nodes-base` * :package: Update `package-lock.json` * :wrench: Adjust renamed filesystem rules * :pencil2: Alphabetize ruleset * :zap: Categorize overrides * :zap: Set renamings in lint exceptions * :zap: Run baseline `lintfix` * :zap: Update linting scripts * :shirt: Apply `node-param-description-missing-from-dynamic-multi-options` * :shirt: Apply `cred-class-field-name-missing-oauth2` (#3627) * Rule working as intended * Removed comments * Move cred rule to different rule set * :shirt: Apply `node-param-array-type-assertion` * :shirt: Apply `node-dirname-against-convention` * Apply `cred-class-field-display-name-oauth2` (#3628) * Apply `node-execute-block-wrong-error-thrown` * Apply `node-class-description-display-name-unsuffixed-trigger-node` * Apply `node-class-description-name-unsuffixed-trigger-node` * Apply `cred-class-name-missing-oauth2-suffix` (#3636) * Rule working as intended, add exception to existing nodes * :shirt: Apply `cred-class-field-name-uppercase-first-char` (#3638) * :arrow_up: Upgrade to plugin version 1.2.28 * :package: Update `package-lock.json` * :shirt: Update lintings with 1.2.8 change * :shirt: Apply `cred-class-field-name-unsuffixed` * :shirt: Apply `cred-class-name-unsuffixed` * :shirt: Apply `node-class-description-credentials-name-unsuffixed` * :pencil2: Alphabetize rules * :heavy_minus_sign: Remove `nodelinter` package * :package: Update `package-lock.json` * :zap: Consolidate `lint` and `lintfix` scripts Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com>
2022-07-04 02:12:08 -07:00
NodeOperationError,
} from 'n8n-workflow';
import { readFile, rm, writeFile } from 'fs/promises';
import { file } from 'tmp-promise';
const nodeSSH = require('node-ssh');
export class Ssh implements INodeType {
description: INodeTypeDescription = {
displayName: 'SSH',
2021-05-30 12:00:15 -07:00
name: 'ssh',
icon: 'fa:terminal',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Execute commands via SSH',
defaults: {
name: 'SSH',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'sshPassword',
required: true,
displayOptions: {
show: {
authentication: ['password'],
},
},
},
{
name: 'sshPrivateKey',
required: true,
displayOptions: {
show: {
authentication: ['privateKey'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Password',
value: 'password',
},
{
name: 'Private Key',
value: 'privateKey',
},
],
default: 'password',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
refactor: Apply more nodelinting rules (#3324) * :pencil2: Alphabetize lint rules * :fire: Remove duplicates * :zap: Update `lintfix` script * :shirt: Apply `node-param-operation-without-no-data-expression` (#3329) * :shirt: Apply `node-param-operation-without-no-data-expression` * :shirt: Add exceptions * :shirt: Apply `node-param-description-weak` (#3328) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-value-duplicate` (#3331) * :shirt: Apply `node-param-description-miscased-json` (#3337) * :shirt: Apply `node-param-display-name-excess-inner-whitespace` (#3335) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-type-options-missing-from-limit` (#3336) * Rule workig as intended * :pencil2: Uncomment rules Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-name-duplicate` (#3338) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-wrong-for-simplify` (#3334) * :zap: fix * :zap: exceptions * :zap: changed rule ignoring from file to line * :shirt: Apply `node-param-resource-without-no-data-expression` (#3339) * :shirt: Apply `node-param-display-name-untrimmed` (#3341) * :shirt: Apply `node-param-display-name-miscased-id` (#3340) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-resource-with-plural-option` (#3342) * :shirt: Apply `node-param-description-wrong-for-upsert` (#3333) * :zap: fix * :zap: replaced record with contact in description * :zap: fix Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-description-identical-to-name` (#3343) * :shirt: Apply `node-param-option-name-containing-star` (#3347) * :shirt: Apply `node-param-display-name-wrong-for-update-fields` (#3348) * :shirt: Apply `node-param-option-name-wrong-for-get-all` (#3345) * :zap: fix * :zap: exceptions * :shirt: Apply node-param-display-name-wrong-for-simplify (#3344) * Rule working as intended * Uncomented other rules * :shirt: Undo and add exceptions Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :zap: Alphabetize lint rules * :zap: Restore `lintfix` script Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com>
2022-05-20 14:47:24 -07:00
noDataExpression: true,
options: [
{
name: 'Command',
value: 'command',
},
{
name: 'File',
value: 'file',
},
],
default: 'command',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
refactor: Apply more nodelinting rules (#3324) * :pencil2: Alphabetize lint rules * :fire: Remove duplicates * :zap: Update `lintfix` script * :shirt: Apply `node-param-operation-without-no-data-expression` (#3329) * :shirt: Apply `node-param-operation-without-no-data-expression` * :shirt: Add exceptions * :shirt: Apply `node-param-description-weak` (#3328) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-value-duplicate` (#3331) * :shirt: Apply `node-param-description-miscased-json` (#3337) * :shirt: Apply `node-param-display-name-excess-inner-whitespace` (#3335) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-type-options-missing-from-limit` (#3336) * Rule workig as intended * :pencil2: Uncomment rules Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-name-duplicate` (#3338) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-wrong-for-simplify` (#3334) * :zap: fix * :zap: exceptions * :zap: changed rule ignoring from file to line * :shirt: Apply `node-param-resource-without-no-data-expression` (#3339) * :shirt: Apply `node-param-display-name-untrimmed` (#3341) * :shirt: Apply `node-param-display-name-miscased-id` (#3340) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-resource-with-plural-option` (#3342) * :shirt: Apply `node-param-description-wrong-for-upsert` (#3333) * :zap: fix * :zap: replaced record with contact in description * :zap: fix Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-description-identical-to-name` (#3343) * :shirt: Apply `node-param-option-name-containing-star` (#3347) * :shirt: Apply `node-param-display-name-wrong-for-update-fields` (#3348) * :shirt: Apply `node-param-option-name-wrong-for-get-all` (#3345) * :zap: fix * :zap: exceptions * :shirt: Apply node-param-display-name-wrong-for-simplify (#3344) * Rule working as intended * Uncomented other rules * :shirt: Undo and add exceptions Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :zap: Alphabetize lint rules * :zap: Restore `lintfix` script Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com>
2022-05-20 14:47:24 -07:00
noDataExpression: true,
displayOptions: {
show: {
resource: ['command'],
},
},
options: [
{
name: 'Execute',
value: 'execute',
description: 'Execute a command',
action: 'Execute a command',
},
],
default: 'execute',
},
{
displayName: 'Command',
name: 'command',
type: 'string',
displayOptions: {
show: {
resource: ['command'],
operation: ['execute'],
},
},
default: '',
description: 'The command to be executed on a remote device',
},
{
displayName: 'Working Directory',
name: 'cwd',
type: 'string',
displayOptions: {
show: {
resource: ['command'],
operation: ['execute'],
},
},
default: '/',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
refactor: Apply more nodelinting rules (#3324) * :pencil2: Alphabetize lint rules * :fire: Remove duplicates * :zap: Update `lintfix` script * :shirt: Apply `node-param-operation-without-no-data-expression` (#3329) * :shirt: Apply `node-param-operation-without-no-data-expression` * :shirt: Add exceptions * :shirt: Apply `node-param-description-weak` (#3328) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-value-duplicate` (#3331) * :shirt: Apply `node-param-description-miscased-json` (#3337) * :shirt: Apply `node-param-display-name-excess-inner-whitespace` (#3335) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-type-options-missing-from-limit` (#3336) * Rule workig as intended * :pencil2: Uncomment rules Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-name-duplicate` (#3338) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-wrong-for-simplify` (#3334) * :zap: fix * :zap: exceptions * :zap: changed rule ignoring from file to line * :shirt: Apply `node-param-resource-without-no-data-expression` (#3339) * :shirt: Apply `node-param-display-name-untrimmed` (#3341) * :shirt: Apply `node-param-display-name-miscased-id` (#3340) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-resource-with-plural-option` (#3342) * :shirt: Apply `node-param-description-wrong-for-upsert` (#3333) * :zap: fix * :zap: replaced record with contact in description * :zap: fix Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-description-identical-to-name` (#3343) * :shirt: Apply `node-param-option-name-containing-star` (#3347) * :shirt: Apply `node-param-display-name-wrong-for-update-fields` (#3348) * :shirt: Apply `node-param-option-name-wrong-for-get-all` (#3345) * :zap: fix * :zap: exceptions * :shirt: Apply node-param-display-name-wrong-for-simplify (#3344) * Rule working as intended * Uncomented other rules * :shirt: Undo and add exceptions Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :zap: Alphabetize lint rules * :zap: Restore `lintfix` script Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com>
2022-05-20 14:47:24 -07:00
noDataExpression: true,
displayOptions: {
show: {
resource: ['file'],
},
},
options: [
{
name: 'Download',
value: 'download',
description: 'Download a file',
action: 'Download a file',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file',
action: 'Upload a file',
},
],
default: 'upload',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: ['upload'],
resource: ['file'],
},
},
placeholder: '',
description:
'Name of the binary property which contains the data for the file to be uploaded',
},
{
displayName: 'Target Directory',
name: 'path',
type: 'string',
displayOptions: {
show: {
resource: ['file'],
operation: ['upload'],
},
},
default: '',
required: true,
placeholder: '/home/user',
description:
'The directory to upload the file to. The name of the file does not need to be specified, it\'s taken from the binary data file name. To override this behavior, set the parameter "File Name" under options.',
},
{
displayName: 'Path',
displayOptions: {
show: {
resource: ['file'],
operation: ['download'],
},
},
name: 'path',
type: 'string',
default: '',
placeholder: '/home/user/invoice.txt',
description:
'The file path of the file to download. Has to contain the full path including file name.',
required: true,
},
{
displayName: 'Binary Property',
displayOptions: {
show: {
resource: ['file'],
operation: ['download'],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
description: 'Object property name which holds binary data',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
displayOptions: {
show: {
resource: ['file'],
operation: ['upload'],
},
},
default: {},
options: [
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'Overrides the binary data file name',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnItems: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
const authentication = this.getNodeParameter('authentication', 0) as string;
2021-05-28 21:53:17 -07:00
const temporaryFiles: string[] = [];
const ssh = new nodeSSH.NodeSSH();
try {
if (authentication === 'password') {
const credentials = await this.getCredentials('sshPassword');
await ssh.connect({
host: credentials.host as string,
username: credentials.username as string,
port: credentials.port as number,
password: credentials.password as string,
});
} else if (authentication === 'privateKey') {
const credentials = await this.getCredentials('sshPrivateKey');
const { path } = await file({ prefix: 'n8n-ssh-' });
2021-05-28 21:53:17 -07:00
temporaryFiles.push(path);
await writeFile(path, credentials.privateKey as string);
const options = {
host: credentials.host as string,
username: credentials.username as string,
port: credentials.port as number,
privateKey: path,
} as any; // tslint:disable-line: no-any
if (credentials.passphrase) {
options.passphrase = credentials.passphrase as string;
}
await ssh.connect(options);
}
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'command') {
if (operation === 'execute') {
const command = this.getNodeParameter('command', i) as string;
const cwd = this.getNodeParameter('cwd', i) as string;
returnItems.push({
json: await ssh.execCommand(command, { cwd }),
pairedItem: {
item: i,
},
});
}
}
if (resource === 'file') {
if (operation === 'download') {
const dataPropertyNameDownload = this.getNodeParameter(
'binaryPropertyName',
i,
) as string;
const parameterPath = this.getNodeParameter('path', i) as string;
const { path } = await file({ prefix: 'n8n-ssh-' });
temporaryFiles.push(path);
await ssh.getFile(path, parameterPath);
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
pairedItem: {
item: i,
},
};
if (items[i].binary !== undefined && newItem.binary) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const data = await readFile(path as string);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
data,
parameterPath,
);
}
if (operation === 'upload') {
const parameterPath = this.getNodeParameter('path', i) as string;
const fileName = this.getNodeParameter('options.fileName', i, '') as string;
const item = items[i];
if (item.binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!', {
itemIndex: i,
});
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
const binaryData = item.binary[propertyNameUpload] as IBinaryData;
if (item.binary[propertyNameUpload] === undefined) {
throw new NodeOperationError(
this.getNode(),
`No binary data property "${propertyNameUpload}" does not exists on item!`,
{ itemIndex: i },
);
}
const dataBuffer = await this.helpers.getBinaryDataBuffer(i, propertyNameUpload);
const { path } = await file({ prefix: 'n8n-ssh-' });
temporaryFiles.push(path);
await writeFile(path, dataBuffer);
await ssh.putFile(
path,
`${parameterPath}${
parameterPath.charAt(parameterPath.length - 1) === '/' ? '' : '/'
}${fileName || binaryData.fileName}`,
);
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: i,
},
});
}
}
} catch (error) {
if (this.continueOnFail()) {
if (resource === 'file' && operation === 'download') {
items[i] = {
json: {
error: error.message,
2021-05-29 20:04:35 -07:00
},
};
} else {
returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
}
continue;
}
throw error;
}
}
} catch (error) {
ssh.dispose();
2021-05-28 21:53:17 -07:00
for (const tempFile of temporaryFiles) await rm(tempFile);
throw error;
}
2021-05-28 21:53:17 -07:00
for (const tempFile of temporaryFiles) await rm(tempFile);
ssh.dispose();
if (resource === 'file' && operation === 'download') {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return this.prepareOutputData(returnItems);
}
}
}