<?php

//Here is the documentation from valve that made this possible
//Queries: http://developer.valvesoftware.com/wiki/Source_Server_Queries
//Browser Protocol: http://developer.valvesoftware.com/wiki/The_steam://_browser_protocol

//Load the config file
require_once("queryConf.php");

//Start the main Class
class SourceQuery
{

// A2S_INFO
var $netVersion;
var $hostName;
var $map;
var $gameDir;
var $gameType;
var $appID;
var $numPlayers;
var $maxPlayers;
var $numBots;
var $Dedicated;
var $OS;
var $password;
var $secure;
var $gameVersion;
var $IError;

// A2S_PLAYER (Arrays)
var $playerIndex;
var $playerName;
var $playerKills;
var $playerTime;
var $PError;

// A2S_RULES
var $numRules;
var $ruleName;		//Array
var $ruleValue;	//Array
var $RError;

//More Vars
var $serverIP;
var $serverPort;
var $serverPing;
var $success;
var $error;


function SourceQuery($ip, $port)
{
	$this->success = FALSE;

	if (is_null($ip) || is_null($port)) {
		$this->error = "IP ($ip) or Port ($port) were not supplied.";
		return;
	}
	if (!is_numeric($port)) {
		$this->error = "Port ($port) supplied is not valid.";
		return;
	}

	if ($this->getInfo($ip, $port) == FALSE) return;
	if ($this->getPlayers($ip, $port) == FALSE) return;
	if ($this->getRules($ip, $port) == FALSE) return;
	if ($this->getPing($ip, $port) == FALSE) return;

	$this->success = TRUE;
}

//A2S_SERVERQUERY_GETCHALLENGE - Returns a challenge number for use in the player and rules query.
function getChallenge($ip, $port)
{
	global $conf;

	$challenge = '';
	$sock = fsockopen("udp://" . $ip, $port);

	if (!$sock) {
		$this->error = "Could Not Open Socket in A2S_SERVERQUERY_GETCHALLENGE";
		return FALSE;
	}

	stream_set_timeout($sock, $conf['timeout']);
	fwrite($sock, pack("V", -1) . 'W');
	$packet = fread($sock, 128);
	fclose($sock);

	if (!$packet) {
		$this->error = "No Packet Returned in A2S_SERVERQUERY_GETCHALLENGE";
		return FALSE;
	}

	if(mb_substr($packet, 4, 1) == 'A')
	{
		//We will just send it back packed in binary since thats how it needs to be sent
		return mb_substr($packet, 5);
	}
	else
	{
		$this->error = "Incorrect Query String Returned (" . mb_substr($packet, 4, 1) . ") in A2S_SERVERQUERY_GETCHALLENGE";
		return FALSE;
	}
}

//A2S_INFO - Basic information about the server.
function getInfo($ip, $port)
{
	global $conf;

	$this->serverIP = $ip;
	$this->serverPort = $port;

	$sock = fsockopen("udp://" . $ip, $port);

	if (!$sock) {
		$this->IError = "Could Not Open Socket in A2S_INFO";
		return FALSE;
	}

	stream_set_timeout($sock, $conf['timeout']);
	//fwrite($sock, pack("H*", "FFFFFFFF54536F7572636520456E67696E6520517565727900"));
	//fwrite($sock, pack("H*a*", "FFFFFFFF54", "Source Engine Query"));
	fwrite($sock, pack("V", -1) . 'T' . pack("a*","Source Engine Query"));
	$packet = $this->getPacket($sock);
	fclose($sock);

	if (!$packet) {
		$this->IError = "No Packet Returned in A2S_INFO";
		return FALSE;
	}

	//Start at Position 4
	$strpos = 4;

	//Source - byte - Should be equal to 'I' (0x49)
	if(mb_substr($packet, $strpos, 1) == 'I')
	{
		$strpos++;

		$this->netVersion = ord(mb_substr($packet, $strpos++, 1));

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->hostName = mb_substr($packet, $strpos, $strlen );
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->map = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->gameDir = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->gameType = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$unpacked = unpack("s",mb_substr($packet, $strpos, 2));
		$this->appID = $unpacked[1];
		$strpos += 2;

		$this->numPlayers = ord(mb_substr($packet, $strpos++, 1));
		$this->maxPlayers = ord(mb_substr($packet, $strpos++, 1));
		$this->numBots = ord(mb_substr($packet, $strpos++, 1));
		$this->Dedicated = mb_substr($packet, $strpos++, 1) == 'd' ? "1" : "0";
		$this->OS = mb_substr($packet, $strpos++, 1);
		$this->password = ord(mb_substr($packet, $strpos++, 1));
		$this->secure = ord(mb_substr($packet, $strpos++, 1));

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->gameVersion = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;
	}

	//HL1 - byte - Should be equal to 'm' (0x6D)
	elseif (mb_substr($packet, $strpos, 1) == 'm')
	{
		$strpos++;

		//Skip IP:PORT since we have it already
		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->hostName = mb_substr($packet, $strpos, $strlen );
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->map = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->gameDir = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
		$this->gameType = mb_substr($packet, $strpos, $strlen);
		$strpos += $strlen + 1;

		$this->numPlayers = ord(mb_substr($packet, $strpos++, 1));
		$this->maxPlayers = ord(mb_substr($packet, $strpos++, 1));
		$this->netVersion = ord(mb_substr($packet, $strpos++, 1));
		$this->Dedicated = mb_substr($packet, $strpos++, 1) == 'd' ? "1" : "0";
		$this->OS = mb_substr($packet, $strpos++, 1);
		$this->password = ord(mb_substr($packet, $strpos++, 1));

		//IsMod - If set to 1 this packet has a mod details section, 0 otherwise
		if (ord(mb_substr($packet, $strpos++, 1)) == 1) {

			//URLInfo - (MOD Detail Only) URL containing information about this mod
			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$strpos += $strlen + 1;

			//URLDL - (MOD Detail Only) URL to download this mod
			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$strpos += $strlen + 1;

			//Unused - (MOD Detail Only) Will always be an empty string
			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$strpos += $strlen + 1;

			//ModVersion - (MOD Detail Only) Version of the installed mod
			$strpos += 4;

			//ModSize - (MOD Detail Only) The download size of this mod
			$strpos += 4;

			//SvOnly - (MOD Detail Only) If 1 this is a server side only mod
			$strpos++;

			//ClDLL - (MOD Detail Only) If 1 this mod has a custom client dll
			$strpos++;
		}

		$this->secure = ord(mb_substr($packet, $strpos++, 1));
		$this->numBots = ord(mb_substr($packet, $strpos++, 1));

	}
	else
	{
		$this->IError = "Incorrect Query String Returned (" . mb_substr($packet, 4, 1) . ") in A2S_INFO";
		return FALSE;
	}
	return TRUE;
}

//A2S_PLAYER - Details about each player on the server.
function getPlayers($ip, $port)
{
	global $conf;

	$sock = fsockopen("udp://" . $ip, $port);

	if (!$sock) {
		$this->PError = "Could Not Open Socket in A2S_PLAYER";
		return FALSE;
	}

	stream_set_timeout($sock, $conf['timeout']);
	fwrite($sock,pack("V", -1) . 'U' . $this->getChallenge($ip, $port));
	$packet = $this->getPacket($sock);
	fclose($sock);

	if (!$packet) {
		$this->PError = "No Packet Returned in A2S_PLAYER";
		return FALSE;
	}

	//Start at Position 4
	$strpos = 4;

	if(mb_substr($packet, $strpos, 1) == 'D')
	{
		$strpos++;

		$this->numPlayers = ord(mb_substr($packet, $strpos++, 1));

		for($x = 0; $x < $this->numPlayers; $x++)
		{
			//If we ran our of players the rest are connecting
			if ($strpos >= mb_strlen($packet)) {
				$this->playerName[$x] = "*** Connecting ***";
				$this->playerKills[$x] = 0;
				$this->playerTime[$x] = "00:00";
				continue;
			}

			$this->playerIndex[$x] = ord(mb_substr($packet, $strpos++, 1));

			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$this->playerName[$x] = mb_substr($packet, $strpos, $strlen );
			$strpos += $strlen + 1;

			$unpacked = unpack("l",mb_substr($packet, $strpos, 4));
			$this->playerKills[$x] = $unpacked[1];
			$strpos += 4;

			$unpacked = unpack("f",mb_substr($packet, $strpos, 4));
			$time = (int)$unpacked[1];

			//Bots send "-1" as the time
			if ($time == -1) $time = 0;

			$ms = date("i:s",$time);
			$hours = (int)($time / 3600);
			$hours = $hours > 0 ? $hours . ":" : "";
			$this->playerTime[$x] = $hours . $ms;
			$strpos += 4;
		}
	}
	else
	{
		$this->PError = "Incorrect Query String Returned (" . mb_substr($packet, 4, 1) . ") in A2S_PLAYER";
	}
	return TRUE;
}

//A2S_RULES - The rules the server is using.
function getRules($ip, $port)
{
	global $conf;

	$sock = fsockopen("udp://" . $ip, $port);

	if (!$sock) {
		$this->RError = "Could Not Open Socket in A2S_RULES";
		return FALSE;
	}

	stream_set_timeout($sock, $conf['timeout']);

	fwrite($sock, pack("V", -1) . 'V' . $this->getChallenge($ip, $port));
	$packet = $this->getPacket($sock);
	fclose($sock);

	if (!$packet) {
		$this->RError = "No Packet Returned in A2S_RULES";
		return FALSE;
	}

	//Start at Position 4
	$strpos = 4;

	if (mb_substr($packet, $strpos, 1) == 'E')
	{
		$strpos++;

		$unpacked = unpack("s",mb_substr($packet, $strpos, 2));
		$this->numRules = $unpacked[1];
		$strpos += 2;

		for($x = 0; $x < $this->numRules; $x++)
		{
			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$this->ruleName[$x] = mb_substr($packet, $strpos, $strlen );
			$strpos += $strlen + 1;

			$strlen = mb_strpos($packet, "\0", $strpos) - $strpos;
			$this->ruleValue[$x] = mb_substr($packet, $strpos, $strlen );
			$strpos += $strlen + 1;
		}
	}
	else
	{
		$this->RError = "Incorrect Query String Returned (" . mb_substr($packet, 4, 1) . ") in A2S_RULES";
	}
	return TRUE;
}

//This function will ping a server
function getPing($ip, $port)
{
	global $conf;

	$sock = fsockopen("udp://" . $ip, $port);

	if (!$sock) {
		$this->error = "Could Not Open Socket in getPing";
		return FALSE;
	}

	stream_set_timeout($sock, $conf['timeout']);

	$starttime = getMicroTime();
	fwrite($sock, pack("V", -1) . 'i');
	$packet = fread($sock, 128);
	$ping = ceil((getMicroTime() - $starttime) * 1000);

	fclose($sock);

	if (!$packet) {
		$this->error = "No Packet Returned in getPing";
		return FALSE;
	}

	if(mb_substr($packet, 4, 1) == 'j')
	{
		$this->serverPing = $ping;
	}
	else
	{
		$this->error = "Incorrect Query String Returned (" . mb_substr($packet, 4, 1) . ") in getPing";
		return FALSE;
	}
	return TRUE;

}

//This function retrieves the UDP packets and checks to make sure all the data is pulled in.
//This is mostly taken from code found in PsychoStats 2. Thanks goes to StormTrooper.
function getPacket($sock)
{
	$packets = array();		// stores each packet seperately, so we can combine them afterwards

	$expected = 0;						// # of packets we're expecting
	do {
		$packet = fread($sock, 1500);
		if (!$packet) return FALSE;

		$header = mb_substr($packet, 0, 4);				// get the 4 byte header
		$ack = unpack("N1split", $header);
		$split = sprintf("%u", $ack['split']);

		if ($split == 0xFeFFFFFF)					// we need to deal with multiple packets
		{
			$packet = mb_substr($packet, 4);				// strip off the leading 4 bytes
			$header = mb_substr($packet, 0, 5);			// get the 'sub-header ack'
			$packet = mb_substr($packet, 5);				// strip off 32bit int ID, seq# and total packet#
			$info = unpack("N1id/C1byte", $header);		// we don't really care about the ID

			if (!$expected) $expected = $info['byte'] & 0x0F;	// now we know how many packets to receive
			$seq = (int)($info['byte'] >> 4);			// get the sequence number of this packet
			$packets[$seq] = $packet;				// store the packet
			$expected--;
		}
		elseif ($split == 0xFFFFFFFF)					// we're dealing with a single packet
		{
			$packets[0] = $packet;
			$expected = 0;
		}
	} while ($expected > 0);

	ksort($packets, SORT_NUMERIC);
	return implode('', $packets);				// glue the packets together to make our final data string
}

} //End of SourceQuery Class

//Gets the current time in microseconds (returned as a float)
function getMicroTime() {
	list($usec, $sec) = explode(" ", microtime());
	return ((float)$usec + (float)$sec);
}

/* **************************************
Basic Example of how to use This Class
*****************************************

$query = new SourceQuery("SERVER_IP_HERE","SERVER_PORT");
echo "Hostname: " . $query->hostName;
echo "<br>";
echo "Map: " . $query->map;
echo "<br>";
echo "Game Type: " . $query->gameDir;
echo "<br>";
echo "Game Name: " . $query->gameType;
echo "<br>";
echo "AppID: " . $query->appID;
echo "<br>";
echo "Number of Players: " . $query->numPlayers;
echo "<br>";
echo "Max Players: " . $query->maxPlayers;
echo "<br>";
echo "Number of bots: " . $query->numBots;
echo "<br>";
echo "Dedicated? " . $query->Dedicated;
echo "<br>";
echo "OS: " . $query->OS;
echo "<br>";
echo "Passworded? " . $query->password;
echo "<br>";
echo "Secure? " . $query->secure;
echo "<br>";
echo "Game Version: " . $query->gameVersion;
echo "<br>";
echo "Network Version: " . $query->netVersion;
echo "<br> <br>";

for($x = 0; $x<$query->numPlayers; $x++)
{
	echo "Name: " . $query->playerName[$x] . "<br>";
	echo "Index: " . $query->playerIndex[$x] . "<br>";
	echo "Kills: " . $query->playerKills[$x] . "<br>";
	echo "Time Connected: " . $query->playerTime[$x] . "<br> <br>";
}

echo "<br><br>";

for($x = 0; $x<$query->numRules; $x++)
{
	echo $x . " Rule: " . $query->ruleName[$x] . " Value: " . $query->ruleValue[$x] . "<br>";
}

************************************** */
