summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorCristei Gabriel <cristei.g772@gmail.com>2023-11-26 14:45:11 +0200
committerCristei Gabriel <cristei.g772@gmail.com>2023-11-26 14:45:11 +0200
commitc29160ec194bebb6dbbd18cc078cb6556f937f98 (patch)
tree09bf07021442c894046f0e4184dd77346bf32396 /web
parent6a292a21a7fcacde51f0551ea9ae76fe36972183 (diff)
add minecraft status
Diffstat (limited to 'web')
-rw-r--r--web/index.php41
-rw-r--r--web/minecraft.php300
2 files changed, 341 insertions, 0 deletions
diff --git a/web/index.php b/web/index.php
index 0ff5f57..283df74 100644
--- a/web/index.php
+++ b/web/index.php
@@ -1,4 +1,6 @@
<?php
+require __DIR__ . '\minecraft.php';
+
function var_dump_pre($mixed = null) {
echo '<pre>';
var_dump($mixed);
@@ -60,6 +62,27 @@ function query_source_server($ip = null, $port = null) {
return $server;
}
+function query_minecraft_server($ip = null, $port = null) {
+ $server['result_valid'] = false;
+ try {
+ $mq = new MinecraftQuery();
+ $mq->Connect($ip, intval($port), 3, true);
+ $players = $mq->GetPlayers();
+ if ($players == false) $server['players'] = 0;
+ else $server['players'] = count($players);
+ $info = $mq->GetInfo();
+ if ($info == false) return $server;
+ $server['version'] = $info['Version'];
+ $server['software'] = $info['Software'];
+ $server['ip'] = "{$ip}";
+ $server['port'] = "{$port}";
+ $server['playersmax'] = $info['MaxPlayers'];
+ $server['result_valid'] = true;
+ } catch (MinecraftQueryException $e) { }
+
+
+ return $server;
+}
function generate_status_div($server = null) {
@@ -77,6 +100,20 @@ function generate_status_div($server = null) {
echo "</div>\n";
}
+function generate_mc_status_div($server = null) {
+
+ echo "<div class=\"statusbox\">\n";
+ echo "<H5>~~~ networkheaven.net ~~~</H5>\n";
+ echo "<H6>" . $server['ip'] . ":" . $server['port'] . "</H6>\n";
+ echo "<hr/>\n";
+ echo "Players: " . $server['players'] . "/" . ($server['playersmax']) . "\n";
+ echo "Version: " . $server['version'] . "\n";
+ echo "Software: " . $server['software'] . "\n\n";
+ echo "<hr/>\n";
+
+ echo "</div>\n";
+}
+
// ok we are running stuff thats crasy $$$$
@@ -85,6 +122,10 @@ function generate_status_div($server = null) {
// only server at the moment
$server1data = query_source_server("45.33.90.90", "27015");
generate_status_div($server1data);
+ $mcserverdata = query_minecraft_server("45.33.90.90", "25565");
+ if ($mcserverdata['result_valid'] == true) {
+ generate_mc_status_div($mcserverdata);
+ }
echo file_get_contents("static_elements/f.html"); // Footer
?>
diff --git a/web/minecraft.php b/web/minecraft.php
new file mode 100644
index 0000000..5eebb2e
--- /dev/null
+++ b/web/minecraft.php
@@ -0,0 +1,300 @@
+<?php
+
+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, $ResolveSRV = true )
+ {
+ if( !is_int( $Timeout ) || $Timeout < 0 )
+ {
+ throw new \InvalidArgumentException( 'Timeout must be an integer.' );
+ }
+
+ if( $ResolveSRV )
+ {
+ $this->ResolveSRV( $Ip, $Port );
+ }
+
+ $this->Socket = @\fsockopen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, (float)$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 );
+ }
+ finally
+ {
+ \fclose( $this->Socket );
+ }
+ }
+
+ public function ConnectBedrock( $Ip, $Port = 19132, $Timeout = 3, $ResolveSRV = true )
+ {
+ if( !is_int( $Timeout ) || $Timeout < 0 )
+ {
+ throw new \InvalidArgumentException( 'Timeout must be an integer.' );
+ }
+
+ if( $ResolveSRV )
+ {
+ $this->ResolveSRV( $Ip, $Port );
+ }
+
+ $this->Socket = @\fsockopen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, (float)$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
+ {
+ $this->GetBedrockStatus();
+ }
+ finally
+ {
+ \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( !isset( $Keys[ $Value ] ) )
+ {
+ $Last = false;
+ continue;
+ }
+
+ $Last = $Keys[ $Value ];
+ $Info[ $Last ] = '';
+ }
+ else if( $Last != false )
+ {
+ $Info[ $Last ] = \mb_convert_encoding( $Value, 'UTF-8' );
+ }
+ }
+
+ // Ints
+ $Info[ 'Players' ] = (int)$Info[ 'Players' ];
+ $Info[ 'MaxPlayers' ] = (int)$Info[ 'MaxPlayers' ];
+ $Info[ 'HostPort' ] = (int)$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 GetBedrockStatus( )
+ {
+ // hardcoded magic https://github.com/facebookarchive/RakNet/blob/1a169895a900c9fc4841c556e16514182b75faf8/Source/RakPeer.cpp#L135
+ $OFFLINE_MESSAGE_DATA_ID = \pack( 'c*', 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78 );
+
+ $Command = \pack( 'cQ', 0x01, time() ); // DefaultMessageIDTypes::ID_UNCONNECTED_PING + 64bit current time
+ $Command .= $OFFLINE_MESSAGE_DATA_ID;
+ $Command .= \pack( 'Q', 2 ); // 64bit guid
+ $Length = \strlen( $Command );
+
+ if( $Length !== \fwrite( $this->Socket, $Command, $Length ) )
+ {
+ throw new MinecraftQueryException( "Failed to write on socket." );
+ }
+
+ $Data = \fread( $this->Socket, 4096 );
+
+ if( empty( $Data ) )
+ {
+ throw new MinecraftQueryException( "Failed to read from socket." );
+ }
+
+ if( $Data[ 0 ] !== "\x1C" ) // DefaultMessageIDTypes::ID_UNCONNECTED_PONG
+ {
+ throw new MinecraftQueryException( "First byte is not ID_UNCONNECTED_PONG." );
+ }
+
+ if( \substr( $Data, 17, 16 ) !== $OFFLINE_MESSAGE_DATA_ID )
+ {
+ throw new MinecraftQueryException( "Magic bytes do not match." );
+ }
+
+ // TODO: What are the 2 bytes after the magic?
+ $Data = \substr( $Data, 35 );
+
+ // TODO: If server-name contains a ';' it is not escaped, and will break this parsing
+ $Data = \explode( ';', $Data );
+
+ $this->Info =
+ [
+ 'GameName' => $Data[ 0 ] ?? null,
+ 'HostName' => $Data[ 1 ] ?? null,
+ 'Protocol' => $Data[ 2 ] ?? null,
+ 'Version' => $Data[ 3 ] ?? null,
+ 'Players' => isset( $Data[ 4 ] ) ? (int)$Data[ 4 ] : 0,
+ 'MaxPlayers' => isset( $Data[ 5 ] ) ? (int)$Data[ 5 ] : 0,
+ 'ServerId' => $Data[ 6 ] ?? null,
+ 'Map' => $Data[ 7 ] ?? null,
+ 'GameMode' => $Data[ 8 ] ?? null,
+ 'NintendoLimited' => $Data[ 9 ] ?? null,
+ 'IPv4Port' => isset( $Data[ 10 ] ) ? (int)$Data[ 10 ] : 0,
+ 'IPv6Port' => isset( $Data[ 11 ] ) ? (int)$Data[ 11 ] : 0,
+ 'Extra' => $Data[ 12 ] ?? null, // What is this?
+ ];
+ $this->Players = null;
+ }
+
+ 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 );
+ }
+
+ private function ResolveSRV( &$Address, &$Port )
+ {
+ if( \ip2long( $Address ) !== false )
+ {
+ return;
+ }
+
+ $Record = @\dns_get_record( '_minecraft._tcp.' . $Address, DNS_SRV );
+
+ if( empty( $Record ) )
+ {
+ return;
+ }
+
+ if( isset( $Record[ 0 ][ 'target' ] ) )
+ {
+ $Address = $Record[ 0 ][ 'target' ];
+ }
+ }
+}