commit
ae6c8a1419
@ -0,0 +1,3 @@ |
||||
Order deny,allow |
||||
Deny from all |
||||
Allow from localhost |
@ -0,0 +1,156 @@ |
||||
<?php |
||||
/* |
||||
Copyright (c) 2014 Anders G. Jørgensen - http://spirit55555.dk |
||||
|
||||
This program is free software: you can redistribute it and/or modify |
||||
it under the terms of the GNU General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
This program 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 General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
class MinecraftColors { |
||||
const REGEX = '/(?:§|&)([0-9a-fklmnor])/i'; |
||||
|
||||
const START_TAG = '<span style="%s">'; |
||||
const CLOSE_TAG = '</span>'; |
||||
const CSS_COLOR = 'color: #'; |
||||
const EMPTY_TAGS = '/<[^\/>]*>([\s]?)*<\/[^>]*>/'; |
||||
const LINE_BREAK = '<br />'; |
||||
|
||||
static private $colors = array( |
||||
'0' => '000000', //Black |
||||
'1' => '0000AA', //Dark Blue |
||||
'2' => '00AA00', //Dark Green |
||||
'3' => '00AAAA', //Dark Aqua |
||||
'4' => 'AA0000', //Dark Red |
||||
'5' => 'AA00AA', //Dark Purple |
||||
'6' => 'FFAA00', //Gold |
||||
'7' => 'AAAAAA', //Gray |
||||
'8' => '555555', //Dark Gray |
||||
'9' => '5555FF', //Blue |
||||
'a' => '55FF55', //Green |
||||
'b' => '55FFFF', //Aqua |
||||
'c' => 'FF5555', //Red |
||||
'd' => 'FF55FF', //Light Purple |
||||
'e' => 'FFFF55', //Yellow |
||||
'f' => 'FFFFFF' //White |
||||
); |
||||
|
||||
static private $formatting = array( |
||||
'k' => '', //Obfuscated |
||||
'l' => 'font-weight: bold;', //Bold |
||||
'm' => 'text-decoration: line-through;', //Strikethrough |
||||
'n' => 'text-decoration: underline;', //Underline |
||||
'o' => 'font-style: italic;', //Italic |
||||
'r' => '' //Reset |
||||
); |
||||
|
||||
static private function UFT8Encode($text) { |
||||
//Encode the text in UTF-8, but only if it's not already. |
||||
if (mb_detect_encoding($text) != 'UTF-8') |
||||
$text = utf8_encode($text); |
||||
|
||||
return $text; |
||||
} |
||||
|
||||
static public function clean($text) { |
||||
$text = self::UFT8Encode($text); |
||||
$text = htmlspecialchars($text); |
||||
|
||||
return preg_replace(self::REGEX, '', $text); |
||||
} |
||||
|
||||
static public function convertToMOTD($text, $sign = '\u00A7') { |
||||
$text = self::UFT8Encode($text); |
||||
$text = htmlspecialchars($text); |
||||
|
||||
$text = preg_replace(self::REGEX, $sign.'${1}', $text); |
||||
$text = str_replace("\n", '\n', $text); |
||||
|
||||
return $text; |
||||
} |
||||
|
||||
static public function convertToHTML($text, $line_break_element = false) { |
||||
$text = self::UFT8Encode($text); |
||||
$text = htmlspecialchars($text); |
||||
|
||||
preg_match_all(self::REGEX, $text, $offsets); |
||||
|
||||
$colors = $offsets[0]; //This is what we are going to replace with HTML. |
||||
$color_codes = $offsets[1]; //This is the color numbers/characters only. |
||||
|
||||
//No colors? Just return the text. |
||||
if (empty($colors)) |
||||
return $text; |
||||
|
||||
$open_tags = 0; |
||||
|
||||
foreach ($colors as $index => $color) { |
||||
$color_code = strtolower($color_codes[$index]); |
||||
|
||||
//We have a normal color. |
||||
if (isset(self::$colors[$color_code])) { |
||||
$html = sprintf(self::START_TAG, self::CSS_COLOR.self::$colors[$color_code]); |
||||
|
||||
//New color clears the other colors and formatting. |
||||
if ($open_tags != 0) { |
||||
$html = str_repeat(self::CLOSE_TAG, $open_tags).$html; |
||||
$open_tags = 0; |
||||
} |
||||
|
||||
$open_tags++; |
||||
} |
||||
|
||||
//We have some formatting. |
||||
else { |
||||
switch ($color_code) { |
||||
//Reset is special, just close all open tags. |
||||
case 'r': |
||||
$html = ''; |
||||
|
||||
if ($open_tags != 0) { |
||||
$html = str_repeat(self::CLOSE_TAG, $open_tags); |
||||
$open_tags = 0; |
||||
} |
||||
|
||||
break; |
||||
|
||||
//Can't do obfuscated in CSS... |
||||
case 'k': |
||||
$html = ''; |
||||
|
||||
break; |
||||
|
||||
default: |
||||
$html = sprintf(self::START_TAG, self::$formatting[$color_code]); |
||||
$open_tags++; |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
//Replace the color with the HTML code. We use preg_replace because of the limit parameter. |
||||
$text = preg_replace('/'.$color.'/', $html, $text, 1); |
||||
} |
||||
|
||||
//Still open tags? Close them! |
||||
if ($open_tags != 0) |
||||
$text = $text.str_repeat(self::CLOSE_TAG, $open_tags); |
||||
|
||||
//Replace \n with <br /> |
||||
if ($line_break_element) |
||||
$text = str_replace("\n", self::LINE_BREAK, $text); |
||||
|
||||
//Return the text without empty HTML tags. Only to clean up bad color formatting from the user. |
||||
return preg_replace(self::EMPTY_TAGS, '', $text); |
||||
} |
||||
} |
||||
?> |
@ -0,0 +1,157 @@ |
||||
<?php |
||||
namespace xPaw; |
||||
class MinecraftQuery |
||||
{ |
||||
/* |
||||
* Class written by xPaw |
||||
* |
||||
* Website: http://xpaw.me |
||||
* GitHub: https://github.com/xPaw/PHP-Minecraft-Query |
||||
*/ |
||||
const STATISTIC = 0x00; |
||||
const HANDSHAKE = 0x09; |
||||
private $Socket; |
||||
private $Players; |
||||
private $Info; |
||||
public function Connect( $Ip, $Port = 25565, $Timeout = 3 ) |
||||
{ |
||||
if( !is_int( $Timeout ) || $Timeout < 0 ) |
||||
{ |
||||
throw new \InvalidArgumentException( 'Timeout must be an integer.' ); |
||||
} |
||||
$this->Socket = @FSockOpen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, $Timeout ); |
||||
if( $ErrNo || $this->Socket === false ) |
||||
{ |
||||
throw new MinecraftQueryException( 'Could not create socket: ' . $ErrStr ); |
||||
} |
||||
Stream_Set_Timeout( $this->Socket, $Timeout ); |
||||
Stream_Set_Blocking( $this->Socket, true ); |
||||
try |
||||
{ |
||||
$Challenge = $this->GetChallenge( ); |
||||
$this->GetStatus( $Challenge ); |
||||
} |
||||
// We catch this because we want to close the socket, not very elegant |
||||
catch( MinecraftQueryException $e ) |
||||
{ |
||||
FClose( $this->Socket ); |
||||
throw new MinecraftQueryException( $e->getMessage( ) ); |
||||
} |
||||
FClose( $this->Socket ); |
||||
} |
||||
public function GetInfo( ) |
||||
{ |
||||
return isset( $this->Info ) ? $this->Info : false; |
||||
} |
||||
public function GetPlayers( ) |
||||
{ |
||||
return isset( $this->Players ) ? $this->Players : false; |
||||
} |
||||
private function GetChallenge( ) |
||||
{ |
||||
$Data = $this->WriteData( self :: HANDSHAKE ); |
||||
if( $Data === false ) |
||||
{ |
||||
throw new MinecraftQueryException( 'Failed to receive challenge.' ); |
||||
} |
||||
return Pack( 'N', $Data ); |
||||
} |
||||
private function GetStatus( $Challenge ) |
||||
{ |
||||
$Data = $this->WriteData( self :: STATISTIC, $Challenge . Pack( 'c*', 0x00, 0x00, 0x00, 0x00 ) ); |
||||
if( !$Data ) |
||||
{ |
||||
throw new MinecraftQueryException( 'Failed to receive status.' ); |
||||
} |
||||
$Last = ''; |
||||
$Info = Array( ); |
||||
$Data = SubStr( $Data, 11 ); // splitnum + 2 int |
||||
$Data = Explode( "\x00\x00\x01player_\x00\x00", $Data ); |
||||
if( Count( $Data ) !== 2 ) |
||||
{ |
||||
throw new MinecraftQueryException( 'Failed to parse server\'s response.' ); |
||||
} |
||||
$Players = SubStr( $Data[ 1 ], 0, -2 ); |
||||
$Data = Explode( "\x00", $Data[ 0 ] ); |
||||
// Array with known keys in order to validate the result |
||||
// It can happen that server sends custom strings containing bad things (who can know!) |
||||
$Keys = Array( |
||||
'hostname' => 'HostName', |
||||
'gametype' => 'GameType', |
||||
'version' => 'Version', |
||||
'plugins' => 'Plugins', |
||||
'map' => 'Map', |
||||
'numplayers' => 'Players', |
||||
'maxplayers' => 'MaxPlayers', |
||||
'hostport' => 'HostPort', |
||||
'hostip' => 'HostIp', |
||||
'game_id' => 'GameName' |
||||
); |
||||
foreach( $Data as $Key => $Value ) |
||||
{ |
||||
if( ~$Key & 1 ) |
||||
{ |
||||
if( !Array_Key_Exists( $Value, $Keys ) ) |
||||
{ |
||||
$Last = false; |
||||
continue; |
||||
} |
||||
$Last = $Keys[ $Value ]; |
||||
$Info[ $Last ] = ''; |
||||
} |
||||
else if( $Last != false ) |
||||
{ |
||||
$Info[ $Last ] = utf8_encode($Value); |
||||
} |
||||
} |
||||
// Ints |
||||
$Info[ 'Players' ] = IntVal( $Info[ 'Players' ] ); |
||||
$Info[ 'MaxPlayers' ] = IntVal( $Info[ 'MaxPlayers' ] ); |
||||
$Info[ 'HostPort' ] = IntVal( $Info[ 'HostPort' ] ); |
||||
// Parse "plugins", if any |
||||
if( $Info[ 'Plugins' ] ) |
||||
{ |
||||
$Data = Explode( ": ", $Info[ 'Plugins' ], 2 ); |
||||
$Info[ 'RawPlugins' ] = $Info[ 'Plugins' ]; |
||||
$Info[ 'Software' ] = $Data[ 0 ]; |
||||
if( Count( $Data ) == 2 ) |
||||
{ |
||||
$Info[ 'Plugins' ] = Explode( "; ", $Data[ 1 ] ); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
$Info[ 'Software' ] = 'Vanilla'; |
||||
} |
||||
$this->Info = $Info; |
||||
if( empty( $Players ) ) |
||||
{ |
||||
$this->Players = null; |
||||
} |
||||
else |
||||
{ |
||||
$this->Players = Explode( "\x00", $Players ); |
||||
} |
||||
} |
||||
private function WriteData( $Command, $Append = "" ) |
||||
{ |
||||
$Command = Pack( 'c*', 0xFE, 0xFD, $Command, 0x01, 0x02, 0x03, 0x04 ) . $Append; |
||||
$Length = StrLen( $Command ); |
||||
if( $Length !== FWrite( $this->Socket, $Command, $Length ) ) |
||||
{ |
||||
throw new MinecraftQueryException( "Failed to write on socket." ); |
||||
} |
||||
$Data = FRead( $this->Socket, 4096 ); |
||||
if( $Data === false ) |
||||
{ |
||||
throw new MinecraftQueryException( "Failed to read from socket." ); |
||||
} |
||||
if( StrLen( $Data ) < 5 || $Data[ 0 ] != $Command[ 2 ] ) |
||||
{ |
||||
return false; |
||||
} |
||||
return SubStr( $Data, 5 ); |
||||
} |
||||
} |
||||
|
||||
?> |
@ -0,0 +1,7 @@ |
||||
<?php |
||||
namespace xPaw; |
||||
class MinecraftQueryException extends \Exception |
||||
{ |
||||
// Exception thrown by MinecraftQuery class |
||||
} |
||||
?> |
@ -0,0 +1,131 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\InvalidPacketException; |
||||
|
||||
/** |
||||
* Base socket interface |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\InvalidPacketException |
||||
*/ |
||||
abstract class BaseSocket |
||||
{ |
||||
public $Socket; |
||||
public $Engine; |
||||
|
||||
public $Address; |
||||
public $Port; |
||||
public $Timeout; |
||||
|
||||
public function __destruct( ) |
||||
{ |
||||
$this->Close( ); |
||||
} |
||||
|
||||
abstract public function Close( ); |
||||
abstract public function Open( $Address, $Port, $Timeout, $Engine ); |
||||
abstract public function Write( $Header, $String = '' ); |
||||
abstract public function Read( $Length = 1400 ); |
||||
|
||||
protected function ReadInternal( $Buffer, $Length, $SherlockFunction ) |
||||
{ |
||||
if( $Buffer->Remaining( ) === 0 ) |
||||
{ |
||||
throw new InvalidPacketException( 'Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY ); |
||||
} |
||||
|
||||
$Header = $Buffer->GetLong( ); |
||||
|
||||
if( $Header === -1 ) // Single packet |
||||
{ |
||||
// We don't have to do anything |
||||
} |
||||
else if( $Header === -2 ) // Split packet |
||||
{ |
||||
$Packets = []; |
||||
$IsCompressed = false; |
||||
$ReadMore = false; |
||||
|
||||
do |
||||
{ |
||||
$RequestID = $Buffer->GetLong( ); |
||||
|
||||
switch( $this->Engine ) |
||||
{ |
||||
case SourceQuery::GOLDSOURCE: |
||||
{ |
||||
$PacketCountAndNumber = $Buffer->GetByte( ); |
||||
$PacketCount = $PacketCountAndNumber & 0xF; |
||||
$PacketNumber = $PacketCountAndNumber >> 4; |
||||
|
||||
break; |
||||
} |
||||
case SourceQuery::SOURCE: |
||||
{ |
||||
$IsCompressed = ( $RequestID & 0x80000000 ) !== 0; |
||||
$PacketCount = $Buffer->GetByte( ); |
||||
$PacketNumber = $Buffer->GetByte( ) + 1; |
||||
|
||||
if( $IsCompressed ) |
||||
{ |
||||
$Buffer->GetLong( ); // Split size |
||||
|
||||
$PacketChecksum = $Buffer->GetUnsignedLong( ); |
||||
} |
||||
else |
||||
{ |
||||
$Buffer->GetShort( ); // Split size |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
$Packets[ $PacketNumber ] = $Buffer->Get( ); |
||||
|
||||
$ReadMore = $PacketCount > sizeof( $Packets ); |
||||
} |
||||
while( $ReadMore && $SherlockFunction( $Buffer, $Length ) ); |
||||
|
||||
$Data = Implode( $Packets ); |
||||
|
||||
// TODO: Test this |
||||
if( $IsCompressed ) |
||||
{ |
||||
// Let's make sure this function exists, it's not included in PHP by default |
||||
if( !Function_Exists( 'bzdecompress' ) ) |
||||
{ |
||||
throw new \RuntimeException( 'Received compressed packet, PHP doesn\'t have Bzip2 library installed, can\'t decompress.' ); |
||||
} |
||||
|
||||
$Data = bzdecompress( $Data ); |
||||
|
||||
if( CRC32( $Data ) !== $PacketChecksum ) |
||||
{ |
||||
throw new InvalidPacketException( 'CRC32 checksum mismatch of uncompressed packet data.', InvalidPacketException::CHECKSUM_MISMATCH ); |
||||
} |
||||
} |
||||
|
||||
$Buffer->Set( SubStr( $Data, 4 ) ); |
||||
} |
||||
else |
||||
{ |
||||
throw new InvalidPacketException( 'Socket read: Raw packet header mismatch. (0x' . DecHex( $Header ) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
|
||||
return $Buffer; |
||||
} |
||||
} |
@ -0,0 +1,199 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\InvalidPacketException; |
||||
|
||||
/** |
||||
* Class Buffer |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\InvalidPacketException |
||||
*/ |
||||
class Buffer |
||||
{ |
||||
/** |
||||
* Buffer |
||||
* |
||||
* @var string |
||||
*/ |
||||
private $Buffer; |
||||
|
||||
/** |
||||
* Buffer length |
||||
* |
||||
* @var int |
||||
*/ |
||||
private $Length; |
||||
|
||||
/** |
||||
* Current position in buffer |
||||
* |
||||
* @var int |
||||
*/ |
||||
private $Position; |
||||
|
||||
/** |
||||
* Sets buffer |
||||
* |
||||
* @param string $Buffer Buffer |
||||
*/ |
||||
public function Set( $Buffer ) |
||||
{ |
||||
$this->Buffer = $Buffer; |
||||
$this->Length = StrLen( $Buffer ); |
||||
$this->Position = 0; |
||||
} |
||||
|
||||
/** |
||||
* Get remaining bytes |
||||
* |
||||
* @return int Remaining bytes in buffer |
||||
*/ |
||||
public function Remaining( ) |
||||
{ |
||||
return $this->Length - $this->Position; |
||||
} |
||||
|
||||
/** |
||||
* Gets data from buffer |
||||
* |
||||
* @param int $Length Bytes to read |
||||
* |
||||
* @return string |
||||
*/ |
||||
public function Get( $Length = -1 ) |
||||
{ |
||||
if( $Length === 0 ) |
||||
{ |
||||
return ''; |
||||
} |
||||
|
||||
$Remaining = $this->Remaining( ); |
||||
|
||||
if( $Length === -1 ) |
||||
{ |
||||
$Length = $Remaining; |
||||
} |
||||
else if( $Length > $Remaining ) |
||||
{ |
||||
return ''; |
||||
} |
||||
|
||||
$Data = SubStr( $this->Buffer, $this->Position, $Length ); |
||||
|
||||
$this->Position += $Length; |
||||
|
||||
return $Data; |
||||
} |
||||
|
||||
/** |
||||
* Get byte from buffer |
||||
* |
||||
* @return int |
||||
*/ |
||||
public function GetByte( ) |
||||
{ |
||||
return Ord( $this->Get( 1 ) ); |
||||
} |
||||
|
||||
/** |
||||
* Get short from buffer |
||||
* |
||||
* @return int |
||||
*/ |
||||
public function GetShort( ) |
||||
{ |
||||
if( $this->Remaining( ) < 2 ) |
||||
{ |
||||
throw new InvalidPacketException( 'Not enough data to unpack a short.', InvalidPacketException::BUFFER_EMPTY ); |
||||
} |
||||
|
||||
$Data = UnPack( 'v', $this->Get( 2 ) ); |
||||
|
||||
return $Data[ 1 ]; |
||||
} |
||||
|
||||
/** |
||||
* Get long from buffer |
||||
* |
||||
* @return int |
||||
*/ |
||||
public function GetLong( ) |
||||
{ |
||||
if( $this->Remaining( ) < 4 ) |
||||
{ |
||||
throw new InvalidPacketException( 'Not enough data to unpack a long.', InvalidPacketException::BUFFER_EMPTY ); |
||||
} |
||||
|
||||
$Data = UnPack( 'l', $this->Get( 4 ) ); |
||||
|
||||
return $Data[ 1 ]; |
||||
} |
||||
|
||||
/** |
||||
* Get float from buffer |
||||
* |
||||
* @return float |
||||
*/ |
||||
public function GetFloat( ) |
||||
{ |
||||
if( $this->Remaining( ) < 4 ) |
||||
{ |
||||
throw new InvalidPacketException( 'Not enough data to unpack a float.', InvalidPacketException::BUFFER_EMPTY ); |
||||
} |
||||
|
||||
$Data = UnPack( 'f', $this->Get( 4 ) ); |
||||
|
||||
return $Data[ 1 ]; |
||||
} |
||||
|
||||
/** |
||||
* Get unsigned long from buffer |
||||
* |
||||
* @return int |
||||
*/ |
||||
public function GetUnsignedLong( ) |
||||
{ |
||||
if( $this->Remaining( ) < 4 ) |
||||
{ |
||||
throw new InvalidPacketException( 'Not enough data to unpack an usigned long.', InvalidPacketException::BUFFER_EMPTY ); |
||||
} |
||||
|
||||
$Data = UnPack( 'V', $this->Get( 4 ) ); |
||||
|
||||
return $Data[ 1 ]; |
||||
} |
||||
|
||||
/** |
||||
* Read one string from buffer ending with null byte |
||||
* |
||||
* @return string |
||||
*/ |
||||
public function GetString( ) |
||||
{ |
||||
$ZeroBytePosition = StrPos( $this->Buffer, "\0", $this->Position ); |
||||
|
||||
if( $ZeroBytePosition === false ) |
||||
{ |
||||
return ''; |
||||
} |
||||
|
||||
$String = $this->Get( $ZeroBytePosition - $this->Position ); |
||||
|
||||
$this->Position++; |
||||
|
||||
return $String; |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery\Exception; |
||||
|
||||
class AuthenticationException extends SourceQueryException |
||||
{ |
||||
const BAD_PASSWORD = 1; |
||||
const BANNED = 2; |
||||
} |
@ -0,0 +1,18 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery\Exception; |
||||
|
||||
class InvalidArgumentException extends SourceQueryException |
||||
{ |
||||
const TIMEOUT_NOT_INTEGER = 1; |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery\Exception; |
||||
|
||||
class InvalidPacketException extends SourceQueryException |
||||
{ |
||||
const PACKET_HEADER_MISMATCH = 1; |
||||
const BUFFER_EMPTY = 2; |
||||
const BUFFER_NOT_EMPTY = 3; |
||||
const CHECKSUM_MISMATCH = 4; |
||||
} |
@ -0,0 +1,20 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery\Exception; |
||||
|
||||
class SocketException extends SourceQueryException |
||||
{ |
||||
const COULD_NOT_CREATE_SOCKET = 1; |
||||
const NOT_CONNECTED = 2; |
||||
const CONNECTION_FAILED = 3; |
||||
} |
@ -0,0 +1,18 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery\Exception; |
||||
|
||||
abstract class SourceQueryException extends \Exception |
||||
{ |
||||
// Base exception class |
||||
} |
@ -0,0 +1,140 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\AuthenticationException; |
||||
|
||||
/** |
||||
* Class GoldSourceRcon |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\AuthenticationException |
||||
*/ |
||||
class GoldSourceRcon |
||||
{ |
||||
/** |
||||
* Points to socket class |
||||
* |
||||
* @var Socket |
||||
*/ |
||||
private $Socket; |
||||
|
||||
private $RconPassword; |
||||
private $RconRequestId; |
||||
private $RconChallenge; |
||||
|
||||
public function __construct( $Socket ) |
||||
{ |
||||
$this->Socket = $Socket; |
||||
} |
||||
|
||||
public function Close( ) |
||||
{ |
||||
$this->RconChallenge = 0; |
||||
$this->RconRequestId = 0; |
||||
$this->RconPassword = 0; |
||||
} |
||||
|
||||
public function Open( ) |
||||
{ |
||||
// |
||||
} |
||||
|
||||
public function Write( $Header, $String = '' ) |
||||
{ |
||||
$Command = Pack( 'cccca*', 0xFF, 0xFF, 0xFF, 0xFF, $String ); |
||||
$Length = StrLen( $Command ); |
||||
|
||||
return $Length === FWrite( $this->Socket->Socket, $Command, $Length ); |
||||
} |
||||
|
||||
/** |
||||
* @param int $Length |
||||
* @throws AuthenticationException |
||||
* @return bool |
||||
*/ |
||||
public function Read( $Length = 1400 ) |
||||
{ |
||||
// GoldSource RCON has same structure as Query |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
if( $Buffer->GetByte( ) !== SourceQuery::S2A_RCON ) |
||||
{ |
||||
throw new InvalidPacketException( 'Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
|
||||
$Buffer = $Buffer->Get( ); |
||||
$Trimmed = Trim( $Buffer ); |
||||
|
||||
if( $Trimmed === 'Bad rcon_password.' ) |
||||
{ |
||||
throw new AuthenticationException( $Trimmed, AuthenticationException::BAD_PASSWORD ); |
||||
} |
||||
else if( $Trimmed === 'You have been banned from this server.' ) |
||||
{ |
||||
throw new AuthenticationException( $Trimmed, AuthenticationException::BANNED ); |
||||
} |
||||
|
||||
$ReadMore = false; |
||||
|
||||
// There is no indentifier of the end, so we just need to continue reading |
||||
// TODO: Needs to be looked again, it causes timeouts |
||||
do |
||||
{ |
||||
$this->Socket->Read( ); |
||||
|
||||
$ReadMore = $Buffer->Remaining( ) > 0 && $Buffer->GetByte( ) === SourceQuery::S2A_RCON; |
||||
|
||||
if( $ReadMore ) |
||||
{ |
||||
$Packet = $Buffer->Get( ); |
||||
$Buffer .= SubStr( $Packet, 0, -2 ); |
||||
|
||||
// Let's assume if this packet is not long enough, there are no more after this one |
||||
$ReadMore = StrLen( $Packet ) > 1000; // use 1300? |
||||
} |
||||
} |
||||
while( $ReadMore ); |
||||
|
||||
$Buffer->Set( Trim( $Buffer ) ); |
||||
} |
||||
|
||||
public function Command( $Command ) |
||||
{ |
||||
if( !$this->RconChallenge ) |
||||
{ |
||||
throw new AuthenticationException( 'Tried to execute a RCON command before successful authorization.', AuthenticationException::BAD_PASSWORD ); |
||||
} |
||||
|
||||
$this->Write( 0, 'rcon ' . $this->RconChallenge . ' "' . $this->RconPassword . '" ' . $Command . "\0" ); |
||||
$Buffer = $this->Read( ); |
||||
|
||||
return $Buffer->Get( ); |
||||
} |
||||
|
||||
public function Authorize( $Password ) |
||||
{ |
||||
$this->RconPassword = $Password; |
||||
|
||||
$this->Write( 0, 'challenge rcon' ); |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
if( $Buffer->Get( 14 ) !== 'challenge rcon' ) |
||||
{ |
||||
throw new AuthenticationException( 'Failed to get RCON challenge.', AuthenticationException::BAD_PASSWORD ); |
||||
} |
||||
|
||||
$this->RconChallenge = Trim( $Buffer->Get( ) ); |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\InvalidPacketException; |
||||
use xPaw\SourceQuery\Exception\SocketException; |
||||
|
||||
/** |
||||
* Class Socket |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\InvalidPacketException |
||||
* @uses xPaw\SourceQuery\Exception\SocketException |
||||
*/ |
||||
class Socket extends BaseSocket |
||||
{ |
||||
public function Close( ) |
||||
{ |
||||
if( $this->Socket ) |
||||
{ |
||||
FClose( $this->Socket ); |
||||
|
||||
$this->Socket = null; |
||||
} |
||||
} |
||||
|
||||
public function Open( $Address, $Port, $Timeout, $Engine ) |
||||
{ |
||||
$this->Timeout = $Timeout; |
||||
$this->Engine = $Engine; |
||||
$this->Port = $Port; |
||||
$this->Address = $Address; |
||||
|
||||
$this->Socket = @FSockOpen( 'udp://' . $Address, $Port, $ErrNo, $ErrStr, $Timeout ); |
||||
|
||||
if( $ErrNo || $this->Socket === false ) |
||||
{ |
||||
throw new SocketException( 'Could not create socket: ' . $ErrStr, SocketException::COULD_NOT_CREATE_SOCKET ); |
||||
} |
||||
|
||||
Stream_Set_Timeout( $this->Socket, $Timeout ); |
||||
Stream_Set_Blocking( $this->Socket, true ); |
||||
} |
||||
|
||||
public function Write( $Header, $String = '' ) |
||||
{ |
||||
$Command = Pack( 'ccccca*', 0xFF, 0xFF, 0xFF, 0xFF, $Header, $String ); |
||||
$Length = StrLen( $Command ); |
||||
|
||||
return $Length === FWrite( $this->Socket, $Command, $Length ); |
||||
} |
||||
|
||||
/** |
||||
* Reads from socket and returns Buffer. |
||||
* |
||||
* @throws InvalidPacketException |
||||
* |
||||
* @return Buffer Buffer |
||||
*/ |
||||
public function Read( $Length = 1400 ) |
||||
{ |
||||
$Buffer = new Buffer( ); |
||||
$Buffer->Set( FRead( $this->Socket, $Length ) ); |
||||
|
||||
$this->ReadInternal( $Buffer, $Length, [ $this, 'Sherlock' ] ); |
||||
|
||||
return $Buffer; |
||||
} |
||||
|
||||
public function Sherlock( $Buffer, $Length ) |
||||
{ |
||||
$Data = FRead( $this->Socket, $Length ); |
||||
|
||||
if( StrLen( $Data ) < 4 ) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
$Buffer->Set( $Data ); |
||||
|
||||
return $Buffer->GetLong( ) === -2; |
||||
} |
||||
} |
@ -0,0 +1,559 @@ |
||||
<?php |
||||
/** |
||||
* This class provides the public interface to the PHP-Source-Query library. |
||||
* |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\InvalidArgumentException; |
||||
use xPaw\SourceQuery\Exception\InvalidPacketException; |
||||
use xPaw\SourceQuery\Exception\SocketException; |
||||
|
||||
/** |
||||
* Class SourceQuery |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\InvalidArgumentException |
||||
* @uses xPaw\SourceQuery\Exception\InvalidPacketException |
||||
* @uses xPaw\SourceQuery\Exception\SocketException |
||||
*/ |
||||
class SourceQuery |
||||
{ |
||||
/** |
||||
* Engines |
||||
*/ |
||||
const GOLDSOURCE = 0; |
||||
const SOURCE = 1; |
||||
|
||||
/** |
||||
* Packets sent |
||||
*/ |
||||
const A2S_PING = 0x69; |
||||
const A2S_INFO = 0x54; |
||||
const A2S_PLAYER = 0x55; |
||||
const A2S_RULES = 0x56; |
||||
const A2S_SERVERQUERY_GETCHALLENGE = 0x57; |
||||
|
||||
/** |
||||
* Packets received |
||||
*/ |
||||
const S2A_PING = 0x6A; |
||||
const S2A_CHALLENGE = 0x41; |
||||
const S2A_INFO = 0x49; |
||||
const S2A_INFO_OLD = 0x6D; // Old GoldSource, HLTV uses it |
||||
const S2A_PLAYER = 0x44; |
||||
const S2A_RULES = 0x45; |
||||
const S2A_RCON = 0x6C; |
||||
|
||||
/** |
||||
* Source rcon sent |
||||
*/ |
||||
const SERVERDATA_EXECCOMMAND = 2; |
||||
const SERVERDATA_AUTH = 3; |
||||
|
||||
/** |
||||
* Source rcon received |
||||
*/ |
||||
const SERVERDATA_RESPONSE_VALUE = 0; |
||||
const SERVERDATA_AUTH_RESPONSE = 2; |
||||
|
||||
/** |
||||
* Points to rcon class |
||||
* |
||||
* @var SourceRcon |
||||
*/ |
||||
private $Rcon; |
||||
|
||||
/** |
||||
* Points to socket class |
||||
* |
||||
* @var Socket |
||||
*/ |
||||
private $Socket; |
||||
|
||||
/** |
||||
* True if connection is open, false if not |
||||
* |
||||
* @var bool |
||||
*/ |
||||
private $Connected; |
||||
|
||||
/** |
||||
* Contains challenge |
||||
* |
||||
* @var string |
||||
*/ |
||||
private $Challenge; |
||||
|
||||
/** |
||||
* Use old method for getting challenge number |
||||
* |
||||
* @var bool |
||||
*/ |
||||
private $UseOldGetChallengeMethod; |
||||
|
||||
public function __construct( BaseSocket $Socket = null ) |
||||
{ |
||||
$this->Socket = $Socket ?: new Socket( ); |
||||
} |
||||
|
||||
public function __destruct( ) |
||||
{ |
||||
$this->Disconnect( ); |
||||
} |
||||
|
||||
/** |
||||
* Opens connection to server |
||||
* |
||||
* @param string $Address Server ip |
||||
* @param int $Port Server port |
||||
* @param int $Timeout Timeout period |
||||
* @param int $Engine Engine the server runs on (goldsource, source) |
||||
* |
||||
* @throws InvalidArgumentException |
||||
* @throws SocketException |
||||
*/ |
||||
public function Connect( $Address, $Port, $Timeout = 3, $Engine = self::SOURCE ) |
||||
{ |
||||
$this->Disconnect( ); |
||||
|
||||
if( !is_int( $Timeout ) || $Timeout < 0 ) |
||||
{ |
||||
throw new InvalidArgumentException( 'Timeout must be an integer.', InvalidArgumentException::TIMEOUT_NOT_INTEGER ); |
||||
} |
||||
|
||||
$this->Socket->Open( $Address, (int)$Port, $Timeout, (int)$Engine ); |
||||
|
||||
$this->Connected = true; |
||||
} |
||||
|
||||
/** |
||||
* Forces GetChallenge to use old method for challenge retrieval because some games use outdated protocol (e.g Starbound) |
||||
* |
||||
* @param bool $Value Set to true to force old method |
||||
* |
||||
* @returns bool Previous value |
||||
*/ |
||||
public function SetUseOldGetChallengeMethod( $Value ) |
||||
{ |
||||
$Previous = $this->UseOldGetChallengeMethod; |
||||
|
||||
$this->UseOldGetChallengeMethod = $Value === true; |
||||
|
||||
return $Previous; |
||||
} |
||||
|
||||
/** |
||||
* Closes all open connections |
||||
*/ |
||||
public function Disconnect( ) |
||||
{ |
||||
$this->Connected = false; |
||||
$this->Challenge = 0; |
||||
|
||||
$this->Socket->Close( ); |
||||
|
||||
if( $this->Rcon ) |
||||
{ |
||||
$this->Rcon->Close( ); |
||||
|
||||
$this->Rcon = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sends ping packet to the server |
||||
* NOTE: This may not work on some games (TF2 for example) |
||||
* |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
* |
||||
* @return bool True on success, false on failure |
||||
*/ |
||||
public function Ping( ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
$this->Socket->Write( self::A2S_PING ); |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
return $Buffer->GetByte( ) === self::S2A_PING; |
||||
} |
||||
|
||||
/** |
||||
* Get server information |
||||
* |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
* |
||||
* @return array Returns an array with information on success |
||||
*/ |
||||
public function GetInfo( ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
$this->Socket->Write( self::A2S_INFO, "Source Engine Query\0" ); |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
$Type = $Buffer->GetByte( ); |
||||
|
||||
// Old GoldSource protocol, HLTV still uses it |
||||
if( $Type === self::S2A_INFO_OLD && $this->Socket->Engine === self::GOLDSOURCE ) |
||||
{ |
||||
/** |
||||
* If we try to read data again, and we get the result with type S2A_INFO (0x49) |
||||
* That means this server is running dproto, |
||||
* Because it sends answer for both protocols |
||||
*/ |
||||
|
||||
$Server[ 'Address' ] = $Buffer->GetString( ); |
||||
$Server[ 'HostName' ] = $Buffer->GetString( ); |
||||
$Server[ 'Map' ] = $Buffer->GetString( ); |
||||
$Server[ 'ModDir' ] = $Buffer->GetString( ); |
||||
$Server[ 'ModDesc' ] = $Buffer->GetString( ); |
||||
$Server[ 'Players' ] = $Buffer->GetByte( ); |
||||
$Server[ 'MaxPlayers' ] = $Buffer->GetByte( ); |
||||
$Server[ 'Protocol' ] = $Buffer->GetByte( ); |
||||
$Server[ 'Dedicated' ] = Chr( $Buffer->GetByte( ) ); |
||||
$Server[ 'Os' ] = Chr( $Buffer->GetByte( ) ); |
||||
$Server[ 'Password' ] = $Buffer->GetByte( ) === 1; |
||||
$Server[ 'IsMod' ] = $Buffer->GetByte( ) === 1; |
||||
|
||||
if( $Server[ 'IsMod' ] ) |
||||
{ |
||||
$Mod[ 'Url' ] = $Buffer->GetString( ); |
||||
$Mod[ 'Download' ] = $Buffer->GetString( ); |
||||
$Buffer->Get( 1 ); // NULL byte |
||||
$Mod[ 'Version' ] = $Buffer->GetLong( ); |
||||
$Mod[ 'Size' ] = $Buffer->GetLong( ); |
||||
$Mod[ 'ServerSide' ] = $Buffer->GetByte( ) === 1; |
||||
$Mod[ 'CustomDLL' ] = $Buffer->GetByte( ) === 1; |
||||
} |
||||
|
||||
$Server[ 'Secure' ] = $Buffer->GetByte( ) === 1; |
||||
$Server[ 'Bots' ] = $Buffer->GetByte( ); |
||||
|
||||
if( isset( $Mod ) ) |
||||
{ |
||||
$Server[ 'Mod' ] = $Mod; |
||||
} |
||||
|
||||
return $Server; |
||||
} |
||||
|
||||
if( $Type !== self::S2A_INFO ) |
||||
{ |
||||
throw new InvalidPacketException( 'GetInfo: Packet header mismatch. (0x' . DecHex( $Type ) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
|
||||
$Server[ 'Protocol' ] = $Buffer->GetByte( ); |
||||
$Server[ 'HostName' ] = $Buffer->GetString( ); |
||||
$Server[ 'Map' ] = $Buffer->GetString( ); |
||||
$Server[ 'ModDir' ] = $Buffer->GetString( ); |
||||
$Server[ 'ModDesc' ] = $Buffer->GetString( ); |
||||
$Server[ 'AppID' ] = $Buffer->GetShort( ); |
||||
$Server[ 'Players' ] = $Buffer->GetByte( ); |
||||
$Server[ 'MaxPlayers' ] = $Buffer->GetByte( ); |
||||
$Server[ 'Bots' ] = $Buffer->GetByte( ); |
||||
$Server[ 'Dedicated' ] = Chr( $Buffer->GetByte( ) ); |
||||
$Server[ 'Os' ] = Chr( $Buffer->GetByte( ) ); |
||||
$Server[ 'Password' ] = $Buffer->GetByte( ) === 1; |
||||
$Server[ 'Secure' ] = $Buffer->GetByte( ) === 1; |
||||
|
||||
// The Ship (they violate query protocol spec by modifying the response) |
||||
if( $Server[ 'AppID' ] === 2400 ) |
||||
{ |
||||
$Server[ 'GameMode' ] = $Buffer->GetByte( ); |
||||
$Server[ 'WitnessCount' ] = $Buffer->GetByte( ); |
||||
$Server[ 'WitnessTime' ] = $Buffer->GetByte( ); |
||||
} |
||||
|
||||
$Server[ 'Version' ] = $Buffer->GetString( ); |
||||
|
||||
// Extra Data Flags |
||||
if( $Buffer->Remaining( ) > 0 ) |
||||
{ |
||||
$Server[ 'ExtraDataFlags' ] = $Flags = $Buffer->GetByte( ); |
||||
|
||||
// The server's game port |
||||
if( $Flags & 0x80 ) |
||||
{ |
||||
$Server[ 'GamePort' ] = $Buffer->GetShort( ); |
||||
} |
||||
|
||||
// The server's steamid |
||||
// Want to play around with this? |
||||
// You can use https://github.com/xPaw/SteamID.php |
||||
if( $Flags & 0x10 ) |
||||
{ |
||||
$SteamIDLower = $Buffer->GetUnsignedLong( ); |
||||
$SteamIDInstance = $Buffer->GetUnsignedLong( ); // This gets shifted by 32 bits, which should be steamid instance |
||||
$SteamID = 0; |
||||
|
||||
if( PHP_INT_SIZE === 4 ) |
||||
{ |
||||
if( extension_loaded( 'gmp' ) ) |
||||
{ |
||||
$SteamIDLower = gmp_abs( $SteamIDLower ); |
||||
$SteamIDInstance = gmp_abs( $SteamIDInstance ); |
||||
$SteamID = gmp_strval( gmp_or( $SteamIDLower, gmp_mul( $SteamIDInstance, gmp_pow( 2, 32 ) ) ) ); |
||||
} |
||||
else |
||||
{ |
||||
throw new \RuntimeException( 'Either 64-bit PHP installation or "gmp" module is required to correctly parse server\'s steamid.' ); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
$SteamID = $SteamIDLower | ( $SteamIDInstance << 32 ); |
||||
} |
||||
|
||||
$Server[ 'SteamID' ] = $SteamID; |
||||
|
||||
unset( $SteamIDLower, $SteamIDInstance, $SteamID ); |
||||
} |
||||
|
||||
// The spectator port and then the spectator server name |
||||
if( $Flags & 0x40 ) |
||||
{ |
||||
$Server[ 'SpecPort' ] = $Buffer->GetShort( ); |
||||
$Server[ 'SpecName' ] = $Buffer->GetString( ); |
||||
} |
||||
|
||||
// The game tag data string for the server |
||||
if( $Flags & 0x20 ) |
||||
{ |
||||
$Server[ 'GameTags' ] = $Buffer->GetString( ); |
||||
} |
||||
|
||||
// GameID -- alternative to AppID? |
||||
if( $Flags & 0x01 ) |
||||
{ |
||||
$Server[ 'GameID' ] = $Buffer->GetUnsignedLong( ) | ( $Buffer->GetUnsignedLong( ) << 32 ); |
||||
} |
||||
|
||||
if( $Buffer->Remaining( ) > 0 ) |
||||
{ |
||||
throw new InvalidPacketException( 'GetInfo: unread data? ' . $Buffer->Remaining( ) . ' bytes remaining in the buffer. Please report it to the library developer.', |
||||
InvalidPacketException::BUFFER_NOT_EMPTY ); |
||||
} |
||||
} |
||||
|
||||
return $Server; |
||||
} |
||||
|
||||
/** |
||||
* Get players on the server |
||||
* |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
* |
||||
* @return array Returns an array with players on success |
||||
*/ |
||||
public function GetPlayers( ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
$this->GetChallenge( self::A2S_PLAYER, self::S2A_PLAYER ); |
||||
|
||||
$this->Socket->Write( self::A2S_PLAYER, $this->Challenge ); |
||||
$Buffer = $this->Socket->Read( 14000 ); // Moronic Arma 3 developers do not split their packets, so we have to read more data |
||||
// This violates the protocol spec, and they probably should fix it: https://developer.valvesoftware.com/wiki/Server_queries#Protocol |
||||
|
||||
$Type = $Buffer->GetByte( ); |
||||
|
||||
if( $Type !== self::S2A_PLAYER ) |
||||
{ |
||||
throw new InvalidPacketException( 'GetPlayers: Packet header mismatch. (0x' . DecHex( $Type ) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
|
||||
$Players = []; |
||||
$Count = $Buffer->GetByte( ); |
||||
|
||||
while( $Count-- > 0 && $Buffer->Remaining( ) > 0 ) |
||||
{ |
||||
$Player[ 'Id' ] = $Buffer->GetByte( ); // PlayerID, is it just always 0? |
||||
$Player[ 'Name' ] = $Buffer->GetString( ); |
||||
$Player[ 'Frags' ] = $Buffer->GetLong( ); |
||||
$Player[ 'Time' ] = (int)$Buffer->GetFloat( ); |
||||
$Player[ 'TimeF' ] = GMDate( ( $Player[ 'Time' ] > 3600 ? "H:i:s" : "i:s" ), $Player[ 'Time' ] ); |
||||
|
||||
$Players[ ] = $Player; |
||||
} |
||||
|
||||
return $Players; |
||||
} |
||||
|
||||
/** |
||||
* Get rules (cvars) from the server |
||||
* |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
* |
||||
* @return array Returns an array with rules on success |
||||
*/ |
||||
public function GetRules( ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
$this->GetChallenge( self::A2S_RULES, self::S2A_RULES ); |
||||
|
||||
$this->Socket->Write( self::A2S_RULES, $this->Challenge ); |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
$Type = $Buffer->GetByte( ); |
||||
|
||||
if( $Type !== self::S2A_RULES ) |
||||
{ |
||||
throw new InvalidPacketException( 'GetRules: Packet header mismatch. (0x' . DecHex( $Type ) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
|
||||
$Rules = []; |
||||
$Count = $Buffer->GetShort( ); |
||||
|
||||
while( $Count-- > 0 && $Buffer->Remaining( ) > 0 ) |
||||
{ |
||||
$Rule = $Buffer->GetString( ); |
||||
$Value = $Buffer->GetString( ); |
||||
|
||||
if( !Empty( $Rule ) ) |
||||
{ |
||||
$Rules[ $Rule ] = $Value; |
||||
} |
||||
} |
||||
|
||||
return $Rules; |
||||
} |
||||
|
||||
/** |
||||
* Get challenge (used for players/rules packets) |
||||
* |
||||
* @param $Header |
||||
* @param $ExpectedResult |
||||
* |
||||
* @throws InvalidPacketException |
||||
*/ |
||||
private function GetChallenge( $Header, $ExpectedResult ) |
||||
{ |
||||
if( $this->Challenge ) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
if( $this->UseOldGetChallengeMethod ) |
||||
{ |
||||
$Header = self::A2S_SERVERQUERY_GETCHALLENGE; |
||||
} |
||||
|
||||
$this->Socket->Write( $Header, "\xFF\xFF\xFF\xFF" ); |
||||
$Buffer = $this->Socket->Read( ); |
||||
|
||||
$Type = $Buffer->GetByte( ); |
||||
|
||||
switch( $Type ) |
||||
{ |
||||
case self::S2A_CHALLENGE: |
||||
{ |
||||
$this->Challenge = $Buffer->Get( 4 ); |
||||
|
||||
return; |
||||
} |
||||
case $ExpectedResult: |
||||
{ |
||||
// Goldsource (HLTV) |
||||
|
||||
return; |
||||
} |
||||
case 0: |
||||
{ |
||||
throw new InvalidPacketException( 'GetChallenge: Failed to get challenge.' ); |
||||
} |
||||
default: |
||||
{ |
||||
throw new InvalidPacketException( 'GetChallenge: Packet header mismatch. (0x' . DecHex( $Type ) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH ); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets rcon password, for future use in Rcon() |
||||
* |
||||
* @param string $Password Rcon Password |
||||
* |
||||
* @throws AuthenticationException |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
*/ |
||||
public function SetRconPassword( $Password ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
switch( $this->Socket->Engine ) |
||||
{ |
||||
case SourceQuery::GOLDSOURCE: |
||||
{ |
||||
$this->Rcon = new GoldSourceRcon( $this->Socket ); |
||||
|
||||
break; |
||||
} |
||||
case SourceQuery::SOURCE: |
||||
{ |
||||
$this->Rcon = new SourceRcon( $this->Socket ); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
$this->Rcon->Open( ); |
||||
$this->Rcon->Authorize( $Password ); |
||||
} |
||||
|
||||
/** |
||||
* Sends a command to the server for execution. |
||||
* |
||||
* @param string $Command Command to execute |
||||
* |
||||
* @throws AuthenticationException |
||||
* @throws InvalidPacketException |
||||
* @throws SocketException |
||||
* |
||||
* @return string Answer from server in string |
||||
*/ |
||||
public function Rcon( $Command ) |
||||
{ |
||||
if( !$this->Connected ) |
||||
{ |
||||
throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
if( $this->Rcon === null ) |
||||
{ |
||||
throw new SocketException( 'You must set a RCON password before trying to execute a RCON command.', SocketException::NOT_CONNECTED ); |
||||
} |
||||
|
||||
return $this->Rcon->Command( $Command ); |
||||
} |
||||
} |
@ -0,0 +1,201 @@ |
||||
<?php |
||||
/** |
||||
* @author Pavel Djundik <sourcequery@xpaw.me> |
||||
* |
||||
* @link https://xpaw.me |
||||
* @link https://github.com/xPaw/PHP-Source-Query |
||||
* |
||||
* @license GNU Lesser General Public License, version 2.1 |
||||
* |
||||
* @internal |
||||
*/ |
||||
|
||||
namespace xPaw\SourceQuery; |
||||
|
||||
use xPaw\SourceQuery\Exception\AuthenticationException; |
||||
use xPaw\SourceQuery\Exception\InvalidPacketException; |
||||
use xPaw\SourceQuery\Exception\SocketException; |
||||
|
||||
/** |
||||
* Class SourceRcon |
||||
* |
||||
* @package xPaw\SourceQuery |
||||
* |
||||
* @uses xPaw\SourceQuery\Exception\AuthenticationException |
||||
* @uses xPaw\SourceQuery\Exception\InvalidPacketException |
||||
* @uses xPaw\SourceQuery\Exception\SocketException |
||||
*/ |
||||
class SourceRcon |
||||
{ |
||||
/** |
||||
* Points to socket class |
||||
* |
||||
* @var Socket |
||||
*/ |
||||
private $Socket; |
||||
|
||||
private $RconSocket; |
||||
private $RconRequestId; |
||||
|
||||
public function __construct( $Socket ) |
||||
{ |
||||
$this->Socket = $Socket; |
||||
} |
||||
|
||||
public function Close( ) |
||||
{ |
||||
if( $this->RconSocket ) |
||||
{ |
||||
FClose( $this->RconSocket ); |
||||
|
||||
$this->RconSocket = null; |
||||
} |
||||
|
||||
$this->RconRequestId = 0; |
||||
} |
||||
|
||||
public function Open( ) |
||||
{ |
||||
if( !$this->RconSocket ) |
||||
{ |
||||
$this->RconSocket = @FSockOpen( $this->Socket->Address, $this->Socket->Port, $ErrNo, $ErrStr, $this->Socket->Timeout ); |
||||
|
||||
if( $ErrNo || !$this->RconSocket ) |
||||
{ |
||||
throw new SocketException( 'Can\'t connect to RCON server: ' . $ErrStr, SocketException::CONNECTION_FAILED ); |
||||
} |
||||
|