Source   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 33
eloc 178
c 3
b 0
f 0
dl 0
loc 474
ccs 145
cts 145
cp 1
rs 9.76

7 Methods

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