boot($loop); $address = $server->address(); $this->assertNotNull($address); $port = (int) parse_url($address, PHP_URL_PORT); $handshakeOk = false; $welcome = null; (new Connector([], $loop)) ->connect("127.0.0.1:{$port}") ->then(function (ConnectionInterface $conn) use (&$handshakeOk, &$welcome, $loop) { $buffer = ''; $upgraded = false; $conn->on('data', function ($data) use (&$buffer, &$upgraded, &$handshakeOk, &$welcome, $conn, $loop) { $buffer .= $data; if (! $upgraded) { $end = strpos($buffer, "\r\n\r\n"); if ($end === false) { return; } $handshakeOk = str_contains(substr($buffer, 0, $end), ' 101 '); $buffer = substr($buffer, $end + 4); $upgraded = true; if (! $handshakeOk) { $loop->stop(); return; } } // The server pushes a `welcome` frame on connect (no client frame needed). $decoded = self::decodeServerTextFrame($buffer); if ($decoded !== null) { $welcome = json_decode($decoded, true); $conn->close(); $loop->stop(); } }); $key = base64_encode(random_bytes(16)); $conn->write( "GET / HTTP/1.1\r\n". "Host: 127.0.0.1\r\n". "Upgrade: websocket\r\n". "Connection: Upgrade\r\n". "Sec-WebSocket-Key: {$key}\r\n". "Sec-WebSocket-Version: 13\r\n\r\n" ); }); $loop->addTimer(3.0, fn () => $loop->stop()); $loop->run(); $server->shutdown(); $this->assertTrue($handshakeOk, 'server should return a 101 upgrade'); $this->assertSame('welcome', $welcome['type'] ?? null); $this->assertArrayHasKey('peer', (array) $welcome); } /** Decode one unmasked server->client text frame (payload < 126 bytes), or null. */ private static function decodeServerTextFrame(string $data): ?string { if (strlen($data) < 2) { return null; } $length = ord($data[1]) & 0x7F; if ($length >= 126 || strlen($data) < 2 + $length) { return null; } return substr($data, 2, $length); } }