- Fix 'error in libcrypto' issue by using persistent key files instead of temporary ones - Add SSH key pair generation feature with 'Generate Key Pair' button - Add 'View Public Key' button for generated keys with copy-to-clipboard functionality - Remove confusing 'both' authentication option, now only supports 'password' OR 'key' - Add persistent storage in data/ssh-keys/ directory with proper permissions - Update database schema with ssh_key_path and key_generated columns - Add API endpoints for key generation and public key retrieval - Enhance UX by hiding manual key input when key pair is generated - Update HelpModal documentation to reflect new SSH key features - Fix all TypeScript compilation errors and linting issues Resolves SSH authentication failures during script execution
741 lines
22 KiB
JavaScript
741 lines
22 KiB
JavaScript
import { spawn } from 'child_process';
|
|
import { writeFileSync, unlinkSync, chmodSync, mkdtempSync, rmdirSync, readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
class SSHService {
|
|
/**
|
|
* Test SSH connection with actual login verification
|
|
* This method tests if the user can actually log in with the provided credentials
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testConnection(server) {
|
|
const { auth_type = 'password' } = server;
|
|
|
|
return new Promise((resolve) => {
|
|
const timeout = 15000; // 15 seconds timeout for login test
|
|
let resolved = false;
|
|
|
|
// Choose authentication method based on auth_type
|
|
let authPromise;
|
|
if (auth_type === 'key') {
|
|
authPromise = this.testWithSSHKey(server);
|
|
} else {
|
|
// Default to password authentication
|
|
authPromise = this.testWithSshpass(server).catch(() => this.testWithExpect(server));
|
|
}
|
|
|
|
authPromise.then(result => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve(result);
|
|
}
|
|
}).catch(() => {
|
|
// If primary method fails, return error
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve({
|
|
success: false,
|
|
message: `SSH login test failed for ${auth_type} authentication`,
|
|
details: {
|
|
method: 'auth_failed',
|
|
auth_type: auth_type
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Set up overall timeout
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH login test timeout - server did not respond within 15 seconds',
|
|
details: { timeout: true, method: 'ssh_login_test' }
|
|
});
|
|
}
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test SSH connection using sshpass
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testWithSshpass(server) {
|
|
const { ip, user, password, ssh_port = 22 } = server;
|
|
|
|
if (!password) {
|
|
throw new Error('Password is required for password authentication');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = 10000;
|
|
let resolved = false;
|
|
|
|
const sshCommand = spawn('sshpass', [
|
|
'-p', password,
|
|
'ssh',
|
|
'-p', ssh_port.toString(),
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'PasswordAuthentication=yes',
|
|
'-o', 'PubkeyAuthentication=no',
|
|
`${user}@${ip}`,
|
|
'echo "SSH_LOGIN_SUCCESS"'
|
|
], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
sshCommand.kill('SIGTERM');
|
|
reject(new Error('SSH login timeout'));
|
|
}
|
|
}, timeout);
|
|
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
sshCommand.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
sshCommand.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
|
|
sshCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
if (code === 0 && output.includes('SSH_LOGIN_SUCCESS')) {
|
|
resolve({
|
|
success: true,
|
|
message: 'SSH login successful - credentials verified',
|
|
details: {
|
|
server: server.name || 'Unknown',
|
|
ip: ip,
|
|
user: user,
|
|
method: 'sshpass_verified'
|
|
}
|
|
});
|
|
} else {
|
|
let errorMessage = 'SSH login failed';
|
|
|
|
if (errorOutput.includes('Permission denied') || errorOutput.includes('Authentication failed')) {
|
|
errorMessage = 'Authentication failed - check username and password';
|
|
} else if (errorOutput.includes('Connection refused')) {
|
|
errorMessage = 'Connection refused - server may be down or SSH not running';
|
|
} else if (errorOutput.includes('Name or service not known') || errorOutput.includes('No route to host')) {
|
|
errorMessage = 'Host not found - check IP address';
|
|
} else if (errorOutput.includes('Connection timed out')) {
|
|
errorMessage = 'Connection timeout - server may be unreachable';
|
|
} else {
|
|
errorMessage = `SSH login failed: ${errorOutput.trim()}`;
|
|
}
|
|
|
|
reject(new Error(errorMessage));
|
|
}
|
|
}
|
|
});
|
|
|
|
sshCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test SSH connection using expect
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testWithExpect(server) {
|
|
const { ip, user, password, ssh_port = 22 } = server;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = 10000;
|
|
let resolved = false;
|
|
|
|
const expectScript = `#!/usr/bin/expect -f
|
|
set timeout 10
|
|
spawn ssh -p ${ssh_port} -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=yes -o PubkeyAuthentication=no ${user}@${ip} "echo SSH_LOGIN_SUCCESS"
|
|
expect {
|
|
"password:" {
|
|
send "${password}\r"
|
|
exp_continue
|
|
}
|
|
"Password:" {
|
|
send "${password}\r"
|
|
exp_continue
|
|
}
|
|
"SSH_LOGIN_SUCCESS" {
|
|
exit 0
|
|
}
|
|
timeout {
|
|
exit 1
|
|
}
|
|
eof {
|
|
exit 1
|
|
}
|
|
}`;
|
|
|
|
const expectCommand = spawn('expect', ['-c', expectScript], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
expectCommand.kill('SIGTERM');
|
|
reject(new Error('SSH login timeout'));
|
|
}
|
|
}, timeout);
|
|
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
expectCommand.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
expectCommand.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
|
|
expectCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
if (code === 0) {
|
|
resolve({
|
|
success: true,
|
|
message: 'SSH login successful - credentials verified',
|
|
details: {
|
|
server: server.name || 'Unknown',
|
|
ip: ip,
|
|
user: user,
|
|
method: 'expect_verified'
|
|
}
|
|
});
|
|
} else {
|
|
let errorMessage = 'SSH login failed';
|
|
|
|
if (errorOutput.includes('Permission denied') || errorOutput.includes('Authentication failed')) {
|
|
errorMessage = 'Authentication failed - check username and password';
|
|
} else if (errorOutput.includes('Connection refused')) {
|
|
errorMessage = 'Connection refused - server may be down or SSH not running';
|
|
} else if (errorOutput.includes('Name or service not known') || errorOutput.includes('No route to host')) {
|
|
errorMessage = 'Host not found - check IP address';
|
|
} else if (errorOutput.includes('Connection timed out')) {
|
|
errorMessage = 'Connection timeout - server may be unreachable';
|
|
} else {
|
|
errorMessage = `SSH login failed: ${errorOutput.trim()}`;
|
|
}
|
|
|
|
reject(new Error(errorMessage));
|
|
}
|
|
}
|
|
});
|
|
|
|
expectCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test SSH connection using basic connectivity check (fallback method)
|
|
* This method tests if the SSH port is open and reachable
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testConnectionBasic(server) {
|
|
const { ip, user, password } = server;
|
|
|
|
return new Promise((resolve) => {
|
|
const timeout = 10000; // 10 seconds timeout
|
|
let resolved = false;
|
|
|
|
// First, test if the SSH port is open using netcat or telnet
|
|
const portTestCommand = spawn('nc', ['-z', '-w', '5', ip, '22'], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
// Set up timeout
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
portTestCommand.kill('SIGTERM');
|
|
resolve({
|
|
success: false,
|
|
message: 'Connection timeout - server did not respond within 10 seconds',
|
|
details: { timeout: true, method: 'port_check' }
|
|
});
|
|
}
|
|
}, timeout);
|
|
|
|
// Handle port test results
|
|
portTestCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
if (code === 0) {
|
|
// Port is open, now try a basic SSH connection test
|
|
this.testSSHConnection(server).then(resolve).catch(() => {
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH port is open but connection failed - check credentials',
|
|
details: {
|
|
portOpen: true,
|
|
method: 'ssh_connection_test'
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH port (22) is not accessible - server may be down or SSH not running',
|
|
details: {
|
|
portOpen: false,
|
|
exitCode: code,
|
|
method: 'port_check'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle port test errors
|
|
portTestCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
// If netcat is not available, try with telnet
|
|
this.testWithTelnet(server).then(resolve).catch(() => {
|
|
resolve({
|
|
success: false,
|
|
message: 'Cannot test SSH connectivity - netcat and telnet not available',
|
|
details: {
|
|
error: error.message,
|
|
method: 'port_check_fallback'
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test SSH connection using telnet as fallback
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testWithTelnet(server) {
|
|
const { ip } = server;
|
|
|
|
return new Promise((resolve) => {
|
|
const timeout = 5000;
|
|
let resolved = false;
|
|
|
|
const telnetCommand = spawn('timeout', ['5', 'telnet', ip, '22'], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
telnetCommand.kill('SIGTERM');
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH port test timeout',
|
|
details: { method: 'telnet_timeout' }
|
|
});
|
|
}
|
|
}, timeout);
|
|
|
|
let output = '';
|
|
|
|
telnetCommand.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
telnetCommand.stderr.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
telnetCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
if (output.includes('Connected') || output.includes('SSH')) {
|
|
resolve({
|
|
success: true,
|
|
message: 'SSH port is accessible - basic connectivity confirmed',
|
|
details: {
|
|
portOpen: true,
|
|
method: 'telnet_test'
|
|
}
|
|
});
|
|
} else {
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH port is not accessible',
|
|
details: {
|
|
portOpen: false,
|
|
output: output.trim(),
|
|
method: 'telnet_test'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
telnetCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
resolve({
|
|
success: false,
|
|
message: 'Cannot test SSH connectivity - required tools not available',
|
|
details: {
|
|
error: error.message,
|
|
method: 'telnet_error'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test actual SSH connection (without password authentication)
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testSSHConnection(server) {
|
|
const { ip, user, ssh_port = 22 } = server;
|
|
|
|
return new Promise((resolve) => {
|
|
const timeout = 5000;
|
|
let resolved = false;
|
|
|
|
const sshCommand = spawn('ssh', [
|
|
'-p', ssh_port.toString(),
|
|
'-o', 'ConnectTimeout=5',
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'PasswordAuthentication=no',
|
|
'-o', 'PubkeyAuthentication=no',
|
|
'-o', 'PreferredAuthentications=none',
|
|
`${user}@${ip}`,
|
|
'exit'
|
|
], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
sshCommand.kill('SIGTERM');
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH connection timeout',
|
|
details: { method: 'ssh_timeout' }
|
|
});
|
|
}
|
|
}, timeout);
|
|
|
|
let errorOutput = '';
|
|
|
|
sshCommand.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
|
|
sshCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
// SSH connection was established but authentication failed
|
|
// This is actually a good sign - it means SSH is working
|
|
if (errorOutput.includes('Permission denied') || errorOutput.includes('Authentication failed')) {
|
|
resolve({
|
|
success: true,
|
|
message: 'SSH service is running and accessible - authentication required',
|
|
details: {
|
|
server: server.name || 'Unknown',
|
|
ip: ip,
|
|
user: user,
|
|
method: 'ssh_auth_required'
|
|
}
|
|
});
|
|
} else if (errorOutput.includes('Connection refused')) {
|
|
resolve({
|
|
success: false,
|
|
message: 'SSH connection refused - service may not be running',
|
|
details: {
|
|
error: errorOutput.trim(),
|
|
method: 'ssh_connection_refused'
|
|
}
|
|
});
|
|
} else {
|
|
resolve({
|
|
success: false,
|
|
message: `SSH connection failed: ${errorOutput.trim()}`,
|
|
details: {
|
|
error: errorOutput.trim(),
|
|
method: 'ssh_connection_failed'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
sshCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
resolve({
|
|
success: false,
|
|
message: `SSH command failed: ${error.message}`,
|
|
details: {
|
|
error: error.message,
|
|
method: 'ssh_command_error'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test SSH connection using SSH key authentication
|
|
* @param {import('../types/server').Server} server - Server configuration
|
|
* @returns {Promise<Object>} Connection test result
|
|
*/
|
|
async testWithSSHKey(server) {
|
|
const { ip, user, ssh_key_path, ssh_key_passphrase, ssh_port = 22 } = server;
|
|
|
|
if (!ssh_key_path || !existsSync(ssh_key_path)) {
|
|
throw new Error('SSH key file not found');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = 10000;
|
|
let resolved = false;
|
|
|
|
try {
|
|
// Build SSH command
|
|
const sshArgs = [
|
|
'-i', ssh_key_path,
|
|
'-p', ssh_port.toString(),
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'PasswordAuthentication=no',
|
|
'-o', 'PubkeyAuthentication=yes',
|
|
`${user}@${ip}`,
|
|
'echo "SSH_LOGIN_SUCCESS"'
|
|
];
|
|
|
|
// Use sshpass if passphrase is provided
|
|
let command, args;
|
|
if (ssh_key_passphrase) {
|
|
command = 'sshpass';
|
|
args = ['-P', 'passphrase', '-p', ssh_key_passphrase, 'ssh', ...sshArgs];
|
|
} else {
|
|
command = 'ssh';
|
|
args = sshArgs;
|
|
}
|
|
|
|
const sshCommand = spawn(command, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
sshCommand.kill('SIGTERM');
|
|
reject(new Error('SSH key login timeout'));
|
|
}
|
|
}, timeout);
|
|
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
sshCommand.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
sshCommand.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
|
|
sshCommand.on('close', (code) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
|
|
if (code === 0 && output.includes('SSH_LOGIN_SUCCESS')) {
|
|
resolve({
|
|
success: true,
|
|
message: 'SSH key authentication successful - credentials verified',
|
|
details: {
|
|
server: server.name || 'Unknown',
|
|
ip: ip,
|
|
user: user,
|
|
method: 'ssh_key_verified'
|
|
}
|
|
});
|
|
} else {
|
|
let errorMessage = 'SSH key authentication failed';
|
|
|
|
if (errorOutput.includes('Permission denied') || errorOutput.includes('Authentication failed')) {
|
|
errorMessage = 'SSH key authentication failed - check key and permissions';
|
|
} else if (errorOutput.includes('Connection refused')) {
|
|
errorMessage = 'Connection refused - server may be down or SSH not running';
|
|
} else if (errorOutput.includes('Name or service not known') || errorOutput.includes('No route to host')) {
|
|
errorMessage = 'Host not found - check IP address';
|
|
} else if (errorOutput.includes('Connection timed out')) {
|
|
errorMessage = 'Connection timeout - server may be unreachable';
|
|
} else if (errorOutput.includes('Load key') || errorOutput.includes('invalid format')) {
|
|
errorMessage = 'Invalid SSH key format';
|
|
} else if (errorOutput.includes('Enter passphrase')) {
|
|
errorMessage = 'SSH key passphrase required but not provided';
|
|
} else {
|
|
errorMessage = `SSH key authentication failed: ${errorOutput.trim()}`;
|
|
}
|
|
|
|
reject(new Error(errorMessage));
|
|
}
|
|
}
|
|
});
|
|
|
|
sshCommand.on('error', (error) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
reject(error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate SSH key pair for a server
|
|
* @param {number} serverId - Server ID for key file naming
|
|
* @returns {Promise<{privateKey: string, publicKey: string}>}
|
|
*/
|
|
async generateKeyPair(serverId) {
|
|
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
|
const keyPath = join(sshKeysDir, `server_${serverId}_key`);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const sshKeygen = spawn('ssh-keygen', [
|
|
'-t', 'ed25519',
|
|
'-f', keyPath,
|
|
'-N', '', // No passphrase
|
|
'-C', 'pve-scripts-local'
|
|
], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
let errorOutput = '';
|
|
|
|
sshKeygen.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
|
|
sshKeygen.on('close', (code) => {
|
|
if (code === 0) {
|
|
try {
|
|
// Read the generated private key
|
|
const privateKey = readFileSync(keyPath, 'utf8');
|
|
|
|
// Read the generated public key
|
|
const publicKeyPath = keyPath + '.pub';
|
|
const publicKey = readFileSync(publicKeyPath, 'utf8');
|
|
|
|
// Set proper permissions
|
|
chmodSync(keyPath, 0o600);
|
|
chmodSync(publicKeyPath, 0o644);
|
|
|
|
resolve({
|
|
privateKey,
|
|
publicKey: publicKey.trim()
|
|
});
|
|
} catch (error) {
|
|
reject(new Error(`Failed to read generated key files: ${error instanceof Error ? error.message : String(error)}`));
|
|
}
|
|
} else {
|
|
reject(new Error(`ssh-keygen failed: ${errorOutput}`));
|
|
}
|
|
});
|
|
|
|
sshKeygen.on('error', (error) => {
|
|
reject(new Error(`Failed to run ssh-keygen: ${error.message}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get public key from private key file
|
|
* @param {string} keyPath - Path to private key file
|
|
* @returns {string} Public key content
|
|
*/
|
|
getPublicKey(keyPath) {
|
|
const publicKeyPath = keyPath + '.pub';
|
|
|
|
if (!existsSync(publicKeyPath)) {
|
|
throw new Error('Public key file not found');
|
|
}
|
|
|
|
return readFileSync(publicKeyPath, 'utf8').trim();
|
|
}
|
|
|
|
}
|
|
|
|
// Singleton instance
|
|
/** @type {SSHService | null} */
|
|
let sshInstance = null;
|
|
|
|
export function getSSHService() {
|
|
if (!sshInstance) {
|
|
sshInstance = new SSHService();
|
|
}
|
|
return sshInstance;
|
|
}
|
|
|
|
export default SSHService;
|