From 58925787824733c60728788c933b5ec28984587c Mon Sep 17 00:00:00 2001 From: thisfro Date: Tue, 6 Oct 2020 13:31:35 +0200 Subject: [PATCH] initial commit --- README | 1 + index.php | 24 ++++ main.css | 48 +++++++ minestat.php | 372 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 README create mode 100644 index.php create mode 100644 main.css create mode 100644 minestat.php diff --git a/README b/README new file mode 100644 index 0000000..f372461 --- /dev/null +++ b/README @@ -0,0 +1 @@ +# Minecraft-server status diff --git a/index.php b/index.php new file mode 100644 index 0000000..ac39388 --- /dev/null +++ b/index.php @@ -0,0 +1,24 @@ + + + + + + is_online()) { +

Join on server.thisfro.ch!

+

The Server is:

+

online

+

running version

+

1.16.3


+

Players online

+

0 / 10

+ } + else { +

The Server is:

+

offline

+ } + ?> + + \ No newline at end of file diff --git a/main.css b/main.css new file mode 100644 index 0000000..48bbd9b --- /dev/null +++ b/main.css @@ -0,0 +1,48 @@ +body { + width: 50%; + margin: auto; + margin-top: 50px; + text-align: center; + font-family: Arial, Helvetica, sans-serif; +} + +h2 { + font-size: 2.5pc; +} + +h3 { + background-color: #387eee; + color: #fff; + padding: 1pc; +} + +.status { + padding: 1pc; + color:#fff; +} + +.online { + background-color: #1bae5b; +} + +.offline { + background-color: rgb(248, 48, 88); +} + +@media only screen and (max-width: 1000px) { + body { + width: 80%; + } + + h2 { + font-size: 6pc; + } + + h3 { + font-size: 3pc; + } + + p { + font-size: 3pc; + } +} \ No newline at end of file diff --git a/minestat.php b/minestat.php new file mode 100644 index 0000000..e036019 --- /dev/null +++ b/minestat.php @@ -0,0 +1,372 @@ +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; + } +} +?>