initial commit
This commit is contained in:
commit
5892578782
4 changed files with 445 additions and 0 deletions
1
README
Normal file
1
README
Normal file
|
@ -0,0 +1 @@
|
|||
# Minecraft-server status
|
24
index.php
Normal file
24
index.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
require_once('minestat.php');
|
||||
$ms = new MineStat("localhost");
|
||||
if($ms->is_online()) {
|
||||
<h3>Join on server.thisfro.ch!</h3>
|
||||
<p>The Server is: </p>
|
||||
<h2 class="online status">online</h2>
|
||||
<p>running version</p>
|
||||
<h2>1.16.3</h2><br>
|
||||
<p>Players online</p>
|
||||
<h2>0 / 10</h2>
|
||||
}
|
||||
else {
|
||||
<p>The Server is: </p>
|
||||
<h2 class="offline status">offline</h2>
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
48
main.css
Normal file
48
main.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
372
minestat.php
Normal file
372
minestat.php
Normal file
|
@ -0,0 +1,372 @@
|
|||
<?php
|
||||
/*
|
||||
* minestat.php - A Minecraft server status checker
|
||||
* Copyright (C) 2014-2020 Lloyd Dilley
|
||||
* http://www.dilley.me/
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
class MineStat
|
||||
{
|
||||
const NUM_FIELDS = 6; // number of values expected from server
|
||||
const NUM_FIELDS_BETA = 3; // number of values expected from a 1.8b/1.3 server
|
||||
const MAX_VARINT_SIZE = 5; // maximum number of bytes a varint can be
|
||||
// No enums or class nesting in PHP, so this is our workaround for return values
|
||||
const RETURN_SUCCESS = 0;
|
||||
const RETURN_CONNFAIL = -1;
|
||||
const RETURN_TIMEOUT = -2;
|
||||
const RETURN_UNKNOWN = -3;
|
||||
private $address; // hostname or IP address of the Minecraft server
|
||||
private $port; // port number the Minecraft server accepts connections on
|
||||
private $online; // online or offline?
|
||||
private $version; // Minecraft server version
|
||||
private $motd; // message of the day
|
||||
private $current_players; // current number of players online
|
||||
private $max_players; // maximum player capacity
|
||||
private $protocol; // protocol level
|
||||
private $json_data; // JSON data for 1.7 queries
|
||||
private $latency; // ping time to server in milliseconds
|
||||
private $timeout; // timeout in seconds
|
||||
private $socket; // network socket
|
||||
|
||||
public function __construct($address, $port = 25565, $timeout = 5)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
?>
|
Loading…
Reference in a new issue