| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877 |
- <?php
- /**
- * This file is part of GameQ.
- *
- * GameQ is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * GameQ is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- /*
- * Init some stuff
- */
- // Figure out where we are so we can set the proper references
- define('GAMEQ_BASE', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR);
- // Define the autoload so we can require files easy
- spl_autoload_register(array('GameQ', 'auto_load'));
- /**
- * Base GameQ Class
- *
- * This class should be the only one that is included when you use GameQ to query
- * any games servers. All necessary sub-classes are loaded as needed.
- *
- * Requirements: See wiki or README for more information on the requirements
- * - PHP 5.2+ (Recommended 5.3+)
- * * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
- * * Zlib - http://www.php.net/manual/en/book.zlib.php
- *
- * @author Austin Bischoff <[email protected]>
- */
- class GameQ
- {
- /*
- * Constants
- */
- const VERSION = '2.0.4';
- /*
- * Server array keys
- */
- const SERVER_TYPE = 'type';
- const SERVER_HOST = 'host';
- const SERVER_ID = 'id';
- const SERVER_OPTIONS = 'options';
- /* Static Section */
- protected static $instance = NULL;
- /**
- * Create a new instance of this class
- */
- public static function factory()
- {
- // Create a new instance
- self::$instance = new self();
- // Return this new instance
- return self::$instance;
- }
- /**
- * Attempt to auto-load a class based on the name
- *
- * @param string $class
- * @throws GameQException
- */
- public static function auto_load($class)
- {
- try
- {
- // Transform the class name into a path
- $file = str_replace('_', '/', strtolower($class));
- // Find the file and return the full path, if it exists
- if ($path = self::find_file($file))
- {
- // Load the class file
- require $path;
- // Class has been found
- return TRUE;
- }
- // Class is not in the filesystem
- return FALSE;
- }
- catch (Exception $e)
- {
- throw new GameQException($e->getMessage(), $e->getCode(), $e);
- die;
- }
- }
- /**
- * Try to find the file based on the class passed.
- *
- * @param string $file
- */
- public static function find_file($file)
- {
- $found = FALSE; // By default we did not find anything
- // Create a partial path of the filename
- $path = GAMEQ_BASE.$file.'.php';
- // Is a file so we can include it
- if(is_file($path))
- {
- $found = $path;
- }
- return $found;
- }
- /* Dynamic Section */
- /**
- * Defined options by default
- *
- * @var array()
- */
- protected $options = array(
- 'debug' => FALSE,
- 'timeout' => 3, // Seconds
- 'filters' => array(),
- // Advanced settings
- 'stream_timeout' => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
- 'write_wait' => 500, // How long (in micro-seconds) to pause between writting to server sockets, helps cpu usage
- );
- /**
- * Array of servers being queried
- *
- * @var array
- */
- protected $servers = array();
- /**
- * Holds the list of active sockets. This array is automaically cleaned as needed
- *
- * @var array
- */
- protected $sockets = array();
- /**
- * Make new class and check for requirements
- *
- * @throws GameQException
- * @return boolean
- */
- public function __construct()
- {
- // @todo: Add PHP version check?
- }
- /**
- * Get an option's value
- *
- * @param string $option
- */
- public function __get($option)
- {
- return isset($this->options[$option]) ? $this->options[$option] : NULL;
- }
- /**
- * Set an option's value
- *
- * @param string $option
- * @param mixed $value
- * @return boolean
- */
- public function __set($option, $value)
- {
- $this->options[$option] = $value;
- return TRUE;
- }
- /**
- * Chainable call to __set, uses set as the actual setter
- *
- * @param string $var
- * @param mixed $value
- * @return GameQ
- */
- public function setOption($var, $value)
- {
- // Use magic
- $this->{$var} = $value;
- return $this; // Make chainable
- }
- /**
- * Set an output filter.
- *
- * @param string $name
- * @param array $params
- * @return GameQ
- */
- public function setFilter($name, $params = array())
- {
- // Create the proper filter class name
- $filter_class = 'GameQ_Filters_'.$name;
- try
- {
- // Pass any parameters and make the class
- $this->options['filters'][$name] = new $filter_class($params);
- }
- catch (GameQ_FiltersException $e)
- {
- // We catch the exception here, thus the filter is not applied
- // but we issue a warning
- error_log($e->getMessage(), E_USER_WARNING);
- }
- return $this; // Make chainable
- }
- /**
- * Remove a global output filter.
- *
- * @param string $name
- * @return GameQ
- */
- public function removeFilter($name)
- {
- unset($this->options['filters'][$name]);
- return $this; // Make chainable
- }
- /**
- * Add a server to be queried
- *
- * Example:
- * $this->addServer(array(
- * // Required keys
- * 'type' => 'cs',
- * 'host' => '127.0.0.1:27015', '127.0.0.1' or 'somehost.com:27015'
- * Port not required, but will use the default port in the class which may not be correct for the
- * specific server being queried.
- *
- * // Optional keys
- * 'id' => 'someServerId', // By default will use pased host info (i.e. 127.0.0.1:27015)
- * 'options' => array('timeout' => 5), // By default will use global options
- * ));
- *
- * @param array $server_info
- * @throws GameQException
- * @return boolean|GameQ
- */
- public function addServer(Array $server_info=NULL)
- {
- // Check for server type
- if(!key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE]))
- {
- throw new GameQException("Missing server info key '".self::SERVER_TYPE."'");
- return FALSE;
- }
- // Check for server host
- if(!key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST]))
- {
- throw new GameQException("Missing server info key '".self::SERVER_HOST."'");
- return FALSE;
- }
- // Check for server id
- if(!key_exists(self::SERVER_ID, $server_info) || empty($server_info[self::SERVER_ID]))
- {
- // Make an id so each server has an id when returned
- $server_info[self::SERVER_ID] = $server_info[self::SERVER_HOST];
- }
- // Check for options
- if(!key_exists(self::SERVER_OPTIONS, $server_info)
- || !is_array($server_info[self::SERVER_OPTIONS])
- || empty($server_info[self::SERVER_OPTIONS]))
- {
- // Default the options to an empty array
- $server_info[self::SERVER_OPTIONS] = array();
- }
- // Define these
- $server_id = $server_info[self::SERVER_ID];
- $server_ip = '127.0.0.1';
- $server_port = FALSE;
- // We have an IPv6 address (and maybe a port)
- if(substr_count($server_info[self::SERVER_HOST], ':') > 1)
- {
- // See if we have a port, input should be in the format [::1]:27015 or similar
- if(strstr($server_info[self::SERVER_HOST], ']:'))
- {
- // Explode to get port
- $server_addr = explode(':', $server_info[self::SERVER_HOST]);
- // Port is the last item in the array, remove it and save
- $server_port = array_pop($server_addr);
- // The rest is the address, recombine
- $server_ip = implode(':', $server_addr);
- unset($server_addr);
- }
- // Just the IPv6 address, no port defined
- else
- {
- $server_ip = $server_info[self::SERVER_HOST];
- }
- // Now let's validate the IPv6 value sent, remove the square brackets ([]) first
- if(!filter_var(trim($server_ip, '[]'), FILTER_VALIDATE_IP, array(
- 'flags' => FILTER_FLAG_IPV6,
- )))
- {
- throw new GameQException("The IPv6 address '{$server_ip}' is invalid.");
- return FALSE;
- }
- }
- // IPv4
- else
- {
- // We have a port defined
- if(strstr($server_info[self::SERVER_HOST], ':'))
- {
- list($server_ip, $server_port) = explode(':', $server_info[self::SERVER_HOST]);
- }
- // No port, just IPv4
- else
- {
- $server_ip = $server_info[self::SERVER_HOST];
- }
- // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname. Try to resolve
- if(!filter_var($server_ip, FILTER_VALIDATE_IP, array(
- 'flags' => FILTER_FLAG_IPV4,
- )))
- {
- // When gethostbyname() fails it returns the original string
- // so if ip and the result from gethostbyname() are equal this failed.
- if($server_ip === gethostbyname($server_ip))
- {
- throw new GameQException("The host '{$server_ip}' is unresolvable to an IP address.");
- return FALSE;
- }
- }
- }
- // Create the class so we can reference it properly later
- $protocol_class = 'GameQ_Protocols_'.ucfirst($server_info[self::SERVER_TYPE]);
- // Create the new instance and add it to the servers list
- $this->servers[$server_id] = new $protocol_class(
- $server_ip,
- $server_port,
- array_merge($this->options, $server_info[self::SERVER_OPTIONS])
- );
- return $this; // Make calls chainable
- }
- /**
- * Add multiple servers at once
- *
- * @param array $servers
- * @return GameQ
- */
- public function addServers(Array $servers=NULL)
- {
- // Loop thru all the servers and add them
- foreach($servers AS $server_info)
- {
- $this->addServer($server_info);
- }
- return $this; // Make calls chainable
- }
- /**
- * Clear all the added servers. Creates clean instance.
- *
- * @return GameQ
- */
- public function clearServers()
- {
- // Reset all the servers
- $this->servers = array();
- $this->sockets = array();
- return $this; // Make Chainable
- }
- /**
- * Make all the data requests (i.e. challenges, queries, etc...)
- *
- * @return multitype:Ambigous <multitype:, multitype:boolean string mixed >
- */
- public function requestData()
- {
- // Data returned array
- $data = array();
- // Init the query array
- $queries = array(
- 'multi' => array(
- 'challenges' => array(),
- 'info' => array(),
- ),
- 'linear' => array(),
- );
- // Loop thru all of the servers added and categorize them
- foreach($this->servers AS $server_id => $instance)
- {
- // Check to see what kind of server this is and how we can send packets
- if($instance->packet_mode() == GameQ_Protocols::PACKET_MODE_LINEAR)
- {
- $queries['linear'][$server_id] = $instance;
- }
- else // We can send this out in a multi request
- {
- // Check to see if we should issue a challenge first
- if($instance->hasChallenge())
- {
- $queries['multi']['challenges'][$server_id] = $instance;
- }
- // Add this instance to do info query
- $queries['multi']['info'][$server_id] = $instance;
- }
- }
- // First lets do the faster, multi queries
- if(count($queries['multi']['info']) > 0)
- {
- $this->requestMulti($queries['multi']);
- }
- // Now lets do the slower linear queries.
- if(count($queries['linear']) > 0)
- {
- $this->requestLinear($queries['linear']);
- }
- // Now let's loop the servers and process the response data
- foreach($this->servers AS $server_id => $instance)
- {
- // Lets process this and filter
- $data[$server_id] = $this->filterResponse($instance);
- }
- // Send back the data array, could be empty if nothing went to plan
- return $data;
- }
- /* Working Methods */
- /**
- * Apply all set filters to the data returned by gameservers.
- *
- * @param GameQ_Protocols $protocol_instance
- * @return array
- */
- protected function filterResponse(GameQ_Protocols $protocol_instance)
- {
- // Let's pull out the "raw" data we are going to filter
- $data = $protocol_instance->processResponse();
- // Loop each of the filters we have attached
- foreach($this->options['filters'] AS $filter_name => $filter_instance)
- {
- // Overwrite the data with the "filtered" data
- $data = $filter_instance->filter($data, $protocol_instance);
- }
- return $data;
- }
- /**
- * Process "linear" servers. Servers that do not support multiple packet calls at once. So Slow!
- * This method also blocks the socket, you have been warned!!
- *
- * @param array $servers
- * @return boolean
- */
- protected function requestLinear($servers=array())
- {
- // Loop thru all the linear servers
- foreach($servers AS $server_id => $instance)
- {
- // First we need to get a socket and we need to block because this is linear
- if(($socket = $this->socket_open($instance, TRUE)) === FALSE)
- {
- // Skip it
- continue;
- }
- // Socket id
- $socket_id = (int) $socket;
- // See if we have challenges to send off
- if($instance->hasChallenge())
- {
- // Now send off the challenge packet
- fwrite($socket, $instance->getPacket('challenge'));
- // Read in the challenge response
- $instance->challengeResponse(array(fread($socket, 4096)));
- // Now we need to parse and apply the challenge response to all the packets that require it
- $instance->challengeVerifyAndParse();
- }
- // Invoke the beforeSend method
- $instance->beforeSend();
- // Grab the packets we need to send, minus the challenge packet
- $packets = $instance->getPacket('!challenge');
- // Now loop the packets, begin the slowness
- foreach($packets AS $packet_type => $packet)
- {
- // Add the socket information so we can retreive it easily
- $this->sockets = array(
- $socket_id => array(
- 'server_id' => $server_id,
- 'packet_type' => $packet_type,
- 'socket' => $socket,
- )
- );
- // Write the packet
- fwrite($socket, $packet);
- // Get the responses from the query
- $responses = $this->sockets_listen();
- // Lets look at our responses
- foreach($responses AS $socket_id => $response)
- {
- // Save the response from this packet
- $instance->packetResponse($packet_type, $response);
- }
- }
- }
- // Now close all the socket(s) and clean up any data
- $this->sockets_close();
- return TRUE;
- }
- /**
- * Process the servers that support multi requests. That means multiple packets can be sent out at once.
- *
- * @param array $servers
- * @return boolean
- */
- protected function requestMulti($servers=array())
- {
- // See if we have any challenges to send off
- if(count($servers['challenges']) > 0)
- {
- // Now lets send off all the challenges
- $this->sendChallenge($servers['challenges']);
- // Now let's process the challenges
- // Loop thru all the instances
- foreach($servers['challenges'] AS $server_id => $instance)
- {
- $instance->challengeVerifyAndParse();
- }
- }
- // Send out all the query packets to get data for
- $this->queryServerInfo($servers['info']);
- return TRUE;
- }
- /**
- * Send off needed challenges and get the response
- *
- * @param array $instances
- * @return boolean
- */
- protected function sendChallenge(Array $instances=NULL)
- {
- // Loop thru all the instances we need to send out challenges for
- foreach($instances AS $server_id => $instance)
- {
- // Make a new socket
- if(($socket = $this->socket_open($instance)) === FALSE)
- {
- // Skip it
- continue;
- }
- // Now write the challenge packet to the socket.
- fwrite($socket, $instance->getPacket(GameQ_Protocols::PACKET_CHALLENGE));
- // Add the socket information so we can retreive it easily
- $this->sockets[(int) $socket] = array(
- 'server_id' => $server_id,
- 'packet_type' => GameQ_Protocols::PACKET_CHALLENGE,
- 'socket' => $socket,
- );
- // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
- usleep($this->write_wait);
- }
- // Now we need to listen for challenge response(s)
- $responses = $this->sockets_listen();
- // Lets look at our responses
- foreach($responses AS $socket_id => $response)
- {
- // Back out the server_id we need to update the challenge response for
- $server_id = $this->sockets[$socket_id]['server_id'];
- // Now set the proper response for the challenge because we will need it later
- $this->servers[$server_id]->challengeResponse($response);
- }
- // Now close all the socket(s) and clean up any data
- $this->sockets_close();
- return TRUE;
- }
- /**
- * Query the server for actual server information (i.e. info, players, rules, etc...)
- *
- * @param array $instances
- * @return boolean
- */
- protected function queryServerInfo(Array $instances=NULL)
- {
- // Loop all the server instances
- foreach($instances AS $server_id => $instance)
- {
- // Invoke the beforeSend method
- $instance->beforeSend();
- // Get all the non-challenge packets we need to send
- $packets = $instance->getPacket('!challenge');
- if(count($packets) == 0)
- {
- // Skip nothing else to do for some reason.
- continue;
- }
- // Now lets send off the packets
- foreach($packets AS $packet_type => $packet)
- {
- // Make a new socket
- if(($socket = $this->socket_open($instance)) === FALSE)
- {
- // Skip it
- continue;
- }
- // Now write the packet to the socket.
- fwrite($socket, $packet);
- // Add the socket information so we can retreive it easily
- $this->sockets[(int) $socket] = array(
- 'server_id' => $server_id,
- 'packet_type' => $packet_type,
- 'socket' => $socket,
- );
- // Let's sleep shortly so we are not hammering out calls raipd fire style
- usleep($this->write_wait);
- }
- }
- // Now we need to listen for packet response(s)
- $responses = $this->sockets_listen();
- // Lets look at our responses
- foreach($responses AS $socket_id => $response)
- {
- // Back out the server_id
- $server_id = $this->sockets[$socket_id]['server_id'];
- // Back out the packet type
- $packet_type = $this->sockets[$socket_id]['packet_type'];
- // Save the response from this packet
- $this->servers[$server_id]->packetResponse($packet_type, $response);
- }
- // Now close all the socket(s) and clean up any data
- $this->sockets_close();
- return TRUE;
- }
- /* Sockets/streams stuff */
- /**
- * Open a new socket based on the instance information
- *
- * @param GameQ_Protocols $instance
- * @param bool $blocking
- * @throws GameQException
- * @return boolean|resource
- */
- protected function socket_open(GameQ_Protocols $instance, $blocking=FALSE)
- {
- // Create the remote address
- $remote_addr = sprintf("%s://%s:%d", $instance->transport(), $instance->ip(), $instance->port());
- // Create context
- $context = stream_context_create(array(
- 'socket' => array(
- 'bindto' => '0:0', // Bind to any available IP and OS decided port
- ),
- ));
- // Create the socket
- if(($socket = @stream_socket_client($remote_addr, $errno = NULL, $errstr = NULL, $this->timeout, STREAM_CLIENT_CONNECT, $context)) !== FALSE)
- {
- // Set the read timeout on the streams
- stream_set_timeout($socket, $this->timeout);
- // Set blocking mode
- stream_set_blocking($socket, $blocking);
- }
- else // Throw an error
- {
- // Check to see if we are in debug mode, if so throw the exception
- if($this->debug)
- {
- throw new GameQException(__METHOD__." Error creating socket to server {$remote_addr}. Error: ".$errstr, $errno);
- }
- // We didnt create so we need to return false.
- return FALSE;
- }
- unset($context, $remote_addr);
- // return the socket
- return $socket;
- }
- /**
- * Listen to all the created sockets and return the responses
- *
- * @return array
- */
- protected function sockets_listen()
- {
- // Set the loop to active
- $loop_active = TRUE;
- // To store the responses
- $responses = array();
- // To store the sockets
- $sockets = array();
- // Loop and pull out all the actual sockets we need to listen on
- foreach($this->sockets AS $socket_id => $socket_data)
- {
- // Append the actual socket we are listening to
- $sockets[$socket_id] = $socket_data['socket'];
- }
- // Init some variables
- $read = $sockets;
- $write = NULL;
- $except = NULL;
- // Check to see if $read is empty, if so stream_select() will throw a warning
- if(empty($read))
- {
- return $responses;
- }
- // This is when it should stop
- $time_stop = microtime(TRUE) + $this->timeout;
- // Let's loop until we break something.
- while ($loop_active && microtime(TRUE) < $time_stop)
- {
- // Now lets listen for some streams, but do not cross the streams!
- $streams = stream_select($read, $write, $except, 0, $this->stream_timeout);
- // We had error or no streams left, kill the loop
- if($streams === FALSE || ($streams <= 0))
- {
- $loop_active = FALSE;
- break;
- }
- // Loop the sockets that received data back
- foreach($read AS $socket)
- {
- // See if we have a response
- if(($response = stream_socket_recvfrom($socket, 8192)) === FALSE)
- {
- continue; // No response yet so lets continue.
- }
- // Check to see if the response is empty, if so we are done
- // @todo: Verify that this does not affect other protocols, added for Minequery
- // Initial testing showed this change did not affect any of the other protocols
- if(strlen($response) == 0)
- {
- // End the while loop
- $loop_active = FALSE;
- break;
- }
- // Add the response we got back
- $responses[(int) $socket][] = $response;
- }
- // Because stream_select modifies read we need to reset it each
- // time to the original array of sockets
- $read = $sockets;
- }
- // Free up some memory
- unset($streams, $read, $write, $except, $sockets, $time_stop, $response);
- return $responses;
- }
- /**
- * Close all the open sockets
- */
- protected function sockets_close()
- {
- // Loop all the existing sockets, valid or not
- foreach($this->sockets AS $socket_id => $data)
- {
- fclose($data['socket']);
- unset($this->sockets[$socket_id]);
- }
- return TRUE;
- }
- }
- /**
- * GameQ Exception Class
- *
- * Thrown when there is any kind of internal configuration error or
- * some unhandled or unexpected error or response.
- *
- * @author Austin Bischoff <[email protected]>
- */
- class GameQException extends Exception {}
|