address = $address; $this->port = $port; $this->timeout = $timeout; $this->online = false; $retval = $this->json_query(); // 1.7 if($retval != MineStat::RETURN_SUCCESS && $retval != MineStat::RETURN_CONNFAIL) $retval = $this->new_query(); // 1.6 if($retval != MineStat::RETURN_SUCCESS && $retval != MineStat::RETURN_CONNFAIL) $retval = $this->legacy_query(); // 1.4/1.5 if($retval != MineStat::RETURN_SUCCESS && $retval != MineStat::RETURN_CONNFAIL) $retval = $this->beta_query(); // 1.8b/1.3 } public function __destruct() { if(@socket_read($this->socket, 1)) { socket_shutdown($this->socket); socket_close($this->socket); $this->socket = null; } } public function get_address() { return $this->address; } public function get_port() { return $this->port; } public function is_online() { return $this->online; } public function get_version() { return $this->version; } public function get_motd() { return $this->motd; } public function get_current_players() { return $this->current_players; } public function get_max_players() { return $this->max_players; } public function get_protocol() { return $this->protocol; } public function get_json() { return $this->json_data; } public function get_latency() { return $this->latency; } /* Connects to remote server */ private function connect() { $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0)); if($this->socket === false) return MineStat::RETURN_CONNFAIL; // Since socket_connect() does not respect timeout, we have to toggle non-blocking mode and enforce the timeout socket_set_nonblock($this->socket); $time = time(); $start_time = microtime(true); while(!@socket_connect($this->socket, $this->address, $this->port)) { if((time() - $time) >= $this->timeout) { socket_close($this->socket); return MineStat::RETURN_TIMEOUT; } usleep(0); } $result = @socket_connect($this->socket, $this->address, $this->port); $this->latency = round((microtime(true) - $start_time) * 1000); socket_set_block($this->socket); if($result === false && socket_last_error($this->socket) != SOCKET_EISCONN) return MineStat::RETURN_CONNFAIL; return MineStat::RETURN_SUCCESS; } /* Populates object fields after connecting */ private function parse_data($delimiter, $is_beta = false) { $response = @unpack('C', socket_read($this->socket, 1)); //socket_recv($this->socket, $response, 2, MSG_PEEK); if(!empty($response) && $response[1] == 0xFF) // kick packet (255) { $len = unpack('n', socket_read($this->socket, 2))[1]; $raw_data = mb_convert_encoding(socket_read($this->socket, ($len * 2)), "UTF-8", "UTF-16BE"); socket_close($this->socket); } else { socket_close($this->socket); return MineStat::RETURN_UNKNOWN; } if(isset($raw_data)) { $server_info = explode($delimiter, $raw_data); // split on delimiter if($is_beta) $num_fields = MineStat::NUM_FIELDS_BETA; else $num_fields = MineStat::NUM_FIELDS; if(isset($server_info) && sizeof($server_info) >= $num_fields) { if($is_beta) { $this->version = "1.8b/1.3"; // since server does not return version, set it $this->motd = $server_info[0]; $this->current_players = (int)$server_info[1]; $this->max_players = (int)$server_info[2]; $this->online = true; } else { // $server_info[0] contains the section symbol and 1 $this->protocol = (int)$server_info[1]; // contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example) $this->version = $server_info[2]; $this->motd = $server_info[3]; $this->current_players = (int)$server_info[4]; $this->max_players = (int)$server_info[5]; $this->online = true; } } else return MineStat::RETURN_UNKNOWN; } else return MineStat::RETURN_UNKNOWN; return MineStat::RETURN_SUCCESS; } /* * 1.8b/1.3 * 1.8 beta through 1.3 servers communicate as follows for a ping query: * 1. Client sends \xFE (server list ping) * 2. Server responds with: * 2a. \xFF (kick packet) * 2b. data length * 2c. 3 fields delimited by \u00A7 (section symbol) * The 3 fields, in order, are: message of the day, current players, and max players */ public function beta_query() { try { $retval = $this->connect(); if($retval != MineStat::RETURN_SUCCESS) return $retval; // Start the handshake and attempt to acquire data socket_write($this->socket, "\xFE"); $retval = $this->parse_data("\xA7", true); } catch(Exception $e) { return MineStat::RETURN_UNKNOWN; } return $retval; } /* * 1.4/1.5 * 1.4 and 1.5 servers communicate as follows for a ping query: * 1. Client sends: * 1a. \xFE (server list ping) * 1b. \x01 (server list ping payload) * 2. Server responds with: * 2a. \xFF (kick packet) * 2b. data length * 2c. 6 fields delimited by \x00 (null) * The 6 fields, in order, are: the section symbol and 1, protocol version, * server version, message of the day, current players, and max players. * The protocol version corresponds with the server version and can be the * same for different server versions. */ public function legacy_query() { try { $retval = $this->connect(); if($retval != MineStat::RETURN_SUCCESS) return $retval; // Start the handshake and attempt to acquire data socket_write($this->socket, "\xFE\x01"); $retval = $this->parse_data("\x00"); } catch(Exception $e) { return MineStat::RETURN_UNKNOWN; } return $retval; } /* * 1.6 * 1.6 servers communicate as follows for a ping query: * 1. Client sends: * 1a. \xFE (server list ping) * 1b. \x01 (server list ping payload) * 1c. \xFA (plugin message) * 1d. \x00\x0B (11 which is the length of "MC|PingHost") * 1e. "MC|PingHost" encoded as a UTF-16BE string * 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7 * 1g. arbitrary 1.6 protocol version (\x4E for example for 78) * 1h. length of remote address as a short * 1i. remote address encoded as a UTF-16BE string * 1j. remote port as an int * 2. Server responds with: * 2a. \xFF (kick packet) * 2b. data length * 2c. 6 fields delimited by \x00 (null) * The 6 fields, in order, are: the section symbol and 1, protocol version, * server version, message of the day, current players, and max players. * The protocol version corresponds with the server version and can be the * same for different server versions. */ public function new_query() { try { $retval = $this->connect(); if($retval != MineStat::RETURN_SUCCESS) return $retval; // Start the handshake and attempt to acquire data socket_write($this->socket, "\xFE\x01\xFA"); socket_write($this->socket, "\x00\x0B"); // 11 (length of "MC|PingHost") socket_write($this->socket, mb_convert_encoding("MC|PingHost", "UTF-16BE")); // requires PHP mbstring socket_write($this->socket, pack('n', (7 + 2 * strlen($this->address)))); socket_write($this->socket, "\x4E"); // 78 (protocol version of 1.6.4) socket_write($this->socket, pack('n', strlen($this->address))); socket_write($this->socket, mb_convert_encoding($this->address, "UTF-16BE")); socket_write($this->socket, pack('N', $this->port)); $retval = $this->parse_data("\x00"); } catch(Exception $e) { return MineStat::RETURN_UNKNOWN; } return $retval; } /* * 1.7 * 1.7 to current servers communicate as follows for a ping query: * 1. Client sends: * 1a. \x00 (handshake packet containing the fields specified below) * 1b. \x00 (request) * The handshake packet contains the following fields respectively: * 1. protocol version as a varint (\x00 suffices) * 2. remote address as a string * 3. remote port as an unsigned short * 4. state as a varint (should be 1 for status) * 2. Server responds with: * 2a. \x00 (JSON response) * An example JSON string contains: * {'players': {'max': 20, 'online': 0}, * 'version': {'protocol': 404, 'name': '1.13.2'}, * 'description': {'text': 'A Minecraft Server'}} */ public function json_query() { try { $retval = $this->connect(); if($retval != MineStat::RETURN_SUCCESS) return $retval; // Start handshake $payload = "\x00\x00"; $payload .= pack('c', strlen($this->address)) . $this->address; $payload .= pack('n', $this->port); $payload .= "\x01"; $payload = pack('c', strlen($payload)) . $payload; socket_write($this->socket, $payload); socket_write($this->socket, "\x01\x00"); // Acquire data $total_len = $this->unpack_varint(); if($this->unpack_varint() != 0) return MineStat::RETURN_UNKNOWN; $json_len = $this->unpack_varint(); socket_recv($this->socket, $response, $json_len, MSG_WAITALL); socket_close($this->socket); $json_data = json_decode($response, true); if(json_last_error() != 0) { //echo(json_last_error_msg()); return MineStat::RETURN_UNKNOWN; } $this->json_data = $json_data; // Parse data //var_dump($json_data); $this->protocol = (int)@$json_data['version']['protocol']; $this->version = @$json_data['version']['name']; $this->motd = @$json_data['description']['text']; $this->current_players = (int)@$json_data['players']['online']; $this->max_players = (int)@$json_data['players']['max']; if(isset($this->version) && isset($this->motd) && isset($this->current_players) && isset($this->max_players)) $this->online = true; else return MineStat::RETURN_UNKNOWN; } catch(Exception $e) { return MineStat::RETURN_UNKNOWN; } return MineStat::RETURN_SUCCESS; } /* Returns value of varint type */ private function unpack_varint() { $vint = 0; for($i = 0; $i <= MineStat::MAX_VARINT_SIZE; $i++) { $data = socket_read($this->socket, 1); if(!$data) return 0; $data = ord($data); $vint |= ($data & 0x7F) << $i++ * 7; if(($data & 0x80) != 128) break; } return $vint; } } ?>