true, 'opcode' => null, 'masked' => false, 'mask' => null, 'rsv1' => false, 'rsv2' => false, 'rsv3' => false, 'payload' => null ], ...$frame ]; $len = strlen($frame['payload']); if ($len > 65535) { $size0 = 127; $size1 = ($len >> 24) & 0xFF; $size2 = ($len >> 16) & 0xFF; $size3 = ($len >> 8) & 0xFF; $size4 = $len & 0xFF; } elseif ($len > 126) { $size0 = 126; $size1 = ($len >> 8) & 0xFF; $size2 = $len & 0xFF; $size3 = null; $size4 = null; } else { $size0 = $len; $size1 = null; $size2 = null; $size3 = null; $size4 = null; } $encoded .= chr(($frame['final']?0x80:0x00) | ($frame['rsv1']?0x40:0x00) | ($frame['rsv2']?0x20:0x00) | ($frame['rsv3']?0x10:0x00) | ($frame['opcode'] & 0xF)); $encoded .= chr(($frame['masked']?0x80:0x00) | ($size0 & 0x7F)); if ($size1 !== null) { $encoded .= chr($size1) . chr($size2); } if ($size3 !== null) { $encoded .= chr($size3) . chr($size4); } if ($frame['masked'] === true) { if ($frame['mask'] === null || strlen($frame['mask']) !== 4) { $frame['mask'] = chr(mt_rand(0,255)).chr(mt_rand(0,255)).chr(mt_rand(0,255)).chr(mt_rand(0,255)); } $encoded .= $frame['mask']; $encoded .= $this->mask($frame['payload'], $frame['mask']); } else { $encoded .= $frame['payload']; } return $encoded; } /** * Decode a websocket frame and return an array with the keys: * * opcode (int) - the opcode * final (bool) - final frame * rsv1-3 (bool) - reserved bits * masked (bool) - if the frame was masked * mask (string) - the mask bytes, if masked * length (int) - length of payload * payload (string) - the payload * * @param string $frame The frame data to decode * @return array The decoded frame */ public function decode(string $frame): array { // Decoded frame $decoded = []; // Keep track of the number of bytes in the header $header = 2; // Peek at the first byte, holding flags and opcode $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); $decoded['rsv1'] = !!($byte1 & 0x40); $decoded['rsv2'] = !!($byte1 & 0x20); $decoded['rsv3'] = !!($byte1 & 0x10); $len = $byte1 & 0x7F; // Read extended length if present if ($len == 126) { $len = (ord($frame[$header+0]) << 8) | (ord($frame[$header+1])); $header += 2; } elseif ($len == 127) { $len = (ord($frame[$header+0]) << 24) | (ord($frame[$header+1]) << 16) | (ord($frame[$header+2]) << 8) | (ord($frame[$header+3])); $header += 4; } // Now for the masking, if present. if ($masked) { $mask = substr($frame, $header, 4); $header += 4; } // 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; } $decoded['length'] = $len; $decoded['payload'] = $payload; return $decoded; } /** * Masking is reversible, and simply xors a repeated 4-byte key with the data. * * @param string $payload The unmasked (or masked) input * @param string $mask The mask to use (4 bytes) * @return string The masked (or unmasked) output */ 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)); } }