Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
1.43% |
3 / 210 |
|
14.29% |
1 / 7 |
CRAP | |
0.00% |
0 / 1 |
| Ventrilo | |
1.43% |
3 / 210 |
|
14.29% |
1 / 7 |
3743.60 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| query_server | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
756 | |||
| query | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
| getProtocolName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getVersion | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| processPackets | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
240 | |||
| decryptPackets | |
0.00% |
0 / 89 |
|
0.00% |
0 / 1 |
272 | |||
| 1 | <?php declare(strict_types=1); |
| 2 | |
| 3 | /** |
| 4 | * Clansuite Server Query |
| 5 | * |
| 6 | * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch |
| 7 | * SPDX-License-Identifier: MIT |
| 8 | * |
| 9 | * For the full copyright and license information, please view |
| 10 | * the LICENSE file that was distributed with this source code. |
| 11 | */ |
| 12 | |
| 13 | namespace Clansuite\ServerQuery\ServerProtocols; |
| 14 | |
| 15 | use function array_combine; |
| 16 | use function array_pad; |
| 17 | use function chr; |
| 18 | use function count; |
| 19 | use function define; |
| 20 | use function defined; |
| 21 | use function explode; |
| 22 | use function implode; |
| 23 | use function is_array; |
| 24 | use function is_int; |
| 25 | use function is_string; |
| 26 | use function ksort; |
| 27 | use function pack; |
| 28 | use function preg_replace_callback; |
| 29 | use function preg_split; |
| 30 | use function socket_close; |
| 31 | use function socket_create; |
| 32 | use function socket_recvfrom; |
| 33 | use function socket_sendto; |
| 34 | use function socket_set_option; |
| 35 | use function strlen; |
| 36 | use function strpos; |
| 37 | use function strtolower; |
| 38 | use function substr; |
| 39 | use function trim; |
| 40 | use function unpack; |
| 41 | use Clansuite\Capture\Protocol\ProtocolInterface; |
| 42 | use Clansuite\Capture\ServerAddress; |
| 43 | use Clansuite\Capture\ServerInfo; |
| 44 | use Clansuite\ServerQuery\CSQuery; |
| 45 | use Exception; |
| 46 | use Override; |
| 47 | use RuntimeException; |
| 48 | |
| 49 | /** |
| 50 | * Queries Ventrilo voice communication servers. |
| 51 | * |
| 52 | * Retrieves server information including status, connected clients, channels, and settings |
| 53 | * by sending a specific UDP query packet and parsing the encrypted response. |
| 54 | * Enables monitoring and display of Ventrilo server details in game server query systems. |
| 55 | */ |
| 56 | class Ventrilo extends CSQuery implements ProtocolInterface |
| 57 | { |
| 58 | public string $name = 'Ventrilo'; |
| 59 | |
| 60 | /** @var array<string> */ |
| 61 | public array $supportedGames = ['Ventrilo']; |
| 62 | public string $protocol = 'ventrilo'; |
| 63 | |
| 64 | /** |
| 65 | * @var array<string> |
| 66 | */ |
| 67 | public array $channels = []; |
| 68 | |
| 69 | /** |
| 70 | * Initializes the Ventrilo query instance. |
| 71 | * |
| 72 | * @param null|string $address The server address to query |
| 73 | * @param null|int $queryport The query port for the Ventrilo server |
| 74 | */ |
| 75 | public function __construct(?string $address = null, ?int $queryport = null) |
| 76 | { |
| 77 | parent::__construct(); |
| 78 | $this->address = $address ?? ''; |
| 79 | $this->queryport = $queryport ?? 0; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Queries the Ventrilo server and populates server information. |
| 84 | * |
| 85 | * Sends a UDP query packet to the server and processes the encrypted response |
| 86 | * to extract server details, player information, and rules. |
| 87 | * |
| 88 | * @param bool $getPlayers Whether to retrieve player information |
| 89 | * @param bool $getRules Whether to retrieve server rules/settings |
| 90 | * |
| 91 | * @return bool True on successful query, false on failure |
| 92 | */ |
| 93 | #[Override] |
| 94 | public function query_server(bool $getPlayers = true, bool $getRules = true): bool |
| 95 | { |
| 96 | // We'll use UDP sockets to send the known Ventrilo status packet and receive response(s). |
| 97 | |
| 98 | // Build query packet |
| 99 | $packet = "V\xC8\xF4\xF9`\xA2\x1E\xA5M\xFB\x03\xCCQN\xA1\x10\x95\xAF\xB2g\x17g\x812\xFBW\xFD\x8E\xD2\"r\x034z\xBB\x98"; |
| 100 | |
| 101 | $ip = (string) $this->address; |
| 102 | $port = (int) $this->queryport; |
| 103 | |
| 104 | // Create UDP socket |
| 105 | if (!defined('AF_INET')) { |
| 106 | define('AF_INET', 2); |
| 107 | define('SOCK_DGRAM', 2); |
| 108 | define('SOL_UDP', 17); |
| 109 | define('SOL_SOCKET', 1); |
| 110 | define('SO_RCVTIMEO', 20); |
| 111 | } |
| 112 | $sock = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); |
| 113 | |
| 114 | if ($sock === false) { |
| 115 | $this->errstr = 'Unable to create UDP socket for Ventrilo'; |
| 116 | |
| 117 | return false; |
| 118 | } |
| 119 | |
| 120 | // Set a short receive timeout via socket options if available |
| 121 | @socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 3, 'usec' => 0]); |
| 122 | |
| 123 | // Send packet |
| 124 | $sent = @socket_sendto($sock, $packet, strlen($packet), 0, $ip, $port); |
| 125 | |
| 126 | if ($sent === false) { |
| 127 | socket_close($sock); |
| 128 | $this->errstr = 'Failed to send Ventrilo query packet'; |
| 129 | |
| 130 | return false; |
| 131 | } |
| 132 | |
| 133 | // Try to receive multiple packets (Ventrilo may split into several). We'll collect up to 8 packets. |
| 134 | $packets = []; |
| 135 | |
| 136 | for ($i = 0; $i < 8; $i++) { |
| 137 | $buf = ''; |
| 138 | $from = ''; |
| 139 | $fromPort = 0; |
| 140 | $recv = @socket_recvfrom($sock, $buf, 8192, 0, $from, $fromPort); |
| 141 | |
| 142 | if ($recv === false || $recv === 0 || $buf === '') { |
| 143 | break; |
| 144 | } |
| 145 | $packets[] = $buf; |
| 146 | |
| 147 | // if fewer than 8192 bytes received, likely last packet |
| 148 | if ($recv < 8192) { |
| 149 | break; |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | socket_close($sock); |
| 154 | |
| 155 | if (count($packets) === 0) { |
| 156 | $this->errstr = 'No response from Ventrilo server'; |
| 157 | |
| 158 | return false; |
| 159 | } |
| 160 | |
| 161 | try { |
| 162 | $result = $this->processPackets($packets); |
| 163 | } catch (Exception $e) { |
| 164 | $this->errstr = 'Failed to parse Ventrilo response: ' . $e->getMessage(); |
| 165 | |
| 166 | return false; |
| 167 | } |
| 168 | |
| 169 | // Map result to our properties |
| 170 | $this->servertitle = isset($result['name']) && is_string($result['name']) ? $result['name'] : (isset($result['gq_name']) && is_string($result['gq_name']) ? $result['gq_name'] : $this->address ?? ''); |
| 171 | $this->numplayers = isset($result['clientcount']) && is_int($result['clientcount']) ? $result['clientcount'] : (isset($result['client_count']) && is_int($result['client_count']) ? $result['client_count'] : 0); |
| 172 | $this->maxplayers = isset($result['maxclients']) && is_int($result['maxclients']) ? $result['maxclients'] : (isset($result['max_players']) && is_int($result['max_players']) ? $result['max_players'] : 0); |
| 173 | $this->players = isset($result['players']) && is_array($result['players']) ? $result['players'] : []; |
| 174 | $this->channels = isset($result['teams']) && is_array($result['teams']) ? $result['teams'] : []; |
| 175 | $this->online = true; |
| 176 | |
| 177 | return true; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Performs a query on the specified Ventrilo server address. |
| 182 | * |
| 183 | * Updates internal state with server information and returns a ServerInfo object |
| 184 | * containing the query results. |
| 185 | * |
| 186 | * @param ServerAddress $addr The server address and port to query |
| 187 | * |
| 188 | * @return ServerInfo Server information including status, players, and settings |
| 189 | */ |
| 190 | #[Override] |
| 191 | public function query(ServerAddress $addr): ServerInfo |
| 192 | { |
| 193 | $this->address = $addr->ip; |
| 194 | $this->queryport = $addr->port; |
| 195 | $this->query_server(true, true); |
| 196 | |
| 197 | return new ServerInfo( |
| 198 | address: $this->address, |
| 199 | queryport: $this->queryport, |
| 200 | online: $this->online, |
| 201 | servertitle: $this->servertitle, |
| 202 | numplayers: $this->numplayers, |
| 203 | maxplayers: $this->maxplayers, |
| 204 | players: $this->players, |
| 205 | errstr: $this->errstr, |
| 206 | ); |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Returns the protocol name for Ventrilo. |
| 211 | * |
| 212 | * @return string The protocol identifier 'ventrilo' |
| 213 | */ |
| 214 | #[Override] |
| 215 | public function getProtocolName(): string |
| 216 | { |
| 217 | return $this->protocol; |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Extracts the Ventrilo server version from server information. |
| 222 | * |
| 223 | * @param ServerInfo $info The server information object |
| 224 | * |
| 225 | * @return string The server version string, or 'unknown' if not available |
| 226 | */ |
| 227 | #[Override] |
| 228 | public function getVersion(ServerInfo $info): string |
| 229 | { |
| 230 | return $info->gameversion ?? 'unknown'; |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Process and decrypt raw Ventrilo packets (array of binary strings) |
| 235 | * and return parsed associative array. |
| 236 | * |
| 237 | * @param array<mixed> $packets |
| 238 | * |
| 239 | * @throws RuntimeException |
| 240 | * |
| 241 | * @return array<mixed> |
| 242 | */ |
| 243 | protected function processPackets(array $packets): array |
| 244 | { |
| 245 | // decrypt packets into one string |
| 246 | $decrypted = $this->decryptPackets($packets); |
| 247 | |
| 248 | // convert %HEX sequences |
| 249 | $decrypted = preg_replace_callback( |
| 250 | '|%([0-9A-F]{2})|i', |
| 251 | static fn (array $matches): string => pack('H*', $matches[1]), |
| 252 | $decrypted, |
| 253 | ) ?? $decrypted; |
| 254 | |
| 255 | $lines = preg_split('/\r?\n/', $decrypted); |
| 256 | |
| 257 | if ($lines === false) { |
| 258 | $lines = []; |
| 259 | } |
| 260 | |
| 261 | $result = []; |
| 262 | $players = []; |
| 263 | $teams = []; |
| 264 | |
| 265 | $channelFields = 5; |
| 266 | $playerFields = 7; |
| 267 | |
| 268 | foreach ($lines as $line) { |
| 269 | $line = trim($line); |
| 270 | |
| 271 | if ($line === '') { |
| 272 | continue; |
| 273 | } |
| 274 | |
| 275 | $colonPos = strpos($line, ':'); |
| 276 | |
| 277 | if ($colonPos === false) { |
| 278 | continue; |
| 279 | } |
| 280 | |
| 281 | if ($colonPos <= 0) { |
| 282 | continue; |
| 283 | } |
| 284 | |
| 285 | $key = strtolower(substr($line, 0, $colonPos)); |
| 286 | $value = trim(substr($line, $colonPos + 1)); |
| 287 | |
| 288 | switch ($key) { |
| 289 | case 'client': |
| 290 | $items = explode(',', $value, $playerFields); |
| 291 | $p = []; |
| 292 | |
| 293 | foreach ($items as $item) { |
| 294 | $parts = array_pad(explode('=', $item, 2), 2, ''); |
| 295 | $k = strtolower((string) $parts[0]); |
| 296 | $v = $parts[1] ?? ''; |
| 297 | $p[$k] = $v; |
| 298 | } |
| 299 | $players[] = $p; |
| 300 | |
| 301 | break; |
| 302 | |
| 303 | case 'channel': |
| 304 | $items = explode(',', $value, $channelFields); |
| 305 | $c = []; |
| 306 | |
| 307 | foreach ($items as $item) { |
| 308 | $parts = array_pad(explode('=', $item, 2), 2, ''); |
| 309 | $k = strtolower((string) $parts[0]); |
| 310 | $v = $parts[1] ?? ''; |
| 311 | $c[$k] = $v; |
| 312 | } |
| 313 | $teams[] = $c; |
| 314 | |
| 315 | break; |
| 316 | |
| 317 | case 'channelfields': |
| 318 | $channelFields = count(explode(',', $value)); |
| 319 | |
| 320 | break; |
| 321 | |
| 322 | case 'clientfields': |
| 323 | $playerFields = count(explode(',', $value)); |
| 324 | |
| 325 | break; |
| 326 | |
| 327 | default: |
| 328 | $result[$key] = $value; |
| 329 | |
| 330 | break; |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | if ($players !== []) { |
| 335 | $result['players'] = $players; |
| 336 | } |
| 337 | |
| 338 | if ($teams !== []) { |
| 339 | $result['teams'] = $teams; |
| 340 | } |
| 341 | |
| 342 | return $result; |
| 343 | } |
| 344 | |
| 345 | /** |
| 346 | * Decrypt Ventrilo header/data packets and return combined plaintext string. |
| 347 | * |
| 348 | * @param array<mixed> $packets |
| 349 | * |
| 350 | * @throws RuntimeException |
| 351 | */ |
| 352 | protected function decryptPackets(array $packets): string |
| 353 | { |
| 354 | $head_encrypt_table = [ |
| 355 | 0x80, 0xE5, 0x0E, 0x38, 0xBA, 0x63, 0x4C, 0x99, 0x88, 0x63, 0x4C, 0xD6, 0x54, 0xB8, 0x65, 0x7E, |
| 356 | 0xBF, 0x8A, 0xF0, 0x17, 0x8A, 0xAA, 0x4D, 0x0F, 0xB7, 0x23, 0x27, 0xF6, 0xEB, 0x12, 0xF8, 0xEA, |
| 357 | 0x17, 0xB7, 0xCF, 0x52, 0x57, 0xCB, 0x51, 0xCF, 0x1B, 0x14, 0xFD, 0x6F, 0x84, 0x38, 0xB5, 0x24, |
| 358 | 0x11, 0xCF, 0x7A, 0x75, 0x7A, 0xBB, 0x78, 0x74, 0xDC, 0xBC, 0x42, 0xF0, 0x17, 0x3F, 0x5E, 0xEB, |
| 359 | 0x74, 0x77, 0x04, 0x4E, 0x8C, 0xAF, 0x23, 0xDC, 0x65, 0xDF, 0xA5, 0x65, 0xDD, 0x7D, 0xF4, 0x3C, |
| 360 | 0x4C, 0x95, 0xBD, 0xEB, 0x65, 0x1C, 0xF4, 0x24, 0x5D, 0x82, 0x18, 0xFB, 0x50, 0x86, 0xB8, 0x53, |
| 361 | 0xE0, 0x4E, 0x36, 0x96, 0x1F, 0xB7, 0xCB, 0xAA, 0xAF, 0xEA, 0xCB, 0x20, 0x27, 0x30, 0x2A, 0xAE, |
| 362 | 0xB9, 0x07, 0x40, 0xDF, 0x12, 0x75, 0xC9, 0x09, 0x82, 0x9C, 0x30, 0x80, 0x5D, 0x8F, 0x0D, 0x09, |
| 363 | 0xA1, 0x64, 0xEC, 0x91, 0xD8, 0x8A, 0x50, 0x1F, 0x40, 0x5D, 0xF7, 0x08, 0x2A, 0xF8, 0x60, 0x62, |
| 364 | 0xA0, 0x4A, 0x8B, 0xBA, 0x4A, 0x6D, 0x00, 0x0A, 0x93, 0x32, 0x12, 0xE5, 0x07, 0x01, 0x65, 0xF5, |
| 365 | 0xFF, 0xE0, 0xAE, 0xA7, 0x81, 0xD1, 0xBA, 0x25, 0x62, 0x61, 0xB2, 0x85, 0xAD, 0x7E, 0x9D, 0x3F, |
| 366 | 0x49, 0x89, 0x26, 0xE5, 0xD5, 0xAC, 0x9F, 0x0E, 0xD7, 0x6E, 0x47, 0x94, 0x16, 0x84, 0xC8, 0xFF, |
| 367 | 0x44, 0xEA, 0x04, 0x40, 0xE0, 0x33, 0x11, 0xA3, 0x5B, 0x1E, 0x82, 0xFF, 0x7A, 0x69, 0xE9, 0x2F, |
| 368 | 0xFB, 0xEA, 0x9A, 0xC6, 0x7B, 0xDB, 0xB1, 0xFF, 0x97, 0x76, 0x56, 0xF3, 0x52, 0xC2, 0x3F, 0x0F, |
| 369 | 0xB6, 0xAC, 0x77, 0xC4, 0xBF, 0x59, 0x5E, 0x80, 0x74, 0xBB, 0xF2, 0xDE, 0x57, 0x62, 0x4C, 0x1A, |
| 370 | 0xFF, 0x95, 0x6D, 0xC7, 0x04, 0xA2, 0x3B, 0xC4, 0x1B, 0x72, 0xC7, 0x6C, 0x82, 0x60, 0xD1, 0x0D, |
| 371 | ]; |
| 372 | |
| 373 | $data_encrypt_table = [ |
| 374 | 0x82, 0x8B, 0x7F, 0x68, 0x90, 0xE0, 0x44, 0x09, 0x19, 0x3B, 0x8E, 0x5F, 0xC2, 0x82, 0x38, 0x23, |
| 375 | 0x6D, 0xDB, 0x62, 0x49, 0x52, 0x6E, 0x21, 0xDF, 0x51, 0x6C, 0x6C, 0x76, 0x37, 0x86, 0x50, 0x7D, |
| 376 | 0x48, 0x1F, 0x65, 0xE7, 0x52, 0x6A, 0x88, 0xAA, 0xC1, 0x32, 0x2F, 0xF7, 0x54, 0x4C, 0xAA, 0x6D, |
| 377 | 0x7E, 0x6D, 0xA9, 0x8C, 0x0D, 0x3F, 0xFF, 0x6C, 0x09, 0xB3, 0xA5, 0xAF, 0xDF, 0x98, 0x02, 0xB4, |
| 378 | 0xBE, 0x6D, 0x69, 0x0D, 0x42, 0x73, 0xE4, 0x34, 0x50, 0x07, 0x30, 0x79, 0x41, 0x2F, 0x08, 0x3F, |
| 379 | 0x42, 0x73, 0xA7, 0x68, 0xFA, 0xEE, 0x88, 0x0E, 0x6E, 0xA4, 0x70, 0x74, 0x22, 0x16, 0xAE, 0x3C, |
| 380 | 0x81, 0x14, 0xA1, 0xDA, 0x7F, 0xD3, 0x7C, 0x48, 0x7D, 0x3F, 0x46, 0xFB, 0x6D, 0x92, 0x25, 0x17, |
| 381 | 0x36, 0x26, 0xDB, 0xDF, 0x5A, 0x87, 0x91, 0x6F, 0xD6, 0xCD, 0xD4, 0xAD, 0x4A, 0x29, 0xDD, 0x7D, |
| 382 | 0x59, 0xBD, 0x15, 0x34, 0x53, 0xB1, 0xD8, 0x50, 0x11, 0x83, 0x79, 0x66, 0x21, 0x9E, 0x87, 0x5B, |
| 383 | 0x24, 0x2F, 0x4F, 0xD7, 0x73, 0x34, 0xA2, 0xF7, 0x09, 0xD5, 0xD9, 0x42, 0x9D, 0xF8, 0x15, 0xDF, |
| 384 | 0x0E, 0x10, 0xCC, 0x05, 0x04, 0x35, 0x81, 0xB2, 0xD5, 0x7A, 0xD2, 0xA0, 0xA5, 0x7B, 0xB8, 0x75, |
| 385 | 0xD2, 0x35, 0x0B, 0x39, 0x8F, 0x1B, 0x44, 0x0E, 0xCE, 0x66, 0x87, 0x1B, 0x64, 0xAC, 0xE1, 0xCA, |
| 386 | 0x67, 0xB4, 0xCE, 0x33, 0xDB, 0x89, 0xFE, 0xD8, 0x8E, 0xCD, 0x58, 0x92, 0x41, 0x50, 0x40, 0xCB, |
| 387 | 0x08, 0xE1, 0x15, 0xEE, 0xF4, 0x64, 0xFE, 0x1C, 0xEE, 0x25, 0xE7, 0x21, 0xE6, 0x6C, 0xC6, 0xA6, |
| 388 | 0x2E, 0x52, 0x23, 0xA7, 0x20, 0xD2, 0xD7, 0x28, 0x07, 0x23, 0x14, 0x24, 0x3D, 0x45, 0xA5, 0xC7, |
| 389 | 0x90, 0xDB, 0x77, 0xDD, 0xEA, 0x38, 0x59, 0x89, 0x32, 0xBC, 0x00, 0x3A, 0x6D, 0x61, 0x4E, 0xDB, |
| 390 | 0x29, |
| 391 | ]; |
| 392 | |
| 393 | // Decrypt each packet's header and data |
| 394 | $decryptedParts = []; |
| 395 | |
| 396 | foreach ($packets as $packet) { |
| 397 | if (strlen($packet) < 20) { |
| 398 | throw new RuntimeException('Ventrilo packet too short'); |
| 399 | } |
| 400 | |
| 401 | $header = substr($packet, 0, 20); |
| 402 | |
| 403 | // unpack first 2 bytes as unsigned short (n) |
| 404 | $tmp = @unpack('n', $header); |
| 405 | |
| 406 | if (!is_array($tmp) || !isset($tmp[1])) { |
| 407 | throw new RuntimeException('Invalid header unpack'); |
| 408 | } |
| 409 | $u = (int) $tmp[1]; |
| 410 | |
| 411 | $unpacked = @unpack('C*', substr($header, 2)); |
| 412 | $chars = is_array($unpacked) ? $unpacked : []; |
| 413 | |
| 414 | $a1 = $u & 0xFF; |
| 415 | $a2 = $u >> 8; |
| 416 | |
| 417 | if ($a1 === 0) { |
| 418 | throw new RuntimeException('Header key invalid'); |
| 419 | } |
| 420 | |
| 421 | $header_items = []; |
| 422 | $table = $head_encrypt_table; |
| 423 | $characterCount = count($chars); |
| 424 | $key = 0; |
| 425 | |
| 426 | for ($index = 1; $index <= $characterCount; $index++) { |
| 427 | $chars[$index] = (($chars[$index] ?? 0) - (($table[$a2] ?? 0) + (($index - 1) % 5))) & 0xFF; |
| 428 | $a2 = ($a2 + $a1) & 0xFF; |
| 429 | |
| 430 | if (($index % 2) === 0) { |
| 431 | $b1 = $chars[$index - 1] ?? 0; |
| 432 | $b2 = $chars[$index] ?? 0; |
| 433 | $packed = chr($b1) . chr($b2); |
| 434 | $short_array = @unpack('n', $packed); |
| 435 | |
| 436 | if (is_array($short_array) && isset($short_array[1])) { |
| 437 | $header_items[$key] = (int) $short_array[1]; |
| 438 | $key++; |
| 439 | } |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | $keys = ['zero', 'cmd', 'id', 'totlen', 'len', 'totpck', 'pck', 'datakey', 'crc']; |
| 444 | |
| 445 | $header_assoc = array_combine($keys, $header_items); |
| 446 | |
| 447 | // decrypt data |
| 448 | $table = $data_encrypt_table; |
| 449 | |
| 450 | if (!isset($header_assoc['datakey']) || !isset($header_assoc['pck'])) { |
| 451 | throw new RuntimeException('Header missing datakey or pck'); |
| 452 | } |
| 453 | |
| 454 | $a1 = (int) $header_assoc['datakey'] & 0xFF; |
| 455 | $a2 = (int) $header_assoc['datakey'] >> 8; |
| 456 | |
| 457 | if ($a1 === 0) { |
| 458 | throw new RuntimeException('Data key invalid'); |
| 459 | } |
| 460 | |
| 461 | $charsDataUnpacked = @unpack('C*', substr($packet, 20)); |
| 462 | $charsData = is_array($charsDataUnpacked) ? $charsDataUnpacked : []; |
| 463 | $data = ''; |
| 464 | $characterCount = count($charsData); |
| 465 | |
| 466 | for ($index = 1; $index <= $characterCount; $index++) { |
| 467 | $byte = ($charsData[$index] ?? 0); |
| 468 | $byte = ($byte - (($table[$a2] ?? 0) + (($index - 1) % 72))) & 0xFF; |
| 469 | $charsData[$index] = $byte; |
| 470 | $a2 = ($a2 + $a1) & 0xFF; |
| 471 | $data .= chr($byte); |
| 472 | } |
| 473 | |
| 474 | $decryptedParts[(int) $header_assoc['pck']] = $data; |
| 475 | } |
| 476 | |
| 477 | ksort($decryptedParts); |
| 478 | |
| 479 | return implode('', $decryptedParts); |
| 480 | } |
| 481 | } |