diff --git a/src/WebSocketConnection.php b/src/WebSocketConnection.php index b357aa5..d985608 100644 --- a/src/WebSocketConnection.php +++ b/src/WebSocketConnection.php @@ -221,19 +221,33 @@ class WebSocketConnection implements WebSocketInterface /** * {@inheritDoc} + * + * @see writeBinary() to write binary frames as opposed to text frames. */ public function write($data) { return $this->send(self::OP_FRAME_TEXT, $data); } + /** + * Write binary frames. + * + * @param string $data + * @return bool + */ public function writeBinary($data) { return $this->send(self::OP_FRAME_BINARY, $data); } /** + * Encode and send a frame. * + * @param int $opcode + * @param string $data + * @param bool $final + * @param bool $masked + * @return bool */ public function send(int $opcode, string $data, bool $final = true, bool $masked = false) { @@ -245,9 +259,7 @@ class WebSocketConnection implements WebSocketInterface 'masked' => $masked ]); - $this->outStream->write($frame); - - return true; + return $this->outStream->write($frame); } /** @@ -261,7 +273,10 @@ class WebSocketConnection implements WebSocketInterface $this->emit('close', []); } - public function closeWithReason(string $reason, int $code=1000) + /** + * {@inheritDoc} + */ + public function closeWithReason(string $reason, int $code=1000): void { $payload = chr(($code >> 8) & 0xFF) . chr($code & 0xFF) . $reason; $this->send(self::OP_CLOSE, $payload); diff --git a/src/WebSocketInterface.php b/src/WebSocketInterface.php index ee4dea7..212ca18 100644 --- a/src/WebSocketInterface.php +++ b/src/WebSocketInterface.php @@ -16,15 +16,46 @@ interface WebSocketInterface extends ConnectionInterface const EVENT_GROUP_JOIN = 'join'; const EVENT_GROUP_LEAVE = 'leave'; + /** + * Close the connection with a reason and code. + * + * @param string $reason + * @param int $code + * @return void + */ + public function closeWithReason(string $reason, int $code=1000): void; - public function setGroup(?string $name): void; - - public function getGroupName(): ?string; - - public function getGroup(): ?ConnectionGroup; - - public function closeWithReason(string $reason, int $code=1000); - + /** + * Get the initial HTTP request sent to the server. + * + * @return ServerRequestInterface + */ public function getServerRequest(): ServerRequestInterface; -} \ No newline at end of file + /** + * Assign this connection to a connection group. If the connection is already + * part of a group, it will leave the current group before joining the new + * group. + * + * @param null|string $name The group name to join + * @return void + */ + public function setGroup(?string $name): void; + + /** + * Get the current connection group. + * + * @see getGroupName() if you want the name of the group. + * + * @return null|ConnectionGroup + */ + public function getGroup(): ?ConnectionGroup; + + /** + * Get the name of the current connection group + * + * @return null|string + */ + public function getGroupName(): ?string; + +} diff --git a/src/WebSocketMiddleware.php b/src/WebSocketMiddleware.php index e075edd..3d767f2 100644 --- a/src/WebSocketMiddleware.php +++ b/src/WebSocketMiddleware.php @@ -18,6 +18,8 @@ class WebSocketMiddleware implements EventEmitterInterface const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + const VERSION = 13; + use EventEmitterTrait; private GroupManager $groupManager; @@ -32,7 +34,7 @@ class WebSocketMiddleware implements EventEmitterInterface public function addRoute(string $path, callable $handler, array $allowedOrigins=[]): void { - + // TODO implement or remove } public function __invoke(ServerRequestInterface $request, callable $next) @@ -62,12 +64,14 @@ class WebSocketMiddleware implements EventEmitterInterface $this->emit(self::EVENT_CONNECTION, [ $websocket ]); //}); + // TODO would it be possible or rather useful for the 'connection' event to set additional response headers to be sent here? return new Response( Response::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'websocket', 'Connection' => 'upgrade', - 'Sec-WebSocket-Accept' => $handshakeResponse + 'Sec-WebSocket-Accept' => $handshakeResponse, + 'Sec-WebSocket-Version' => self::VERSION ), $stream ); diff --git a/src/WebSocketProtocol.php b/src/WebSocketProtocol.php index 1c8de35..a1c9ca5 100644 --- a/src/WebSocketProtocol.php +++ b/src/WebSocketProtocol.php @@ -125,6 +125,7 @@ class WebSocketProtocol $byte0 = ord($frame[0]); $decoded['final'] = !!($byte0 & 0x80); $decoded['opcode'] = $byte0 & 0x0F; + // Peek at the second byte, holding mask bit and len $byte1 = ord($frame[1]); $decoded['masked'] = $masked = !!($byte1 & 0x80); @@ -146,14 +147,16 @@ class WebSocketProtocol $header += 4; } - // Now for the masking + // Now for the masking, if present. if ($masked) { $mask = substr($frame, $header, 4); $header += 4; } - // Extract and unmask payload + // Extract the payload, and unmask it if needed. The mask() function handles + // both masking and unmasing as the algorithm uses xor. $payload = substr($frame, $header, $len); + // TODO check that extracted payload len equals expected len if ($masked) { $payload = $this->mask($payload, $mask); $decoded['mask'] = $mask; @@ -174,14 +177,17 @@ class WebSocketProtocol */ private function mask(string $payload, string $mask): string { + // Unpack the payload and mask into byte values $payloadData = array_map("ord", str_split($payload,1)); $maskData = array_map("ord", str_split($mask,1)); + // TODO check that mask len==4 $unmasked = []; for ($n = 0; $n < count($payloadData); $n++) { $unmasked[] = $payloadData[$n] ^ $maskData[$n % 4]; } + // Return the masked byte values packed into a string return join("", array_map("chr", $unmasked)); }