Advertisement
Guest User

Untitled

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