Location: PHPKode > scripts > Counter-Strike > counter-strike/class_cstrike.php
<?php
/**
 * CStrike 1.6 Server Query class
 * 
 * @author Markus Schanz <hide@address.com>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @subpackage cstrike
 * @version 1.0
 * @link http://developer.valvesoftware.com/wiki/Server_Queries Developerwiki
 */
class cstrike
{
	/**
	 * @var string IP Address or hostname of the server [e.g. 12.14.15.16 or cs.example.com]
	 */
	private $ip;
	
	/**
	 * @var int Port of the server [default: 27015]
	 */
	private $port;
	
	/**
	 * @var bool True if connection was successful, false on failure
	 */
	private $connect;
	
	/**
	 * @var bool|handle Contains the handle of the socket, or false on failure
	 */
	private $resource;
	
	/**
	 * @var string Contains the challenge number [HEX-String]
	 */
	private $challengeNumber;
	
	/**
	 * @var array Contains all availible serverdata
	 * [type]		= Packet ID [should be 49]
	 * [version]	= Protocol version that is used by the server
	 * [servername]	= Hostname of the server
	 * [map]		= Current map that is played
	 * [gamedir]	= Gamedirectory [usaly cstrike]
	 * [gamedesc]	= Short description of the game
	 * [appid]		= Steam Application ID
	 * [player_cur]	= Number of current players
	 * [player_max]	= Number of players the server can hold
	 * [bots]		= Current number of bots on the server
	 * [dedicated]	= "l" = listen, "d" = dedicated, "p" = SourceTV
	 * [system]		= "l" = Linux, "w" = Windows
	 * [password]	= 0 = No password, 1 = Password set
	 * [secure]		= 0 = No VAC, 1 = VAC activated
	 * [gameversion]= The version of the game
	 * 
	 * More array elements can differ from server to server since the extra data flag (EDF) determinate what data is send
	 */
	private $serverinfo;
	
	/**
	 * @var array Contains playerdata
	 * [type]			= Packet ID [should be 44]
	 * [player]			= Number of players that this array contains
	 * NOTE: If [player] is equal 0 [zero], the following array index does NOT exists!
	 * [players][n][index]		= Index for this entry [currently 0 for every entry]
	 * [players][n][name]		= The name of the player
	 * [players][n][kills]		= The kills that the player made already [still buggy if player made negative kills, e.g. -1]
	 * [players][n][connected]	= The time in seconds since the player is connected to the server
	 */
	private $playerinfo;
	
	/**
	 * @var array Contains servervariables
	 * [type]			= Packet ID [should be 45]
	 * [rule]			= Number of rules that this array contains
	 * [rules][n][name]	= Name of the rule / variable
	 * [rules][n][value]= Value of the rule / variable
	 */
	private $serverrules;
	
	/**
	 * Init
	 * 
	 * @param string $ip IP Address or hostname of the server [e.g. 12.14.15.16 or cs.example.com]
	 * @param int $port Port of the server [default: 27015]
	 * @return void
	 */
	public function __construct($ip, $port = 27015)
	{
		$this->ip = $this->isIp($ip) ? $ip : gethostbyname($ip);
		$this->port = $port;
		
		$this->serverinfo = array();
		$this->playerinfo = array();
		$this->serverrules = array();
	}
	
	/**
	 * Destruct
	 * 
	 * This will close all open connections
	 * Conections will be closed automaticaly after calling getInfo()
	 * 
	 * @return void
	 */
	public function __destruct()
	{
		$this->close();
	}
	
	/**
	 * Returns an array with the choosen data
	 * [serverinfo] = {@see $serverinfo}
	 * [playerinfo] = {@see $playerinfo}
	 * [serverrules] = {@see $serverrules}
	 * Function returns array's based on the flag given by $info
	 * If $info is not specified, it returns all data
	 * 
	 * 1: Serverinfo
	 * 2: Playerinfo
	 * 4: Serverrules
	 * 
	 * @param int $info[optional]
	 * @return array
	 */
	public function getInfo($info = 7)
	{
		$this->connect = $this->connect();
		
		$this->getChallengeNumber();
		
		if($info & 1)
			$this->getServerinfo();
		
		if($info & 2)
			$this->getPlayerinfo();
			
		if($info & 4)
			$this->getServerrules();
		
		$rtn["serverinfo"] = $this->serverinfo;
		$rtn["playerinfo"] = $this->playerinfo;
		$rtn["serverrules"] = $this->serverrules;
		
		$this->close();
		
		return $rtn;
	}
	
	/**
	 * Creates a socket and then try to connect to the server
	 * 
	 * @return bool True on success, false on failure
	 */
	private function connect()
	{
		$this->resource = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
		
		if($this->resource === false)
			return false;
		
		if(@socket_connect($this->resource, $this->ip, $this->port) === false)
			return false;
		
		return true;
	}
	
	/**
	 * Closes the socket connection
	 * 
	 * @return void
	 */
	private function close()
	{
		if($this->connect)
		{
			$this->connect = false;
			socket_shutdown($this->resource, 2);
			socket_close($this->resource);
		}
	}
	
	/**
	 * Executes an command and returns the result (header is already cut, so it begins with the packet type)
	 * Multiple packets get merged automatically
	 * 
	 * @param string $cmd The binary data to execute
	 * @return bool|string Result as hexstring, false on failure
	 */
	private function cmd($cmd)
	{
		if(!$this->connect)
			return false;
		
		if(@socket_write($this->resource, $cmd, 1400) === false)
			return false;
		
		if(($response = @socket_read($this->resource, 1400)) === false)
			return false;
		
		list($response) = unpack("H*0", $response);
		
		switch($this->getPacketData($response, "long"))
		{
			case "fffffffe":
				$this->getPacketData($response, "long");
				$num = $this->getPacketData($response, "byte");
				$this->getPacketData($response, "long");
				
				for($i = 1; $i < hexdec($num{1}); $i++)
				{
					if(($nextresponse = @socket_read($this->resource, 1400)) === false)
						return false;
					
					list($nextresponse) = unpack("H*0", $nextresponse);
					$this->getPacketData($nextresponse, "long");
					$this->getPacketData($nextresponse, "long");
					$this->getPacketData($nextresponse, "byte");
					
					$response .= $nextresponse;
				}
		}
		
		return $response;
	}
	
	/**
	 * Get the challenge number needed for queries
	 * 
	 * @return string The challenge number
	 */
	private function getChallengeNumber()
	{
		$data = $this->cmd("\xFF\xFF\xFF\xFF\x55\xFF\xFF\xFF\xFF");
		$this->getPacketData($data, "byte");
		$this->challengeNumber = $this->getPacketData($data, "long", false, false);
	}
	
	/**
	 * Get the serverinfo by sending the command "TSource Engine Query"
	 * The results are written into $serverinfo
	 * 
	 * @return void
	 */
	private function getServerinfo()
	{
		$data = $this->cmd("\xFF\xFF\xFF\xFFTSource Engine Query\x00");
		
		$rtn["type"]		= $this->getPacketData($data, "byte");
		$rtn["version"]		= $this->getPacketData($data, "byte", true);
		$rtn["servername"]	= $this->getPacketString($data);
		$rtn["map"]			= $this->getPacketString($data);
		$rtn["gamedir"]		= $this->getPacketString($data);
		$rtn["gamedesc"]	= $this->getPacketString($data);
		$rtn["appid"]		= $this->getPacketData($data, "short", true);
		$rtn["player_cur"]	= $this->getPacketData($data, "byte", true);
		$rtn["player_max"]	= $this->getPacketData($data, "byte", true);
		$rtn["bots"]		= $this->getPacketData($data, "byte", true);
		$rtn["dedicated"]	= chr($this->getPacketData($data, "byte", true));
		$rtn["system"]		= chr($this->getPacketData($data, "byte", true));
		$rtn["password"]	= $this->getPacketData($data, "byte", true);
		$rtn["secure"]		= $this->getPacketData($data, "byte", true);
		$rtn["gameversion"]	= $this->getPacketString($data);
		$rtn["ip"]			= $this->ip;
		
		// Extra data flag
		switch($this->getPacketData($data, "byte"))
		{
			case "80":
				$rtn["port"] = $this->getPacketData($data, "short", true);
				break;
			case "40":
				$rtn["specport"] = $this->getPacketData($data, "short", true);
				$rtn["specstr"] = $this->getPacketString($data);
				break;
			case "20":
				$rtn["gametag"] = $this->getPacketString($data);
		}
		
		$this->serverinfo = $rtn;
	}
	
	/**
	 * First this method gets the challenge number and write it into $challengeNumber
	 * After that it get the playerinfo by sending the byte 0x55 + $challengeNumber
	 * The results are written into $playerinfo
	 * 
	 * @return void
	 */
	private function getPlayerinfo()
	{
		$data = $this->cmd("\xFF\xFF\xFF\xFF\x55" . pack("H*", $this->challengeNumber));
		
		$rtn["type"]	= $this->getPacketData($data, "byte");
		$rtn["player"]	= $this->getPacketData($data, "byte", true);
		
		for($i = 0; $i < $rtn["player"]; $i++)
		{
			$rtn["players"][$i]["index"]	= $this->getPacketData($data, "byte", true);
			$rtn["players"][$i]["name"]	= $this->getPacketString($data);
			$rtn["players"][$i]["kills"]	= $this->getPacketData($data, "long", true);
			$rtn["players"][$i]["connected"]= $this->hex2float(hexdec($this->getPacketData($data, "float")));
		}
		
		$this->playerinfo = $rtn;
	}
	
	/**
	 * Get the serverrules by sending the byte 0x56 + $challengeNumber
	 * The results are written into $serverrules
	 * 
	 * @return void
	 */
	private function getServerrules()
	{
		$data = $this->cmd("\xFF\xFF\xFF\xFF\x56" . pack("H*", $this->challengeNumber));
		
		$rtn["type"] = $this->getPacketData($data, "byte");
		$rtn["rule"] = $this->getPacketData($data, "short", true);
		
		for($i = 0; $i < $rtn["rule"]; $i++)
		{
			$rtn["rules"][$i]["name"] = $this->getPacketString($data);
			$rtn["rules"][$i]["value"]= $this->getPacketString($data);
		}
		
		$this->serverrules = $rtn;
	}
	
	/**
	 * This method transform every byte in $hexstr into a ASCII char until the byte 0x00 is found
	 * The given param is given as a reference, and beeing cut!
	 * 
	 * @param string &$hexstr Datastring as hex values
	 * @return string String that was read
	 */
	private function getPacketString(&$hexstr)
	{
		$rtn = "";
		
		while(true)
		{
			$hex = $hexstr{0} . $hexstr{1};
			$hexstr = substr($hexstr, 2);
			if($hex == "00")
				break;
			
			$rtn .= chr(hexdec($hex));
		}
		
		return $rtn;
	}
	
	/**
	 * This method cut a byte / short / long from a datastring (given as hex values) and return it
	 * 
	 * @param string &$hexstr Datastring as hex values
	 * @param string $type Datatype [can be byte, short or long]
	 * @param bool $hexdec If true the value will be turned into an integer before returned, default: false
	 * @param bool $switchbytes If true the bytes are switched (FF 01 => 01 FF), default: true
	 * @return string Hex value that was read
	 */
	private function getPacketData(&$hexstr, $type, $hexdec = false, $switchbytes = true)
	{
		switch($type)
		{
			case "byte":
				$bytes = 1;
				break;
			case "short":
				$bytes = 2;
				break;
			case "long":
				$bytes = 4;
				break;
			default:
				$bytes = 4;
		}
		
		$rtn = "";
		
		for($i = 0; $i < $bytes; $i++)
		{
			if($switchbytes)
				$rtn = $hexstr{0} . $hexstr{1} . $rtn;
			else
				$rtn .= $hexstr{0} . $hexstr{1};
			
			$hexstr = substr($hexstr, 2);
		}
		
		if($hexdec)
			return hexdec($rtn);
		
		return $rtn;
	}
	
	/**
	 * This method is needed to convert the long, given at the playerinfo query, into a readable number
	 * The value is returned as an integer (NOT float!)
	 * 
	 * @param string $hex The hexvalues to convert
	 * @return int The converted value
	 */
	private function hex2float($hex)
	{
		$bin = str_pad(decbin($hex), 32, 0, STR_PAD_LEFT);
		
		$exp = bindec(substr($bin, 1, 8)) - 127;
		$mantisse = bindec(substr($bin, 9, 23));
		
		$float = (1 + ($mantisse / pow(2, 23))) * pow(2, $exp);
		$float = $bin{0} ? ($float * (-1)) : $float;
		
		return intval($float);
	}
	
	/**
	 * This method checks if the given parameter is an IP-Address or not
	 * 
	 * @param string $ip The string to check
	 * @return bool True if $ip is a IP-Address, false if not
	 */
	private function isIp($ip)
	{
		if(preg_match("/([0-9]{1,3}\.){3}[0-9]{1,3}/", $ip) == 1)
			return true;
		
		return false;
	}
}
?>
Return current item: Counter-Strike