GameQ.php 18 KB

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