| 1: | <?php declare(strict_types=1); |
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | namespace Clansuite\ServerQuery\ServerProtocols; |
| 14: | |
| 15: | use function array_key_exists; |
| 16: | use function array_map; |
| 17: | use function array_values; |
| 18: | use function assert; |
| 19: | use function chr; |
| 20: | use function count; |
| 21: | use function date; |
| 22: | use function in_array; |
| 23: | use function is_array; |
| 24: | use function is_int; |
| 25: | use function is_numeric; |
| 26: | use function microtime; |
| 27: | use function mktime; |
| 28: | use function preg_match; |
| 29: | use function round; |
| 30: | use function strlen; |
| 31: | use function strpos; |
| 32: | use function substr; |
| 33: | use function unpack; |
| 34: | use Clansuite\ServerQuery\CSQuery; |
| 35: | use Override; |
| 36: | |
| 37: | |
| 38: | |
| 39: | |
| 40: | |
| 41: | |
| 42: | class Halflife extends CSQuery |
| 43: | { |
| 44: | |
| 45: | |
| 46: | |
| 47: | public string $name = 'Half-Life'; |
| 48: | |
| 49: | |
| 50: | |
| 51: | |
| 52: | public string $protocol = 'Halflife'; |
| 53: | |
| 54: | |
| 55: | |
| 56: | |
| 57: | public array $game_series_list = ['Half-Life']; |
| 58: | |
| 59: | |
| 60: | |
| 61: | |
| 62: | |
| 63: | |
| 64: | public array $supportedGames = ['Half-Life']; |
| 65: | public string $playerFormat = '/sscore/x2/ftime'; |
| 66: | |
| 67: | |
| 68: | |
| 69: | |
| 70: | |
| 71: | |
| 72: | |
| 73: | public function __construct(string $address, int $queryport) |
| 74: | { |
| 75: | parent::__construct(); |
| 76: | $this->address = $address; |
| 77: | $this->queryport = $queryport; |
| 78: | } |
| 79: | |
| 80: | |
| 81: | |
| 82: | |
| 83: | |
| 84: | |
| 85: | |
| 86: | |
| 87: | |
| 88: | public function rcon_query_server(string $command, string $rcon_pwd): false|string |
| 89: | { |
| 90: | $get_challenge = "\xFF\xFF\xFF\xFFchallenge rcon\n"; |
| 91: | |
| 92: | $address = $this->address ?? ''; |
| 93: | $queryport = $this->queryport ?? 0; |
| 94: | |
| 95: | if (($challenge_rcon = $this->sendCommand($address, $queryport, $get_challenge)) === '' || ($challenge_rcon = $this->sendCommand($address, $queryport, $get_challenge)) === '0' || ($challenge_rcon = $this->sendCommand($address, $queryport, $get_challenge)) === false) { |
| 96: | $this->debug['Command send ' . $command] = 'No challenge rcon received'; |
| 97: | |
| 98: | return false; |
| 99: | } |
| 100: | |
| 101: | if (in_array(preg_match('/challenge rcon ([0-9]+)/D', $challenge_rcon), [0, false], true)) { |
| 102: | $this->debug['Command send ' . $command] = 'No valid challenge rcon received'; |
| 103: | |
| 104: | return false; |
| 105: | } |
| 106: | $challenge_rcon = substr($challenge_rcon, 19, 10); |
| 107: | $command = "\xFF\xFF\xFF\xFFrcon \"" . $challenge_rcon . '" ' . $rcon_pwd . ' ' . $command . "\n"; |
| 108: | |
| 109: | if (($result = $this->sendCommand($address, $queryport, $command)) === '' || ($result = $this->sendCommand($address, $queryport, $command)) === '0' || ($result = $this->sendCommand($address, $queryport, $command)) === false) { |
| 110: | $this->debug['Command send ' . $command] = 'No reply received'; |
| 111: | |
| 112: | return false; |
| 113: | } |
| 114: | |
| 115: | return substr($result, 5); |
| 116: | } |
| 117: | |
| 118: | |
| 119: | |
| 120: | |
| 121: | |
| 122: | |
| 123: | |
| 124: | |
| 125: | |
| 126: | #[Override] |
| 127: | public function query_server(bool $getPlayers = true, bool $getRules = true): bool |
| 128: | { |
| 129: | if ($this->online) { |
| 130: | $this->reset(); |
| 131: | } |
| 132: | |
| 133: | $address = $this->address ?? ''; |
| 134: | $queryport = $this->queryport ?? 0; |
| 135: | |
| 136: | $starttime = microtime(true); |
| 137: | |
| 138: | |
| 139: | $command = "\xFF\xFF\xFF\xFFTSource Engine Query\x00"; |
| 140: | |
| 141: | if (($result = $this->sendCommand($address, $queryport, $command)) === '' || ($result = $this->sendCommand($address, $queryport, $command)) === '0' || ($result = $this->sendCommand($address, $queryport, $command)) === false) { |
| 142: | print '_sendCommand Problem while query_server halflife'; |
| 143: | |
| 144: | return false; |
| 145: | } |
| 146: | |
| 147: | $endtime = microtime(true); |
| 148: | $diff = round(($endtime - $starttime) * 1000, 0); |
| 149: | |
| 150: | $this->response = round($diff, 2); |
| 151: | |
| 152: | |
| 153: | |
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: | |
| 161: | |
| 162: | $data = unpack('Iheader/cindicator/c*', $result); |
| 163: | |
| 164: | if ($data === false) { |
| 165: | return false; |
| 166: | } |
| 167: | |
| 168: | assert(isset($data['header'], $data['indicator'])); |
| 169: | |
| 170: | |
| 171: | if (($data['header'] ?? null) !== -1) { |
| 172: | $this->debug[$command] = 'Not a hl server, expected 0xFF 0xFF 0xFF 0xFF in first 4 bytes'; |
| 173: | |
| 174: | return false; |
| 175: | } |
| 176: | |
| 177: | if (!isset($data['indicator']) || $data['indicator'] !== 0x6D) { |
| 178: | $this->debug[$command] = 'Not a hl server, expected 0x6D in byte 5'; |
| 179: | |
| 180: | return false; |
| 181: | } |
| 182: | |
| 183: | $pos = 1; |
| 184: | |
| 185: | $gameip = $this->get_string($data, $pos); |
| 186: | $pos += strlen($gameip) + 1; |
| 187: | |
| 188: | $hostname = $this->get_string($data, $pos); |
| 189: | $pos += strlen($hostname) + 1; |
| 190: | |
| 191: | $map = $this->get_string($data, $pos); |
| 192: | $pos += strlen($map) + 1; |
| 193: | |
| 194: | $gametype = $this->get_string($data, $pos); |
| 195: | $pos += strlen($gametype) + 1; |
| 196: | |
| 197: | $gamedesc = $this->get_string($data, $pos); |
| 198: | $pos += strlen($gamedesc) + 1; |
| 199: | |
| 200: | $numplayers = isset($data[$pos]) && is_numeric($data[$pos]) ? (int) $data[$pos] : 0; |
| 201: | $pos++; |
| 202: | |
| 203: | $maxplayers = isset($data[$pos]) && is_numeric($data[$pos]) ? (int) $data[$pos] : 0; |
| 204: | $pos++; |
| 205: | $pos++; |
| 206: | $pos++; |
| 207: | $pos++; |
| 208: | |
| 209: | $password = isset($data[$pos]) && is_numeric($data[$pos]) ? (int) $data[$pos] : 0; |
| 210: | $pos++; |
| 211: | |
| 212: | $ismod = isset($data[$pos]) && is_numeric($data[$pos]) ? (int) $data[$pos] : 0; |
| 213: | $pos++; |
| 214: | |
| 215: | |
| 216: | if ($ismod === 1) { |
| 217: | $modurlinfo = $this->get_string($data, $pos); |
| 218: | $pos += strlen($modurlinfo) + 1; |
| 219: | |
| 220: | $modurldownload = $this->get_string($data, $pos); |
| 221: | $pos += strlen($modurldownload) + 1; |
| 222: | |
| 223: | $unused = $this->get_string($data, $pos); |
| 224: | $pos += strlen($unused) + 1; |
| 225: | |
| 226: | $modversion = $this->get_long($data, $pos); |
| 227: | $pos += 4; |
| 228: | |
| 229: | $modsize = $this->get_long($data, $pos); |
| 230: | $pos += 4; |
| 231: | |
| 232: | $serverside = $data[$pos]; |
| 233: | $pos++; |
| 234: | |
| 235: | $customclientdll = $data[$pos]; |
| 236: | $pos++; |
| 237: | } |
| 238: | $pos++; |
| 239: | $pos++; |
| 240: | |
| 241: | $this->gamename = $gamedesc; |
| 242: | $this->gametype = $gametype; |
| 243: | $this->hostport = $this->queryport ?? 0; |
| 244: | $this->servertitle = $hostname; |
| 245: | $this->mapname = $map; |
| 246: | $this->numplayers = $numplayers; |
| 247: | |
| 248: | $this->maxplayers = $maxplayers; |
| 249: | $this->gameversion = ''; |
| 250: | $this->maptitle = ''; |
| 251: | $this->password = $password; |
| 252: | |
| 253: | |
| 254: | $command = "\xFF\xFF\xFF\xFF\x57"; |
| 255: | |
| 256: | if (($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '0' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === false) { |
| 257: | return false; |
| 258: | } |
| 259: | |
| 260: | $data = unpack('Iheader/cindicator/c4', $result); |
| 261: | |
| 262: | if (($data['header'] ?? null) !== -1) { |
| 263: | $this->debug[$command] = 'Invalid challenge no reponse, expected 0xFF 0xFF 0xFF 0xFF in first 4 bytes'; |
| 264: | |
| 265: | return false; |
| 266: | } |
| 267: | |
| 268: | if (!isset($data['indicator']) || $data['indicator'] !== 0x41) { |
| 269: | $this->debug[$command] = 'Invalid challenge no reponse, expected 0x41 in byte 5'; |
| 270: | |
| 271: | return false; |
| 272: | } |
| 273: | |
| 274: | |
| 275: | $b1 = isset($data[1]) && is_numeric($data[1]) ? (int) $data[1] : 0; |
| 276: | $b2 = isset($data[2]) && is_numeric($data[2]) ? (int) $data[2] : 0; |
| 277: | $b3 = isset($data[3]) && is_numeric($data[3]) ? (int) $data[3] : 0; |
| 278: | $b4 = isset($data[4]) && is_numeric($data[4]) ? (int) $data[4] : 0; |
| 279: | |
| 280: | $challengeno = chr($b1) . chr($b2) . chr($b3) . chr($b4); |
| 281: | |
| 282: | |
| 283: | if ($this->numplayers > 0 && $getPlayers) { |
| 284: | $command = "\xFF\xFF\xFF\xFF\x55" . $challengeno; |
| 285: | |
| 286: | if (($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '0' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === false) { |
| 287: | return false; |
| 288: | } |
| 289: | |
| 290: | $data = unpack('Iheader/cindicator/cnumplayers/c*', $result); |
| 291: | |
| 292: | if (($data['header'] ?? null) !== -1) { |
| 293: | $this->debug[$command] = 'Invlaid player reponse, expected 0xFF 0xFF 0xFF 0xFF in first 4 bytes'; |
| 294: | |
| 295: | return false; |
| 296: | } |
| 297: | |
| 298: | if (!isset($data['indicator']) || $data['indicator'] !== 0x44) { |
| 299: | $this->debug[$command] = 'Invlaid player reponse, expected 0x44 in byte 5'; |
| 300: | |
| 301: | return false; |
| 302: | } |
| 303: | |
| 304: | $numplayers = isset($data['numplayers']) ? (int) $data['numplayers'] : 0; |
| 305: | |
| 306: | $pos = 1; |
| 307: | |
| 308: | $players = []; |
| 309: | |
| 310: | for ($i = 0; $i < $numplayers; $i++) { |
| 311: | $index = isset($data[$pos]) ? (int) $data[$pos] : 0; |
| 312: | $pos++; |
| 313: | |
| 314: | $players[$index]['name'] = $this->get_string($data, $pos); |
| 315: | $pos += strlen($players[$index]['name']) + 1; |
| 316: | |
| 317: | $players[$index]['score'] = $this->get_long($data, $pos); |
| 318: | $pos += 4; |
| 319: | |
| 320: | |
| 321: | $pos += 4; |
| 322: | } |
| 323: | |
| 324: | $this->playerkeys['name'] = true; |
| 325: | $this->playerkeys['score'] = true; |
| 326: | |
| 327: | |
| 328: | |
| 329: | $this->players = array_values(array_map(static function ($p) |
| 330: | { |
| 331: | return $p; |
| 332: | }, $players)); |
| 333: | } |
| 334: | |
| 335: | |
| 336: | $command = "\xFF\xFF\xFF\xFF\x56" . $challengeno; |
| 337: | |
| 338: | if (($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '0' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === false) { |
| 339: | return false; |
| 340: | } |
| 341: | |
| 342: | |
| 343: | |
| 344: | |
| 345: | $offset = 0; |
| 346: | $newresult = ''; |
| 347: | |
| 348: | while ($offset < strlen($result)) { |
| 349: | $newresult = $newresult . substr($result, $offset + 9, 1391); |
| 350: | $offset += 1400; |
| 351: | } |
| 352: | $result = $newresult; |
| 353: | |
| 354: | |
| 355: | |
| 356: | $data = unpack('Iheader/cindicator/snumrules/c*', $result); |
| 357: | |
| 358: | if (($data['header'] ?? null) !== -1) { |
| 359: | $this->debug[$command] = 'Invlaid rules reponse, expected 0xFF 0xFF 0xFF 0xFF in first 4 bytes'; |
| 360: | |
| 361: | return false; |
| 362: | } |
| 363: | |
| 364: | if (!isset($data['indicator']) || $data['indicator'] !== 0x45) { |
| 365: | $this->debug[$command] = 'Invlaid rules reponse, expected 0x45 in byte 5'; |
| 366: | |
| 367: | return false; |
| 368: | } |
| 369: | |
| 370: | $numrules = isset($data['numrules']) ? (int) $data['numrules'] : 0; |
| 371: | |
| 372: | $pos = 1; |
| 373: | |
| 374: | for ($i = 1; $i < $numrules; $i++) { |
| 375: | $rulename = $this->get_string($data, $pos); |
| 376: | $pos += strlen($rulename) + 1; |
| 377: | |
| 378: | $rulevalue = $this->get_string($data, $pos); |
| 379: | $pos += strlen($rulevalue) + 1; |
| 380: | |
| 381: | $this->rules[$rulename] = $rulevalue; |
| 382: | } |
| 383: | |
| 384: | return true; |
| 385: | } |
| 386: | |
| 387: | |
| 388: | |
| 389: | |
| 390: | |
| 391: | |
| 392: | public function processPlayers(string $data, string $format, int $formatLength): bool |
| 393: | { |
| 394: | $len = strlen($data); |
| 395: | |
| 396: | for ($i = 6; $i < $len; $i = $endPlayerName + $formatLength + 1) { |
| 397: | |
| 398: | $endPlayerName = strpos($data, "\x00", ++$i); |
| 399: | |
| 400: | if ($endPlayerName === false) { |
| 401: | return false; |
| 402: | } |
| 403: | |
| 404: | $curPlayer = unpack('@' . ($endPlayerName + 1) . $format, $data); |
| 405: | |
| 406: | if ($curPlayer === false) { |
| 407: | continue; |
| 408: | } |
| 409: | |
| 410: | |
| 411: | if (array_key_exists('time', $curPlayer) && is_int($curPlayer['time'])) { |
| 412: | $timestamp = mktime(0, 0, $curPlayer['time']); |
| 413: | |
| 414: | if ($timestamp !== false) { |
| 415: | $curPlayer['time'] = date('H:i:s', $timestamp); |
| 416: | } |
| 417: | } |
| 418: | |
| 419: | $curPlayer['name'] = substr($data, $i, $endPlayerName - $i); |
| 420: | |
| 421: | $this->players[] = $curPlayer; |
| 422: | } |
| 423: | |
| 424: | return true; |
| 425: | } |
| 426: | |
| 427: | |
| 428: | |
| 429: | |
| 430: | |
| 431: | |
| 432: | |
| 433: | |
| 434: | |
| 435: | public function get_string(mixed $data, int $pos): string |
| 436: | { |
| 437: | $string = ''; |
| 438: | |
| 439: | if (!is_array($data)) { |
| 440: | return ''; |
| 441: | } |
| 442: | |
| 443: | $len = count($data); |
| 444: | |
| 445: | while ($pos < $len && isset($data[$pos]) && $data[$pos] !== 0) { |
| 446: | $string .= chr((int) $data[$pos]); |
| 447: | $pos++; |
| 448: | } |
| 449: | |
| 450: | return $string; |
| 451: | } |
| 452: | |
| 453: | |
| 454: | |
| 455: | |
| 456: | |
| 457: | |
| 458: | |
| 459: | |
| 460: | |
| 461: | public function get_long(mixed $data, int $pos): int |
| 462: | { |
| 463: | if (!is_array($data) || $pos + 3 >= count($data)) { |
| 464: | return 0; |
| 465: | } |
| 466: | |
| 467: | $long = (int) ($data[$pos] ?? 0); |
| 468: | |
| 469: | for ($i = 1; $i < 4; $i++) { |
| 470: | $pos++; |
| 471: | $long += ((int) ($data[$pos] ?? 0)) << (8 * $i); |
| 472: | } |
| 473: | |
| 474: | return $long; |
| 475: | } |
| 476: | } |
| 477: | |