Passed
Push — v3 ( 836575...105154 )
by
unknown
02:40
created

Gamespy3   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Test Coverage

Coverage 97.5%

Importance

Changes 6
Bugs 2 Features 0
Metric Value
wmc 23
eloc 83
c 6
b 2
f 0
dl 0
loc 295
ccs 78
cts 80
cp 0.975
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
B cleanPackets() 0 44 7
B processPlayersAndTeams() 0 73 8
A challengeParseAndApply() 0 22 2
A processDetails() 0 9 3
A processResponse() 0 58 3
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 <[email protected]>
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 6
    public function challengeParseAndApply(Buffer $challenge_buffer)
86
    {
87
        // Pull out the challenge
88 6
        $challenge = substr(preg_replace("/[^0-9\-]/si", "", $challenge_buffer->getBuffer()), 1);
89
90
        // By default, no challenge result (see #197)
91 6
        $challenge_result = '';
92
93
        // Check for valid challenge (see #197)
94 6
        if ($challenge) {
95
            // Encode chellenge result
96 6
            $challenge_result = sprintf(
97 6
                "%c%c%c%c",
98 6
                ($challenge >> 24),
99 6
                ($challenge >> 16),
100 6
                ($challenge >> 8),
101 6
                ($challenge >> 0)
102 6
            );
103
        }
104
105
        // Apply the challenge and return
106 6
        return $this->challengeApply($challenge_result);
107
    }
108
109
    /**
110
     * Process the response
111
     *
112
     * @return array
113
     * @throws \GameQ\Exception\Protocol
114
     */
115 132
    public function processResponse()
116
    {
117
        // Holds the processed packets
118 132
        $processed = [];
119
120
        // Iterate over the packets
121 132
        foreach ($this->packets_response as $response) {
122
            // Make a buffer
123 132
            $buffer = new Buffer($response, Buffer::NUMBER_TYPE_BIGENDIAN);
124
125
            // Packet type = 0
126 132
            $buffer->readInt8();
127
128
            // Session Id
129 132
            $buffer->readInt32();
130
131
            // We need to burn the splitnum\0 because it is not used
132 132
            $buffer->skip(9);
133
134
            // Get the id
135 132
            $id = $buffer->readInt8();
136
137
            // Burn next byte not sure what it is used for
138 132
            $buffer->skip(1);
139
140
            // Add this packet to the processed
141 132
            $processed[$id] = $buffer->getBuffer();
142
143 132
            unset($buffer, $id);
144
        }
145
146
        // Sort packets, reset index
147 132
        ksort($processed);
148
149
        // Offload cleaning up the packets if they happen to be split
150 132
        $packets = $this->cleanPackets(array_values($processed));
151
152
        // Split the packets by type general and the rest (i.e. players & teams)
153 132
        $split = preg_split($this->packetSplit, implode('', $packets));
154
155
        // Create a new result
156 132
        $result = new Result();
157
158
        // Assign variable due to pass by reference in PHP 7+
159 132
        $buffer = new Buffer($split[0], Buffer::NUMBER_TYPE_BIGENDIAN);
160
161
        // First key should be server details and rules
162 132
        $this->processDetails($buffer, $result);
163
164
        // The rest should be the player and team information, if it exists
165 132
        if (array_key_exists(1, $split)) {
166 132
            $buffer = new Buffer($split[1], Buffer::NUMBER_TYPE_BIGENDIAN);
167 132
            $this->processPlayersAndTeams($buffer, $result);
168
        }
169
170 132
        unset($buffer);
171
172 132
        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 132
    protected function cleanPackets(array $packets = [])
186
    {
187
        // Get the number of packets
188 132
        $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 132
        for ($i = 0, $x = $packetCount; $i < $x - 1; $i++) {
194
            // First packet
195 30
            $fst = substr($packets[$i], 0, -1);
196
            // Second packet
197 30
            $snd = $packets[$i + 1];
198
            // Get last variable from first packet
199 30
            $fstvar = substr($fst, strrpos($fst, "\x00") + 1);
200
            // Get first variable from last packet
201 30
            $snd = substr($snd, strpos($snd, "\x00") + 2);
202 30
            $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 30
            if (!empty($fstvar) && strpos($sndvar, $fstvar) !== false) {
206 18
                $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 132
        for ($x = 1; $x < $packetCount; $x++) {
212 30
            $buffer = new Buffer($packets[$x], Buffer::NUMBER_TYPE_BIGENDIAN);
213
214 30
            $prefix = $buffer->readString();
215
216
            // Check to see if the return before has the same prefix present
217 30
            if ($prefix != null && strstr($packets[($x - 1)], $prefix)) {
218
                // Update the return by removing the prefix plus 2 chars
219 18
                $packets[$x] = substr(str_replace($prefix, '', $packets[$x]), 2);
220
            }
221
222 30
            unset($buffer);
223
        }
224
225 132
        unset($x, $i, $snd, $sndvar, $fst, $fstvar);
226
227
        // Return cleaned packets
228 132
        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 132
    protected function processDetails(Buffer &$buffer, Result &$result)
240
    {
241
        // We go until we hit an empty key
242 132
        while ($buffer->getLength()) {
243 132
            $key = $buffer->readString();
244 132
            if (strlen($key) == 0) {
245
                break;
246
            }
247 132
            $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 114
    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 114
        $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 114
        $item_group = '';
271
272
        // By default the item_type is blank, this will be set on each loop
273 114
        $item_type = '';
274
275
        // Save count as variable
276 114
        $count = count($data);
277
278
        // Loop through all of the $data for information and pull it out into the result
279 114
        for ($x = 0; $x < $count - 1; $x++) {
280
            // Pull out the item
281 90
            $item = $data[$x];
282
            // If this is an empty item, move on
283 90
            if ($item == '' || $item == "\x00") {
284 12
                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 90
            if (substr($item, -1) == '_') {
300
                // Set the item group
301 90
                $item_group = 'players';
302
                // Set the item type, rip off any trailing stuff and bad chars
303 90
                $item_type = rtrim(str_replace("\x01", '', $item), '_');
304 84
            } 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 48
                $item_group = 'teams';
308
                // Set the item type, rip off any trailing stuff and bad chars
309 48
                $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 84
                $buf_temp = new Buffer($item, Buffer::NUMBER_TYPE_BIGENDIAN);
315
                // Get the values
316 84
                while ($buf_temp->getLength()) {
317
                    // No value so break the loop, end of string
318 84
                    if (($val = $buf_temp->readString()) === '') {
319
                        break;
320
                    }
321
                    // Add the value to the proper item in the correct group
322 84
                    $result->addSub($item_group, $item_type, Str::isoToUtf8(trim($val)));
323
                }
324
                // Unset our buffer
325 84
                unset($buf_temp);
326
            }
327
        }
328
        // Free up some memory
329 114
        unset($count, $data, $item, $item_group, $item_type, $val);
330
    }
331
}
332