diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 30e6d46..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,8 +0,0 @@
-# http://editorconfig.org
-
-root = true
-
-[*]
-charset = utf-8
-indent_style = tab
-insert_final_newline = true
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 176a458..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-* text=auto
diff --git a/.gitignore b/.gitignore
index d1502b0..b26a347 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
vendor/
composer.lock
+.php-cs-fixer.cache
+.idea/
diff --git a/Examples/Example.php b/Examples/Example.php
index caa7efb..a8bc770 100644
--- a/Examples/Example.php
+++ b/Examples/Example.php
@@ -1,34 +1,32 @@
- define( 'SQ_SERVER_ADDR', 'localhost' );
- define( 'SQ_SERVER_PORT', 27015 );
- define( 'SQ_TIMEOUT', 1 );
- define( 'SQ_ENGINE', SourceQuery::SOURCE );
- // Edit this <-
-
- $Query = new SourceQuery( );
-
- try
- {
- $Query->Connect( SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE );
-
- print_r( $Query->GetInfo( ) );
- print_r( $Query->GetPlayers( ) );
- print_r( $Query->GetRules( ) );
- }
- catch( Exception $e )
- {
- echo $e->getMessage( );
- }
- finally
- {
- $Query->Disconnect( );
- }
+
+declare(strict_types=1);
+
+require __DIR__ . '/../SourceQuery/bootstrap.php';
+
+use xPaw\SourceQuery\SourceQuery;
+
+// For the sake of this example
+header('Content-Type: text/plain');
+header('X-Content-Type-Options: nosniff');
+
+// Edit this ->
+define('SQ_SERVER_ADDR', 'localhost');
+define('SQ_SERVER_PORT', 27015);
+define('SQ_TIMEOUT', 1);
+define('SQ_ENGINE', SourceQuery::SOURCE);
+// Edit this <-
+
+$Query = new SourceQuery();
+
+try {
+ $Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
+
+ print_r($Query->GetInfo());
+ print_r($Query->GetPlayers());
+ print_r($Query->GetRules());
+} catch (Exception $e) {
+ echo $e->getMessage();
+} finally {
+ $Query->Disconnect();
+}
diff --git a/Examples/RconExample.php b/Examples/RconExample.php
index 5888415..d1e22de 100644
--- a/Examples/RconExample.php
+++ b/Examples/RconExample.php
@@ -1,34 +1,32 @@
- define( 'SQ_SERVER_ADDR', 'localhost' );
- define( 'SQ_SERVER_PORT', 27015 );
- define( 'SQ_TIMEOUT', 1 );
- define( 'SQ_ENGINE', SourceQuery::SOURCE );
- // Edit this <-
-
- $Query = new SourceQuery( );
-
- try
- {
- $Query->Connect( SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE );
-
- $Query->SetRconPassword( 'my_awesome_password' );
-
- var_dump( $Query->Rcon( 'say hello' ) );
- }
- catch( Exception $e )
- {
- echo $e->getMessage( );
- }
- finally
- {
- $Query->Disconnect( );
- }
+
+declare(strict_types=1);
+
+require __DIR__ . '/../SourceQuery/bootstrap.php';
+
+use xPaw\SourceQuery\SourceQuery;
+
+// For the sake of this example
+header('Content-Type: text/plain');
+header('X-Content-Type-Options: nosniff');
+
+// Edit this ->
+define('SQ_SERVER_ADDR', 'localhost');
+define('SQ_SERVER_PORT', 27015);
+define('SQ_TIMEOUT', 1);
+define('SQ_ENGINE', SourceQuery::SOURCE);
+// Edit this <-
+
+$Query = new SourceQuery();
+
+try {
+ $Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
+
+ $Query->SetRconPassword('my_awesome_password');
+
+ var_dump($Query->Rcon('say hello'));
+} catch (Exception $e) {
+ echo $e->getMessage();
+} finally {
+ $Query->Disconnect();
+}
diff --git a/Examples/View.php b/Examples/View.php
index 11cb15e..fb21e20 100644
--- a/Examples/View.php
+++ b/Examples/View.php
@@ -1,43 +1,42 @@
- define( 'SQ_SERVER_ADDR', 'localhost' );
- define( 'SQ_SERVER_PORT', 27015 );
- define( 'SQ_TIMEOUT', 3 );
- define( 'SQ_ENGINE', SourceQuery::SOURCE );
- // Edit this <-
-
- $Timer = microtime( true );
-
- $Query = new SourceQuery( );
-
- $Info = [];
- $Rules = [];
- $Players = [];
- $Exception = null;
-
- try
- {
- $Query->Connect( SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE );
- //$Query->SetUseOldGetChallengeMethod( true ); // Use this when players/rules retrieval fails on games like Starbound
-
- $Info = $Query->GetInfo( );
- $Players = $Query->GetPlayers( );
- $Rules = $Query->GetRules( );
- }
- catch( Exception $e )
- {
- $Exception = $e;
- }
- finally
- {
- $Query->Disconnect( );
- }
-
- $Timer = number_format( microtime( true ) - $Timer, 4, '.', '' );
+declare(strict_types=1);
+
+require __DIR__ . '/../SourceQuery/bootstrap.php';
+
+use xPaw\SourceQuery\SourceQuery;
+
+// Edit this ->
+define('SQ_SERVER_ADDR', 'localhost');
+define('SQ_SERVER_PORT', 27015);
+define('SQ_TIMEOUT', 3);
+define('SQ_ENGINE', SourceQuery::SOURCE);
+// Edit this <-
+
+$Timer = microtime(true);
+
+$Query = new SourceQuery();
+
+$Info = [];
+$Rules = [];
+$Players = [];
+$Exception = null;
+
+try {
+ $Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
+ //$Query->SetUseOldGetChallengeMethod( true ); // Use this when players/rules retrieval fails on games like Starbound
+
+ $Info = $Query->GetInfo();
+ $Players = $Query->GetPlayers();
+ $Rules = $Query->GetRules();
+} catch (Exception $e) {
+ $Exception = $e;
+} finally {
+ $Query->Disconnect();
+}
+
+$Timer = number_format(microtime(true) - $Timer, 4, '.', '');
+
?>
@@ -88,9 +87,9 @@
-
+
-
__toString( ) ); ?>
+
__toString()); ?>
@@ -103,32 +102,24 @@
-
- $InfoValue ): ?>
+
+ $InfoValue): ?>
- |
+ |
";
- print_r( $InfoValue );
- echo "";
- }
- else
- {
- if( $InfoValue === true )
- {
- echo 'true';
- }
- else if( $InfoValue === false )
- {
- echo 'false';
- }
- else
- {
- echo htmlspecialchars( $InfoValue );
- }
- }
+ if (is_array($InfoValue)) {
+ echo "";
+ print_r($InfoValue);
+ echo " ";
+ } else {
+ if ($InfoValue === true) {
+ echo 'true';
+ } elseif ($InfoValue === false) {
+ echo 'false';
+ } else {
+ echo htmlspecialchars($InfoValue);
+ }
+ }
?> |
@@ -144,16 +135,16 @@
- Player |
+ Player |
Frags |
Time |
-
-
+
+
- |
+ |
|
|
@@ -172,15 +163,15 @@
- Rules |
+ Rules |
-
- $Value ): ?>
+
+ $Value): ?>
- |
- |
+ |
+ |
diff --git a/SourceQuery/BaseSocket.php b/SourceQuery/BaseSocket.php
index 1ce405e..5909f48 100644
--- a/SourceQuery/BaseSocket.php
+++ b/SourceQuery/BaseSocket.php
@@ -1,139 +1,127 @@
Close( );
- }
-
- abstract public function Close( ) : void;
- abstract public function Open( string $Address, int $Port, int $Timeout, int $Engine ) : void;
- abstract public function Write( int $Header, string $String = '' ) : bool;
- abstract public function Read( int $Length = 1400 ) : Buffer;
-
- protected function ReadInternal( Buffer $Buffer, int $Length, callable $SherlockFunction ) : Buffer
- {
- 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;
- $PacketChecksum = null;
-
- 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;
- }
- default:
- {
- throw new SocketException( 'Unknown engine.', SocketException::INVALID_ENGINE );
- }
- }
-
- $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( !is_string( $Data ) || 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;
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * @author Pavel Djundik
+ *
+ * @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;
+
+/**
+ * Base socket interface
+ *
+ * @package xPaw\SourceQuery
+ *
+ * @uses xPaw\SourceQuery\Exception\InvalidPacketException
+ * @uses xPaw\SourceQuery\Exception\SocketException
+ */
+abstract class BaseSocket
+{
+ /** @var ?resource */
+ public $Socket;
+ public int $Engine;
+
+ public string $Address;
+ public int $Port;
+ public int $Timeout;
+
+ public function __destruct()
+ {
+ $this->Close();
+ }
+
+ abstract public function Close(): void;
+ abstract public function Open(string $Address, int $Port, int $Timeout, int $Engine): void;
+ abstract public function Write(int $Header, string $String = ''): bool;
+ abstract public function Read(int $Length = 1400): Buffer;
+
+ protected function ReadInternal(Buffer $Buffer, int $Length, callable $SherlockFunction): Buffer
+ {
+ 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
+ } elseif ($Header === -2) { // Split packet
+ $Packets = [];
+ $IsCompressed = false;
+ $ReadMore = false;
+ $PacketChecksum = null;
+
+ 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;
+ }
+ default:
+ {
+ throw new SocketException('Unknown engine.', SocketException::INVALID_ENGINE);
+ }
+ }
+
+ $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 (!is_string($Data) || 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;
+ }
+}
diff --git a/SourceQuery/Buffer.php b/SourceQuery/Buffer.php
index 2452c99..1896261 100644
--- a/SourceQuery/Buffer.php
+++ b/SourceQuery/Buffer.php
@@ -1,177 +1,171 @@
Buffer = $Buffer;
- $this->Length = strlen( $Buffer );
- $this->Position = 0;
- }
-
- /**
- * Get remaining bytes
- *
- * @return int Remaining bytes in buffer
- */
- public function Remaining( ) : int
- {
- return $this->Length - $this->Position;
- }
-
- /**
- * Gets data from buffer
- *
- * @param int $Length Bytes to read
- */
- public function Get( int $Length = -1 ) : string
- {
- 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
- */
- public function GetByte( ) : int
- {
- return ord( $this->Get( 1 ) );
- }
-
- /**
- * Get short from buffer
- */
- public function GetShort( ) : int
- {
- if( $this->Remaining( ) < 2 )
- {
- throw new InvalidPacketException( 'Not enough data to unpack a short.', InvalidPacketException::BUFFER_EMPTY );
- }
-
- $Data = unpack( 'v', $this->Get( 2 ) );
-
- return (int)$Data[ 1 ];
- }
-
- /**
- * Get long from buffer
- */
- public function GetLong( ) : int
- {
- if( $this->Remaining( ) < 4 )
- {
- throw new InvalidPacketException( 'Not enough data to unpack a long.', InvalidPacketException::BUFFER_EMPTY );
- }
-
- $Data = unpack( 'l', $this->Get( 4 ) );
-
- return (int)$Data[ 1 ];
- }
-
- /**
- * Get float from buffer
- */
- public function GetFloat( ) : float
- {
- if( $this->Remaining( ) < 4 )
- {
- throw new InvalidPacketException( 'Not enough data to unpack a float.', InvalidPacketException::BUFFER_EMPTY );
- }
-
- $Data = unpack( 'f', $this->Get( 4 ) );
-
- return (float)$Data[ 1 ];
- }
-
- /**
- * Get unsigned long from buffer
- */
- public function GetUnsignedLong( ) : int
- {
- 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 (int)$Data[ 1 ];
- }
-
- /**
- * Read one string from buffer ending with null byte
- */
- public function GetString( ) : string
- {
- $ZeroBytePosition = strpos( $this->Buffer, "\0", $this->Position );
-
- if( $ZeroBytePosition === false )
- {
- return '';
- }
-
- $String = $this->Get( $ZeroBytePosition - $this->Position );
-
- $this->Position++;
-
- return $String;
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * @author Pavel Djundik
+ *
+ * @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
+ */
+final class Buffer
+{
+ /**
+ * Buffer
+ */
+ private string $Buffer = '';
+
+ /**
+ * Buffer length
+ */
+ private int $Length = 0;
+
+ /**
+ * Current position in buffer
+ */
+ private int $Position = 0;
+
+ /**
+ * Sets buffer
+ */
+ public function Set(string $Buffer): void
+ {
+ $this->Buffer = $Buffer;
+ $this->Length = strlen($Buffer);
+ $this->Position = 0;
+ }
+
+ /**
+ * Get remaining bytes
+ *
+ * @return int Remaining bytes in buffer
+ */
+ public function Remaining(): int
+ {
+ return $this->Length - $this->Position;
+ }
+
+ /**
+ * Gets data from buffer
+ *
+ * @param int $Length Bytes to read
+ */
+ public function Get(int $Length = -1): string
+ {
+ if ($Length === 0) {
+ return '';
+ }
+
+ $Remaining = $this->Remaining();
+
+ if ($Length === -1) {
+ $Length = $Remaining;
+ } elseif ($Length > $Remaining) {
+ return '';
+ }
+
+ $Data = substr($this->Buffer, $this->Position, $Length);
+
+ $this->Position += $Length;
+
+ return $Data;
+ }
+
+ /**
+ * Get byte from buffer
+ */
+ public function GetByte(): int
+ {
+ return ord($this->Get(1));
+ }
+
+ /**
+ * Get short from buffer
+ */
+ public function GetShort(): int
+ {
+ if ($this->Remaining() < 2) {
+ throw new InvalidPacketException('Not enough data to unpack a short.', InvalidPacketException::BUFFER_EMPTY);
+ }
+
+ $Data = unpack('v', $this->Get(2));
+
+ return (int)$Data[ 1 ];
+ }
+
+ /**
+ * Get long from buffer
+ */
+ public function GetLong(): int
+ {
+ if ($this->Remaining() < 4) {
+ throw new InvalidPacketException('Not enough data to unpack a long.', InvalidPacketException::BUFFER_EMPTY);
+ }
+
+ $Data = unpack('l', $this->Get(4));
+
+ return (int)$Data[ 1 ];
+ }
+
+ /**
+ * Get float from buffer
+ */
+ public function GetFloat(): float
+ {
+ if ($this->Remaining() < 4) {
+ throw new InvalidPacketException('Not enough data to unpack a float.', InvalidPacketException::BUFFER_EMPTY);
+ }
+
+ $Data = unpack('f', $this->Get(4));
+
+ return (float)$Data[ 1 ];
+ }
+
+ /**
+ * Get unsigned long from buffer
+ */
+ public function GetUnsignedLong(): int
+ {
+ 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 (int)$Data[ 1 ];
+ }
+
+ /**
+ * Read one string from buffer ending with null byte
+ */
+ public function GetString(): string
+ {
+ $ZeroBytePosition = strpos($this->Buffer, "\0", $this->Position);
+
+ if ($ZeroBytePosition === false) {
+ return '';
+ }
+
+ $String = $this->Get($ZeroBytePosition - $this->Position);
+
+ $this->Position++;
+
+ return $String;
+ }
+}
diff --git a/SourceQuery/Exception/AuthenticationException.php b/SourceQuery/Exception/AuthenticationException.php
index 3e90f6e..2dd78c9 100644
--- a/SourceQuery/Exception/AuthenticationException.php
+++ b/SourceQuery/Exception/AuthenticationException.php
@@ -1,19 +1,22 @@
Socket = $Socket;
- }
-
- public function Close( ) : void
- {
- $this->RconChallenge = '';
- $this->RconPassword = '';
- }
-
- public function Open( ) : void
- {
- //
- }
-
- public function Write( int $Header, string $String = '' ) : bool
- {
- $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 Buffer
- */
- public function Read( int $Length = 1400 ) : Buffer
- {
- // GoldSource RCON has same structure as Query
- $Buffer = $this->Socket->Read( );
-
- $StringBuffer = '';
- $ReadMore = false;
-
- // There is no indentifier of the end, so we just need to continue reading
- do
- {
- $ReadMore = $Buffer->Remaining( ) > 0;
-
- if( $ReadMore )
- {
- if( $Buffer->GetByte( ) !== SourceQuery::S2A_RCON )
- {
- throw new InvalidPacketException( 'Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH );
- }
-
- $Packet = $Buffer->Get( );
- $StringBuffer .= $Packet;
- //$StringBuffer .= 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?
-
- if( $ReadMore )
- {
- $Buffer = $this->Socket->Read( );
- }
- }
- }
- while( $ReadMore );
-
- $Trimmed = trim( $StringBuffer );
-
- 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 );
- }
-
- $Buffer->Set( $Trimmed );
-
- return $Buffer;
- }
-
- public function Command( string $Command ) : string
- {
- 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( string $Password ) : void
- {
- $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( ) );
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * @author Pavel Djundik
+ *
+ * @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;
+
+/**
+ * Class GoldSourceRcon
+ *
+ * @package xPaw\SourceQuery
+ *
+ * @uses AuthenticationException
+ * @uses InvalidPacketException
+ */
+final class GoldSourceRcon
+{
+ /**
+ * Points to socket class
+ *
+ * @var BaseSocket
+ */
+ private $Socket;
+
+ private string $RconPassword = '';
+ private string $RconChallenge = '';
+
+ public function __construct(BaseSocket $Socket)
+ {
+ $this->Socket = $Socket;
+ }
+
+ public function Close(): void
+ {
+ $this->RconChallenge = '';
+ $this->RconPassword = '';
+ }
+
+ public function Open(): void
+ {
+ //
+ }
+
+ public function Write(int $Header, string $String = ''): bool
+ {
+ $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 Buffer
+ */
+ public function Read(int $Length = 1400): Buffer
+ {
+ // GoldSource RCON has same structure as Query
+ $Buffer = $this->Socket->Read();
+
+ $StringBuffer = '';
+ $ReadMore = false;
+
+ // There is no indentifier of the end, so we just need to continue reading
+ do {
+ $ReadMore = $Buffer->Remaining() > 0;
+
+ if ($ReadMore) {
+ if ($Buffer->GetByte() !== SourceQuery::S2A_RCON) {
+ throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
+ }
+
+ $Packet = $Buffer->Get();
+ $StringBuffer .= $Packet;
+ //$StringBuffer .= 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?
+
+ if ($ReadMore) {
+ $Buffer = $this->Socket->Read();
+ }
+ }
+ } while ($ReadMore);
+
+ $Trimmed = trim($StringBuffer);
+
+ if ($Trimmed === 'Bad rcon_password.') {
+ throw new AuthenticationException($Trimmed, AuthenticationException::BAD_PASSWORD);
+ } elseif ($Trimmed === 'You have been banned from this server.') {
+ throw new AuthenticationException($Trimmed, AuthenticationException::BANNED);
+ }
+
+ $Buffer->Set($Trimmed);
+
+ return $Buffer;
+ }
+
+ public function Command(string $Command): string
+ {
+ 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(string $Password): void
+ {
+ $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());
+ }
+}
diff --git a/SourceQuery/Socket.php b/SourceQuery/Socket.php
index 726a4d1..f5d8bb5 100644
--- a/SourceQuery/Socket.php
+++ b/SourceQuery/Socket.php
@@ -1,95 +1,95 @@
Socket !== null )
- {
- fclose( $this->Socket );
-
- $this->Socket = null;
- }
- }
-
- public function Open( string $Address, int $Port, int $Timeout, int $Engine ) : void
- {
- $this->Timeout = $Timeout;
- $this->Engine = $Engine;
- $this->Port = $Port;
- $this->Address = $Address;
-
- $Socket = @fsockopen( 'udp://' . $Address, $Port, $ErrNo, $ErrStr, $Timeout );
-
- if( $ErrNo || $Socket === false )
- {
- throw new SocketException( 'Could not create socket: ' . $ErrStr, SocketException::COULD_NOT_CREATE_SOCKET );
- }
-
- $this->Socket = $Socket;
- stream_set_timeout( $this->Socket, $Timeout );
- stream_set_blocking( $this->Socket, true );
- }
-
- public function Write( int $Header, string $String = '' ) : bool
- {
- $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( int $Length = 1400 ) : Buffer
- {
- $Buffer = new Buffer( );
- $Buffer->Set( fread( $this->Socket, $Length ) );
-
- $this->ReadInternal( $Buffer, $Length, [ $this, 'Sherlock' ] );
-
- return $Buffer;
- }
-
- public function Sherlock( Buffer $Buffer, int $Length ) : bool
- {
- $Data = fread( $this->Socket, $Length );
-
- if( strlen( $Data ) < 4 )
- {
- return false;
- }
-
- $Buffer->Set( $Data );
-
- return $Buffer->GetLong( ) === -2;
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * @author Pavel Djundik
+ *
+ * @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 InvalidPacketException
+ * @uses SocketException
+ */
+final class Socket extends BaseSocket
+{
+ public function Close(): void
+ {
+ if ($this->Socket !== null) {
+ fclose($this->Socket);
+
+ $this->Socket = null;
+ }
+ }
+
+ public function Open(string $Address, int $Port, int $Timeout, int $Engine): void
+ {
+ $this->Timeout = $Timeout;
+ $this->Engine = $Engine;
+ $this->Port = $Port;
+ $this->Address = $Address;
+
+ $Socket = @fsockopen('udp://' . $Address, $Port, $ErrNo, $ErrStr, $Timeout);
+
+ if ($ErrNo || $Socket === false) {
+ throw new SocketException('Could not create socket: ' . $ErrStr, SocketException::COULD_NOT_CREATE_SOCKET);
+ }
+
+ $this->Socket = $Socket;
+ stream_set_timeout($this->Socket, $Timeout);
+ stream_set_blocking($this->Socket, true);
+ }
+
+ public function Write(int $Header, string $String = ''): bool
+ {
+ $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(int $Length = 1400): Buffer
+ {
+ $Buffer = new Buffer();
+ $Buffer->Set(fread($this->Socket, $Length));
+
+ $this->ReadInternal($Buffer, $Length, [ $this, 'Sherlock' ]);
+
+ return $Buffer;
+ }
+
+ public function Sherlock(Buffer $Buffer, int $Length): bool
+ {
+ $Data = fread($this->Socket, $Length);
+
+ if (strlen($Data) < 4) {
+ return false;
+ }
+
+ $Buffer->Set($Data);
+
+ return $Buffer->GetLong() === -2;
+ }
+}
diff --git a/SourceQuery/SourceQuery.php b/SourceQuery/SourceQuery.php
index 74f7f57..f1a895b 100644
--- a/SourceQuery/SourceQuery.php
+++ b/SourceQuery/SourceQuery.php
@@ -1,570 +1,536 @@
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( string $Address, int $Port, int $Timeout = 3, int $Engine = self::SOURCE ) : void
- {
- $this->Disconnect( );
-
- if( $Timeout < 0 )
- {
- throw new InvalidArgumentException( 'Timeout must be a positive integer.', InvalidArgumentException::TIMEOUT_NOT_INTEGER );
- }
-
- $this->Socket->Open( $Address, $Port, $Timeout, $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( bool $Value ) : bool
- {
- $Previous = $this->UseOldGetChallengeMethod;
-
- $this->UseOldGetChallengeMethod = $Value === true;
-
- return $Previous;
- }
-
- /**
- * Closes all open connections
- */
- public function Disconnect( ) : void
- {
- $this->Connected = false;
- $this->Challenge = '';
-
- $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( ) : bool
- {
- if( !$this->Connected )
- {
- throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED );
- }
-
- $this->Socket->Write( self::A2A_PING );
- $Buffer = $this->Socket->Read( );
-
- return $Buffer->GetByte( ) === self::A2A_ACK;
- }
-
- /**
- * Get server information
- *
- * @throws InvalidPacketException
- * @throws SocketException
- *
- * @return array Returns an array with information on success
- */
- public function GetInfo( ) : array
- {
- if( !$this->Connected )
- {
- throw new SocketException( 'Not connected.', SocketException::NOT_CONNECTED );
- }
-
- if( $this->Challenge )
- {
- $this->Socket->Write( self::A2S_INFO, "Source Engine Query\0" . $this->Challenge );
- }
- else
- {
- $this->Socket->Write( self::A2S_INFO, "Source Engine Query\0" );
- }
-
- $Buffer = $this->Socket->Read( );
- $Type = $Buffer->GetByte( );
- $Server = [];
-
- if( $Type === self::S2C_CHALLENGE )
- {
- $this->Challenge = $Buffer->Get( 4 );
-
- $this->Socket->Write( self::A2S_INFO, "Source Engine Query\0" . $this->Challenge );
- $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 = [];
- $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[ 'Mod' ] = $Mod;
- }
-
- $Server[ 'Secure' ] = $Buffer->GetByte( ) === 1;
- $Server[ 'Bots' ] = $Buffer->GetByte( );
-
- return $Server;
- }
-
- if( $Type !== self::S2A_INFO_SRC )
- {
- 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( );
-
- // S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port.
- if( $Flags & 0x80 )
- {
- $Server[ 'GamePort' ] = $Buffer->GetShort( );
- }
-
- // S2A_EXTRA_DATA_HAS_STEAMID - Next 8 bytes are the 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 );
- }
-
- // S2A_EXTRA_DATA_HAS_SPECTATOR_DATA - Next 2 bytes include the spectator port, then the spectator server name.
- if( $Flags & 0x40 )
- {
- $Server[ 'SpecPort' ] = $Buffer->GetShort( );
- $Server[ 'SpecName' ] = $Buffer->GetString( );
- }
-
- // S2A_EXTRA_DATA_HAS_GAMETAG_DATA - Next bytes are the game tag string
- if( $Flags & 0x20 )
- {
- $Server[ 'GameTags' ] = $Buffer->GetString( );
- }
-
- // S2A_EXTRA_DATA_GAMEID - Next 8 bytes are the gameID of the server
- 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( ) : array
- {
- 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 = [];
- $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( ) : array
- {
- 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)
- *
- * @throws InvalidPacketException
- */
- private function GetChallenge( int $Header, int $ExpectedResult ) : void
- {
- 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::S2C_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( string $Password ) : void
- {
- 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;
- }
- default:
- {
- throw new SocketException( 'Unknown engine.', SocketException::INVALID_ENGINE );
- }
- }
-
- $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( string $Command ) : string
- {
- 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 );
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * This class provides the public interface to the PHP-Source-Query library.
+ *
+ * @author Pavel Djundik
+ *
+ * @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\AuthenticationException;
+use xPaw\SourceQuery\Exception\InvalidArgumentException;
+use xPaw\SourceQuery\Exception\InvalidPacketException;
+use xPaw\SourceQuery\Exception\SocketException;
+
+/**
+ * Class SourceQuery
+ *
+ * @package xPaw\SourceQuery
+ *
+ * @uses AuthenticationException
+ * @uses InvalidArgumentException
+ * @uses InvalidPacketException
+ * @uses SocketException
+ */
+final class SourceQuery
+{
+ /**
+ * Engines
+ */
+ public const GOLDSOURCE = 0;
+ public const SOURCE = 1;
+
+ /**
+ * Packets sent
+ */
+ private const A2A_PING = 0x69;
+ private const A2S_INFO = 0x54;
+ private const A2S_PLAYER = 0x55;
+ private const A2S_RULES = 0x56;
+ private const A2S_SERVERQUERY_GETCHALLENGE = 0x57;
+
+ /**
+ * Packets received
+ */
+ private const A2A_ACK = 0x6A;
+ private const S2C_CHALLENGE = 0x41;
+ private const S2A_INFO_SRC = 0x49;
+ private const S2A_INFO_OLD = 0x6D; // Old GoldSource, HLTV uses it (actually called S2A_INFO_DETAILED)
+ private const S2A_PLAYER = 0x44;
+ private const S2A_RULES = 0x45;
+ public const S2A_RCON = 0x6C;
+
+ /**
+ * Source rcon sent
+ */
+ public const SERVERDATA_REQUESTVALUE = 0;
+ public const SERVERDATA_EXECCOMMAND = 2;
+ public const SERVERDATA_AUTH = 3;
+
+ /**
+ * Source rcon received
+ */
+ public const SERVERDATA_RESPONSE_VALUE = 0;
+ public const SERVERDATA_AUTH_RESPONSE = 2;
+
+ /**
+ * Points to rcon class
+ *
+ * @var SourceRcon|GoldSourceRcon|null
+ */
+ private $Rcon;
+
+ /**
+ * Points to socket class
+ */
+ private BaseSocket $Socket;
+
+ /**
+ * True if connection is open, false if not
+ */
+ private bool $Connected = false;
+
+ /**
+ * Contains challenge
+ */
+ private string $Challenge = '';
+
+ /**
+ * Use old method for getting challenge number
+ */
+ private bool $UseOldGetChallengeMethod = false;
+
+ 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(string $Address, int $Port, int $Timeout = 3, int $Engine = self::SOURCE): void
+ {
+ $this->Disconnect();
+
+ if ($Timeout < 0) {
+ throw new InvalidArgumentException('Timeout must be a positive integer.', InvalidArgumentException::TIMEOUT_NOT_INTEGER);
+ }
+
+ $this->Socket->Open($Address, $Port, $Timeout, $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(bool $Value): bool
+ {
+ $Previous = $this->UseOldGetChallengeMethod;
+
+ $this->UseOldGetChallengeMethod = $Value === true;
+
+ return $Previous;
+ }
+
+ /**
+ * Closes all open connections
+ */
+ public function Disconnect(): void
+ {
+ $this->Connected = false;
+ $this->Challenge = '';
+
+ $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(): bool
+ {
+ if (!$this->Connected) {
+ throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
+ }
+
+ $this->Socket->Write(self::A2A_PING);
+ $Buffer = $this->Socket->Read();
+
+ return $Buffer->GetByte() === self::A2A_ACK;
+ }
+
+ /**
+ * Get server information
+ *
+ * @throws InvalidPacketException
+ * @throws SocketException
+ *
+ * @return array Returns an array with information on success
+ */
+ public function GetInfo(): array
+ {
+ if (!$this->Connected) {
+ throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
+ }
+
+ if ($this->Challenge) {
+ $this->Socket->Write(self::A2S_INFO, "Source Engine Query\0" . $this->Challenge);
+ } else {
+ $this->Socket->Write(self::A2S_INFO, "Source Engine Query\0");
+ }
+
+ $Buffer = $this->Socket->Read();
+ $Type = $Buffer->GetByte();
+ $Server = [];
+
+ if ($Type === self::S2C_CHALLENGE) {
+ $this->Challenge = $Buffer->Get(4);
+
+ $this->Socket->Write(self::A2S_INFO, "Source Engine Query\0" . $this->Challenge);
+ $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 = [];
+ $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[ 'Mod' ] = $Mod;
+ }
+
+ $Server[ 'Secure' ] = $Buffer->GetByte() === 1;
+ $Server[ 'Bots' ] = $Buffer->GetByte();
+
+ return $Server;
+ }
+
+ if ($Type !== self::S2A_INFO_SRC) {
+ 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();
+
+ // S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port.
+ if ($Flags & 0x80) {
+ $Server[ 'GamePort' ] = $Buffer->GetShort();
+ }
+
+ // S2A_EXTRA_DATA_HAS_STEAMID - Next 8 bytes are the 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);
+ }
+
+ // S2A_EXTRA_DATA_HAS_SPECTATOR_DATA - Next 2 bytes include the spectator port, then the spectator server name.
+ if ($Flags & 0x40) {
+ $Server[ 'SpecPort' ] = $Buffer->GetShort();
+ $Server[ 'SpecName' ] = $Buffer->GetString();
+ }
+
+ // S2A_EXTRA_DATA_HAS_GAMETAG_DATA - Next bytes are the game tag string
+ if ($Flags & 0x20) {
+ $Server[ 'GameTags' ] = $Buffer->GetString();
+ }
+
+ // S2A_EXTRA_DATA_GAMEID - Next 8 bytes are the gameID of the server
+ 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(): array
+ {
+ 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 = [];
+ $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(): array
+ {
+ 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)
+ *
+ * @throws InvalidPacketException
+ */
+ private function GetChallenge(int $Header, int $ExpectedResult): void
+ {
+ 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::S2C_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(string $Password): void
+ {
+ 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;
+ }
+ default:
+ {
+ throw new SocketException('Unknown engine.', SocketException::INVALID_ENGINE);
+ }
+ }
+
+ $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(string $Command): string
+ {
+ 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);
+ }
+}
diff --git a/SourceQuery/SourceRcon.php b/SourceQuery/SourceRcon.php
index 56919e0..88625b9 100644
--- a/SourceQuery/SourceRcon.php
+++ b/SourceQuery/SourceRcon.php
@@ -1,199 +1,186 @@
Socket = $Socket;
- }
-
- public function Close( ) : void
- {
- if( $this->RconSocket )
- {
- fclose( $this->RconSocket );
-
- $this->RconSocket = null;
- }
-
- $this->RconRequestId = 0;
- }
-
- public function Open( ) : void
- {
- if( !$this->RconSocket )
- {
- $RconSocket = @fsockopen( $this->Socket->Address, $this->Socket->Port, $ErrNo, $ErrStr, $this->Socket->Timeout );
-
- if( $ErrNo || !$RconSocket )
- {
- throw new SocketException( 'Can\'t connect to RCON server: ' . $ErrStr, SocketException::CONNECTION_FAILED );
- }
-
- $this->RconSocket = $RconSocket;
- stream_set_timeout( $this->RconSocket, $this->Socket->Timeout );
- stream_set_blocking( $this->RconSocket, true );
- }
- }
-
- public function Write( int $Header, string $String = '' ) : bool
- {
- // Pack the packet together
- $Command = pack( 'VV', ++$this->RconRequestId, $Header ) . $String . "\x00\x00";
-
- // Prepend packet length
- $Command = pack( 'V', strlen( $Command ) ) . $Command;
- $Length = strlen( $Command );
-
- return $Length === fwrite( $this->RconSocket, $Command, $Length );
- }
-
- public function Read( ) : Buffer
- {
- $Buffer = new Buffer( );
- $Buffer->Set( fread( $this->RconSocket, 4 ) );
-
- if( $Buffer->Remaining( ) < 4 )
- {
- throw new InvalidPacketException( 'Rcon read: Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY );
- }
-
- $PacketSize = $Buffer->GetLong( );
-
- $Buffer->Set( fread( $this->RconSocket, $PacketSize ) );
-
- $Data = $Buffer->Get( );
-
- $Remaining = $PacketSize - strlen( $Data );
-
- while( $Remaining > 0 )
- {
- $Data2 = fread( $this->RconSocket, $Remaining );
-
- $PacketSize = strlen( $Data2 );
-
- if( $PacketSize === 0 )
- {
- throw new InvalidPacketException( 'Read ' . strlen( $Data ) . ' bytes from socket, ' . $Remaining . ' remaining', InvalidPacketException::BUFFER_EMPTY );
- }
-
- $Data .= $Data2;
- $Remaining -= $PacketSize;
- }
-
- $Buffer->Set( $Data );
-
- return $Buffer;
- }
-
- public function Command( string $Command ) : string
- {
- $this->Write( SourceQuery::SERVERDATA_EXECCOMMAND, $Command );
- $Buffer = $this->Read( );
-
- $Buffer->GetLong( ); // RequestID
-
- $Type = $Buffer->GetLong( );
-
- if( $Type === SourceQuery::SERVERDATA_AUTH_RESPONSE )
- {
- throw new AuthenticationException( 'Bad rcon_password.', AuthenticationException::BAD_PASSWORD );
- }
- else if( $Type !== SourceQuery::SERVERDATA_RESPONSE_VALUE )
- {
- throw new InvalidPacketException( 'Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH );
- }
-
- $Data = $Buffer->Get( );
-
- // We do this stupid hack to handle split packets
- // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses
- if( strlen( $Data ) >= 4000 )
- {
- $this->Write( SourceQuery::SERVERDATA_REQUESTVALUE );
-
- do
- {
- $Buffer = $this->Read( );
-
- $Buffer->GetLong( ); // RequestID
-
- if( $Buffer->GetLong( ) !== SourceQuery::SERVERDATA_RESPONSE_VALUE )
- {
- break;
- }
-
- $Data2 = $Buffer->Get( );
-
- if( $Data2 === "\x00\x01\x00\x00\x00\x00" )
- {
- break;
- }
-
- $Data .= $Data2;
- }
- while( true );
- }
-
- return rtrim( $Data, "\0" );
- }
-
- public function Authorize( string $Password ) : void
- {
- $this->Write( SourceQuery::SERVERDATA_AUTH, $Password );
- $Buffer = $this->Read( );
-
- $RequestID = $Buffer->GetLong( );
- $Type = $Buffer->GetLong( );
-
- // If we receive SERVERDATA_RESPONSE_VALUE, then we need to read again
- // More info: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Additional_Comments
-
- if( $Type === SourceQuery::SERVERDATA_RESPONSE_VALUE )
- {
- $Buffer = $this->Read( );
-
- $RequestID = $Buffer->GetLong( );
- $Type = $Buffer->GetLong( );
- }
-
- if( $RequestID === -1 || $Type !== SourceQuery::SERVERDATA_AUTH_RESPONSE )
- {
- throw new AuthenticationException( 'RCON authorization failed.', AuthenticationException::BAD_PASSWORD );
- }
- }
- }
+
+declare(strict_types=1);
+
+/**
+ * @author Pavel Djundik
+ *
+ * @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 AuthenticationException
+ * @uses InvalidPacketException
+ * @uses SocketException
+ */
+final class SourceRcon
+{
+ /**
+ * Points to socket class
+ */
+ private BaseSocket $Socket;
+
+ /** @var ?resource */
+ private $RconSocket;
+ private int $RconRequestId = 0;
+
+ public function __construct(BaseSocket $Socket)
+ {
+ $this->Socket = $Socket;
+ }
+
+ public function Close(): void
+ {
+ if ($this->RconSocket) {
+ fclose($this->RconSocket);
+
+ $this->RconSocket = null;
+ }
+
+ $this->RconRequestId = 0;
+ }
+
+ public function Open(): void
+ {
+ if (!$this->RconSocket) {
+ $RconSocket = @fsockopen($this->Socket->Address, $this->Socket->Port, $ErrNo, $ErrStr, $this->Socket->Timeout);
+
+ if ($ErrNo || !$RconSocket) {
+ throw new SocketException('Can\'t connect to RCON server: ' . $ErrStr, SocketException::CONNECTION_FAILED);
+ }
+
+ $this->RconSocket = $RconSocket;
+ stream_set_timeout($this->RconSocket, $this->Socket->Timeout);
+ stream_set_blocking($this->RconSocket, true);
+ }
+ }
+
+ public function Write(int $Header, string $String = ''): bool
+ {
+ // Pack the packet together
+ $Command = pack('VV', ++$this->RconRequestId, $Header) . $String . "\x00\x00";
+
+ // Prepend packet length
+ $Command = pack('V', strlen($Command)) . $Command;
+ $Length = strlen($Command);
+
+ return $Length === fwrite($this->RconSocket, $Command, $Length);
+ }
+
+ public function Read(): Buffer
+ {
+ $Buffer = new Buffer();
+ $Buffer->Set(fread($this->RconSocket, 4));
+
+ if ($Buffer->Remaining() < 4) {
+ throw new InvalidPacketException('Rcon read: Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
+ }
+
+ $PacketSize = $Buffer->GetLong();
+
+ $Buffer->Set(fread($this->RconSocket, $PacketSize));
+
+ $Data = $Buffer->Get();
+
+ $Remaining = $PacketSize - strlen($Data);
+
+ while ($Remaining > 0) {
+ $Data2 = fread($this->RconSocket, $Remaining);
+
+ $PacketSize = strlen($Data2);
+
+ if ($PacketSize === 0) {
+ throw new InvalidPacketException('Read ' . strlen($Data) . ' bytes from socket, ' . $Remaining . ' remaining', InvalidPacketException::BUFFER_EMPTY);
+ }
+
+ $Data .= $Data2;
+ $Remaining -= $PacketSize;
+ }
+
+ $Buffer->Set($Data);
+
+ return $Buffer;
+ }
+
+ public function Command(string $Command): string
+ {
+ $this->Write(SourceQuery::SERVERDATA_EXECCOMMAND, $Command);
+ $Buffer = $this->Read();
+
+ $Buffer->GetLong(); // RequestID
+
+ $Type = $Buffer->GetLong();
+
+ if ($Type === SourceQuery::SERVERDATA_AUTH_RESPONSE) {
+ throw new AuthenticationException('Bad rcon_password.', AuthenticationException::BAD_PASSWORD);
+ } elseif ($Type !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
+ throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
+ }
+
+ $Data = $Buffer->Get();
+
+ // We do this stupid hack to handle split packets
+ // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses
+ if (strlen($Data) >= 4000) {
+ $this->Write(SourceQuery::SERVERDATA_REQUESTVALUE);
+
+ do {
+ $Buffer = $this->Read();
+
+ $Buffer->GetLong(); // RequestID
+
+ if ($Buffer->GetLong() !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
+ break;
+ }
+
+ $Data2 = $Buffer->Get();
+
+ if ($Data2 === "\x00\x01\x00\x00\x00\x00") {
+ break;
+ }
+
+ $Data .= $Data2;
+ } while (true);
+ }
+
+ return rtrim($Data, "\0");
+ }
+
+ public function Authorize(string $Password): void
+ {
+ $this->Write(SourceQuery::SERVERDATA_AUTH, $Password);
+ $Buffer = $this->Read();
+
+ $RequestID = $Buffer->GetLong();
+ $Type = $Buffer->GetLong();
+
+ // If we receive SERVERDATA_RESPONSE_VALUE, then we need to read again
+ // More info: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Additional_Comments
+
+ if ($Type === SourceQuery::SERVERDATA_RESPONSE_VALUE) {
+ $Buffer = $this->Read();
+
+ $RequestID = $Buffer->GetLong();
+ $Type = $Buffer->GetLong();
+ }
+
+ if ($RequestID === -1 || $Type !== SourceQuery::SERVERDATA_AUTH_RESPONSE) {
+ throw new AuthenticationException('RCON authorization failed.', AuthenticationException::BAD_PASSWORD);
+ }
+ }
+}
diff --git a/SourceQuery/bootstrap.php b/SourceQuery/bootstrap.php
index 9cb6571..c11049e 100644
--- a/SourceQuery/bootstrap.php
+++ b/SourceQuery/bootstrap.php
@@ -1,27 +1,30 @@
*/
- private \SplQueue $PacketQueue;
-
- public function __construct( )
- {
- /** @var \SplQueue */
- $this->PacketQueue = new \SplQueue();
- $this->PacketQueue->setIteratorMode( \SplDoublyLinkedList::IT_MODE_DELETE );
-
- }
-
- public function Queue( string $Data ) : void
- {
- $this->PacketQueue->push( $Data );
- }
-
- public function Close( ) : void
- {
- //
- }
-
- public function Open( string $Address, int $Port, int $Timeout, int $Engine ) : void
- {
- $this->Timeout = $Timeout;
- $this->Engine = $Engine;
- $this->Port = $Port;
- $this->Address = $Address;
- }
-
- public function Write( int $Header, string $String = '' ) : bool
- {
- return true;
- }
-
- public function Read( int $Length = 1400 ) : Buffer
- {
- $Buffer = new Buffer( );
- $Buffer->Set( $this->PacketQueue->shift() );
-
- $this->ReadInternal( $Buffer, $Length, [ $this, 'Sherlock' ] );
-
- return $Buffer;
- }
-
- public function Sherlock( Buffer $Buffer, int $Length ) : bool
- {
- if( $this->PacketQueue->isEmpty() )
- {
- return false;
- }
-
- $Buffer->Set( $this->PacketQueue->shift() );
-
- return $Buffer->GetLong( ) === -2;
- }
- }
-
- class Tests extends \PHPUnit\Framework\TestCase
- {
- private TestableSocket $Socket;
- private SourceQuery $SourceQuery;
-
- public function setUp() : void
- {
- $this->Socket = new TestableSocket();
- $this->SourceQuery = new SourceQuery( $this->Socket );
- $this->SourceQuery->Connect( '', 2 );
- }
-
- public function tearDown() : void
- {
- $this->SourceQuery->Disconnect();
-
- unset( $this->Socket, $this->SourceQuery );
- }
-
- public function testInvalidTimeout() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\InvalidArgumentException::class );
- $SourceQuery = new SourceQuery( );
- $SourceQuery->Connect( '', 2, -1 );
- }
-
- public function testNotConnectedGetInfo() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->GetInfo();
- }
-
- public function testNotConnectedPing() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->Ping();
- }
-
- public function testNotConnectedGetPlayers() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->GetPlayers();
- }
-
- /**
- * @expectedException xPaw\SourceQuery\Exception\SocketException
- */
- public function testNotConnectedGetRules() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->GetRules();
- }
-
- public function testNotConnectedSetRconPassword() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->SetRconPassword('a');
- }
-
- public function testNotConnectedRcon() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Disconnect();
- $this->SourceQuery->Rcon('a');
- }
-
- public function testRconWithoutPassword() : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\SocketException::class );
- $this->SourceQuery->Rcon('a');
- }
-
- /**
- * @dataProvider InfoProvider
- */
- public function testGetInfo( string $RawInput, array $ExpectedOutput ) : void
- {
- if( isset( $ExpectedOutput[ 'IsMod' ] ) )
- {
- $this->Socket->Engine = SourceQuery::GOLDSOURCE;
- }
-
- $this->Socket->Queue( $RawInput );
-
- $RealOutput = $this->SourceQuery->GetInfo();
-
- $this->assertEquals( $ExpectedOutput, $RealOutput );
- }
-
- public function InfoProvider() : array
- {
- $DataProvider = [];
-
- $Files = glob( __DIR__ . '/Info/*.raw', GLOB_ERR );
-
- foreach( $Files as $File )
- {
- $DataProvider[] =
- [
- hex2bin( trim( file_get_contents( $File ) ) ),
- json_decode( file_get_contents( str_replace( '.raw', '.json', $File ) ), true )
- ];
- }
-
- return $DataProvider;
- }
-
- /**
- * @dataProvider BadPacketProvider
- */
- public function testBadGetInfo( string $Data ) : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\InvalidPacketException::class );
- $this->Socket->Queue( $Data );
-
- $this->SourceQuery->GetInfo();
- }
-
- /**
- * @dataProvider BadPacketProvider
- */
- public function testBadGetChallengeViaPlayers( string $Data ) : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\InvalidPacketException::class );
- $this->Socket->Queue( $Data );
-
- $this->SourceQuery->GetPlayers();
- }
-
- /**
- * @dataProvider BadPacketProvider
- */
- public function testBadGetPlayersAfterCorrectChallenge( string $Data ) : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\InvalidPacketException::class );
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11" );
- $this->Socket->Queue( $Data );
-
- $this->SourceQuery->GetPlayers();
- }
-
- /**
- * @dataProvider BadPacketProvider
- */
- public function testBadGetRulesAfterCorrectChallenge( string $Data ) : void
- {
- $this->expectException( xPaw\SourceQuery\Exception\InvalidPacketException::class );
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11" );
- $this->Socket->Queue( $Data );
-
- $this->SourceQuery->GetRules();
- }
-
- public function BadPacketProvider( ) : array
- {
- return
- [
- [ "" ],
- [ "\xff\xff\xff\xff" ], // No type
- [ "\xff\xff\xff\xff\x49" ], // Correct type, but no data after
- [ "\xff\xff\xff\xff\x6D" ], // Old info packet, but tests are done for source
- [ "\xff\xff\xff\xff\x11" ], // Wrong type
- [ "\x11\x11\x11\x11" ], // Wrong header
- [ "\xff" ], // Should be 4 bytes, but it's 1
- ];
- }
-
- public function testGetChallengeTwice( ) : void
- {
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11" );
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x45\x01\x00ayy\x00lmao\x00" );
- $this->assertEquals( [ 'ayy' => 'lmao' ], $this->SourceQuery->GetRules() );
-
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x45\x01\x00wow\x00much\x00" );
- $this->assertEquals( [ 'wow' => 'much' ], $this->SourceQuery->GetRules() );
- }
-
- /**
- * @dataProvider RulesProvider
- * @param array $RawInput
- */
- public function testGetRules( array $RawInput, array $ExpectedOutput ) : void
- {
- $this->Socket->Queue( hex2bin( "ffffffff4104fce20e" ) ); // Challenge
-
- foreach( $RawInput as $Packet )
- {
- $this->Socket->Queue( hex2bin( $Packet ) );
- }
-
- $RealOutput = $this->SourceQuery->GetRules();
-
- $this->assertEquals( $ExpectedOutput, $RealOutput );
- }
-
- public function RulesProvider() : array
- {
- $DataProvider = [];
-
- $Files = glob( __DIR__ . '/Rules/*.raw', GLOB_ERR );
-
- foreach( $Files as $File )
- {
- $DataProvider[] =
- [
- file( $File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES ),
- json_decode( file_get_contents( str_replace( '.raw', '.json', $File ) ), true )
- ];
- }
-
- return $DataProvider;
- }
-
- /**
- * @dataProvider PlayersProvider
- * @param array $RawInput
- */
- public function testGetPlayers( array $RawInput, array $ExpectedOutput ) : void
- {
- $this->Socket->Queue( hex2bin( "ffffffff4104fce20e" ) ); // Challenge
-
- foreach( $RawInput as $Packet )
- {
- $this->Socket->Queue( hex2bin( $Packet ) );
- }
-
- $RealOutput = $this->SourceQuery->GetPlayers();
-
- $this->assertEquals( $ExpectedOutput, $RealOutput );
- }
-
- public function PlayersProvider() : array
- {
- $DataProvider = [];
-
- $Files = glob( __DIR__ . '/Players/*.raw', GLOB_ERR );
-
- foreach( $Files as $File )
- {
- $DataProvider[] =
- [
- file( $File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES ),
- json_decode( file_get_contents( str_replace( '.raw', '.json', $File ) ), true )
- ];
- }
-
- return $DataProvider;
- }
-
- public function testPing() : void
- {
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\x6A\x00");
- $this->assertTrue( $this->SourceQuery->Ping() );
-
- $this->Socket->Queue( "\xFF\xFF\xFF\xFF\xEE");
- $this->assertFalse( $this->SourceQuery->Ping() );
- }
- }
+
+declare(strict_types=1);
+
+use PHPUnit\Framework\TestCase;
+use xPaw\SourceQuery\BaseSocket;
+use xPaw\SourceQuery\SourceQuery;
+use xPaw\SourceQuery\Buffer;
+
+final class TestableSocket extends BaseSocket
+{
+ /** @var \SplQueue */
+ private \SplQueue $PacketQueue;
+
+ public function __construct()
+ {
+ $this->PacketQueue = new \SplQueue();
+ $this->PacketQueue->setIteratorMode(\SplDoublyLinkedList::IT_MODE_DELETE);
+ }
+
+ public function Queue(string $Data): void
+ {
+ $this->PacketQueue->push($Data);
+ }
+
+ public function Close(): void
+ {
+ //
+ }
+
+ public function Open(string $Address, int $Port, int $Timeout, int $Engine): void
+ {
+ $this->Timeout = $Timeout;
+ $this->Engine = $Engine;
+ $this->Port = $Port;
+ $this->Address = $Address;
+ }
+
+ public function Write(int $Header, string $String = ''): bool
+ {
+ return true;
+ }
+
+ public function Read(int $Length = 1400): Buffer
+ {
+ $Buffer = new Buffer();
+ $Buffer->Set($this->PacketQueue->shift());
+
+ $this->ReadInternal($Buffer, $Length, [ $this, 'Sherlock' ]);
+
+ return $Buffer;
+ }
+
+ public function Sherlock(Buffer $Buffer, int $Length): bool
+ {
+ if ($this->PacketQueue->isEmpty()) {
+ return false;
+ }
+
+ $Buffer->Set($this->PacketQueue->shift());
+
+ return $Buffer->GetLong() === -2;
+ }
+}
+
+final class Tests extends TestCase
+{
+ private TestableSocket $Socket;
+ private SourceQuery $SourceQuery;
+
+ public function setUp(): void
+ {
+ $this->Socket = new TestableSocket();
+ $this->SourceQuery = new SourceQuery($this->Socket);
+ $this->SourceQuery->Connect('', 2);
+ }
+
+ public function tearDown(): void
+ {
+ $this->SourceQuery->Disconnect();
+
+ unset($this->Socket, $this->SourceQuery);
+ }
+
+ public function testInvalidTimeout(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\InvalidArgumentException::class);
+ $SourceQuery = new SourceQuery();
+ $SourceQuery->Connect('', 2, -1);
+ }
+
+ public function testNotConnectedGetInfo(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->GetInfo();
+ }
+
+ public function testNotConnectedPing(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->Ping();
+ }
+
+ public function testNotConnectedGetPlayers(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->GetPlayers();
+ }
+
+ /**
+ * @expectedException xPaw\SourceQuery\Exception\SocketException
+ */
+ public function testNotConnectedGetRules(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->GetRules();
+ }
+
+ public function testNotConnectedSetRconPassword(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->SetRconPassword('a');
+ }
+
+ public function testNotConnectedRcon(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Disconnect();
+ $this->SourceQuery->Rcon('a');
+ }
+
+ public function testRconWithoutPassword(): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\SocketException::class);
+ $this->SourceQuery->Rcon('a');
+ }
+
+ /**
+ * @dataProvider InfoProvider
+ */
+ public function testGetInfo(string $RawInput, array $ExpectedOutput): void
+ {
+ if (isset($ExpectedOutput[ 'IsMod' ])) {
+ $this->Socket->Engine = SourceQuery::GOLDSOURCE;
+ }
+
+ $this->Socket->Queue($RawInput);
+
+ $RealOutput = $this->SourceQuery->GetInfo();
+
+ self::assertEquals($ExpectedOutput, $RealOutput);
+ }
+
+ public function InfoProvider(): array
+ {
+ $DataProvider = [];
+
+ $Files = glob(__DIR__ . '/Info/*.raw', GLOB_ERR);
+
+ foreach ($Files as $File) {
+ $DataProvider[] =
+ [
+ hex2bin(trim(file_get_contents($File))),
+ json_decode(file_get_contents(str_replace('.raw', '.json', $File)), true)
+ ];
+ }
+
+ return $DataProvider;
+ }
+
+ /**
+ * @dataProvider BadPacketProvider
+ */
+ public function testBadGetInfo(string $Data): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\InvalidPacketException::class);
+ $this->Socket->Queue($Data);
+
+ $this->SourceQuery->GetInfo();
+ }
+
+ /**
+ * @dataProvider BadPacketProvider
+ */
+ public function testBadGetChallengeViaPlayers(string $Data): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\InvalidPacketException::class);
+ $this->Socket->Queue($Data);
+
+ $this->SourceQuery->GetPlayers();
+ }
+
+ /**
+ * @dataProvider BadPacketProvider
+ */
+ public function testBadGetPlayersAfterCorrectChallenge(string $Data): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\InvalidPacketException::class);
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
+ $this->Socket->Queue($Data);
+
+ $this->SourceQuery->GetPlayers();
+ }
+
+ /**
+ * @dataProvider BadPacketProvider
+ */
+ public function testBadGetRulesAfterCorrectChallenge(string $Data): void
+ {
+ $this->expectException(xPaw\SourceQuery\Exception\InvalidPacketException::class);
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
+ $this->Socket->Queue($Data);
+
+ $this->SourceQuery->GetRules();
+ }
+
+ public function BadPacketProvider(): array
+ {
+ return
+ [
+ [ "" ],
+ [ "\xff\xff\xff\xff" ], // No type
+ [ "\xff\xff\xff\xff\x49" ], // Correct type, but no data after
+ [ "\xff\xff\xff\xff\x6D" ], // Old info packet, but tests are done for source
+ [ "\xff\xff\xff\xff\x11" ], // Wrong type
+ [ "\x11\x11\x11\x11" ], // Wrong header
+ [ "\xff" ], // Should be 4 bytes, but it's 1
+ ];
+ }
+
+ public function testGetChallengeTwice(): void
+ {
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x45\x01\x00ayy\x00lmao\x00");
+ self::assertEquals([ 'ayy' => 'lmao' ], $this->SourceQuery->GetRules());
+
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x45\x01\x00wow\x00much\x00");
+ self::assertEquals([ 'wow' => 'much' ], $this->SourceQuery->GetRules());
+ }
+
+ /**
+ * @dataProvider RulesProvider
+ * @param array $RawInput
+ */
+ public function testGetRules(array $RawInput, array $ExpectedOutput): void
+ {
+ $this->Socket->Queue(hex2bin("ffffffff4104fce20e")); // Challenge
+
+ foreach ($RawInput as $Packet) {
+ $this->Socket->Queue(hex2bin($Packet));
+ }
+
+ $RealOutput = $this->SourceQuery->GetRules();
+
+ self::assertEquals($ExpectedOutput, $RealOutput);
+ }
+
+ public function RulesProvider(): array
+ {
+ $DataProvider = [];
+
+ $Files = glob(__DIR__ . '/Rules/*.raw', GLOB_ERR);
+
+ foreach ($Files as $File) {
+ $DataProvider[] =
+ [
+ file($File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
+ json_decode(file_get_contents(str_replace('.raw', '.json', $File)), true)
+ ];
+ }
+
+ return $DataProvider;
+ }
+
+ /**
+ * @dataProvider PlayersProvider
+ * @param array $RawInput
+ */
+ public function testGetPlayers(array $RawInput, array $ExpectedOutput): void
+ {
+ $this->Socket->Queue(hex2bin("ffffffff4104fce20e")); // Challenge
+
+ foreach ($RawInput as $Packet) {
+ $this->Socket->Queue(hex2bin($Packet));
+ }
+
+ $RealOutput = $this->SourceQuery->GetPlayers();
+
+ self::assertEquals($ExpectedOutput, $RealOutput);
+ }
+
+ public function PlayersProvider(): array
+ {
+ $DataProvider = [];
+
+ $Files = glob(__DIR__ . '/Players/*.raw', GLOB_ERR);
+
+ foreach ($Files as $File) {
+ $DataProvider[] =
+ [
+ file($File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
+ json_decode(file_get_contents(str_replace('.raw', '.json', $File)), true)
+ ];
+ }
+
+ return $DataProvider;
+ }
+
+ public function testPing(): void
+ {
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\x6A\x00");
+ self::assertTrue($this->SourceQuery->Ping());
+
+ $this->Socket->Queue("\xFF\xFF\xFF\xFF\xEE");
+ self::assertFalse($this->SourceQuery->Ping());
+ }
+}
diff --git a/composer.json b/composer.json
index 5a2d346..ed90e3a 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,8 @@
{
"phpunit/phpunit": "^9.5",
"vimeo/psalm": "^4.7",
- "phpstan/phpstan": "^0.12.83"
+ "phpstan/phpstan": "^0.12.83",
+ "friendsofphp/php-cs-fixer": "^3.0"
},
"autoload":
{