<?phpnamespace App\Xapi;use Evenement\EventEmitter;/** * Desktop only: a TLS connection (HTTPS, port 443) that never blocks, with the events of the engine's TcpConnection (open, data, * close, error). XCP-ng hosts have a self-signed certificate: like XenCenter's first connection, it is accepted, and its * fingerprint is kept (fingerprint()) so that a change can be shown. Polled by StreamTransport. */final class TlsStream extends EventEmitter{ /** @var resource|null */ private $socket = null; private string $state = 'connecting'; private string $out = ''; private float $started; private ?string $fingerprint = null; public function __construct(private string $host, private int $port, private float $timeout = 15.0) { $this->started = microtime(true); $context = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true, 'capture_peer_cert' => true, 'SNI_enabled' => true, 'peer_name' => $host]]); $target = 'tcp://' . (str_contains($host, ':') ? "[$host]" : $host) . ':' . $port; $socket = @stream_socket_client($target, $errno, $error, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, $context); if (!$socket) { $this->state = 'failed:' . ($error !== '' ? $error : "cannot connect ($errno)"); return; } stream_set_blocking($socket, false); $this->socket = $socket; $this->state = 'tcp'; } public static function available(): bool { return extension_loaded('openssl') && !\JumPHP\Kernel\Platform::isWasm(); } public function fingerprint(): ?string { return $this->fingerprint; } public function write(string $bytes): bool { if ($this->state === 'closed') { return false; } $this->out .= $bytes; return true; } /** Bytes written and not sent yet (HttpStreamTransfer waits while there are many: backpressure). */ public function buffered(): int { return strlen($this->out); } public function close(): void { if ($this->state === 'closed') { return; } $this->state = 'closed'; if (is_resource($this->socket)) { @fclose($this->socket); } $this->socket = null; $this->emit('close', [null]); } public function isClosed(): bool { return $this->state === 'closed'; } public function poll(): void { if (str_starts_with($this->state, 'failed:')) { $message = substr($this->state, 7); $this->state = 'closed'; $this->emit('error', [$message]); $this->emit('close', [$message]); return; } if ($this->state === 'closed' || $this->socket === null) { return; } if ($this->state !== 'open' && microtime(true) - $this->started > $this->timeout) { $this->fail('connection timed out'); return; } if ($this->state === 'tcp') { $read = null; $write = [$this->socket]; $except = null; if (@stream_select($read, $write, $except, 0) !== 1) { return; } if (@stream_socket_get_name($this->socket, true) === false) { $this->fail('connection refused'); return; } $this->state = 'handshake'; } if ($this->state === 'handshake') { $done = @stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT); if ($done === 0) { return; // more round trips } if ($done === false) { $this->fail('TLS handshake failed'); return; } $params = stream_context_get_params($this->socket); $cert = $params['options']['ssl']['peer_certificate'] ?? null; if ($cert !== null && function_exists('openssl_x509_fingerprint')) { $this->fingerprint = (string)openssl_x509_fingerprint($cert, 'sha256'); } $this->state = 'open'; $this->emit('open', []); } if ($this->state !== 'open') { return; } if ($this->out !== '') { $written = @fwrite($this->socket, $this->out); if ($written === false) { $this->fail('write failed'); return; } $this->out = (string)substr($this->out, $written); } $chunk = ''; while (is_resource($this->socket) && ($data = @fread($this->socket, 65536)) !== false && $data !== '') { $chunk .= $data; } if ($chunk !== '') { $this->emit('data', [$chunk]); } if (is_resource($this->socket) && feof($this->socket)) { $this->close(); } } private function fail(string $message): void { $this->state = 'closed'; if (is_resource($this->socket)) { @fclose($this->socket); } $this->socket = null; $this->emit('error', [$message]); $this->emit('close', [$message]); }}