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