Code cleanup, return value fix, comments

This commit is contained in:
Chris 2024-02-24 13:59:39 +01:00
parent d603870d7f
commit 899dd3b7e4
4 changed files with 73 additions and 17 deletions

View File

@ -221,19 +221,33 @@ class WebSocketConnection implements WebSocketInterface
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* @see writeBinary() to write binary frames as opposed to text frames.
*/ */
public function write($data) public function write($data)
{ {
return $this->send(self::OP_FRAME_TEXT, $data); return $this->send(self::OP_FRAME_TEXT, $data);
} }
/**
* Write binary frames.
*
* @param string $data
* @return bool
*/
public function writeBinary($data) public function writeBinary($data)
{ {
return $this->send(self::OP_FRAME_BINARY, $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) public function send(int $opcode, string $data, bool $final = true, bool $masked = false)
{ {
@ -245,9 +259,7 @@ class WebSocketConnection implements WebSocketInterface
'masked' => $masked 'masked' => $masked
]); ]);
$this->outStream->write($frame); return $this->outStream->write($frame);
return true;
} }
/** /**
@ -261,7 +273,10 @@ class WebSocketConnection implements WebSocketInterface
$this->emit('close', []); $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; $payload = chr(($code >> 8) & 0xFF) . chr($code & 0xFF) . $reason;
$this->send(self::OP_CLOSE, $payload); $this->send(self::OP_CLOSE, $payload);

View File

@ -16,15 +16,46 @@ interface WebSocketInterface extends ConnectionInterface
const EVENT_GROUP_JOIN = 'join'; const EVENT_GROUP_JOIN = 'join';
const EVENT_GROUP_LEAVE = 'leave'; 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; /**
* Get the initial HTTP request sent to the server.
public function getGroupName(): ?string; *
* @return ServerRequestInterface
public function getGroup(): ?ConnectionGroup; */
public function closeWithReason(string $reason, int $code=1000);
public function getServerRequest(): ServerRequestInterface; public function getServerRequest(): ServerRequestInterface;
/**
* 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;
} }

View File

@ -18,6 +18,8 @@ class WebSocketMiddleware implements EventEmitterInterface
const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const VERSION = 13;
use EventEmitterTrait; use EventEmitterTrait;
private GroupManager $groupManager; private GroupManager $groupManager;
@ -32,7 +34,7 @@ class WebSocketMiddleware implements EventEmitterInterface
public function addRoute(string $path, callable $handler, array $allowedOrigins=[]): void public function addRoute(string $path, callable $handler, array $allowedOrigins=[]): void
{ {
// TODO implement or remove
} }
public function __invoke(ServerRequestInterface $request, callable $next) public function __invoke(ServerRequestInterface $request, callable $next)
@ -62,12 +64,14 @@ class WebSocketMiddleware implements EventEmitterInterface
$this->emit(self::EVENT_CONNECTION, [ $websocket ]); $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( return new Response(
Response::STATUS_SWITCHING_PROTOCOLS, Response::STATUS_SWITCHING_PROTOCOLS,
array( array(
'Upgrade' => 'websocket', 'Upgrade' => 'websocket',
'Connection' => 'upgrade', 'Connection' => 'upgrade',
'Sec-WebSocket-Accept' => $handshakeResponse 'Sec-WebSocket-Accept' => $handshakeResponse,
'Sec-WebSocket-Version' => self::VERSION
), ),
$stream $stream
); );

View File

@ -125,6 +125,7 @@ class WebSocketProtocol
$byte0 = ord($frame[0]); $byte0 = ord($frame[0]);
$decoded['final'] = !!($byte0 & 0x80); $decoded['final'] = !!($byte0 & 0x80);
$decoded['opcode'] = $byte0 & 0x0F; $decoded['opcode'] = $byte0 & 0x0F;
// Peek at the second byte, holding mask bit and len // Peek at the second byte, holding mask bit and len
$byte1 = ord($frame[1]); $byte1 = ord($frame[1]);
$decoded['masked'] = $masked = !!($byte1 & 0x80); $decoded['masked'] = $masked = !!($byte1 & 0x80);
@ -146,14 +147,16 @@ class WebSocketProtocol
$header += 4; $header += 4;
} }
// Now for the masking // Now for the masking, if present.
if ($masked) { if ($masked) {
$mask = substr($frame, $header, 4); $mask = substr($frame, $header, 4);
$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); $payload = substr($frame, $header, $len);
// TODO check that extracted payload len equals expected len
if ($masked) { if ($masked) {
$payload = $this->mask($payload, $mask); $payload = $this->mask($payload, $mask);
$decoded['mask'] = $mask; $decoded['mask'] = $mask;
@ -174,14 +177,17 @@ class WebSocketProtocol
*/ */
private function mask(string $payload, string $mask): string private function mask(string $payload, string $mask): string
{ {
// Unpack the payload and mask into byte values
$payloadData = array_map("ord", str_split($payload,1)); $payloadData = array_map("ord", str_split($payload,1));
$maskData = array_map("ord", str_split($mask,1)); $maskData = array_map("ord", str_split($mask,1));
// TODO check that mask len==4
$unmasked = []; $unmasked = [];
for ($n = 0; $n < count($payloadData); $n++) { for ($n = 0; $n < count($payloadData); $n++) {
$unmasked[] = $payloadData[$n] ^ $maskData[$n % 4]; $unmasked[] = $payloadData[$n] ^ $maskData[$n % 4];
} }
// Return the masked byte values packed into a string
return join("", array_map("chr", $unmasked)); return join("", array_map("chr", $unmasked));
} }