From 287f5faa2300e6ed61aaef875704a82ee7746bc7 Mon Sep 17 00:00:00 2001 From: divinity76 Date: Thu, 1 May 2025 08:26:31 +0200 Subject: [PATCH 1/3] fix size 65536 HybiFrame + nitpicks off-by-1: previously, hybiframes with a 65536 bytes payload would generate a 16 bit size header signaling 0 bytes (pack("n", 65536) => "\x00\x00"), instead of using a 64bit size header. nit: composer.json php version had not been updated since 8.0.2 nit: instead of is_int && in_array, we can use in_array's strict mode. nit: $this->offset_mask and $this->offset_payload were initialized-to-null in constructor and encode, then written internally in getPayloadOffset, then written in encode() again. nit: use pack('J') instead of manually constructing big_endian_u64 (undocumented funfact, J is not available in 32bit php: https://github.com/php/doc-en/pull/4644 , that may be why the original code did not use J) nit: use random_bytes instead of openssl_random_pseudo_bytes(), the original code was written for php5 where random_bytes did not exist. nit: getInitialLength() was calculating the result twice internally. --- composer.json | 2 +- src/Frame/HybiFrame.php | 59 +++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/composer.json b/composer.json index 14a1b2e..b5e855c 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": "^7.4.15 || ^8.0.2", + "php": "^7.4.15 || ^8.0.2 || ^8.1.0 || ^8.2.0 || ^8.3.0 || ^8.4.0", "ext-sockets": "*", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/polyfill-php80": "^1.26" diff --git a/src/Frame/HybiFrame.php b/src/Frame/HybiFrame.php index 0d8a7c1..e9b875f 100644 --- a/src/Frame/HybiFrame.php +++ b/src/Frame/HybiFrame.php @@ -69,58 +69,59 @@ class HybiFrame extends Frame */ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $masked = false): Frame { - if (!\is_int($type) || !\in_array($type, Protocol::FRAME_TYPES)) { + if (!\in_array($type, Protocol::FRAME_TYPES, true)) { throw new InvalidArgumentException('Invalid frame type'); } - $this->type = $type; - $this->masked = $masked; + $this->type = $type; + $this->masked = $masked; $this->payload = $payload; - $this->length = \strlen($this->payload); - $this->offset_mask = null; - $this->offset_payload = null; + $this->length = \strlen($this->payload); $this->buffer = "\x00\x00"; + // FIN + opcode byte $this->buffer[self::BYTE_HEADER] = \chr( (self::BITFIELD_TYPE & $this->type) - | (self::BITFIELD_FINAL & \PHP_INT_MAX) + | self::BITFIELD_FINAL ); - $masked_bit = (self::BITFIELD_MASKED & ($this->masked ? \PHP_INT_MAX : 0)); + $masked_bit = $this->masked ? self::BITFIELD_MASKED : 0; if ($this->length <= 125) { $this->buffer[self::BYTE_INITIAL_LENGTH] = \chr( - (self::BITFIELD_INITIAL_LENGTH & $this->length) | $masked_bit + ($this->length & self::BITFIELD_INITIAL_LENGTH) + | $masked_bit ); - } elseif ($this->length <= 65536) { + } elseif ($this->length <= 65535) { $this->buffer[self::BYTE_INITIAL_LENGTH] = \chr( - (self::BITFIELD_INITIAL_LENGTH & 126) | $masked_bit + (126 & self::BITFIELD_INITIAL_LENGTH) + | $masked_bit ); $this->buffer .= \pack('n', $this->length); } else { $this->buffer[self::BYTE_INITIAL_LENGTH] = \chr( - (self::BITFIELD_INITIAL_LENGTH & 127) | $masked_bit + (127 & self::BITFIELD_INITIAL_LENGTH) + | $masked_bit ); - - if (\PHP_INT_MAX > 2147483647) { - $this->buffer .= \pack('NN', $this->length >> 32, $this->length); - } else { + if (\PHP_INT_SIZE === 4) { + // J is not available on 32-bit PHP $this->buffer .= \pack('NN', 0, $this->length); + } else { + $this->buffer .= \pack('J', $this->length); } } if ($this->masked) { - $this->mask = $this->generateMask(); + $this->mask = $this->generateMask(); + $this->offset_mask = \strlen($this->buffer); $this->buffer .= $this->mask; + $this->offset_payload = \strlen($this->buffer); $this->buffer .= $this->mask($this->payload); } else { + $this->offset_payload = \strlen($this->buffer); $this->buffer .= $this->payload; } - - $this->offset_mask = $this->getMaskOffset(); - $this->offset_payload = $this->getPayloadOffset(); - return $this; } @@ -131,13 +132,7 @@ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $m */ protected function generateMask() { - if (\extension_loaded('openssl')) { - return \openssl_random_pseudo_bytes(4); - } else { - // SHA1 is 128 bit (= 16 bytes) - // So we pack it into 32 bits - return \pack('N', \sha1(\spl_object_hash($this).\mt_rand(0, \PHP_INT_MAX).\uniqid('', true), true)); - } + return \random_bytes(4); } /** @@ -196,10 +191,8 @@ protected function getMaskOffset(): int if (!isset($this->offset_mask)) { $offset = self::BYTE_INITIAL_LENGTH + 1; $offset += $this->getLengthSize(); - $this->offset_mask = $offset; } - return $this->offset_mask; } @@ -233,8 +226,6 @@ protected function getInitialLength(): int if (!isset($this->buffer[self::BYTE_INITIAL_LENGTH])) { throw new FrameException('Cannot yet tell expected length'); } - $a = (int) (\ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH); - return (int) (\ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH); } @@ -258,10 +249,8 @@ protected function getPayloadOffset(): int if (!isset($this->offset_payload)) { $offset = $this->getMaskOffset(); $offset += $this->getMaskSize(); - $this->offset_payload = $offset; } - return $this->offset_payload; } @@ -294,7 +283,7 @@ public function getType(): int $type = (int) (\ord($this->buffer[self::BYTE_HEADER]) & self::BITFIELD_TYPE); - if (!\in_array($type, Protocol::FRAME_TYPES)) { + if (!\in_array($type, Protocol::FRAME_TYPES, true)) { throw new FrameException('Invalid payload type'); } From f2988c294ddb5ae31e6c08f38640e3e26c0a4468 Mon Sep 17 00:00:00 2001 From: divinity76 Date: Thu, 1 May 2025 09:21:46 +0200 Subject: [PATCH 2/3] StyleCI --- src/Frame/HybiFrame.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Frame/HybiFrame.php b/src/Frame/HybiFrame.php index e9b875f..9ddefca 100644 --- a/src/Frame/HybiFrame.php +++ b/src/Frame/HybiFrame.php @@ -73,10 +73,10 @@ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $m throw new InvalidArgumentException('Invalid frame type'); } - $this->type = $type; - $this->masked = $masked; + $this->type = $type; + $this->masked = $masked; $this->payload = $payload; - $this->length = \strlen($this->payload); + $this->length = \strlen($this->payload); $this->buffer = "\x00\x00"; @@ -113,7 +113,7 @@ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $m } if ($this->masked) { - $this->mask = $this->generateMask(); + $this->mask = $this->generateMask(); $this->offset_mask = \strlen($this->buffer); $this->buffer .= $this->mask; $this->offset_payload = \strlen($this->buffer); @@ -122,6 +122,7 @@ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $m $this->offset_payload = \strlen($this->buffer); $this->buffer .= $this->payload; } + return $this; } @@ -193,6 +194,7 @@ protected function getMaskOffset(): int $offset += $this->getLengthSize(); $this->offset_mask = $offset; } + return $this->offset_mask; } @@ -226,6 +228,7 @@ protected function getInitialLength(): int if (!isset($this->buffer[self::BYTE_INITIAL_LENGTH])) { throw new FrameException('Cannot yet tell expected length'); } + return (int) (\ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH); } @@ -251,6 +254,7 @@ protected function getPayloadOffset(): int $offset += $this->getMaskSize(); $this->offset_payload = $offset; } + return $this->offset_payload; } From 8d95a7413c9ff75e7986a0b6e9074b903524e8b5 Mon Sep 17 00:00:00 2001 From: divinity76 Date: Thu, 1 May 2025 12:33:13 +0200 Subject: [PATCH 3/3] PR feedback https://github.com/chrome-php/wrench/pull/24/files#r2070000317 and https://github.com/chrome-php/wrench/pull/24/files#r2069999223 --- composer.json | 2 +- src/Frame/HybiFrame.php | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b5e855c..14a1b2e 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": "^7.4.15 || ^8.0.2 || ^8.1.0 || ^8.2.0 || ^8.3.0 || ^8.4.0", + "php": "^7.4.15 || ^8.0.2", "ext-sockets": "*", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/polyfill-php80": "^1.26" diff --git a/src/Frame/HybiFrame.php b/src/Frame/HybiFrame.php index 9ddefca..d331a7a 100644 --- a/src/Frame/HybiFrame.php +++ b/src/Frame/HybiFrame.php @@ -133,7 +133,13 @@ public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $m */ protected function generateMask() { - return \random_bytes(4); + if (\extension_loaded('openssl')) { + return \openssl_random_pseudo_bytes(4); + } else { + // SHA1 is 128 bit (= 16 bytes) + // So we pack it into 32 bits + return \pack('N', \sha1(\spl_object_hash($this).\mt_rand(0, \PHP_INT_MAX).\uniqid('', true), true)); + } } /**