Passed
Push — v3 ( efc739...bb78f7 )
by Austin
02:17
created

GameQ::process()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 40
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 40
ccs 13
cts 13
cp 1
rs 9.8666
cc 2
nc 2
nop 0
crap 2
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;
20
21
use GameQ\Exception\Protocol as ProtocolException;
22
use GameQ\Exception\Query as QueryException;
23
24
/**
25
 * Base GameQ Class
26
 *
27
 * This class should be the only one that is included when you use GameQ to query
28
 * any games servers.
29
 *
30
 * Requirements: See wiki or README for more information on the requirements
31
 *  - PHP 5.4.14+
32
 *    * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
33
 *
34
 * @author Austin Bischoff <[email protected]>
35
 *
36
 * @property bool   $debug
37
 * @property string $capture_packets_file
38
 * @property int    $stream_timeout
39
 * @property int    $timeout
40
 * @property int    $write_wait
41
 */
42
class GameQ
43
{
44
    /*
45
     * Constants
46
     */
47
    const PROTOCOLS_DIRECTORY = __DIR__ . '/Protocols';
48
49
    /* Static Section */
50
51
    /**
52
     * Holds the instance of itself
53
     *
54
     * @type self
55
     */
56
    protected static $instance = null;
57
58
    /**
59
     * Create a new instance of this class
60
     *
61
     * @return \GameQ\GameQ
62
     */
63 6
    public static function factory()
64
    {
65
66
        // Create a new instance
67 6
        self::$instance = new self();
68
69
        // Return this new instance
70 6
        return self::$instance;
71
    }
72
73
    /* Dynamic Section */
74
75
    /**
76
     * Default options
77
     *
78
     * @type array
79
     */
80
    protected $options = [
81
        'debug'                => false,
82
        'timeout'              => 3, // Seconds
83
        'filters'              => [
84
            // Default normalize
85
            'normalize_d751713988987e9331980363e24189ce' => [
86
                'filter'  => 'normalize',
87
                'options' => [],
88
            ],
89
        ],
90
        // Advanced settings
91
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
92
        'write_wait'           => 500,
93
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
94
95
        // Used for generating protocol test data
96
        'capture_packets_file' => null,
97
    ];
98
99
    /**
100
     * Array of servers being queried
101
     *
102
     * @type array
103
     */
104
    protected $servers = [];
105
106
    /**
107
     * The query library to use.  Default is Native
108
     *
109
     * @type string
110
     */
111
    protected $queryLibrary = 'GameQ\\Query\\Native';
112
113
    /**
114
     * Holds the instance of the queryLibrary
115
     *
116
     * @type \GameQ\Query\Core|null
117
     */
118
    protected $query = null;
119
120
    /**
121
     * GameQ constructor.
122
     *
123
     * Do some checks as needed so this will operate
124
     */
125 1932
    public function __construct()
126
    {
127
        // Check for missing utf8_encode function
128 1932
        if (!function_exists('utf8_encode')) {
129
            throw new \Exception("PHP's utf8_encode() function is required - "
130
                . "http://php.net/manual/en/function.utf8-encode.php.  Check your php installation.");
131
        }
132 322
    }
133
134
    /**
135
     * Get an option's value
136
     *
137
     * @param mixed $option
138
     *
139
     * @return mixed|null
140
     */
141 1878
    public function __get($option)
142
    {
143
144 1878
        return isset($this->options[$option]) ? $this->options[$option] : null;
145
    }
146
147
    /**
148
     * Set an option's value
149
     *
150
     * @param mixed $option
151
     * @param mixed $value
152
     *
153
     * @return bool
154
     */
155 1890
    public function __set($option, $value)
156
    {
157
158 1890
        $this->options[$option] = $value;
159
160 1890
        return true;
161
    }
162
163 18
    public function getServers()
164
    {
165 18
        return $this->servers;
166
    }
167
168
    public function getOptions()
169
    {
170
        return $this->options;
171
    }
172
173
    /**
174
     * Chainable call to __set, uses set as the actual setter
175
     *
176
     * @param mixed $var
177
     * @param mixed $value
178
     *
179
     * @return $this
180
     */
181 1890
    public function setOption($var, $value)
182
    {
183
184
        // Use magic
185 1890
        $this->{$var} = $value;
186
187 1890
        return $this; // Make chainable
188
    }
189
190
    /**
191
     * Add a single server
192
     *
193
     * @param array $server_info
194
     *
195
     * @return $this
196
     */
197 48
    public function addServer(array $server_info = [])
198
    {
199
200
        // Add and validate the server
201 48
        $this->servers[uniqid()] = new Server($server_info);
202
203 48
        return $this; // Make calls chainable
204
    }
205
206
    /**
207
     * Add multiple servers in a single call
208
     *
209
     * @param array $servers
210
     *
211
     * @return $this
212
     */
213 12
    public function addServers(array $servers = [])
214
    {
215
216
        // Loop through all the servers and add them
217 12
        foreach ($servers as $server_info) {
218 12
            $this->addServer($server_info);
219
        }
220
221 12
        return $this; // Make calls chainable
222
    }
223
224
    /**
225
     * Add a set of servers from a file or an array of files.
226
     * Supported formats:
227
     * JSON
228
     *
229
     * @param array $files
230
     *
231
     * @return $this
232
     * @throws \Exception
233
     */
234 6
    public function addServersFromFiles($files = [])
235
    {
236
237
        // Since we expect an array let us turn a string (i.e. single file) into an array
238 6
        if (!is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
239 6
            $files = [$files];
240
        }
241
242
        // Iterate over the file(s) and add them
243 6
        foreach ($files as $file) {
244
            // Check to make sure the file exists and we can read it
245 6
            if (!file_exists($file) || !is_readable($file)) {
246 6
                continue;
247
            }
248
249
            // See if this file is JSON
250 6
            if (($servers = json_decode(file_get_contents($file), true)) === null
251 6
                && json_last_error() !== JSON_ERROR_NONE
252
            ) {
253
                // Type not supported
254 6
                continue;
255
            }
256
257
            // Add this list of servers
258 6
            $this->addServers($servers);
259
        }
260
261 6
        return $this;
262
    }
263
264
    /**
265
     * Clear all of the defined servers
266
     *
267
     * @return $this
268
     */
269 12
    public function clearServers()
270
    {
271
272
        // Reset all the servers
273 12
        $this->servers = [];
274
275 12
        return $this; // Make Chainable
276
    }
277
278
    /**
279
     * Add a filter to the processing list
280
     *
281
     * @param string $filterName
282
     * @param array  $options
283
     *
284
     * @return $this
285
     */
286 18
    public function addFilter($filterName, $options = [])
287
    {
288
        // Create the filter hash so we can run multiple versions of the same filter
289 18
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
290
291
        // Add the filter
292 18
        $this->options['filters'][$filterHash] = [
293 18
            'filter'  => strtolower($filterName),
294 18
            'options' => $options,
295 15
        ];
296
297 18
        unset($filterHash);
298
299 18
        return $this;
300
    }
301
302
    /**
303
     * Remove an added filter
304
     *
305
     * @param string $filterHash
306
     *
307
     * @return $this
308
     */
309 1890
    public function removeFilter($filterHash)
310
    {
311
        // Make lower case
312 1890
        $filterHash = strtolower($filterHash);
313
314
        // Remove this filter if it has been defined
315 1890
        if (array_key_exists($filterHash, $this->options['filters'])) {
316 18
            unset($this->options['filters'][$filterHash]);
317
        }
318
319 1890
        unset($filterHash);
320
321 1890
        return $this;
322
    }
323
324
    /**
325
     * Return the list of applied filters
326
     *
327
     * @return array
328
     */
329 6
    public function listFilters()
330
    {
331 6
        return $this->options['filters'];
332
    }
333
334
    /**
335
     * Main method used to actually process all of the added servers and return the information
336
     *
337
     * @return array
338
     * @throws \Exception
339
     */
340 30
    public function process()
341
    {
342
343
        // Initialize the query library we are using
344 30
        $class = new \ReflectionClass($this->queryLibrary);
345
346
        // Set the query pointer to the new instance of the library
347 30
        $this->query = $class->newInstance();
348
349 30
        unset($class);
350
351
        // Define the return
352 30
        $results = [];
353
354
        // @todo: Add break up into loop to split large arrays into smaller chunks
355
356
        // Do server challenge(s) first, if any
357 30
        $this->doChallenges();
358
359
        // Do packets for server(s) and get query responses
360 30
        $this->doQueries();
361
362
        // Now we should have some information to process for each server
363 30
        foreach ($this->servers as $server) {
364
            /* @var $server \GameQ\Server */
365
366
            // Parse the responses for this server
367 30
            $result = $this->doParseResponse($server);
368
369
            // Apply the filters
370 30
            $result = array_merge($result, $this->doApplyFilters($result, $server));
371
372
            // Sort the keys so they are alphabetical and nicer to look at
373 30
            ksort($result);
374
375
            // Add the result to the results array
376 30
            $results[$server->id()] = $result;
377
        }
378
379 30
        return $results;
380
    }
381
382
    /**
383
     * Do server challenges, where required
384
     */
385 30
    protected function doChallenges()
386
    {
387
388
        // Initialize the sockets for reading
389 30
        $sockets = [];
390
391
        // By default we don't have any challenges to process
392 30
        $server_challenge = false;
393
394
        // Do challenge packets
395 30
        foreach ($this->servers as $server_id => $server) {
396
            /* @var $server \GameQ\Server */
397
398
            // This protocol has a challenge packet that needs to be sent
399 30
            if ($server->protocol()->hasChallenge()) {
400
                // We have a challenge, set the flag
401
                $server_challenge = true;
402
403
                // Let's make a clone of the query class
404
                $socket = clone $this->query;
405
406
                // Set the information for this query socket
407
                $socket->set(
408
                    $server->protocol()->transport(),
409
                    $server->ip,
410
                    $server->port_query,
411
                    $this->timeout
412
                );
413
414
                try {
415
                    // Now write the challenge packet to the socket.
416
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
0 ignored issues
show
Bug introduced by
$server->protocol()->get...ocol::PACKET_CHALLENGE) of type array is incompatible with the type string expected by parameter $data of GameQ\Query\Core::write(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

416
                    $socket->write(/** @scrutinizer ignore-type */ $server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
Loading history...
Bug introduced by
GameQ\Protocol::PACKET_CHALLENGE of type string is incompatible with the type array expected by parameter $type of GameQ\Protocol::getPacket(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

416
                    $socket->write($server->protocol()->getPacket(/** @scrutinizer ignore-type */ Protocol::PACKET_CHALLENGE));
Loading history...
417
418
                    // Add the socket information so we can reference it easily
419
                    $sockets[(int)$socket->get()] = [
420
                        'server_id' => $server_id,
421
                        'socket'    => $socket,
422
                    ];
423
                } catch (QueryException $exception) {
424
                    // Check to see if we are in debug, if so bubble up the exception
425
                    if ($this->debug) {
426
                        throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
427
                    }
428
                }
429
430
                unset($socket);
431
432
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
433
                usleep($this->write_wait);
434
            }
435
        }
436
437
        // We have at least one server with a challenge, we need to listen for responses
438 30
        if ($server_challenge) {
439
            // Now we need to listen for and grab challenge response(s)
440
            $responses = call_user_func_array(
441
                [$this->query, 'getResponses'],
442
                [$sockets, $this->timeout, $this->stream_timeout]
443
            );
444
445
            // Iterate over the challenge responses
446
            foreach ($responses as $socket_id => $response) {
447
                // Back out the server_id we need to update the challenge response for
448
                $server_id = $sockets[$socket_id]['server_id'];
449
450
                // Make this into a buffer so it is easier to manipulate
451
                $challenge = new Buffer(implode('', $response));
452
453
                // Grab the server instance
454
                /* @var $server \GameQ\Server */
455
                $server = $this->servers[$server_id];
456
457
                // Apply the challenge
458
                $server->protocol()->challengeParseAndApply($challenge);
459
460
                // Add this socket to be reused, has to be reused in GameSpy3 for example
461
                $server->socketAdd($sockets[$socket_id]['socket']);
462
463
                // Clear
464
                unset($server);
465
            }
466
        }
467 5
    }
468
469
    /**
470
     * Run the actual queries and get the response(s)
471
     */
472 30
    protected function doQueries()
473
    {
474
475
        // Initialize the array of sockets
476 30
        $sockets = [];
477
478
        // Iterate over the server list
479 30
        foreach ($this->servers as $server_id => $server) {
480
            /* @var $server \GameQ\Server */
481
482
            // Invoke the beforeSend method
483 30
            $server->protocol()->beforeSend($server);
484
485
            // Get all the non-challenge packets we need to send
486 30
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
0 ignored issues
show
Bug introduced by
'!' . GameQ\Protocol::PACKET_CHALLENGE of type string is incompatible with the type array expected by parameter $type of GameQ\Protocol::getPacket(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

486
            $packets = $server->protocol()->getPacket(/** @scrutinizer ignore-type */ '!' . Protocol::PACKET_CHALLENGE);
Loading history...
487
488 30
            if (count($packets) == 0) {
489
                // Skip nothing else to do for some reason.
490
                continue;
491
            }
492
493
            // Try to use an existing socket
494 30
            if (($socket = $server->socketGet()) === null) {
495
                // Let's make a clone of the query class
496 30
                $socket = clone $this->query;
497
498
                // Set the information for this query socket
499 30
                $socket->set(
500 30
                    $server->protocol()->transport(),
501 30
                    $server->ip,
502 30
                    $server->port_query,
503 30
                    $this->timeout
504 25
                );
505
            }
506
507
            try {
508
                // Iterate over all the packets we need to send
509 30
                foreach ($packets as $packet_data) {
510
                    // Now write the packet to the socket.
511 30
                    $socket->write($packet_data);
512
513
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
514
                    usleep($this->write_wait);
515
                }
516
517
                unset($packets);
518
519
                // Add the socket information so we can reference it easily
520
                $sockets[(int)$socket->get()] = [
521
                    'server_id' => $server_id,
522
                    'socket'    => $socket,
523
                ];
524 30
            } catch (QueryException $exception) {
525
                // Check to see if we are in debug, if so bubble up the exception
526 30
                if ($this->debug) {
527
                    throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
528
                }
529
530 30
                continue;
531
            }
532
533
            // Clean up the sockets, if any left over
534
            $server->socketCleanse();
535
        }
536
537
        // Now we need to listen for and grab response(s)
538 30
        $responses = call_user_func_array(
539 30
            [$this->query, 'getResponses'],
540 30
            [$sockets, $this->timeout, $this->stream_timeout]
541 25
        );
542
543
        // Iterate over the responses
544 30
        foreach ($responses as $socket_id => $response) {
545
            // Back out the server_id
546
            $server_id = $sockets[$socket_id]['server_id'];
547
548
            // Grab the server instance
549
            /* @var $server \GameQ\Server */
550
            $server = $this->servers[$server_id];
551
552
            // Save the response from this packet
553
            $server->protocol()->packetResponse($response);
554
555
            unset($server);
556
        }
557
558
        // Now we need to close all of the sockets
559 30
        foreach ($sockets as $socketInfo) {
560
            /* @var $socket \GameQ\Query\Core */
561
            $socket = $socketInfo['socket'];
562
563
            // Close the socket
564
            $socket->close();
565
566
            unset($socket);
567
        }
568
569 30
        unset($sockets);
570 5
    }
571
572
    /**
573
     * Parse the response for a specific server
574
     *
575
     * @param \GameQ\Server $server
576
     *
577
     * @return array
578
     * @throws \Exception
579
     */
580 1872
    protected function doParseResponse(Server $server)
581
    {
582
583
        try {
584
            // @codeCoverageIgnoreStart
585
            // We want to save this server's response to a file (useful for unit testing)
586
            if (!is_null($this->capture_packets_file)) {
0 ignored issues
show
introduced by
The condition is_null($this->capture_packets_file) is always false.
Loading history...
587
                file_put_contents(
588
                    $this->capture_packets_file,
589
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
590
                );
591
            }
592
            // @codeCoverageIgnoreEnd
593
594
            // Get the server response
595 1872
            $results = $server->protocol()->processResponse();
596
597
            // Check for online before we do anything else
598 1626
            $results['gq_online'] = (count($results) > 0);
599 258
        } catch (ProtocolException $e) {
600
            // Check to see if we are in debug, if so bubble up the exception
601 258
            if ($this->debug) {
602 144
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
603
            }
604
605
            // We ignore this server
606 95
            $results = [
607 114
                'gq_online' => false,
608 95
            ];
609
        }
610
611
        // Now add some default stuff
612 1734
        $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip();
613 1734
        $results['gq_port_client'] = $server->portClient();
614 1734
        $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery();
615 1734
        $results['gq_protocol'] = $server->protocol()->getProtocol();
616 1734
        $results['gq_type'] = (string)$server->protocol();
617 1734
        $results['gq_name'] = $server->protocol()->nameLong();
618 1734
        $results['gq_transport'] = $server->protocol()->transport();
619
620
        // Process the join link
621 1734
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
622 1734
            $results['gq_joinlink'] = $server->getJoinLink();
623
        }
624
625 1734
        return $results;
626
    }
627
628
    /**
629
     * Apply any filters to the results
630
     *
631
     * @param array         $results
632
     * @param \GameQ\Server $server
633
     *
634
     * @return array
635
     */
636 42
    protected function doApplyFilters(array $results, Server $server)
637
    {
638
639
        // Loop over the filters
640 42
        foreach ($this->options['filters'] as $filterOptions) {
641
            // Try to do this filter
642
            try {
643
                // Make a new reflection class
644 42
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
645
646
                // Create a new instance of the filter class specified
647 36
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
648
649
                // Apply the filter to the data
650 36
                $results = $filter->apply($results, $server);
651 6
            } catch (\ReflectionException $exception) {
652
                // Invalid, skip it
653 6
                continue;
654
            }
655
        }
656
657 42
        return $results;
658
    }
659
}
660