169 lines
5.1 KiB
PHP
Executable File
169 lines
5.1 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ServerManager\Services;
|
|
|
|
use phpseclib3\Net\SSH2;
|
|
use phpseclib3\Crypt\PublicKeyLoader;
|
|
use ServerManager\Core\App;
|
|
use ServerManager\Core\Encryption;
|
|
use ServerManager\Core\Logger;
|
|
|
|
class SSHService
|
|
{
|
|
private ?SSH2 $connection = null;
|
|
private array $serverConfig;
|
|
private Logger $logger;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->logger = Logger::getInstance();
|
|
}
|
|
|
|
public function connect(array $server, bool $forceReconnect = false): bool
|
|
{
|
|
if ($this->connection !== null && !$forceReconnect) {
|
|
return $this->connection->isConnected();
|
|
}
|
|
|
|
$this->serverConfig = $server;
|
|
$config = App::getConfig()['ssh'];
|
|
|
|
try {
|
|
$host = $server['ip_address'] ?? $server['hostname'];
|
|
$port = (int) ($server['ssh_port'] ?? 22);
|
|
|
|
$this->connection = new SSH2($host, $port, $config['timeout'] ?? 30);
|
|
|
|
if (!empty($server['ssh_key'])) {
|
|
$key = PublicKeyLoader::load($server['ssh_key']);
|
|
$result = $this->connection->login($server['ssh_user'], $key);
|
|
} elseif (!empty($server['ssh_password'])) {
|
|
$result = $this->connection->login($server['ssh_user'], $server['ssh_password']);
|
|
} else {
|
|
throw new \RuntimeException('No authentication method configured');
|
|
}
|
|
|
|
if (!$result) {
|
|
throw new \RuntimeException('SSH authentication failed for ' . $host);
|
|
}
|
|
|
|
if ($config['keepalive_interval'] ?? 0 > 0) {
|
|
$this->connection->setKeepAlive($config['keepalive_interval']);
|
|
}
|
|
|
|
$this->logger->info("SSH connection established to {$host}");
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error("SSH connection failed: " . $e->getMessage(), [
|
|
'host' => $host ?? 'unknown',
|
|
'port' => $port ?? 22,
|
|
]);
|
|
$this->connection = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function disconnect(): void
|
|
{
|
|
if ($this->connection !== null) {
|
|
$this->connection->disconnect();
|
|
$this->connection = null;
|
|
}
|
|
}
|
|
|
|
public function exec(string $command, int $timeout = 0): array
|
|
{
|
|
if ($this->connection === null || !$this->connection->isConnected()) {
|
|
throw new \RuntimeException('No active SSH connection');
|
|
}
|
|
|
|
$timeout = $timeout ?: (App::getConfig()['ssh']['command_timeout'] ?? 60);
|
|
|
|
$this->connection->setTimeout($timeout);
|
|
|
|
$output = $this->connection->exec($command);
|
|
$exitStatus = $this->connection->getExitStatus();
|
|
$stderr = $this->connection->getStdError() ?? '';
|
|
|
|
$this->logger->info("SSH command executed", [
|
|
'command' => $command,
|
|
'exit_status' => $exitStatus,
|
|
]);
|
|
|
|
return [
|
|
'output' => $output,
|
|
'exit_status' => $exitStatus,
|
|
'stderr' => $stderr,
|
|
];
|
|
}
|
|
|
|
public function write(string $data): void
|
|
{
|
|
if ($this->connection !== null && $this->connection->isConnected()) {
|
|
$this->connection->write($data);
|
|
}
|
|
}
|
|
|
|
public function read(string $expected = '', int $mode = SSH2::READ_REGEX): string
|
|
{
|
|
if ($this->connection === null || !$this->connection->isConnected()) {
|
|
throw new \RuntimeException('No active SSH connection');
|
|
}
|
|
return $this->connection->read($expected, $mode);
|
|
}
|
|
|
|
public function getInteractiveShell(): ?SSH2
|
|
{
|
|
return $this->connection;
|
|
}
|
|
|
|
public function testConnection(array $server): bool
|
|
{
|
|
try {
|
|
$result = $this->connect($server, true);
|
|
if ($result) {
|
|
$this->disconnect();
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (\Throwable $e) {
|
|
$this->logger->warning("SSH connection test failed", ['error' => $e->getMessage()]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function getSystemInfo(): array
|
|
{
|
|
$commands = [
|
|
'hostname' => 'hostname',
|
|
'os' => 'cat /etc/os-release | head -1',
|
|
'kernel' => 'uname -r',
|
|
'uptime' => 'uptime -p',
|
|
'cpu_model' => 'cat /proc/cpuinfo | grep "model name" | head -1',
|
|
'cpu_cores' => 'nproc',
|
|
'mem_total' => 'free -b | grep Mem | awk \'{print $2}\'',
|
|
'mem_used' => "free -b | grep Mem | awk '{print $3}'",
|
|
'mem_avail' => "free -b | grep Mem | awk '{print $7}'",
|
|
'disk_total' => "df -B1 / | tail -1 | awk '{print $2}'",
|
|
'disk_used' => "df -B1 / | tail -1 | awk '{print $3}'",
|
|
'disk_avail' => "df -B1 / | tail -1 | awk '{print $4}'",
|
|
'disk_percent' => "df / | tail -1 | awk '{print $5}' | sed 's/%//'",
|
|
];
|
|
|
|
$info = [];
|
|
foreach ($commands as $key => $cmd) {
|
|
$result = $this->exec($cmd);
|
|
$info[$key] = trim($result['output']);
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
public function __destruct()
|
|
{
|
|
$this->disconnect();
|
|
}
|
|
}
|