feat(core): Add a new option to customize SSH tunnel idle timeout (#14522)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-04-10 14:35:10 +02:00
committed by GitHub
parent 78f00f2e3a
commit 965baae093
2 changed files with 14 additions and 4 deletions

View File

@@ -1,9 +1,11 @@
import { mock } from 'jest-mock-extended';
import type { SSHCredentials } from 'n8n-workflow'; import type { SSHCredentials } from 'n8n-workflow';
import { Client } from 'ssh2'; import { Client } from 'ssh2';
import { SSHClientsManager } from '../ssh-clients-manager'; import { SSHClientsManager } from '../ssh-clients-manager';
describe('SSHClientsManager', () => { describe('SSHClientsManager', () => {
const idleTimeout = 5 * 60;
const credentials: SSHCredentials = { const credentials: SSHCredentials = {
sshAuthenticateWith: 'password', sshAuthenticateWith: 'password',
sshHost: 'example.com', sshHost: 'example.com',
@@ -20,7 +22,7 @@ describe('SSHClientsManager', () => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.useFakeTimers(); jest.useFakeTimers();
sshClientsManager = new SSHClientsManager(); sshClientsManager = new SSHClientsManager(mock({ idleTimeout }));
connectSpy.mockImplementation(function (this: Client) { connectSpy.mockImplementation(function (this: Client) {
this.emit('ready'); this.emit('ready');
return this; return this;
@@ -59,7 +61,7 @@ describe('SSHClientsManager', () => {
await sshClientsManager.getClient({ ...credentials, sshHost: 'host2' }); await sshClientsManager.getClient({ ...credentials, sshHost: 'host2' });
await sshClientsManager.getClient({ ...credentials, sshHost: 'host3' }); await sshClientsManager.getClient({ ...credentials, sshHost: 'host3' });
jest.advanceTimersByTime(6 * 60 * 1000); jest.advanceTimersByTime((idleTimeout + 1) * 1000);
sshClientsManager.cleanupStaleConnections(); sshClientsManager.cleanupStaleConnections();
expect(endSpy).toHaveBeenCalledTimes(3); expect(endSpy).toHaveBeenCalledTimes(3);

View File

@@ -1,13 +1,21 @@
import { Config, Env } from '@n8n/config';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { SSHCredentials } from 'n8n-workflow'; import type { SSHCredentials } from 'n8n-workflow';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { Client, type ConnectConfig } from 'ssh2'; import { Client, type ConnectConfig } from 'ssh2';
@Config
class SSHClientsConfig {
/** How many seconds before an idle SSH tunnel is closed */
@Env('N8N_SSH_TUNNEL_IDLE_TIMEOUT')
idleTimeout: number = 5 * 60;
}
@Service() @Service()
export class SSHClientsManager { export class SSHClientsManager {
readonly clients = new Map<string, { client: Client; lastUsed: Date }>(); readonly clients = new Map<string, { client: Client; lastUsed: Date }>();
constructor() { constructor(private readonly config: SSHClientsConfig) {
// Close all SSH connections when the process exits // Close all SSH connections when the process exits
process.on('exit', () => this.onShutdown()); process.on('exit', () => this.onShutdown());
@@ -67,7 +75,7 @@ export class SSHClientsManager {
const now = Date.now(); const now = Date.now();
for (const [hash, { client, lastUsed }] of clients.entries()) { for (const [hash, { client, lastUsed }] of clients.entries()) {
if (now - lastUsed.getTime() > 5 * 60 * 1000) { if (now - lastUsed.getTime() > this.config.idleTimeout * 1000) {
client.end(); client.end();
clients.delete(hash); clients.delete(hash);
} }