• Updated Plan
└ Продолжаю итерацию 5 как отдельный рабочий срез поверх текущих SSH credentials.
□ Добавить модель/миграцию/repository для host_scans и покрыть repository тестами
□ Добавить SSH command runner и LinuxHostScanner с whitelist read-only команд
□ Подключить controller/routes/UI для запуска deep scan и просмотра результата
□ Прогнать проверки и зафиксировать ограничения Docker-верификации
72 lines
2.3 KiB
PHP
72 lines
2.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Domovoy\Services\Ssh;
|
|
|
|
use Domovoy\Models\Credential;
|
|
use Domovoy\Services\Security\CredentialVault;
|
|
use phpseclib3\Crypt\PublicKeyLoader;
|
|
use phpseclib3\Net\SSH2;
|
|
|
|
class SshCredentialTester
|
|
{
|
|
/** @var callable(string, int, int): object */
|
|
private $clientFactory;
|
|
|
|
/**
|
|
* @param callable(string, int, int): object|null $clientFactory
|
|
* @param array<string, int> $settings
|
|
*/
|
|
public function __construct(
|
|
private CredentialVault $vault,
|
|
?callable $clientFactory = null,
|
|
private array $settings = []
|
|
) {
|
|
$this->clientFactory = $clientFactory ?? static fn (string $host, int $port, int $timeout): SSH2 => new SSH2($host, $port, $timeout);
|
|
}
|
|
|
|
/** @return array{status: string, message: string} */
|
|
public function test(Credential $credential, string $host): array
|
|
{
|
|
if ($host === '') {
|
|
return ['status' => 'failed', 'message' => 'У устройства не указан IP или hostname'];
|
|
}
|
|
|
|
try {
|
|
$client = ($this->clientFactory)(
|
|
$host,
|
|
$credential->port,
|
|
(int)($this->settings['connect_timeout'] ?? 5)
|
|
);
|
|
|
|
$auth = $this->buildAuth($credential);
|
|
$ok = $client->login($credential->username, $auth);
|
|
|
|
if ($ok) {
|
|
return ['status' => 'ok', 'message' => 'Подключение успешно'];
|
|
}
|
|
|
|
return ['status' => 'failed', 'message' => 'SSH-аутентификация не прошла'];
|
|
} catch (\Throwable $e) {
|
|
return ['status' => 'failed', 'message' => 'Ошибка SSH: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
private function buildAuth(Credential $credential): mixed
|
|
{
|
|
if ($credential->authMethod === 'private_key') {
|
|
if ($credential->encryptedPrivateKey === null) {
|
|
throw new \RuntimeException('Private key is empty');
|
|
}
|
|
return PublicKeyLoader::loadPrivateKey($this->vault->decrypt($credential->encryptedPrivateKey));
|
|
}
|
|
|
|
if ($credential->encryptedSecret === null) {
|
|
throw new \RuntimeException('Password is empty');
|
|
}
|
|
|
|
return $this->vault->decrypt($credential->encryptedSecret);
|
|
}
|
|
}
|