import { EventEmitter } from 'events';
import { Client as SSHClient, ConnectConfig } from 'ssh2';
import { IProtocol, ProtocolCapabilities, ProtocolHealthStatus } from '../core/ProtocolFactory.js';
import {
ConsoleSession,
ConsoleOutput,
SessionOptions,
SSHConnectionOptions
} from '../types/index.js';
import { Logger } from '../utils/logger.js';
/**
* SSH Protocol implementation for secure remote shell access
*/
export class SSHProtocol extends EventEmitter implements IProtocol {
public readonly type = 'ssh';
public readonly capabilities: ProtocolCapabilities;
public readonly healthStatus: ProtocolHealthStatus;
private logger: Logger;
private sessions: Map<string, SSHSession> = new Map();
private connectionPool: Map<string, SSHClient> = new Map();
private isInitialized: boolean = false;
constructor() {
super();
this.logger = new Logger('SSHProtocol');
this.capabilities = {
supportsStreaming: true,
supportsFileTransfer: true,
supportsX11Forwarding: true,
supportsPortForwarding: true,
supportsAuthentication: true,
supportsEncryption: true,
supportsCompression: true,
supportsMultiplexing: true,
supportsKeepAlive: true,
supportsReconnection: true,
supportsBinaryData: true,
supportsCustomEnvironment: true,
supportsWorkingDirectory: true,
supportsSignals: true,
supportsResizing: true,
supportsPTY: true,
maxConcurrentSessions: 20,
defaultTimeout: 30000,
supportedEncodings: ['utf-8', 'ascii', 'binary'],
supportedAuthMethods: ['password', 'publickey', 'keyboard-interactive'],
platformSupport: {
windows: true,
linux: true,
macos: true,
freebsd: true,
},
};
this.healthStatus = {
isHealthy: true,
lastChecked: new Date(),
errors: [],
warnings: [],
metrics: {
activeSessions: 0,
totalSessions: 0,
averageLatency: 0,
successRate: 1.0,
uptime: 0,
},
dependencies: {
ssh: {
available: true,
version: '2.0',
},
},
};
}
async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
// SSH client is available via ssh2 package
this.isInitialized = true;
this.logger.info('SSH protocol initialized');
} catch (error) {
this.healthStatus.isHealthy = false;
this.healthStatus.errors.push(`Failed to initialize: ${error}`);
throw error;
}
}
async createSession(options: SessionOptions): Promise<ConsoleSession> {
if (!this.isInitialized) {
throw new Error('Protocol not initialized');
}
const sessionId = `ssh-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
// Implementation would create SSH connection and session here
// This is a stub - actual implementation would use ssh2 library
const consoleSession: ConsoleSession = {
id: sessionId,
command: options.command,
args: options.args || [],
cwd: options.cwd || '/',
env: options.env || {},
createdAt: new Date(),
status: 'running',
type: 'ssh',
streaming: options.streaming ?? false,
sshOptions: options.sshOptions,
executionState: 'idle',
activeCommands: new Map(),
};
this.healthStatus.metrics.activeSessions++;
this.healthStatus.metrics.totalSessions++;
this.emit('sessionCreated', consoleSession);
return consoleSession;
} catch (error) {
this.logger.error(`Failed to create SSH session: ${error}`);
throw error;
}
}
async executeCommand(sessionId: string, command: string, args?: string[]): Promise<void> {
// Stub implementation
this.emit('commandExecuted', { sessionId, command, args, timestamp: new Date() });
}
async sendInput(sessionId: string, input: string): Promise<void> {
// Stub implementation
this.emit('inputSent', { sessionId, input, timestamp: new Date() });
}
async getOutput(sessionId: string, since?: Date): Promise<string> {
// Stub implementation
return '';
}
async closeSession(sessionId: string): Promise<void> {
this.emit('sessionClosed', sessionId);
}
async getHealthStatus(): Promise<ProtocolHealthStatus> {
this.healthStatus.lastChecked = new Date();
return { ...this.healthStatus };
}
async dispose(): Promise<void> {
this.logger.info('Disposing SSH protocol');
this.sessions.clear();
this.connectionPool.clear();
this.removeAllListeners();
this.isInitialized = false;
}
}
interface SSHSession {
id: string;
client: SSHClient;
channel: any;
created: Date;
lastActivity: Date;
isActive: boolean;
}