| 1: | <?php |
| 2: | /** |
| 3: | * This file is part of GameQ. |
| 4: | * |
| 5: | * GameQ is free software; you can redistribute it and/or modify |
| 6: | * it under the terms of the GNU Lesser General Public License as published by |
| 7: | * the Free Software Foundation; either version 3 of the License, or |
| 8: | * (at your option) any later version. |
| 9: | * |
| 10: | * GameQ is distributed in the hope that it will be useful, |
| 11: | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12: | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13: | * GNU Lesser General Public License for more details. |
| 14: | * |
| 15: | * You should have received a copy of the GNU Lesser General Public License |
| 16: | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17: | */ |
| 18: | |
| 19: | namespace GameQ\Protocols; |
| 20: | |
| 21: | use GameQ\Buffer; |
| 22: | use GameQ\Helpers\Str; |
| 23: | use GameQ\Protocol; |
| 24: | use GameQ\Result; |
| 25: | |
| 26: | /** |
| 27: | * GameSpy3 Protocol class |
| 28: | * |
| 29: | * Given the ability for non utf-8 characters to be used as hostnames, player names, etc... this |
| 30: | * version returns all strings utf-8 encoded. To access the proper version of a |
| 31: | * string response you must use Str::utf8ToIso() on the specific response. |
| 32: | * |
| 33: | * @author Austin Bischoff <austin@codebeard.com> |
| 34: | */ |
| 35: | class Gamespy3 extends Protocol |
| 36: | { |
| 37: | /** |
| 38: | * Array of packets we want to look up. |
| 39: | * Each key should correspond to a defined method in this or a parent class |
| 40: | * |
| 41: | * @var array |
| 42: | */ |
| 43: | protected $packets = [ |
| 44: | self::PACKET_CHALLENGE => "\xFE\xFD\x09\x10\x20\x30\x40", |
| 45: | self::PACKET_ALL => "\xFE\xFD\x00\x10\x20\x30\x40%s\xFF\xFF\xFF\x01", |
| 46: | ]; |
| 47: | |
| 48: | /** |
| 49: | * The query protocol used to make the call |
| 50: | * |
| 51: | * @var string |
| 52: | */ |
| 53: | protected $protocol = 'gamespy3'; |
| 54: | |
| 55: | /** |
| 56: | * String name of this protocol class |
| 57: | * |
| 58: | * @var string |
| 59: | */ |
| 60: | protected $name = 'gamespy3'; |
| 61: | |
| 62: | /** |
| 63: | * Longer string name of this protocol class |
| 64: | * |
| 65: | * @var string |
| 66: | */ |
| 67: | protected $name_long = "GameSpy3 Server"; |
| 68: | |
| 69: | /** |
| 70: | * This defines the split between the server info and player/team info. |
| 71: | * This value can vary by game. This value is the default split. |
| 72: | * |
| 73: | * @var string |
| 74: | */ |
| 75: | protected $packetSplit = "/\\x00\\x00\\x01/m"; |
| 76: | |
| 77: | /** |
| 78: | * Parse the challenge response and apply it to all the packet types |
| 79: | * |
| 80: | * @param \GameQ\Buffer $challenge_buffer |
| 81: | * |
| 82: | * @return bool |
| 83: | * @throws \GameQ\Exception\Protocol |
| 84: | */ |
| 85: | public function challengeParseAndApply(Buffer $challenge_buffer) |
| 86: | { |
| 87: | // Pull out the challenge |
| 88: | $challenge = substr(preg_replace("/[^0-9\-]/si", "", $challenge_buffer->getBuffer()), 1); |
| 89: | |
| 90: | // By default, no challenge result (see #197) |
| 91: | $challenge_result = ''; |
| 92: | |
| 93: | // Check for valid challenge (see #197) |
| 94: | if ($challenge) { |
| 95: | // Encode chellenge result |
| 96: | $challenge_result = sprintf( |
| 97: | "%c%c%c%c", |
| 98: | ($challenge >> 24), |
| 99: | ($challenge >> 16), |
| 100: | ($challenge >> 8), |
| 101: | ($challenge >> 0) |
| 102: | ); |
| 103: | } |
| 104: | |
| 105: | // Apply the challenge and return |
| 106: | return $this->challengeApply($challenge_result); |
| 107: | } |
| 108: | |
| 109: | /** |
| 110: | * Process the response |
| 111: | * |
| 112: | * @return array |
| 113: | * @throws \GameQ\Exception\Protocol |
| 114: | */ |
| 115: | public function processResponse() |
| 116: | { |
| 117: | // Holds the processed packets |
| 118: | $processed = []; |
| 119: | |
| 120: | // Iterate over the packets |
| 121: | foreach ($this->packets_response as $response) { |
| 122: | // Make a buffer |
| 123: | $buffer = new Buffer($response, Buffer::NUMBER_TYPE_BIGENDIAN); |
| 124: | |
| 125: | // Packet type = 0 |
| 126: | $buffer->readInt8(); |
| 127: | |
| 128: | // Session Id |
| 129: | $buffer->readInt32(); |
| 130: | |
| 131: | // We need to burn the splitnum\0 because it is not used |
| 132: | $buffer->skip(9); |
| 133: | |
| 134: | // Get the id |
| 135: | $id = $buffer->readInt8(); |
| 136: | |
| 137: | // Burn next byte not sure what it is used for |
| 138: | $buffer->skip(1); |
| 139: | |
| 140: | // Add this packet to the processed |
| 141: | $processed[$id] = $buffer->getBuffer(); |
| 142: | |
| 143: | unset($buffer, $id); |
| 144: | } |
| 145: | |
| 146: | // Sort packets, reset index |
| 147: | ksort($processed); |
| 148: | |
| 149: | // Offload cleaning up the packets if they happen to be split |
| 150: | $packets = $this->cleanPackets(array_values($processed)); |
| 151: | |
| 152: | // Split the packets by type general and the rest (i.e. players & teams) |
| 153: | $split = preg_split($this->packetSplit, implode('', $packets)); |
| 154: | |
| 155: | // Create a new result |
| 156: | $result = new Result(); |
| 157: | |
| 158: | // Assign variable due to pass by reference in PHP 7+ |
| 159: | $buffer = new Buffer($split[0], Buffer::NUMBER_TYPE_BIGENDIAN); |
| 160: | |
| 161: | // First key should be server details and rules |
| 162: | $this->processDetails($buffer, $result); |
| 163: | |
| 164: | // The rest should be the player and team information, if it exists |
| 165: | if (array_key_exists(1, $split)) { |
| 166: | $buffer = new Buffer($split[1], Buffer::NUMBER_TYPE_BIGENDIAN); |
| 167: | $this->processPlayersAndTeams($buffer, $result); |
| 168: | } |
| 169: | |
| 170: | unset($buffer); |
| 171: | |
| 172: | return $result->fetch(); |
| 173: | } |
| 174: | |
| 175: | // Internal methods |
| 176: | |
| 177: | /** |
| 178: | * Handles cleaning up packets since the responses can be a bit "dirty" |
| 179: | * |
| 180: | * @param array $packets |
| 181: | * |
| 182: | * @return array |
| 183: | * @throws \GameQ\Exception\Protocol |
| 184: | */ |
| 185: | protected function cleanPackets(array $packets = []) |
| 186: | { |
| 187: | // Get the number of packets |
| 188: | $packetCount = count($packets); |
| 189: | |
| 190: | // Compare last var of current packet with first var of next packet |
| 191: | // On a partial match, remove last var from current packet, |
| 192: | // variable header from next packet |
| 193: | for ($i = 0, $x = $packetCount; $i < $x - 1; $i++) { |
| 194: | // First packet |
| 195: | $fst = substr($packets[$i], 0, -1); |
| 196: | // Second packet |
| 197: | $snd = $packets[$i + 1]; |
| 198: | // Get last variable from first packet |
| 199: | $fstvar = substr($fst, strrpos($fst, "\x00") + 1); |
| 200: | // Get first variable from last packet |
| 201: | $snd = substr($snd, strpos($snd, "\x00") + 2); |
| 202: | $sndvar = substr($snd, 0, strpos($snd, "\x00")); |
| 203: | // Check if fstvar is a substring of sndvar |
| 204: | // If so, remove it from the first string |
| 205: | if (!empty($fstvar) && strpos($sndvar, $fstvar) !== false) { |
| 206: | $packets[$i] = preg_replace("#(\\x00[^\\x00]+\\x00)$#", "\x00", $packets[$i]); |
| 207: | } |
| 208: | } |
| 209: | |
| 210: | // Now let's loop the return and remove any dupe prefixes |
| 211: | for ($x = 1; $x < $packetCount; $x++) { |
| 212: | $buffer = new Buffer($packets[$x], Buffer::NUMBER_TYPE_BIGENDIAN); |
| 213: | |
| 214: | $prefix = $buffer->readString(); |
| 215: | |
| 216: | // Check to see if the return before has the same prefix present |
| 217: | if ($prefix != null && strstr($packets[($x - 1)], $prefix)) { |
| 218: | // Update the return by removing the prefix plus 2 chars |
| 219: | $packets[$x] = substr(str_replace($prefix, '', $packets[$x]), 2); |
| 220: | } |
| 221: | |
| 222: | unset($buffer); |
| 223: | } |
| 224: | |
| 225: | unset($x, $i, $snd, $sndvar, $fst, $fstvar); |
| 226: | |
| 227: | // Return cleaned packets |
| 228: | return $packets; |
| 229: | } |
| 230: | |
| 231: | /** |
| 232: | * Handles processing the details data into a usable format |
| 233: | * |
| 234: | * @param \GameQ\Buffer $buffer |
| 235: | * @param \GameQ\Result $result |
| 236: | * @return void |
| 237: | * @throws \GameQ\Exception\Protocol |
| 238: | */ |
| 239: | protected function processDetails(Buffer &$buffer, Result &$result) |
| 240: | { |
| 241: | // We go until we hit an empty key |
| 242: | while ($buffer->getLength()) { |
| 243: | $key = $buffer->readString(); |
| 244: | if (strlen($key) == 0) { |
| 245: | break; |
| 246: | } |
| 247: | $result->add($key, Str::isoToUtf8($buffer->readString())); |
| 248: | } |
| 249: | } |
| 250: | |
| 251: | /** |
| 252: | * Handles processing the player and team data into a usable format |
| 253: | * |
| 254: | * @param \GameQ\Buffer $buffer |
| 255: | * @param \GameQ\Result $result |
| 256: | */ |
| 257: | protected function processPlayersAndTeams(Buffer &$buffer, Result &$result) |
| 258: | { |
| 259: | /* |
| 260: | * Explode the data into groups. First is player, next is team (item_t) |
| 261: | * Each group should be as follows: |
| 262: | * |
| 263: | * [0] => item_ |
| 264: | * [1] => information for item_ |
| 265: | * ... |
| 266: | */ |
| 267: | $data = explode("\x00\x00", $buffer->getBuffer()); |
| 268: | |
| 269: | // By default item_group is blank, this will be set for each loop thru the data |
| 270: | $item_group = ''; |
| 271: | |
| 272: | // By default the item_type is blank, this will be set on each loop |
| 273: | $item_type = ''; |
| 274: | |
| 275: | // Save count as variable |
| 276: | $count = count($data); |
| 277: | |
| 278: | // Loop through all of the $data for information and pull it out into the result |
| 279: | for ($x = 0; $x < $count - 1; $x++) { |
| 280: | // Pull out the item |
| 281: | $item = $data[$x]; |
| 282: | // If this is an empty item, move on |
| 283: | if ($item == '' || $item == "\x00") { |
| 284: | continue; |
| 285: | } |
| 286: | /* |
| 287: | * Left as reference: |
| 288: | * |
| 289: | * Each block of player_ and team_t have preceding junk chars |
| 290: | * |
| 291: | * player_ is actually \x01player_ |
| 292: | * team_t is actually \x00\x02team_t |
| 293: | * |
| 294: | * Probably a by-product of the change to exploding the data from the original. |
| 295: | * |
| 296: | * For now we just strip out these characters |
| 297: | */ |
| 298: | // Check to see if $item has a _ at the end, this is player info |
| 299: | if (substr($item, -1) == '_') { |
| 300: | // Set the item group |
| 301: | $item_group = 'players'; |
| 302: | // Set the item type, rip off any trailing stuff and bad chars |
| 303: | $item_type = rtrim(str_replace("\x01", '', $item), '_'); |
| 304: | } elseif (substr($item, -2) == '_t') { |
| 305: | // Check to see if $item has a _t at the end, this is team info |
| 306: | // Set the item group |
| 307: | $item_group = 'teams'; |
| 308: | // Set the item type, rip off any trailing stuff and bad chars |
| 309: | $item_type = rtrim(str_replace(["\x00", "\x02"], '', $item), '_t'); |
| 310: | } else { |
| 311: | // We can assume it is data belonging to a previously defined item |
| 312: | |
| 313: | // Make a temp buffer so we have easier access to the data |
| 314: | $buf_temp = new Buffer($item, Buffer::NUMBER_TYPE_BIGENDIAN); |
| 315: | // Get the values |
| 316: | while ($buf_temp->getLength()) { |
| 317: | // No value so break the loop, end of string |
| 318: | if (($val = $buf_temp->readString()) === '') { |
| 319: | break; |
| 320: | } |
| 321: | // Add the value to the proper item in the correct group |
| 322: | $result->addSub($item_group, $item_type, Str::isoToUtf8(trim($val))); |
| 323: | } |
| 324: | // Unset our buffer |
| 325: | unset($buf_temp); |
| 326: | } |
| 327: | } |
| 328: | // Free up some memory |
| 329: | unset($count, $data, $item, $item_group, $item_type, $val); |
| 330: | } |
| 331: | } |
| 332: |