request = $request; $this->inStream = $inStream; $this->outStream = $outStream; $this->groupManager = $groupManager; $this->inStream->on('data', $this->onWebSocketData(...)); } private function onWebSocketData($data) { // Keep track of the number of bytes in the header $header = 2; // Peek at the first byte, holding flags and opcode $byte0 = ord($data[0]); $final = !!($byte0 & 0x80); $opcode = $byte0 & 0x0F; // Peek at the second byte, holding mask bit and len $byte1 = ord($data[1]); $masked = !!($byte1 & 0x80); $len = $byte1 & 0x7F; // Read extended length if present if ($len == 126) { $len = (ord($data[$header+0]) << 8) | (ord($data[$header+1])); $header += 2; } elseif ($len == 127) { $len = (ord($data[$header+0]) << 24) | (ord($data[$header+1]) << 16) | (ord($data[$header+2]) << 8) | (ord($data[$header+3])); $header += 4; } // Now for the masking if ($masked) { $mask = substr($data, $header, 4); $header += 4; } // Extract and unmask payload $payload = substr($data, $header, $len); if ($masked) { $payload = $this->unmask($payload, $mask); } if (!$final) { if ($this->bufferedOp === null) { $this->buffer = $payload; $this->bufferedOp = $opcode; } else { $this->buffer .= $payload; } } if ($final) { $payload = $this->buffer . $payload; $this->buffer = null; if ($this->bufferedOp !== null) { $opcode = $this->bufferedOp; $this->bufferedOp = null; } } switch ($opcode) { case self::OP_PING: $this->sendPong(); return; case self::OP_CONTINUATION: $this->buffer .= $payload; return; case self::OP_FRAME_TEXT: $this->emit('text', [ $payload ]); return; case self::OP_FRAME_BINARY: $this->emit('binary', [ $payload ]); return; } } private function unmask(string $payload, string $mask): string { $payloadData = array_map("ord", str_split($payload,1)); $maskData = array_map("ord", str_split($mask,1)); //printf("Mask: %02x %02x %02x %02x\n", ...$maskData); $unmasked = []; for ($n = 0; $n < count($payloadData); $n++) { $unmasked[] = $payloadData[$n] ^ $maskData[$n % 4]; //printf("%02x ^ %02x = %02x %s\n", $payloadData[$n], $maskData[$n%4], $payloadData[$n]^$maskData[$n%4], chr($payloadData[$n]^$maskData[$n%4])); } return join("", array_map("chr", $unmasked)); } private function sendPong(): void { } public function setGroup(?string $name): void { if ($this->group) { $this->group->remove($this); $this->group = null; } $group = $this->groupManager->getConnectionGroup($name); $group->add($this); $this->group = $group; $this->groupName = $name; } public function getGroupName(): ?string { return $this->groupName; } /** * @return WebSocketInterface[] */ public function getGroupMembers(): array { return $this->group ? $this->group->getAll() : []; } public function getGroup(): ?ConnectionGroup { return $this->group; } public function getRemoteAddress() { return $this->request->getServerParams()['REMOTE_ADDR']; } public function getLocalAddress() { return $this->request->getServerParams()['SERVER_ADDR']; } public function isReadable() { return $this->inStream->isReadable(); } public function pause() { return $this->inStream->pause(); } public function resume() { return $this->inStream->resume(); } public function isWritable() { return $this->outStream->isWritable(); } public function pipe(WritableStreamInterface $dest, array $options = array()) { // TODO implement return $dest; } public function write($data) { // Header; final, text frame, unmasked $frame = chr(0x81) . chr(strlen($data)) . $data; $this->outStream->write($frame); } public function send(int $opcode, bool $final, string $data) { $frame = chr(($final?0x80:0x00) | ($opcode & 0xF)); $len = strlen($data); if ($len > 126) { $frame .= chr(0x7E) . chr(($len >> 8) & 0xFF) . chr($len & 0xFF); } $frame .= $data; $this->outStream->write($frame); } public function close() { $this->outStream->close(); $this->inStream->close(); } public function end($data = null) { } }