Completed
Pull Request — v3 (#484)
by
unknown
11:25
created

GameQ::removeFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 1
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
namespace GameQ;
19
20
use GameQ\Exception\Protocol as ProtocolException;
21
use GameQ\Exception\Query as QueryException;
22
use GameQ\Protocols;
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
48
    /* Static Section */
49
50
    const PROTOCOLS = [
51
        Protocols\Aa3::class,
52
        Protocols\Aapg::class,
53
        Protocols\Arkse::class,
54
        Protocols\Arma3::class,
55
        Protocols\Armedassault2oa::class,
56
        Protocols\Ase::class,
57
        Protocols\Batt1944::class,
58
        Protocols\Bf2::class,
59
        Protocols\Bf3::class,
60
        Protocols\Bf4::class,
61
        Protocols\Bf1942::class,
62
        Protocols\Bfbc2::class,
63
        Protocols\Bfh::class,
64
        Protocols\Cod2::class,
65
        Protocols\Cod4::class,
66
        Protocols\Codmw3::class,
67
        Protocols\Conanexiles::class,
68
        Protocols\Crysiswars::class,
69
        Protocols\Cs15::class,
70
        Protocols\Cs16::class,
71
        Protocols\Cscz::class,
72
        Protocols\Csgo::class,
73
        Protocols\Css::class,
74
        Protocols\Dayz::class,
75
        Protocols\Dayzmod::class,
76
        Protocols\Dod::class,
77
        Protocols\Dods::class,
78
        Protocols\Dow::class,
79
        Protocols\Eco::class,
80
        Protocols\Egs::class,
81
        Protocols\Etqw::class,
82
        Protocols\Ffe::class,
83
        Protocols\Ffow::class,
84
        Protocols\Gamespy::class,
85
        Protocols\Gamespy3::class,
86
        Protocols\Gamespy4::class,
87
        Protocols\Gmod::class,
88
        Protocols\Grav::class,
89
        Protocols\Gta5m::class,
90
        Protocols\Gtan::class,
91
        Protocols\Hl2dm::class,
92
        Protocols\Http::class,
93
        Protocols\Insurgency::class,
94
        Protocols\Jediacademy::class,
95
        Protocols\Jedioutcast::class,
96
        Protocols\Justcause2::class,
97
        Protocols\Justcause3::class,
98
        Protocols\Killingfloor::class,
99
        Protocols\Killingfloor2::class,
100
        Protocols\L4d::class,
101
        Protocols\L4d2::class,
102
        Protocols\Lhmp::class,
103
        Protocols\Minecraft::class,
104
        Protocols\Minecraftpe::class,
105
        Protocols\Mohaa::class,
106
        Protocols\Mta::class,
107
        Protocols\Mumble::class,
108
        Protocols\Ns2::class,
109
        Protocols\Projectrealitybf2::class,
110
        Protocols\Quake2::class,
111
        Protocols\Quake3::class,
112
        Protocols\Quakelive::class,
113
        Protocols\Redorchestra2::class,
114
        Protocols\Risingstorm2::class,
115
        Protocols\Rust::class,
116
        Protocols\Samp::class,
117
        Protocols\Sevendaystodie::class,
118
        Protocols\Ship::class,
119
        Protocols\Soldat::class,
120
        Protocols\Source::class,
121
        Protocols\Spaceengineers::class,
122
        Protocols\Squad::class,
123
        Protocols\Starmade::class,
124
        Protocols\Teamspeak2::class,
125
        Protocols\Teamspeak3::class,
126
        Protocols\Teeworlds::class,
127
        Protocols\Terraria::class,
128
        Protocols\Tf2::class,
129
        Protocols\Theforrest::class,
130
        Protocols\Tibia::class,
131
        Protocols\Tshock::class,
132
        Protocols\Unreal2::class,
133
        Protocols\Unturned::class,
134
        Protocols\Ut::class,
135
        Protocols\Ut3::class,
136
        Protocols\Ut2004::class,
137
        Protocols\Ventrilo::class,
138
        Protocols\Warsow::class,
139
        Protocols\Won::class,
140
        Protocols\Wurm::class
141
    ];
142
143
    /**
144
     * Holds the instance of itself
145
     *
146
     * @type self
147
     */
148
    protected static $instance = null;
149
150
    /**
151
     * Create a new instance of this class
152
     *
153
     * @return \GameQ\GameQ
154
     */
155 1
    public static function factory()
156
    {
157
158
        // Create a new instance
159 1
        self::$instance = new self();
160
161
        // Return this new instance
162 1
        return self::$instance;
163
    }
164
165
    /* Dynamic Section */
166
167
    /**
168
     * Default options
169
     *
170
     * @type array
171
     */
172
    protected $options = [
173
        'debug'                => false,
174
        'timeout'              => 3, // Seconds
175
        'filters'              => [
176
            // Default normalize
177
            'normalize_d751713988987e9331980363e24189ce' => [
178
                'filter'  => 'normalize',
179
                'options' => [],
180
            ],
181
        ],
182
        // Advanced settings
183
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
184
        'write_wait'           => 500,
185
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
186
187
        // Used for generating protocol test data
188
        'capture_packets_file' => null,
189
    ];
190
191
    /**
192
     * Array of servers being queried
193
     *
194
     * @type array
195
     */
196
    protected $servers = [];
197
198
    /**
199
     * The query library to use.  Default is Native
200
     *
201
     * @type string
202
     */
203
    protected $queryLibrary = 'GameQ\\Query\\Native';
204
205
    /**
206
     * Holds the instance of the queryLibrary
207
     *
208
     * @type \GameQ\Query\Core|null
209
     */
210
    protected $query = null;
211
212
    /**
213
     * GameQ constructor.
214
     *
215
     * Do some checks as needed so this will operate
216
     */
217 223
    public function __construct()
218
    {
219
        // Check for missing utf8_encode function
220 223
        if (!function_exists('utf8_encode')) {
221
            throw new \Exception("PHP's utf8_encode() function is required - "
222
                . "http://php.net/manual/en/function.utf8-encode.php.  Check your php installation.");
223
        }
224 223
    }
225
226
    /**
227
     * Get an option's value
228
     *
229
     * @param mixed $option
230
     *
231
     * @return mixed|null
232
     */
233 216
    public function __get($option)
234
    {
235
236 216
        return isset($this->options[$option]) ? $this->options[$option] : null;
237
    }
238
239
    /**
240
     * Set an option's value
241
     *
242
     * @param mixed $option
243
     * @param mixed $value
244
     *
245
     * @return bool
246
     */
247 218
    public function __set($option, $value)
248
    {
249
250 218
        $this->options[$option] = $value;
251
252 218
        return true;
253
    }
254
255
    /**
256
     * Chainable call to __set, uses set as the actual setter
257
     *
258
     * @param mixed $var
259
     * @param mixed $value
260
     *
261
     * @return $this
262
     */
263 218
    public function setOption($var, $value)
264
    {
265
266
        // Use magic
267 218
        $this->{$var} = $value;
268
269 218
        return $this; // Make chainable
270
    }
271
272
    /**
273
     * Add a single server
274
     *
275
     * @param array $server_info
276
     *
277
     * @return $this
278
     */
279 2
    public function addServer(array $server_info = [])
280
    {
281
282
        // Add and validate the server
283 2
        $this->servers[uniqid()] = new Server($server_info);
284
285 2
        return $this; // Make calls chainable
286
    }
287
288
    /**
289
     * Add multiple servers in a single call
290
     *
291
     * @param array $servers
292
     *
293
     * @return $this
294
     */
295 2
    public function addServers(array $servers = [])
296
    {
297
298
        // Loop through all the servers and add them
299 2
        foreach ($servers as $server_info) {
300 2
            $this->addServer($server_info);
301 2
        }
302
303 2
        return $this; // Make calls chainable
304
    }
305
306
    /**
307
     * Add a set of servers from a file or an array of files.
308
     * Supported formats:
309
     * JSON
310
     *
311
     * @param array $files
312
     *
313
     * @return $this
314
     * @throws \Exception
315
     */
316 1
    public function addServersFromFiles($files = [])
317
    {
318
319
        // Since we expect an array let us turn a string (i.e. single file) into an array
320 1
        if (!is_array($files)) {
321 1
            $files = [$files];
322 1
        }
323
324
        // Iterate over the file(s) and add them
325 1
        foreach ($files as $file) {
326
            // Check to make sure the file exists and we can read it
327 1
            if (!file_exists($file) || !is_readable($file)) {
328 1
                continue;
329
            }
330
331
            // See if this file is JSON
332 1
            if (($servers = json_decode(file_get_contents($file), true)) === null
333 1
                && json_last_error() !== JSON_ERROR_NONE
334 1
            ) {
335
                // Type not supported
336 1
                continue;
337
            }
338
339
            // Add this list of servers
340 1
            $this->addServers($servers);
341 1
        }
342
343 1
        return $this;
344
    }
345
346
    /**
347
     * Clear all of the defined servers
348
     *
349
     * @return $this
350
     */
351 2
    public function clearServers()
352
    {
353
354
        // Reset all the servers
355 2
        $this->servers = [];
356
357 2
        return $this; // Make Chainable
358
    }
359
360
    /**
361
     * Add a filter to the processing list
362
     *
363
     * @param string $filterName
364
     * @param array  $options
365
     *
366
     * @return $this
367
     */
368 3
    public function addFilter($filterName, $options = [])
369
    {
370
        // Create the filter hash so we can run multiple versions of the same filter
371 3
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
372
373
        // Add the filter
374 3
        $this->options['filters'][$filterHash] = [
375 3
            'filter'  => strtolower($filterName),
376 3
            'options' => $options,
377
        ];
378
379 3
        unset($filterHash);
380
381 3
        return $this;
382
    }
383
384
    /**
385
     * Remove an added filter
386
     *
387
     * @param string $filterHash
388
     *
389
     * @return $this
390
     */
391 216
    public function removeFilter($filterHash)
392
    {
393
        // Make lower case
394 216
        $filterHash = strtolower($filterHash);
395
396
        // Remove this filter if it has been defined
397 216
        if (array_key_exists($filterHash, $this->options['filters'])) {
398 3
            unset($this->options['filters'][$filterHash]);
399 3
        }
400
401 216
        unset($filterHash);
402
403 216
        return $this;
404
    }
405
406
    /**
407
     * Return the list of applied filters
408
     *
409
     * @return array
410
     */
411
    public function listFilters()
412
    {
413
        return $this->options['filters'];
414
    }
415
416
    /**
417
     * Main method used to actually process all of the added servers and return the information
418
     *
419
     * @return array
420
     * @throws \Exception
421
     */
422
    public function process()
423
    {
424
425
        // Initialize the query library we are using
426
        $class = new \ReflectionClass($this->queryLibrary);
427
428
        // Set the query pointer to the new instance of the library
429
        $this->query = $class->newInstance();
430
431
        unset($class);
432
433
        // Define the return
434
        $results = [];
435
436
        // @todo: Add break up into loop to split large arrays into smaller chunks
437
438
        // Do server challenge(s) first, if any
439
        $this->doChallenges();
440
441
        // Do packets for server(s) and get query responses
442
        $this->doQueries();
443
444
        // Now we should have some information to process for each server
445
        foreach ($this->servers as $server) {
446
            /* @var $server \GameQ\Server */
447
448
            // Parse the responses for this server
449
            $result = $this->doParseResponse($server);
450
451
            // Apply the filters
452
            $result = array_merge($result, $this->doApplyFilters($result, $server));
453
454
            // Sort the keys so they are alphabetical and nicer to look at
455
            ksort($result);
456
457
            // Add the result to the results array
458
            $results[$server->id()] = $result;
459
        }
460
461
        return $results;
462
    }
463
464
    /**
465
     * Do server challenges, where required
466
     */
467
    protected function doChallenges()
468
    {
469
470
        // Initialize the sockets for reading
471
        $sockets = [];
472
473
        // By default we don't have any challenges to process
474
        $server_challenge = false;
475
476
        // Do challenge packets
477
        foreach ($this->servers as $server_id => $server) {
478
            /* @var $server \GameQ\Server */
479
480
            // This protocol has a challenge packet that needs to be sent
481
            if ($server->protocol()->hasChallenge()) {
482
                // We have a challenge, set the flag
483
                $server_challenge = true;
484
485
                // Let's make a clone of the query class
486
                $socket = clone $this->query;
487
488
                // Set the information for this query socket
489
                $socket->set(
490
                    $server->protocol()->transport(),
491
                    $server->ip,
492
                    $server->port_query,
493
                    $this->timeout
494
                );
495
496
                try {
497
                    // Now write the challenge packet to the socket.
498
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
499
500
                    // Add the socket information so we can reference it easily
501
                    $sockets[(int)$socket->get()] = [
502
                        'server_id' => $server_id,
503
                        'socket'    => $socket,
504
                    ];
505
                } catch (QueryException $e) {
506
                    // Check to see if we are in debug, if so bubble up the exception
507
                    if ($this->debug) {
508
                        throw new \Exception($e->getMessage(), $e->getCode(), $e);
509
                    }
510
                }
511
512
                unset($socket);
513
514
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
515
                usleep($this->write_wait);
516
            }
517
        }
518
519
        // We have at least one server with a challenge, we need to listen for responses
520
        if ($server_challenge) {
521
            // Now we need to listen for and grab challenge response(s)
522
            $responses = call_user_func_array(
523
                [$this->query, 'getResponses'],
524
                [$sockets, $this->timeout, $this->stream_timeout]
525
            );
526
527
            // Iterate over the challenge responses
528
            foreach ($responses as $socket_id => $response) {
529
                // Back out the server_id we need to update the challenge response for
530
                $server_id = $sockets[$socket_id]['server_id'];
531
532
                // Make this into a buffer so it is easier to manipulate
533
                $challenge = new Buffer(implode('', $response));
534
535
                // Grab the server instance
536
                /* @var $server \GameQ\Server */
537
                $server = $this->servers[$server_id];
538
539
                // Apply the challenge
540
                $server->protocol()->challengeParseAndApply($challenge);
541
542
                // Add this socket to be reused, has to be reused in GameSpy3 for example
543
                $server->socketAdd($sockets[$socket_id]['socket']);
544
545
                // Clear
546
                unset($server);
547
            }
548
        }
549
    }
550
551
    /**
552
     * Run the actual queries and get the response(s)
553
     */
554
    protected function doQueries()
555
    {
556
557
        // Initialize the array of sockets
558
        $sockets = [];
559
560
        // Iterate over the server list
561
        foreach ($this->servers as $server_id => $server) {
562
            /* @var $server \GameQ\Server */
563
564
            // Invoke the beforeSend method
565
            $server->protocol()->beforeSend($server);
566
567
            // Get all the non-challenge packets we need to send
568
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
569
570
            if (count($packets) == 0) {
571
                // Skip nothing else to do for some reason.
572
                continue;
573
            }
574
575
            // Try to use an existing socket
576
            if (($socket = $server->socketGet()) === null) {
577
                // Let's make a clone of the query class
578
                $socket = clone $this->query;
579
580
                // Set the information for this query socket
581
                $socket->set(
582
                    $server->protocol()->transport(),
583
                    $server->ip,
584
                    $server->port_query,
585
                    $this->timeout
586
                );
587
            }
588
589
            try {
590
                // Iterate over all the packets we need to send
591
                foreach ($packets as $packet_data) {
592
                    // Now write the packet to the socket.
593
                    $socket->write($packet_data);
594
595
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
596
                    usleep($this->write_wait);
597
                }
598
599
                unset($packets);
600
601
                // Add the socket information so we can reference it easily
602
                $sockets[(int)$socket->get()] = [
603
                    'server_id' => $server_id,
604
                    'socket'    => $socket,
605
                ];
606
            } catch (QueryException $e) {
607
                // Check to see if we are in debug, if so bubble up the exception
608
                if ($this->debug) {
609
                    throw new \Exception($e->getMessage(), $e->getCode(), $e);
610
                }
611
612
                break;
613
            }
614
615
            // Clean up the sockets, if any left over
616
            $server->socketCleanse();
617
        }
618
619
        // Now we need to listen for and grab response(s)
620
        $responses = call_user_func_array(
621
            [$this->query, 'getResponses'],
622
            [$sockets, $this->timeout, $this->stream_timeout]
623
        );
624
625
        // Iterate over the responses
626
        foreach ($responses as $socket_id => $response) {
627
            // Back out the server_id
628
            $server_id = $sockets[$socket_id]['server_id'];
629
630
            // Grab the server instance
631
            /* @var $server \GameQ\Server */
632
            $server = $this->servers[$server_id];
633
634
            // Save the response from this packet
635
            $server->protocol()->packetResponse($response);
636
637
            unset($server);
638
        }
639
640
        // Now we need to close all of the sockets
641
        foreach ($sockets as $socketInfo) {
642
            /* @var $socket \GameQ\Query\Core */
643
            $socket = $socketInfo['socket'];
644
645
            // Close the socket
646
            $socket->close();
647
648
            unset($socket);
649
        }
650
651
        unset($sockets);
652
    }
653
654
    /**
655
     * Parse the response for a specific server
656
     *
657
     * @param \GameQ\Server $server
658
     *
659
     * @return array
660
     * @throws \Exception
661
     */
662 215
    protected function doParseResponse(Server $server)
663
    {
664
665
        try {
666
            // @codeCoverageIgnoreStart
667
            // We want to save this server's response to a file (useful for unit testing)
668
            if (!is_null($this->capture_packets_file)) {
669
                file_put_contents(
670
                    $this->capture_packets_file,
671
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
672
                );
673
            }
674
            // @codeCoverageIgnoreEnd
675
676
            // Get the server response
677 215
            $results = $server->protocol()->processResponse();
678
679
            // Check for online before we do anything else
680 182
            $results['gq_online'] = (count($results) > 0);
681 215
        } catch (ProtocolException $e) {
682
            // Check to see if we are in debug, if so bubble up the exception
683 33
            if ($this->debug) {
684 19
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
685
            }
686
687
            // We ignore this server
688
            $results = [
689 14
                'gq_online' => false,
690 14
            ];
691
        }
692
693
        // Now add some default stuff
694 196
        $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip();
695 196
        $results['gq_port_client'] = $server->portClient();
696 196
        $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery();
697 196
        $results['gq_protocol'] = $server->protocol()->getProtocol();
698 196
        $results['gq_type'] = (string)$server->protocol();
699 196
        $results['gq_name'] = $server->protocol()->nameLong();
700 196
        $results['gq_transport'] = $server->protocol()->transport();
701
702
        // Process the join link
703 196
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
704 196
            $results['gq_joinlink'] = $server->getJoinLink();
705 196
        }
706
707 196
        return $results;
708
    }
709
710
    /**
711
     * Apply any filters to the results
712
     *
713
     * @param array         $results
714
     * @param \GameQ\Server $server
715
     *
716
     * @return array
717
     */
718 2
    protected function doApplyFilters(array $results, Server $server)
719
    {
720
721
        // Loop over the filters
722 2
        foreach ($this->options['filters'] as $filterOptions) {
723
            // Try to do this filter
724
            try {
725
                // Make a new reflection class
726 2
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
727
728
                // Create a new instance of the filter class specified
729 1
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
730
731
                // Apply the filter to the data
732 1
                $results = $filter->apply($results, $server);
733 2
            } catch (\ReflectionException $e) {
734
                // Invalid, skip it
735 1
                continue;
736
            }
737 2
        }
738
739 2
        return $results;
740
    }
741
}
742