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: |