Entrar
PT
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / TlsStream.php · commit fb1b7c9
<?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]);    }}