mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
Merge 92a412d3d6 into ec074519d9
This commit is contained in:
commit
05b833b413
|
|
@ -57,5 +57,88 @@ export class Sftp implements ICredentialType {
|
|||
default: '',
|
||||
description: 'For an encrypted private key, this is the passphrase used to decrypt it',
|
||||
},
|
||||
{
|
||||
displayName: 'Advanced Configuration',
|
||||
name: 'advancedConfiguration',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to configure custom SSH2 algorithms',
|
||||
},
|
||||
{
|
||||
displayName: 'Encryption Ciphers',
|
||||
name: 'algorithmsCiphers',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
advancedConfiguration: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. aes128-ctr,aes192-ctr,aes256-ctr',
|
||||
hint: 'Optional comma-separated list of cipher algorithms. Leave empty to use defaults.',
|
||||
description:
|
||||
'Symmetric cipher algorithms for payload encryption. Common values: aes128-ctr, aes192-ctr, aes256-ctr, aes256-gcm@openssh.com',
|
||||
},
|
||||
{
|
||||
displayName: 'Compression Algorithms',
|
||||
name: 'algorithmsCompression',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
advancedConfiguration: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. zlib@openssh.com,zlib,none',
|
||||
hint: 'Optional comma-separated list of compression algorithms. Leave empty to use defaults.',
|
||||
description:
|
||||
'Compression algorithms for the connection. Common values: zlib@openssh.com, zlib, none',
|
||||
},
|
||||
{
|
||||
displayName: 'HMAC Algorithms',
|
||||
name: 'algorithmsHmac',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
advancedConfiguration: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. hmac-sha2-256,hmac-sha2-512',
|
||||
hint: 'Optional comma-separated list of HMAC algorithms. Leave empty to use defaults.',
|
||||
description:
|
||||
'Message authentication code algorithms. Common values: hmac-sha2-256, hmac-sha2-512, hmac-sha1',
|
||||
},
|
||||
{
|
||||
displayName: 'Key Exchange Algorithms',
|
||||
name: 'algorithmsKex',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
advancedConfiguration: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. ecdh-sha2-nistp256,diffie-hellman-group14-sha256',
|
||||
hint: 'Optional comma-separated list of key exchange algorithms. Leave empty to use defaults.',
|
||||
description:
|
||||
'Key exchange algorithms for establishing the connection. Common values: ecdh-sha2-nistp256, diffie-hellman-group14-sha256',
|
||||
},
|
||||
{
|
||||
displayName: 'Server Host Key Formats',
|
||||
name: 'algorithmsServerHostKey',
|
||||
// eslint-disable-next-line n8n-nodes-base/cred-class-field-type-options-password-missing
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
advancedConfiguration: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'e.g. ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-256',
|
||||
hint: 'Optional comma-separated list of server host key formats. Leave empty to use defaults.',
|
||||
description:
|
||||
'Server host key signature formats. Common values: ssh-ed25519, ecdsa-sha2-nistp256, rsa-sha2-256, ssh-rsa',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
INodeType,
|
||||
INodeTypeDescription,
|
||||
JsonObject,
|
||||
Logger,
|
||||
} from 'n8n-workflow';
|
||||
import { basename, dirname } from 'path';
|
||||
import ftpClient from 'promise-ftp';
|
||||
|
|
@ -121,6 +122,63 @@ const timeoutOption: INodeProperties = {
|
|||
default: 10000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse comma-separated algorithm string into array
|
||||
*/
|
||||
function parseAlgorithmList(value: string | undefined): string[] | undefined {
|
||||
if (!value || value.trim() === '') {
|
||||
return undefined;
|
||||
}
|
||||
return value
|
||||
.split(',')
|
||||
.map((algo) => algo.trim())
|
||||
.filter((algo) => algo !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build algorithms object from SFTP credentials
|
||||
*/
|
||||
function buildAlgorithmsConfig(credentials: ICredentialDataDecryptedObject): any {
|
||||
const algorithms: {
|
||||
compress?: string[];
|
||||
cipher?: string[];
|
||||
hmac?: string[];
|
||||
kex?: string[];
|
||||
serverHostKey?: string[];
|
||||
} = {};
|
||||
|
||||
const compression = parseAlgorithmList(credentials.algorithmsCompression as string);
|
||||
if (compression) algorithms.compress = compression;
|
||||
|
||||
const ciphers = parseAlgorithmList(credentials.algorithmsCiphers as string);
|
||||
if (ciphers) algorithms.cipher = ciphers;
|
||||
|
||||
const hmac = parseAlgorithmList(credentials.algorithmsHmac as string);
|
||||
if (hmac) algorithms.hmac = hmac;
|
||||
|
||||
const kex = parseAlgorithmList(credentials.algorithmsKex as string);
|
||||
if (kex) algorithms.kex = kex;
|
||||
|
||||
const serverHostKey = parseAlgorithmList(credentials.algorithmsServerHostKey as string);
|
||||
if (serverHostKey) algorithms.serverHostKey = serverHostKey;
|
||||
|
||||
// Only return if at least one algorithm category is configured
|
||||
return Object.keys(algorithms).length > 0 ? algorithms : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SSH2 debug function that logs to n8n logger
|
||||
*/
|
||||
function createSsh2DebugFunction(
|
||||
logger: Logger,
|
||||
nodeType: string,
|
||||
nodeName: string,
|
||||
): (message: string) => void {
|
||||
return (message: string) => {
|
||||
logger.debug(`[${nodeType}] [${nodeName}] SSH2 Protocol`, { message });
|
||||
};
|
||||
}
|
||||
|
||||
export class Ftp implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'FTP',
|
||||
|
|
@ -551,6 +609,7 @@ export class Ftp implements INodeType {
|
|||
): Promise<INodeCredentialTestResult> {
|
||||
const credentials = credential.data as ICredentialDataDecryptedObject;
|
||||
const sftp = new sftpClient();
|
||||
const algorithmsConfig = buildAlgorithmsConfig(credentials);
|
||||
try {
|
||||
if (credentials.privateKey) {
|
||||
await sftp.connect({
|
||||
|
|
@ -560,6 +619,8 @@ export class Ftp implements INodeType {
|
|||
password: (credentials.password as string) || undefined,
|
||||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
passphrase: credentials.passphrase as string | undefined,
|
||||
algorithms: algorithmsConfig,
|
||||
debug: createSsh2DebugFunction(this.logger, 'SFTP', 'Credential Test'),
|
||||
});
|
||||
} else {
|
||||
await sftp.connect({
|
||||
|
|
@ -567,6 +628,8 @@ export class Ftp implements INodeType {
|
|||
port: credentials.port as number,
|
||||
username: credentials.username as string,
|
||||
password: credentials.password as string,
|
||||
algorithms: algorithmsConfig,
|
||||
debug: createSsh2DebugFunction(this.logger, 'SFTP', 'Credential Test'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -608,6 +671,9 @@ export class Ftp implements INodeType {
|
|||
try {
|
||||
if (protocol === 'sftp') {
|
||||
sftp = new sftpClient();
|
||||
const algorithmsConfig = buildAlgorithmsConfig(credentials);
|
||||
const nodeType = this.getNode().type;
|
||||
const nodeName = this.getNode().name;
|
||||
if (credentials.privateKey) {
|
||||
await sftp.connect({
|
||||
host: credentials.host as string,
|
||||
|
|
@ -617,9 +683,10 @@ export class Ftp implements INodeType {
|
|||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
passphrase: credentials.passphrase as string | undefined,
|
||||
readyTimeout: connectionTimeout,
|
||||
algorithms: {
|
||||
algorithms: algorithmsConfig || {
|
||||
compress: ['zlib@openssh.com', 'zlib', 'none'],
|
||||
},
|
||||
debug: createSsh2DebugFunction(this.logger, nodeType, nodeName),
|
||||
});
|
||||
} else {
|
||||
await sftp.connect({
|
||||
|
|
@ -628,9 +695,10 @@ export class Ftp implements INodeType {
|
|||
username: credentials.username as string,
|
||||
password: credentials.password as string,
|
||||
readyTimeout: connectionTimeout,
|
||||
algorithms: {
|
||||
algorithms: algorithmsConfig || {
|
||||
compress: ['zlib@openssh.com', 'zlib', 'none'],
|
||||
},
|
||||
debug: createSsh2DebugFunction(this.logger, nodeType, nodeName),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ describe('Ftp', () => {
|
|||
executeFunctions.helpers.constructExecutionMetaData.mockImplementation(
|
||||
(data) => data as NodeExecutionWithMetadata[],
|
||||
);
|
||||
executeFunctions.getNode.mockReturnValue({
|
||||
type: 'n8n-nodes-base.ftp',
|
||||
name: 'FTP',
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('should add timeout option with ftp', async () => {
|
||||
|
|
@ -208,4 +212,146 @@ describe('Ftp', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse and apply custom algorithms with sftp', async () => {
|
||||
const connect = jest.fn();
|
||||
jest.spyOn(sftpModule, 'default').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
connect,
|
||||
delete: jest.fn(),
|
||||
end: jest.fn(),
|
||||
}) as unknown as sftp,
|
||||
);
|
||||
executeFunctions.getCredentials.mockResolvedValue({
|
||||
host: 'test.com',
|
||||
port: 22,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
algorithmsCiphers: 'aes128-ctr,aes256-ctr',
|
||||
algorithmsCompression: 'none',
|
||||
algorithmsHmac: 'hmac-sha2-256',
|
||||
algorithmsKex: 'ecdh-sha2-nistp256',
|
||||
algorithmsServerHostKey: 'ssh-ed25519',
|
||||
});
|
||||
executeFunctions.getNodeParameter.mockImplementation((parameterName, _idx, defaultValue) => {
|
||||
switch (parameterName) {
|
||||
case 'operation':
|
||||
return 'delete';
|
||||
case 'protocol':
|
||||
return 'sftp';
|
||||
case 'options.timeout':
|
||||
return 10000;
|
||||
case 'path':
|
||||
return '/test/path';
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
await new Ftp().execute.call(executeFunctions);
|
||||
|
||||
expect(connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
algorithms: {
|
||||
cipher: ['aes128-ctr', 'aes256-ctr'],
|
||||
compress: ['none'],
|
||||
hmac: ['hmac-sha2-256'],
|
||||
kex: ['ecdh-sha2-nistp256'],
|
||||
serverHostKey: ['ssh-ed25519'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle whitespace in algorithm lists', async () => {
|
||||
const connect = jest.fn();
|
||||
jest.spyOn(sftpModule, 'default').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
connect,
|
||||
delete: jest.fn(),
|
||||
end: jest.fn(),
|
||||
}) as unknown as sftp,
|
||||
);
|
||||
executeFunctions.getCredentials.mockResolvedValue({
|
||||
host: 'test.com',
|
||||
port: 22,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
algorithmsCiphers: ' aes128-ctr , aes256-ctr ',
|
||||
});
|
||||
executeFunctions.getNodeParameter.mockImplementation((parameterName, _idx, defaultValue) => {
|
||||
switch (parameterName) {
|
||||
case 'operation':
|
||||
return 'delete';
|
||||
case 'protocol':
|
||||
return 'sftp';
|
||||
case 'options.timeout':
|
||||
return 10000;
|
||||
case 'path':
|
||||
return '/test/path';
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
await new Ftp().execute.call(executeFunctions);
|
||||
|
||||
expect(connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
algorithms: {
|
||||
cipher: ['aes128-ctr', 'aes256-ctr'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default compression when no algorithms configured', async () => {
|
||||
const connect = jest.fn();
|
||||
jest.spyOn(sftpModule, 'default').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
connect,
|
||||
delete: jest.fn(),
|
||||
end: jest.fn(),
|
||||
}) as unknown as sftp,
|
||||
);
|
||||
executeFunctions.getCredentials.mockResolvedValue({
|
||||
host: 'test.com',
|
||||
port: 22,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
});
|
||||
executeFunctions.getNodeParameter.mockImplementation((parameterName, _idx, defaultValue) => {
|
||||
switch (parameterName) {
|
||||
case 'operation':
|
||||
return 'delete';
|
||||
case 'protocol':
|
||||
return 'sftp';
|
||||
case 'options.timeout':
|
||||
return 10000;
|
||||
case 'path':
|
||||
return '/test/path';
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
await new Ftp().execute.call(executeFunctions);
|
||||
|
||||
expect(connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
algorithms: {
|
||||
compress: ['zlib@openssh.com', 'zlib', 'none'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user