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: