1: <?php
2:
3: namespace xPaw;
4:
5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 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: 39: 40: 41: 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:
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: 78: 79:
80: public function GetInfo( )
81: {
82: return isset( $this->Info ) ? $this->Info : false;
83: }
84:
85: 86: 87: 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 );
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:
130:
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:
164: $Info[ 'Players' ] = IntVal( $Info[ 'Players' ] );
165: $Info[ 'MaxPlayers' ] = IntVal( $Info[ 'MaxPlayers' ] );
166: $Info[ 'HostPort' ] = IntVal( $Info[ 'HostPort' ] );
167:
168:
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: