Overview

Namespaces

  • aliuly
    • common
      • selectors
    • loader
  • xPaw

Classes

  • aliuly\common\ArmorItems
  • aliuly\common\BasicCli
  • aliuly\common\BasicHelp
  • aliuly\common\BasicPlugin
  • aliuly\common\ChatSession
  • aliuly\common\Cmd
  • aliuly\common\CmdSelector
  • aliuly\common\ExpandVars
  • aliuly\common\FastTransfer
  • aliuly\common\FileUtils
  • aliuly\common\FreezeSession
  • aliuly\common\GetMotd
  • aliuly\common\GetMotdAsyncTask
  • aliuly\common\InvisibleSession
  • aliuly\common\InvUtils
  • aliuly\common\ItemName
  • aliuly\common\mc
  • aliuly\common\mc2
  • aliuly\common\MoneyAPI
  • aliuly\common\MPMU
  • aliuly\common\Npc
  • aliuly\common\PermUtils
  • aliuly\common\PluginAsyncTask
  • aliuly\common\PluginCallbackTask
  • aliuly\common\PMScript
  • aliuly\common\QueryAsyncTask
  • aliuly\common\Rcon
  • aliuly\common\RconTask
  • aliuly\common\selectors\All
  • aliuly\common\selectors\AllEntity
  • aliuly\common\selectors\BaseSelector
  • aliuly\common\selectors\Random
  • aliuly\common\Session
  • aliuly\common\ShieldSession
  • aliuly\common\ShoppingCart
  • aliuly\common\SignUtils
  • aliuly\common\SkinUtils
  • aliuly\common\SpySession
  • aliuly\common\SubCommandMap
  • aliuly\common\TPUtils
  • aliuly\loader\Main
  • xPaw\MinecraftQuery

Exceptions

  • xPaw\MinecraftQueryException
  • Overview
  • Namespace
  • Class
  1: <?php
  2: 
  3: namespace xPaw;
  4: 
  5: /**
  6:  * Queries remote minecraft servers
  7: *
  8:  * Class written by xPaw
  9:  *
 10:  * - Website: <http://xpaw.me>
 11:  * - GitHub: <https://github.com/xPaw/PHP-Minecraft-Query>
 12:  *
 13:  * * * *
 14:  *
 15:  * <code>
 16:  *      // Sample usage
 17:  *      use xPaw\MinecraftQuery;
 18:  *      use xPaw\MinecraftQueryException;
 19:  *      $query = new MinecraftQuery;
 20:  *      try {
 21:  *         $Query->Connect($host,$port,$timeout);
 22:  *      } catch (MinecraftQueryException $e) {
 23:  *         die($e->getMessage()."\n");
 24:  *      }
 25:  *      print_r($Query->GetInfo());
 26:  *      print_r($Query->GetPlayers());
 27:  * </code>
 28:  */
 29: class MinecraftQuery
 30: {
 31:     const STATISTIC = 0x00;
 32:     const HANDSHAKE = 0x09;
 33: 
 34:     private $Socket;
 35:     private $Players;
 36:     private $Info;
 37: 
 38:   /** Query server
 39:      * @param str $Ip - IP or hostname to query
 40:      * @param int $Port - Port to connect to
 41:      * @param int $Timeout - Timeout in seconds
 42:      */
 43:     public function Connect( $Ip, $Port = 19132, $Timeout = 3 )
 44:     {
 45:         if( !is_int( $Timeout ) || $Timeout < 0 )
 46:         {
 47:             throw new \InvalidArgumentException( 'Timeout must be an integer.' );
 48:         }
 49:         $this->Socket = @FSockOpen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, $Timeout );
 50: 
 51:         if( $ErrNo || $this->Socket === false )
 52:         {
 53:             throw new MinecraftQueryException( 'Could not create socket: ' . $ErrStr );
 54:         }
 55: 
 56:         Stream_Set_Timeout( $this->Socket, $Timeout );
 57:         Stream_Set_Blocking( $this->Socket, true );
 58: 
 59:         try
 60:         {
 61:             $Challenge = $this->GetChallenge( );
 62: 
 63:             $this->GetStatus( $Challenge );
 64:         }
 65:         // We catch this because we want to close the socket, not very elegant
 66:         catch( MinecraftQueryException $e )
 67:         {
 68:             FClose( $this->Socket );
 69: 
 70:             throw new MinecraftQueryException( $e->getMessage( ) );
 71:         }
 72: 
 73:         FClose( $this->Socket );
 74:     }
 75: 
 76:     /**
 77:      * Returns the query data
 78:      * @return  array|false
 79:      */
 80:     public function GetInfo( )
 81:     {
 82:         return isset( $this->Info ) ? $this->Info : false;
 83:     }
 84: 
 85:     /**
 86:      * Returns player list
 87:      * @return  array|false
 88:      */
 89:     public function GetPlayers( )
 90:     {
 91:         return isset( $this->Players ) ? $this->Players : false;
 92:     }
 93: 
 94:     private function GetChallenge( )
 95:     {
 96:         $Data = $this->WriteData( self :: HANDSHAKE );
 97: 
 98:         if( $Data === false )
 99:         {
100:             throw new MinecraftQueryException( 'Failed to receive challenge.' );
101:         }
102: 
103:         return Pack( 'N', $Data );
104:     }
105: 
106:     private function GetStatus( $Challenge )
107:     {
108:         $Data = $this->WriteData( self :: STATISTIC, $Challenge . Pack( 'c*', 0x00, 0x00, 0x00, 0x00 ) );
109: 
110:         if( !$Data )
111:         {
112:             throw new MinecraftQueryException( 'Failed to receive status.' );
113:         }
114: 
115:         $Last = '';
116:         $Info = Array( );
117: 
118:         $Data    = SubStr( $Data, 11 ); // splitnum + 2 int
119:         $Data    = Explode( "\x00\x00\x01player_\x00\x00", $Data );
120: 
121:         if( Count( $Data ) !== 2 )
122:         {
123:             throw new MinecraftQueryException( 'Failed to parse server\'s response.' );
124:         }
125: 
126:         $Players = SubStr( $Data[ 1 ], 0, -2 );
127:         $Data    = Explode( "\x00", $Data[ 0 ] );
128: 
129:         // Array with known keys in order to validate the result
130:         // It can happen that server sends custom strings containing bad things (who can know!)
131:         $Keys = Array(
132:             'hostname'   => 'HostName',
133:             'gametype'   => 'GameType',
134:             'version'    => 'Version',
135:             'plugins'    => 'Plugins',
136:             'map'        => 'Map',
137:             'numplayers' => 'Players',
138:             'maxplayers' => 'MaxPlayers',
139:             'hostport'   => 'HostPort',
140:             'hostip'     => 'HostIp',
141:             'game_id'    => 'GameName'
142:         );
143: 
144:         foreach( $Data as $Key => $Value )
145:         {
146:             if( ~$Key & 1 )
147:             {
148:                 if( !Array_Key_Exists( $Value, $Keys ) )
149:                 {
150:                     $Last = false;
151:                     continue;
152:                 }
153: 
154:                 $Last = $Keys[ $Value ];
155:                 $Info[ $Last ] = '';
156:             }
157:             else if( $Last != false )
158:             {
159:                 $Info[ $Last ] = mb_convert_encoding( $Value, 'UTF-8' );
160:             }
161:         }
162: 
163:         // Ints
164:         $Info[ 'Players' ]    = IntVal( $Info[ 'Players' ] );
165:         $Info[ 'MaxPlayers' ] = IntVal( $Info[ 'MaxPlayers' ] );
166:         $Info[ 'HostPort' ]   = IntVal( $Info[ 'HostPort' ] );
167: 
168:         // Parse "plugins", if any
169:         if( $Info[ 'Plugins' ] )
170:         {
171:             $Data = Explode( ": ", $Info[ 'Plugins' ], 2 );
172: 
173:             $Info[ 'RawPlugins' ] = $Info[ 'Plugins' ];
174:             $Info[ 'Software' ]   = $Data[ 0 ];
175: 
176:             if( Count( $Data ) == 2 )
177:             {
178:                 $Info[ 'Plugins' ] = Explode( "; ", $Data[ 1 ] );
179:             }
180:         }
181:         else
182:         {
183:             $Info[ 'Software' ] = 'Vanilla';
184:         }
185: 
186:         $this->Info = $Info;
187: 
188:         if( empty( $Players ) )
189:         {
190:             $this->Players = null;
191:         }
192:         else
193:         {
194:             $this->Players = Explode( "\x00", $Players );
195:         }
196:     }
197: 
198:     private function WriteData( $Command, $Append = "" )
199:     {
200:         $Command = Pack( 'c*', 0xFE, 0xFD, $Command, 0x01, 0x02, 0x03, 0x04 ) . $Append;
201:         $Length  = StrLen( $Command );
202: 
203:         if( $Length !== FWrite( $this->Socket, $Command, $Length ) )
204:         {
205:             throw new MinecraftQueryException( "Failed to write on socket." );
206:         }
207: 
208:         $Data = FRead( $this->Socket, 4096 );
209: 
210:         if( $Data === false )
211:         {
212:             throw new MinecraftQueryException( "Failed to read from socket." );
213:         }
214: 
215:         if( StrLen( $Data ) < 5 || $Data[ 0 ] != $Command[ 2 ] )
216:         {
217:             return false;
218:         }
219: 
220:         return SubStr( $Data, 5 );
221:     }
222: }
223: 
API documentation generated by ApiGen