Last active
April 7, 2022 19:26
-
-
Save JLChnToZ/f453b04884710182a7f6 to your computer and use it in GitHub Desktop.
Minecraft Server Status Ping/Query Ajax/JSON interface.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Grab server status and return as JSON format... | |
// This simple script is written by Jeremy Lam (JLChnToZ). | |
// Usage http://www.example.com/link/to/json.php/192.168.1.101:25565/ping/legacy_ping | |
// Requet it as usual file, first provide the server IP/host name and port, then is the request type. | |
// For what kind of json data will it return, I suggest to read the description in the mcstat.php | |
// as all data is wrap in there, the only thing what the script below doing is just repacking the reqult to JSON. | |
// mcstat.php is required for using this script | |
// Download from here: https://github.com/winny-/mcstat/blob/master/mcstat.php | |
// --------------------------------------------------------------- | |
// require the mcstat.php | |
require_once './mcstat.php'; | |
// Tell the browser this is a dynamic JSON file. | |
header('Cache-Control: no-cache, must-revalidate'); | |
header('Expires: Sat, 01 Jan 2000 00:00:00 GMT'); | |
header('Content-type: application/json; charset=utf-8'); | |
header('Access-Control-Allow-Origin: *'); | |
// Parse the request | |
$request = substr($_SERVER['REQUEST_URI'], strlen($_SERVER['SCRIPT_NAME'])); | |
if(strpos($request, "/") === 0) $request = substr($request, 1); | |
$request = explode("/", $request); | |
$request[0] = explode(":", $request[0]); | |
if(sizeof($request[0]) < 2) $request[0][1] = 25565; // Default port = 25565 | |
if(sizeof($request) < 1) $request[1] = 'legacy_ping'; // Default request = legacy ping | |
// Construct | |
$mcs = new MinecraftStatus($request[0][0], $request[0][1]); | |
// Try to communicate with the server with each request type until the result is given | |
for($i = 1; $i < sizeof($request); $i++) { | |
try { | |
switch(strtolower($request[$i])) { | |
case 'ping': $data = $mcs->ping(false); break; // ping: basic 1.7+ server ping method | |
case 'legacy_ping': $data = $mcs->ping(true); break; // legacy_ping: old server ping method | |
case 'basic_query': $data = $mcs->query(false); break; // basic_query: basic UT3 query (require "enable-query=true" in server.properties) | |
case 'full_query': $data = $mcs->query(true); break; // full_query: full UT3 query (require "enable-query=true" also) | |
} | |
} catch(Exception $e) { } | |
if(isset($data)) break; | |
} | |
// Dat'z all, bye bye! | |
echo json_encode($data); | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
define('MCSTAT_NETWORK_TIMEOUT', 5); | |
function mcstat_expect($fp, $string) { | |
$recievedString = ''; | |
for ($bytes = strlen($string), $cur = 0; $cur < $bytes; $cur++) { | |
$recievedByte = fread($fp, 1); | |
$expectedByte = $string[$cur]; | |
$recievedString .= $recievedByte; | |
if ($recievedByte !== $expectedByte) { | |
$errorMessage = 'Expected ' . bin2hex($string) . ' but recieved ' . bin2hex($recievedString); | |
$errorMessage .= ' problem byte: ' . bin2hex($recievedByte) . ' (position ' . $cur . ')'; | |
throw new Exception($errorMessage); | |
} | |
} | |
} | |
class MinecraftStatus { | |
public $hostname; | |
public $port; | |
public $lastError; | |
public $stats; | |
const SERVER_LIST_PING = 'Server List Ping'; | |
const SERVER_LIST_PING_1_7 = 'Server List Ping 1.7'; | |
const BASIC_QUERY = 'Basic Query'; | |
const FULL_QUERY = 'Full Query'; | |
private $methodTable; | |
function __construct($hostname, $port = 25565) { | |
$this->hostname = $hostname; | |
$this->port = $port; | |
$this->lastError = null; | |
$this->stats = array(); | |
$this->methodTable = array( | |
self::SERVER_LIST_PING => array( | |
'MinecraftServerListPing', | |
'ping' | |
), | |
self::SERVER_LIST_PING_1_7 => array( | |
'MinecraftServerListPing', | |
'ping17' | |
), | |
self::BASIC_QUERY => array( | |
'MinecraftQuery', | |
'basicQuery' | |
), | |
self::FULL_QUERY => array( | |
'MinecraftQuery', | |
'fullQuery' | |
) | |
); | |
} | |
public function ping($useLegacy = true) { | |
if ($useLegacy) | |
return $this->performStatusMethod(self::SERVER_LIST_PING); | |
return $this->performStatusMethod(self::SERVER_LIST_PING_1_7); | |
} | |
public function query($fullQuery = true) { | |
if ($fullQuery) | |
return $this->performStatusMethod(self::FULL_QUERY); | |
return $this->performStatusMethod(self::BASIC_QUERY); | |
} | |
private function performStatusMethod($statusMethodName) { | |
$method = $this->methodTable[$statusMethodName]; | |
$arguments = array( | |
$this->hostname, | |
$this->port | |
); | |
try { | |
$newStats = call_user_func_array($method, $arguments); | |
} catch (Exception $e) { | |
$newStats = false; | |
$this->lastError = $e->getMessage(); | |
} | |
$this->stats[microtime()] = array( | |
'stats' => $newStats, | |
'method' => $statusMethodName, | |
'hostname' => $this->hostname, | |
'port' => $this->port | |
); | |
return $newStats; | |
} | |
} | |
/* | |
================ | |
Server List Ping | |
================ | |
An example of how to get a Minecraft server status's using a "Server List Ping" packet. | |
See details here: http://www.wiki.vg/Server_List_Ping | |
*/ | |
class MinecraftServerListPing { | |
private static function packString($string) { | |
$letterCount = strlen($string); | |
return pack('n', $letterCount) . mb_convert_encoding($string, 'UTF-16BE'); | |
} | |
// This is needed since UTF-16BE text rendered as UTF-8 contains unnecessary null bytes | |
// and could cause other components, especially string functions to blow up. Boom! | |
private static function decodeUTF16BE($string) { | |
return mb_convert_encoding($string, 'UTF-8', 'UTF-16BE'); | |
} | |
public static function ping($hostname, $port = 25565) { | |
// 1. pack data to send | |
$request = pack('nc', 0xfe01, 0xfa) | |
. self::packString('MC|PingHost') | |
. pack('nc', 7 + 2 * strlen($hostname), 73) | |
. self::packString($hostname) | |
. pack('N', 25565); | |
// 2. open communication socket and make transaction | |
$time = microtime(true); | |
$fp = stream_socket_client('tcp://' . $hostname . ':' . $port, $errno, $errmsg, MCSTAT_NETWORK_TIMEOUT); | |
stream_set_timeout($fp, MCSTAT_NETWORK_TIMEOUT); | |
if (!$fp) | |
throw new Exception($errmsg); | |
fwrite($fp, $request); | |
$response = fread($fp, 2048); | |
$socketInfo = stream_get_meta_data($fp); | |
fclose($fp); | |
if ($socketInfo['timed_out']) | |
throw new Exception('Connection timed out'); | |
$time = round((microtime(true) - $time) * 1000); | |
// 3. unpack data and return | |
if (strpos($response, 0xFF) !== 0) | |
throw new Exception('Bad reply from server'); | |
$response = substr($response, 3); | |
$response = explode(pack('n', 0), $response); | |
return array( | |
'player_count' => self::decodeUTF16BE($response[4]), | |
'player_max' => self::decodeUTF16BE($response[5]), | |
'motd' => self::decodeUTF16BE($response[3]), | |
'server_version' => self::decodeUTF16BE($response[2]), | |
'protocol_version' => self::decodeUTF16BE($response[1]), | |
'latency' => $time | |
); | |
} | |
public static function ping17($hostname, $port = 25565) { | |
$handshakePacket = self::packData(chr(0) | |
. self::packVarInt(4) | |
. self::packData($hostname) | |
. pack('n', (int) $port) | |
. self::packVarInt(1)); | |
$statusRequestPacket = self::packData(chr(0)); | |
$time = microtime(true); | |
$fp = stream_socket_client('tcp://' . $hostname . ':' . $port, $errno, $errmsg, MCSTAT_NETWORK_TIMEOUT); | |
stream_set_timeout($fp, MCSTAT_NETWORK_TIMEOUT); | |
if (!$fp) | |
throw new Exception($errmsg); | |
fwrite($fp, $handshakePacket); | |
fwrite($fp, $statusRequestPacket); | |
self::unpackVarInt($fp); | |
$time = round((microtime(true) - $time) * 1000); | |
self::unpackVarInt($fp); | |
$jsonLength = self::unpackVarInt($fp); | |
for ($jsonString = ''; strlen($jsonString) < $jsonLength; $jsonString .= fread($fp, 2048)); | |
fclose($fp); | |
$json = json_decode($jsonString, true); | |
if (isset($json['players']['sample'])) { | |
foreach ($json['players']['sample'] as $player) | |
$players[] = $player['name']; | |
} else | |
$players = array(); | |
return array( | |
'latency' => $time, | |
'server_version' => $json['version']['name'], | |
'protocol_version' => $json['version']['protocol'], | |
'player_count' => $json['players']['online'], | |
'player_max' => $json['players']['max'], | |
'motd' => $json['description'], | |
'icon' => $json['favicon'], | |
'players' => $players | |
); | |
} | |
private static function packData($data) { | |
return self::packVarInt(strlen($data)) . $data; | |
} | |
private static function unpackVarInt($fp) { | |
$int = 0; | |
$pos = 0; | |
while (true) { | |
$byte = ord(fread($fp, 1)); | |
$int |= ($byte & 0x7F) << $pos++ * 7; | |
if ($pos > 5) | |
throw new Exception('VarInt too big'); | |
if (($byte & 0x80) !== 128) | |
break; | |
} | |
return $int; | |
} | |
private static function packVarInt($int) { | |
$varInt = ''; | |
while (true) { | |
if (($int & 0xFFFFFF80) === 0) { | |
$varInt .= chr($int); | |
return $varInt; | |
} | |
$varInt .= chr($int & 0x7F | 0x80); | |
$int >>= 7; | |
} | |
} | |
} | |
/* | |
===== | |
Query | |
===== | |
This section utilizes the UT3 Query protocol to query a Minecraft server. | |
Read about it here: http://wiki.vg/Query | |
*/ | |
class MinecraftQuery { | |
private static function getString($fp) { | |
$string = ''; | |
while (($lastChar = fread($fp, 1)) !== chr(0)) | |
$string .= $lastChar; | |
return $string; | |
} | |
private static function getStrings($fp, $count) { | |
for ($stringsRecieved = 0; $stringsRecieved < $count; $stringsRecieved++) | |
$strings[] = self::getString($fp); | |
return $strings; | |
} | |
private static function parseKeyValueSection($fp) { | |
$keyValuePairs = array(); | |
while (($key = self::getString($fp)) !== '') { | |
$value = self::getString($fp); | |
$keyValuePairs[$key] = $value; | |
} | |
return $keyValuePairs; | |
} | |
private static function makeSessionId() { | |
return rand(1, 0xFFFFFFFF) & 0x0F0F0F0F; | |
} | |
// Verify packet type and ensure it references our session ID. | |
private static function validateResponse($response, $type, $sessionId) { | |
$invalidType = ($response['type'] !== $type); | |
$invalidSessionId = ($response['sessionId'] !== $sessionId); | |
if ($invalidType || $invalidSessionId) { | |
$errorMessage = 'Invalid Response:'; | |
$errorMessage .= ($invalidType) ? " {$response['type']} !== {$type}" : ''; | |
$errorMessage .= ($invalidSessionId) ? " {$response['sessionId']} !== {$sessionId}" : ''; | |
error_log($errorMessage); | |
return false; | |
} | |
return true; | |
} | |
private static function handleHandshake($fp, $sessionId) { | |
$handshakeRequest = pack('cccN', 0xFE, 0xFD, 9, $sessionId); | |
fwrite($fp, $handshakeRequest); | |
$handshakeResponse = self::readResponseHeader($fp, true); | |
if (!self::validateResponse($handshakeResponse, 9, $sessionId)) | |
return false; | |
return $handshakeResponse['challengeToken']; | |
} | |
private static function readResponseHeader($fp, $withChallengeToken = false) { | |
$header = fread($fp, 5); | |
$unpacked = unpack('ctype/NsessionId', $header); | |
if ($withChallengeToken) | |
$unpacked['challengeToken'] = (int) self::getString($fp); | |
return $unpacked; | |
} | |
private static function startQuery($hostname, $port, $fullQuery) { | |
$sessionId = self::makeSessionId(); | |
$fp = stream_socket_client('udp://' . $hostname . ':' . $port, $errno, $errmsg, MCSTAT_NETWORK_TIMEOUT); | |
stream_set_timeout($fp, MCSTAT_NETWORK_TIMEOUT); | |
if (!$fp) | |
throw new Exception($errmsg); | |
$time = microtime(true); | |
$challengeToken = self::handleHandshake($fp, $sessionId); | |
if (!$challengeToken) { | |
fclose($fp); | |
throw new Exception('Bad handshake response'); | |
} | |
$time = round((microtime(true) - $time) * 1000); | |
$statRequest = pack('cccNN', 0xFE, 0xFD, 0, $sessionId, $challengeToken); | |
if ($fullQuery) | |
$statRequest .= pack('N', 0); | |
fwrite($fp, $statRequest); | |
$statResponseHeader = self::readResponseHeader($fp); | |
if (!self::validateResponse($statResponseHeader, 0, $sessionId)) { | |
fclose($fp); | |
throw new Exception('Bad query response'); | |
} | |
return array( | |
'sessionId' => $sessionId, | |
'challengeToken' => $challengeToken, | |
'fp' => $fp, | |
'time' => $time | |
); | |
} | |
private static function unpackBasicPort($fp) { | |
$unpacked = unpack('vport', fread($fp, 2)); | |
return (string) $unpacked['port']; | |
} | |
public static function basicQuery($hostname, $port = 25565) { | |
$vars = self::startQuery($hostname, $port, false); | |
$fp = $vars['fp']; | |
$stats = array( | |
'motd' => self::getString($fp), | |
'gametype' => self::getString($fp), | |
'map' => self::getString($fp), | |
'player_count' => self::getString($fp), | |
'player_max' => self::getString($fp), | |
'port' => self::unpackBasicPort($fp), | |
'ip' => self::getString($fp), | |
'latency' => $vars['time'] | |
); | |
fclose($fp); | |
return $stats; | |
} | |
public static function fullQuery($hostname, $port = 25565) { | |
$vars = self::startQuery($hostname, $port, true); | |
$fp = $vars['fp']; | |
$stats = array(); | |
$stats['latency'] = $vars['time']; | |
mcstat_expect($fp, "\x73\x70\x6C\x69\x74\x6E\x75\x6D\x00\x80\x00"); | |
foreach (self::parseKeyValueSection($fp) as $key => $value) { | |
switch ($key) { | |
case 'numplayers': | |
$key = 'player_count'; | |
break; | |
case 'maxplayers': | |
$key = 'player_max'; | |
break; | |
case 'hostname': | |
$key = 'motd'; | |
break; | |
case 'hostip': | |
$key = 'ip'; | |
break; | |
case 'hostport': | |
$key = 'port'; | |
break; | |
} | |
$stats[$key] = $value; | |
} | |
mcstat_expect($fp, "\x01\x70\x6C\x61\x79\x65\x72\x5F\x00\x00"); | |
$stats['players'] = array(); | |
while (($player = self::getString($fp)) !== '') | |
$stats['players'][] = $player; | |
fclose($fp); | |
return $stats; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment