Completed
Push — v3 ( a11e41...91b61e )
by Austin
06:49
created

Source::processPackets()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 96
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9

Importance

Changes 3
Bugs 2 Features 1
Metric Value
c 3
b 2
f 1
dl 0
loc 96
ccs 28
cts 28
cp 1
rs 4.9572
cc 9
eloc 36
nc 9
nop 2
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Exception\Protocol as Exception;
23
use GameQ\Protocol;
24
use GameQ\Result;
25
26
/**
27
 * Valve Source Engine Protocol Class (A2S)
28
 *
29
 * This class is used as the basis for all other source based servers
30
 * that rely on the source protocol for game querying.
31
 *
32
 * @SuppressWarnings(PHPMD.NumberOfChildren)
33
 *
34
 * @author Austin Bischoff <[email protected]>
35
 */
36
class Source extends Protocol
37
{
38
39
    /*
40
     * Source engine type constants
41
     */
42
    const SOURCE_ENGINE = 0,
43
        GOLDSOURCE_ENGINE = 1;
44
45
    /**
46
     * Array of packets we want to look up.
47
     * Each key should correspond to a defined method in this or a parent class
48
     *
49
     * @type array
50
     */
51
    protected $packets = [
52
        self::PACKET_CHALLENGE => "\xFF\xFF\xFF\xFF\x56\x00\x00\x00\x00",
53
        self::PACKET_DETAILS   => "\xFF\xFF\xFF\xFFTSource Engine Query\x00",
54
        self::PACKET_PLAYERS   => "\xFF\xFF\xFF\xFF\x55%s",
55
        self::PACKET_RULES     => "\xFF\xFF\xFF\xFF\x56%s",
56
    ];
57
58
    /**
59
     * Use the response flag to figure out what method to run
60
     *
61
     * @type array
62
     */
63
    protected $responses = [
64
        "\x49" => "processDetails", // I
65
        "\x6d" => "processDetailsGoldSource", // m, goldsource
66
        "\x44" => "processPlayers", // D
67
        "\x45" => "processRules", // E
68
    ];
69
70
    /**
71
     * The query protocol used to make the call
72
     *
73
     * @type string
74
     */
75
    protected $protocol = 'source';
76
77
    /**
78
     * String name of this protocol class
79
     *
80
     * @type string
81
     */
82
    protected $name = 'source';
83
84
    /**
85
     * Longer string name of this protocol class
86
     *
87
     * @type string
88
     */
89
    protected $name_long = "Source Server";
90
91
    /**
92
     * Define the Source engine type.  By default it is assumed to be Source
93
     *
94
     * @type int
95
     */
96
    protected $source_engine = self::SOURCE_ENGINE;
97
98
    /**
99
     * The client join link
100
     *
101
     * @type string
102
     */
103
    protected $join_link = "steam://connect/%s:%d/";
104
105
    /**
106
     * Normalize settings for this protocol
107
     *
108
     * @type array
109
     */
110
    protected $normalize = [
111
        // General
112
        'general' => [
113
            // target       => source
114
            'dedicated'  => 'dedicated',
115
            'gametype'   => 'game_descr',
116
            'hostname'   => 'hostname',
117
            'mapname'    => 'map',
118
            'maxplayers' => 'max_players',
119
            'mod'        => 'game_dir',
120
            'numplayers' => 'num_players',
121
            'password'   => 'password',
122
        ],
123
        // Individual
124
        'player'  => [
125
            'name'  => 'name',
126
            'score' => 'score',
127
            'time'  => 'time',
128
        ],
129
    ];
130
131
    /**
132
     * Parse the challenge response and apply it to all the packet types
133
     *
134
     * @param \GameQ\Buffer $challenge_buffer
135
     *
136
     * @return bool
137
     * @throws \GameQ\Exception\Protocol
138
     */
139 1
    public function challengeParseAndApply(Buffer $challenge_buffer)
140
    {
141
142
        // Skip the header
143 1
        $challenge_buffer->skip(5);
144
145
        // Apply the challenge and return
146 1
        return $this->challengeApply($challenge_buffer->read(4));
147
    }
148
149
    /**
150
     * Process the response
151
     *
152
     * @return array
153
     * @throws \GameQ\Exception\Protocol
154
     */
155 70
    public function processResponse()
156
    {
157
158 70
        $results = [];
159
160 70
        $packets = [];
161
162
        // We need to pre-sort these for split packets so we can do extra work where needed
163 70
        foreach ($this->packets_response as $response) {
164 70
            $buffer = new Buffer($response);
165
166
            // Get the header of packet (long)
167 70
            $header = $buffer->readInt32Signed();
168
169
            // Single packet
170 70
            if ($header == -1) {
171
                // We need to peek and see what kind of engine this is for later processing
172 70
                if ($buffer->lookAhead(1) == "\x6d") {
173 4
                    $this->source_engine = self::GOLDSOURCE_ENGINE;
174
                }
175
176 70
                $packets[] = $buffer->getBuffer();
177 70
                continue;
178
            } else {
179
                // Split packet
180
181
                // Packet Id (long)
182 28
                $packet_id = $buffer->readInt32Signed();
183
184
                // Add the buffer to the packet as another array
185 28
                $packets[$packet_id][] = $buffer->getBuffer();
186
            }
187
        }
188
189
        // Now that we have the packets sorted we need to iterate and process them
190 70
        foreach ($packets as $packet_id => $packet) {
191
            // We first need to off load split packets to combine them
192 70
            if (is_array($packet)) {
193 26
                $buffer = new Buffer($this->processPackets($packet_id, $packet));
194
            } else {
195 70
                $buffer = new Buffer($packet);
196
            }
197
198
            // Figure out what packet response this is for
199 70
            $response_type = $buffer->read(1);
200
201
            // Figure out which packet response this is
202 70
            if (!array_key_exists($response_type, $this->responses)) {
203 2
                throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid");
204
            }
205
206
            // Now we need to call the proper method
207 68
            $results = array_merge(
208
                $results,
209 68
                call_user_func_array([$this, $this->responses[$response_type]], [$buffer])
210
            );
211
212 68
            unset($buffer);
213
        }
214
215 68
        unset($packets, $packet);
216
217 68
        return $results;
218
    }
219
220
    /*
221
     * Internal methods
222
     */
223
224
    /**
225
     * Process the split packets and decompress if necessary
226
     *
227
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
228
     *
229
     * @param       $packet_id
230
     * @param array $packets
231
     *
232
     * @return string
233
     * @throws \GameQ\Exception\Protocol
234
     */
235 26
    protected function processPackets($packet_id, array $packets = [ ])
236
    {
237
238
        // Init array so we can order
239 26
        $packs = [ ];
240
241
        // We have multiple packets so we need to get them and order them
242 26
        foreach ($packets as $i => $packet) {
243
            // Make a buffer so we can read this info
244 26
            $buffer = new Buffer($packet);
245
246
            // Gold source
247 26
            if ($this->source_engine == self::GOLDSOURCE_ENGINE) {
248
                // Grab the packet number (byte)
249 9
                $packet_number = $buffer->readInt8();
250
251
                // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop
252 9
                if ($i == 0) {
253 9
                    $buffer->read(4);
254
                }
255
256
                // Now add the rest of the packet to the new array with the packet_number as the id so we can order it
257 9
                $packs[$packet_number] = $buffer->getBuffer();
258
            } else {
259
                // Number of packets in this set (byte)
260 17
                $buffer->readInt8();
261
262
                // The current packet number (byte)
263 17
                $packet_number = $buffer->readInt8();
264
265
                // Check to see if this is compressed
266
                // @todo: Check to make sure these decompress correctly, new changes may affect this loop.
267 17
                if ($packet_id & 0x80000000) {
268
                    // Check to see if we have Bzip2 installed
269 3
                    if (!function_exists('bzdecompress')) {
270
                        // @codeCoverageIgnoreStart
271
                        throw new Exception(
272
                            'Bzip2 is not installed.  See http://www.php.net/manual/en/book.bzip2.php for more info.',
273
                            0
274
                        );
275
                        // @codeCoverageIgnoreEnd
276
                    }
277
278
                    // Get the length of the packet (long)
279 3
                    $packet_length = $buffer->readInt32Signed();
280
281
                    // Checksum for the decompressed packet (long), burn it - doesnt work in split responses
282 3
                    $buffer->readInt32Signed();
283
284
                    // Try to decompress
285 3
                    $result = bzdecompress($buffer->getBuffer());
286
287
                    // Now verify the length
288 3
                    if (strlen($result) != $packet_length) {
289
                        // @codeCoverageIgnoreStart
290
                        throw new Exception(
291
                            "Checksum for compressed packet failed! Length expected: {$packet_length}, length
292
                            returned: " . strlen($result)
293
                        );
294
                        // @codeCoverageIgnoreEnd
295
                    }
296
297
                    // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop
298 3
                    if ($i == 0) {
299 3
                        $result = substr($result, 4);
300
                    }
301
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
302
                } else {
303
                    // Get the packet length (short), burn it
304 14
                    $buffer->readInt16Signed();
305
306
                    // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop
307 14
                    if ($i == 0) {
308 14
                        $buffer->read(4);
309
                    }
310
311
                    // Grab the rest of the buffer as a result
312 14
                    $result = $buffer->getBuffer();
313
                }
314
315
                // Add this packet to the list
316 17
                $packs[$packet_number] = $result;
317
            }
318
319 26
            unset($buffer);
320
        }
321
322
        // Free some memory
323 26
        unset($packets, $packet);
324
325
        // Sort the packets by packet number
326 26
        ksort($packs);
327
328
        // Now combine the packs into one and return
329 26
        return implode("", $packs);
330
    }
331
332
    /**
333
     * Handles processing the details data into a usable format
334
     *
335
     * @param \GameQ\Buffer $buffer
336
     *
337
     * @return mixed
338
     * @throws \GameQ\Exception\Protocol
339
     */
340 66
    protected function processDetails(Buffer $buffer)
341
    {
342
343
        // Set the result to a new result instance
344 66
        $result = new Result();
345
346 66
        $result->add('protocol', $buffer->readInt8());
347 66
        $result->add('hostname', $buffer->readString());
348 66
        $result->add('map', $buffer->readString());
349 66
        $result->add('game_dir', $buffer->readString());
350 66
        $result->add('game_descr', $buffer->readString());
351 66
        $result->add('steamappid', $buffer->readInt16());
352 66
        $result->add('num_players', $buffer->readInt8());
353 66
        $result->add('max_players', $buffer->readInt8());
354 66
        $result->add('num_bots', $buffer->readInt8());
355 66
        $result->add('dedicated', $buffer->read());
356 66
        $result->add('os', $buffer->read());
357 66
        $result->add('password', $buffer->readInt8());
358 66
        $result->add('secure', $buffer->readInt8());
359
360
        // Special result for The Ship only (appid=2400)
361 66
        if ($result->get('steamappid') == 2400) {
362 3
            $result->add('game_mode', $buffer->readInt8());
363 3
            $result->add('witness_count', $buffer->readInt8());
364 3
            $result->add('witness_time', $buffer->readInt8());
365
        }
366
367 66
        $result->add('version', $buffer->readString());
368
369
        // Because of php 5.4...
370 66
        $edfCheck = $buffer->lookAhead(1);
371
372
        // Extra data flag
373 66
        if (!empty($edfCheck)) {
374 63
            $edf = $buffer->readInt8();
375
376 63
            if ($edf & 0x80) {
377 63
                $result->add('port', $buffer->readInt16Signed());
378
            }
379
380 63
            if ($edf & 0x10) {
381 61
                $result->add('steam_id', $buffer->readInt64());
382
            }
383
384 63
            if ($edf & 0x40) {
385 2
                $result->add('sourcetv_port', $buffer->readInt16Signed());
386 2
                $result->add('sourcetv_name', $buffer->readString());
387
            }
388
389 63
            if ($edf & 0x20) {
390 48
                $result->add('keywords', $buffer->readString());
391
            }
392
393 63
            if ($edf & 0x01) {
394 61
                $result->add('game_id', $buffer->readInt64());
395
            }
396
397 63
            unset($edf);
398
        }
399
400 66
        unset($buffer);
401
402 66
        return $result->fetch();
403
    }
404
405
    /**
406
     * Handles processing the server details from goldsource response
407
     *
408
     * @param \GameQ\Buffer $buffer
409
     *
410
     * @return array
411
     * @throws \GameQ\Exception\Protocol
412
     */
413 4
    protected function processDetailsGoldSource(Buffer $buffer)
414
    {
415
416
        // Set the result to a new result instance
417 4
        $result = new Result();
418
419 4
        $result->add('address', $buffer->readString());
420 4
        $result->add('hostname', $buffer->readString());
421 4
        $result->add('map', $buffer->readString());
422 4
        $result->add('game_dir', $buffer->readString());
423 4
        $result->add('game_descr', $buffer->readString());
424 4
        $result->add('num_players', $buffer->readInt8());
425 4
        $result->add('max_players', $buffer->readInt8());
426 4
        $result->add('version', $buffer->readInt8());
427 4
        $result->add('dedicated', $buffer->read());
428 4
        $result->add('os', $buffer->read());
429 4
        $result->add('password', $buffer->readInt8());
430
431
        // Mod section
432 4
        $result->add('ismod', $buffer->readInt8());
433
434
        // We only run these if ismod is 1 (true)
435 4
        if ($result->get('ismod') == 1) {
436 4
            $result->add('mod_urlinfo', $buffer->readString());
437 4
            $result->add('mod_urldl', $buffer->readString());
438 4
            $buffer->skip();
439 4
            $result->add('mod_version', $buffer->readInt32Signed());
440 4
            $result->add('mod_size', $buffer->readInt32Signed());
441 4
            $result->add('mod_type', $buffer->readInt8());
442 4
            $result->add('mod_cldll', $buffer->readInt8());
443
        }
444
445 4
        $result->add('secure', $buffer->readInt8());
446 4
        $result->add('num_bots', $buffer->readInt8());
447
448 4
        unset($buffer);
449
450 4
        return $result->fetch();
451
    }
452
453
    /**
454
     * Handles processing the player data into a usable format
455
     *
456
     * @param \GameQ\Buffer $buffer
457
     *
458
     * @return mixed
459
     */
460 65
    protected function processPlayers(Buffer $buffer)
461
    {
462
463
        // Set the result to a new result instance
464 65
        $result = new Result();
465
466
        // Pull out the number of players
467 65
        $num_players = $buffer->readInt8();
468
469
        // Player count
470 65
        $result->add('num_players', $num_players);
471
472
        // No players so no need to look any further
473 65
        if ($num_players == 0) {
474 10
            return $result->fetch();
475
        }
476
477
        // Players list
478 55
        while ($buffer->getLength()) {
479 55
            $result->addPlayer('id', $buffer->readInt8());
480 55
            $result->addPlayer('name', $buffer->readString());
481 55
            $result->addPlayer('score', $buffer->readInt32Signed());
482 55
            $result->addPlayer('time', $buffer->readFloat32());
483
        }
484
485 55
        unset($buffer);
486
487 55
        return $result->fetch();
488
    }
489
490
    /**
491
     * Handles processing the rules data into a usable format
492
     *
493
     * @param \GameQ\Buffer $buffer
494
     *
495
     * @return mixed
496
     */
497 55
    protected function processRules(Buffer $buffer)
498
    {
499
500
        // Set the result to a new result instance
501 55
        $result = new Result();
502
503
        // Count the number of rules
504 55
        $num_rules = $buffer->readInt16Signed();
505
506
        // Add the count of the number of rules this server has
507 55
        $result->add('num_rules', $num_rules);
508
509
        // Rules
510 55
        while ($buffer->getLength()) {
511 55
            $result->add($buffer->readString(), $buffer->readString());
512
        }
513
514 55
        unset($buffer);
515
516 55
        return $result->fetch();
517
    }
518
}
519