Gistrec

Minecraft query

Feb 23rd, 2017
100
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 4.23 KB | None | 0 0
  1. <?php
  2.  
  3. $Query = new MinecraftQuery;
  4. try{
  5.     $Query->Connect($ip, $port);
  6.     $Query->GetInfo();
  7.     $Query->GetPlayers();
  8. }catch(MinecraftQueryException $e) {
  9.     echo $e->getMessage();
  10. }
  11.  
  12. class MinecraftQuery{
  13.  
  14.     const STATISTIC = 0x00;
  15.     const HANDSHAKE = 0x09;
  16.  
  17.     private $Socket;
  18.     private $Players;
  19.     private $Info;
  20.  
  21.     public function Connect($Ip, $Port = 25565, $Timeout = 3, $ResolveSRV = true){
  22.         if(!is_int($Timeout ) || $Timeout < 0) throw new \InvalidArgumentException('Timeout must be an integer.');
  23.         if($ResolveSRV) $this->ResolveSRV($Ip, $Port);
  24.         $this->Socket = @FSockOpen('udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, $Timeout);
  25.         if($ErrNo || $this->Socket === false)   throw new MinecraftQueryException('Could not create socket: ' . $ErrStr);
  26.         Stream_Set_Timeout( $this->Socket, $Timeout);
  27.         Stream_Set_Blocking($this->Socket, true);
  28.         try{
  29.             $Challenge = $this->GetChallenge();
  30.             $this->GetStatus( $Challenge );
  31.         }
  32.         catch(MinecraftQueryException $e){
  33.             FClose($this->Socket);
  34.             throw new MinecraftQueryException($e->getMessage());
  35.         }
  36.         FClose($this->Socket);
  37.     }
  38.  
  39.     public function GetInfo(){
  40.         return isset($this->Info) ? $this->Info : false;
  41.     }
  42.  
  43.     public function GetPlayers(){
  44.         return isset($this->Players) ? $this->Players : false;
  45.     }
  46.  
  47.     private function GetChallenge(){
  48.         $Data = $this->WriteData(self::HANDSHAKE);
  49.         if ($Data === false) throw new MinecraftQueryException('Failed to receive challenge.');
  50.         return Pack('N', $Data);
  51.     }
  52.  
  53.     private function GetStatus($Challenge){
  54.         $Data = $this->WriteData( self :: STATISTIC, $Challenge . Pack('c*', 0x00, 0x00, 0x00, 0x00));
  55.         if (!$Data) throw new MinecraftQueryException('Failed to receive status.');
  56.         $Last = '';
  57.         $Info = Array( );
  58.         $Data   = SubStr($Data, 11); // splitnum + 2 int
  59.         $Data   = Explode("\x00\x00\x01player_\x00\x00",$Data);
  60.         if(Count($Data) !== 2) throw new MinecraftQueryException('Failed to parse server\'s response.');
  61.         $Players = SubStr($Data[ 1 ], 0, -2);
  62.         $Data    = Explode("\x00", $Data[ 0 ]);
  63.         // Array with known keys in order to validate the result
  64.         // It can happen that server sends custom strings containing bad things (who can know!)
  65.         $Keys = Array(
  66.             'hostname'   => 'HostName',
  67.             'gametype'   => 'GameType',
  68.             'version'    => 'Version',
  69.             'plugins'    => 'Plugins',
  70.             'map'        => 'Map',
  71.             'numplayers' => 'Players',
  72.             'maxplayers' => 'MaxPlayers',
  73.             'hostport'   => 'HostPort',
  74.             'hostip'     => 'HostIp',
  75.             'game_id'    => 'GameName'
  76.         );
  77.         foreach ($Data as $Key => $Value){
  78.             if (~$Key & 1){
  79.                 if(!Array_Key_Exists($Value, $Keys)){
  80.                     $Last = false;
  81.                     continue;
  82.                 }
  83.                 $Last = $Keys[ $Value ];
  84.                 $Info[ $Last ] = '';
  85.             }elseif ( $Last != false ) $Info[$Last] = mb_convert_encoding($Value, 'UTF-8');
  86.         }
  87.         // Ints
  88.         $Info['Players']    = IntVal($Info['Players']);
  89.         $Info['MaxPlayers'] = IntVal($Info['MaxPlayers']);
  90.         $Info['HostPort']   = IntVal($Info['HostPort']);
  91.         // Parse "plugins", if any
  92.         if ($Info['Plugins']){
  93.             $Data = Explode(": ", $Info['Plugins' ], 2);
  94.             $Info['RawPlugins'] = $Info['Plugins'];
  95.             $Info['Software']   = $Data[0];
  96.             if (Count($Data) == 2) $Info['Plugins'] = Explode("; ", $Data[1]);
  97.         } else $Info['Software'] = 'Vanilla';
  98.         $this->Info = $Info;
  99.         if (empty($Players)) $this->Players = null;
  100.         else $this->Players = Explode("\x00", $Players);
  101.     }
  102.  
  103.     private function WriteData($Command, $Append = ""){
  104.         $Command = Pack('c*', 0xFE, 0xFD, $Command, 0x01, 0x02, 0x03, 0x04) . $Append;
  105.         $Length  = StrLen($Command);
  106.         if($Length !== FWrite( $this->Socket, $Command, $Length )) throw new MinecraftQueryException("Failed to write on socket.");
  107.         $Data = FRead($this->Socket, 4096);
  108.         if($Data === false) throw new MinecraftQueryException("Failed to read from socket.");
  109.         if(StrLen($Data) < 5 || $Data[0] != $Command[2]) return false;
  110.         return SubStr($Data, 5);
  111.     }
  112.  
  113.     private function ResolveSRV(&$Address, &$Port){
  114.         if(ip2long($Address) !== false) return;
  115.         $Record = dns_get_record('_minecraft._tcp.' . $Address, DNS_SRV);
  116.         if(empty($Record)) return;
  117.         if(isset($Record[0]['target'])) $Address = $Record[0]['target'];
  118.     }
  119. }
  120.  
  121. class MinecraftQueryException extends \Exception{
  122.     // Exception thrown by MinecraftQuery class
  123. }
Add Comment
Please, Sign In to add comment