login($user, $password)) { throw new RuntimeException("SSH password login failed for {$user}@{$host}"); } $this->ssh = $ssh; } public function connectWithKey(string $host, string $user, string $privateKey): void { $key = PublicKeyLoader::load($privateKey); $ssh = new SSH2($host); if (! $ssh->login($user, $key)) { throw new RuntimeException("SSH key login failed for {$user}@{$host}"); } $this->ssh = $ssh; } public function run(string $command): CommandResult { $stdout = (string) $this->ssh()->exec($command); $exit = $this->ssh()->getExitStatus(); return new CommandResult(is_int($exit) ? $exit : 0, $stdout, ''); } public function putFile(string $remotePath, string $contents): void { $b64 = base64_encode($contents); $dir = escapeshellarg(dirname($remotePath)); $path = escapeshellarg($remotePath); $result = $this->run("mkdir -p {$dir} && printf %s ".escapeshellarg($b64)." | base64 -d > {$path}"); if (! $result->ok()) { throw new RuntimeException("Failed to write remote file {$remotePath}"); } } public function hostKeyFingerprint(): string { $key = $this->ssh()->getServerPublicHostKey(); return $key === false ? '' : 'SHA256:'.base64_encode(hash('sha256', (string) $key, true)); } private function ssh(): SSH2 { if ($this->ssh === null) { throw new RuntimeException('RemoteShell is not connected.'); } return $this->ssh; } }