245 lines
5.1 KiB
PHP
245 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace xPaw;
|
|
|
|
class MinecraftPing
|
|
{
|
|
/*
|
|
* Queries Minecraft server
|
|
* Returns array on success, false on failure.
|
|
*
|
|
* WARNING: This method was added in snapshot 13w41a (Minecraft 1.7)
|
|
*
|
|
* Written by xPaw
|
|
*
|
|
* Website: http://xpaw.me
|
|
* GitHub: https://github.com/xPaw/PHP-Minecraft-Query
|
|
*
|
|
* ---------
|
|
*
|
|
* This method can be used to get server-icon.png too.
|
|
* Something like this:
|
|
*
|
|
* $Server = new MinecraftPing( 'localhost' );
|
|
* $Info = $Server->Query();
|
|
* echo '<img width="64" height="64" src="' . Str_Replace( "\n", "", $Info[ 'favicon' ] ) . '">';
|
|
*
|
|
*/
|
|
|
|
private $Socket;
|
|
private $ServerAddress;
|
|
private $ServerPort;
|
|
private $Timeout;
|
|
|
|
public function __construct( $Address, $Port = 25565, $Timeout = 2, $ResolveSRV = true )
|
|
{
|
|
$this->ServerAddress = $Address;
|
|
$this->ServerPort = (int)$Port;
|
|
$this->Timeout = (int)$Timeout;
|
|
|
|
if( $ResolveSRV )
|
|
{
|
|
$this->ResolveSRV();
|
|
}
|
|
|
|
$this->Connect( );
|
|
}
|
|
|
|
public function __destruct( )
|
|
{
|
|
$this->Close( );
|
|
}
|
|
|
|
public function Close( )
|
|
{
|
|
if( $this->Socket !== null )
|
|
{
|
|
fclose( $this->Socket );
|
|
|
|
$this->Socket = null;
|
|
}
|
|
}
|
|
|
|
public function Connect( )
|
|
{
|
|
$connectTimeout = $this->Timeout;
|
|
$this->Socket = @fsockopen( $this->ServerAddress, $this->ServerPort, $errno, $errstr, $connectTimeout );
|
|
|
|
if( !$this->Socket )
|
|
{
|
|
$this->Socket = null;
|
|
|
|
throw new MinecraftPingException( "Failed to connect or create a socket: $errno ($errstr)" );
|
|
}
|
|
|
|
// Set Read/Write timeout
|
|
stream_set_timeout( $this->Socket, $this->Timeout );
|
|
}
|
|
|
|
public function Query( )
|
|
{
|
|
$TimeStart = microtime(true); // for read timeout purposes
|
|
|
|
// See http://wiki.vg/Protocol (Status Ping)
|
|
$Data = "\x00"; // packet ID = 0 (varint)
|
|
|
|
$Data .= "\x04"; // Protocol version (varint)
|
|
$Data .= Pack( 'c', StrLen( $this->ServerAddress ) ) . $this->ServerAddress; // Server (varint len + UTF-8 addr)
|
|
$Data .= Pack( 'n', $this->ServerPort ); // Server port (unsigned short)
|
|
$Data .= "\x01"; // Next state: status (varint)
|
|
|
|
$Data = Pack( 'c', StrLen( $Data ) ) . $Data; // prepend length of packet ID + data
|
|
|
|
fwrite( $this->Socket, $Data ); // handshake
|
|
fwrite( $this->Socket, "\x01\x00" ); // status ping
|
|
|
|
$Length = $this->ReadVarInt( ); // full packet length
|
|
|
|
if( $Length < 10 )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
fgetc( $this->Socket ); // packet type, in server ping it's 0
|
|
|
|
$Length = $this->ReadVarInt( ); // string length
|
|
|
|
$Data = "";
|
|
do
|
|
{
|
|
if (microtime(true) - $TimeStart > $this->Timeout)
|
|
{
|
|
throw new MinecraftPingException( 'Server read timed out' );
|
|
}
|
|
|
|
$Remainder = $Length - StrLen( $Data );
|
|
$block = fread( $this->Socket, $Remainder ); // and finally the json string
|
|
// abort if there is no progress
|
|
if (!$block)
|
|
{
|
|
throw new MinecraftPingException( 'Server returned too few data' );
|
|
}
|
|
|
|
$Data .= $block;
|
|
} while( StrLen($Data) < $Length );
|
|
|
|
if( $Data === FALSE )
|
|
{
|
|
throw new MinecraftPingException( 'Server didn\'t return any data' );
|
|
}
|
|
|
|
$Data = JSON_Decode( $Data, true );
|
|
|
|
if( JSON_Last_Error( ) !== JSON_ERROR_NONE )
|
|
{
|
|
if( Function_Exists( 'json_last_error_msg' ) )
|
|
{
|
|
throw new MinecraftPingException( JSON_Last_Error_Msg( ) );
|
|
}
|
|
else
|
|
{
|
|
throw new MinecraftPingException( 'JSON parsing failed' );
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return $Data;
|
|
}
|
|
|
|
public function QueryOldPre17( )
|
|
{
|
|
fwrite( $this->Socket, "\xFE\x01" );
|
|
$Data = fread( $this->Socket, 512 );
|
|
$Len = StrLen( $Data );
|
|
|
|
if( $Len < 4 || $Data[ 0 ] !== "\xFF" )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
$Data = SubStr( $Data, 3 ); // Strip packet header (kick message packet and short length)
|
|
$Data = iconv( 'UTF-16BE', 'UTF-8', $Data );
|
|
|
|
// Are we dealing with Minecraft 1.4+ server?
|
|
if( $Data[ 1 ] === "\xA7" && $Data[ 2 ] === "\x31" )
|
|
{
|
|
$Data = Explode( "\x00", $Data );
|
|
|
|
return Array(
|
|
'HostName' => $Data[ 3 ],
|
|
'Players' => IntVal( $Data[ 4 ] ),
|
|
'MaxPlayers' => IntVal( $Data[ 5 ] ),
|
|
'Protocol' => IntVal( $Data[ 1 ] ),
|
|
'Version' => $Data[ 2 ]
|
|
);
|
|
}
|
|
|
|
$Data = Explode( "\xA7", $Data );
|
|
|
|
return Array(
|
|
'HostName' => SubStr( $Data[ 0 ], 0, -1 ),
|
|
'Players' => isset( $Data[ 1 ] ) ? IntVal( $Data[ 1 ] ) : 0,
|
|
'MaxPlayers' => isset( $Data[ 2 ] ) ? IntVal( $Data[ 2 ] ) : 0,
|
|
'Protocol' => 0,
|
|
'Version' => '1.3'
|
|
);
|
|
}
|
|
|
|
private function ReadVarInt( )
|
|
{
|
|
$i = 0;
|
|
$j = 0;
|
|
|
|
while( true )
|
|
{
|
|
$k = @fgetc( $this->Socket );
|
|
|
|
if( $k === FALSE )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
$k = Ord( $k );
|
|
|
|
$i |= ( $k & 0x7F ) << $j++ * 7;
|
|
|
|
if( $j > 5 )
|
|
{
|
|
throw new MinecraftPingException( 'VarInt too big' );
|
|
}
|
|
|
|
if( ( $k & 0x80 ) != 128 )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $i;
|
|
}
|
|
|
|
private function ResolveSRV()
|
|
{
|
|
if( ip2long( $this->ServerAddress ) !== false )
|
|
{
|
|
return;
|
|
}
|
|
|
|
$Record = dns_get_record( '_minecraft._tcp.' . $this->ServerAddress, DNS_SRV );
|
|
|
|
if( empty( $Record ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if( isset( $Record[ 0 ][ 'target' ] ) )
|
|
{
|
|
$this->ServerAddress = $Record[ 0 ][ 'target' ];
|
|
}
|
|
|
|
if( isset( $Record[ 0 ][ 'port' ] ) )
|
|
{
|
|
$this->ServerPort = $Record[ 0 ][ 'port' ];
|
|
}
|
|
}
|
|
}
|