initial commit
Signed-off-by: Brendan Golden <git@brendan.ie>
This commit is contained in:
commit
98aaf7333e
20 changed files with 1067 additions and 0 deletions
55
.gitattributes
vendored
Normal file
55
.gitattributes
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Borrowed from: https://gitlab.skynet.ie/compsoc/skynet/website/2023/-/blob/main/.gitattributes
|
||||
# Documents
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
*.doc filter=lfs diff=lfs merge=lfs -text
|
||||
*.docx filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# Excel
|
||||
*.xls filter=lfs diff=lfs merge=lfs -text
|
||||
*.xlsx filter=lfs diff=lfs merge=lfs -text
|
||||
*.xlsm filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# Powerpoints
|
||||
*.ppt filter=lfs diff=lfs merge=lfs -text
|
||||
*.pptx filter=lfs diff=lfs merge=lfs -text
|
||||
*.ppsx filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# Images
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Fonts
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.eot filter=lfs diff=lfs merge=lfs -text
|
||||
*.woff filter=lfs diff=lfs merge=lfs -text
|
||||
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Video
|
||||
*.mkv filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.wmv filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# Misc
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# ET4011
|
||||
*.cbe filter=lfs diff=lfs merge=lfs -text
|
||||
*.pbs filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# Open/Libre office
|
||||
# from https://www.libreoffice.org/discover/what-is-opendocument/
|
||||
*.odt filter=lfs diff=lfs merge=lfs -text
|
||||
*.ods filter=lfs diff=lfs merge=lfs -text
|
||||
*.odp filter=lfs diff=lfs merge=lfs -text
|
||||
*.odg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
# QT
|
||||
*.ui filter=lfs diff=lfs merge=lfs -text
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.idea
|
||||
boilerplate.html
|
||||
result
|
||||
/result
|
78
css/main.css
Normal file
78
css/main.css
Normal file
|
@ -0,0 +1,78 @@
|
|||
/* CSS is the sound you don't want to hear from a creeper */
|
||||
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background: url(../img/bg.jpg) repeat-x #000;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 490px;
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
p, ol p {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
margin: 12px 0 0 0;
|
||||
padding: 0;
|
||||
line-height: 150%;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
ol {
|
||||
font: italic 1em Georgia, Times, serif;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
ol p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
listitem {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #a0cee5;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
border-bottom: 1px dashed #666;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #a6d4ea;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: small;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.map {
|
||||
text-transform: uppercase;
|
||||
font-size: small;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#info {
|
||||
color: #cbcbcb;
|
||||
border: 1px solid #444;
|
||||
text-align: left;
|
||||
text-shadow: #000 0px 1px 3px;
|
||||
background-color: #2c2c2c;
|
||||
margin-top: 20px;
|
||||
padding: 14px;
|
||||
}
|
BIN
img/CompSoc_Logo_PNG.png
(Stored with Git LFS)
Normal file
BIN
img/CompSoc_Logo_PNG.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
img/bg.jpg
(Stored with Git LFS)
Normal file
BIN
img/bg.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
img/error.png
(Stored with Git LFS)
Normal file
BIN
img/error.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
img/header.jpg
(Stored with Git LFS)
Normal file
BIN
img/header.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
img/success.png
(Stored with Git LFS)
Normal file
BIN
img/success.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
img/warning.png
(Stored with Git LFS)
Normal file
BIN
img/warning.png
(Stored with Git LFS)
Normal file
Binary file not shown.
10
index.html
Normal file
10
index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirect</title>
|
||||
<!--<meta http-equiv = "refresh" content = "1; url = http://games.skynet.ie/map/index.html" />-->
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting . . .</p>
|
||||
</body>
|
||||
</html>
|
69
index.php
Normal file
69
index.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Skynet Games Server</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<img src="img/header.jpg"/>
|
||||
|
||||
<div id="info">
|
||||
<h3>Welcome to the UL CompSoc Minecraft server</h3>
|
||||
<p>To play on the server, you must:
|
||||
<ol>
|
||||
<li><p>Own a copy of <a href="http://www.minecraft.net/">Minecraft</a></p></li>
|
||||
<li><p>Be a member of the UL Computer Society<br/>
|
||||
<span class="hint">Learn how to join the society <a href="https://ulwolves.ie/society/compsoc">here</a></span></p></li>
|
||||
<li><p>Get your Minecraft username added to our server's whitelist<br/>
|
||||
<span class="hint">Email <a href="mailto:minecraft@skynet.ie?subject=Add to MC Whitelist">minecraft@skynet.ie</a></span></p></li>
|
||||
</ol>
|
||||
<p>Once that's done, connect to "<em>games.skynet.ie</em>" from the multiplayer section of Minecraft. Enjoy!</p>
|
||||
<p>You can view a live map of our world at <a href="http://games.skynet.ie/map/index.html">games.skynet.ie/map/</a></p>
|
||||
<!--<p>Some networks block the minecraft port (25565), but you can get around this by connecting to port 21 instead "<em>games.skynet.ie:21</em>"</p>-->
|
||||
<p>If you are interested in adding more games to this server contact <a href="mailto:compsoc@skynet.ie">compsoc@skynet.ie</a> and we will see what we can do, we are trialing this server with just Minecraft at the moment</p>
|
||||
<br/><h3>Minecraft Server Status:</h3>
|
||||
<?php
|
||||
$maintence = file_get_contents("maintence.txt");
|
||||
if($maintence!=1){
|
||||
require_once('query.php');
|
||||
$server = new Query('localhost', 25555);
|
||||
if ($server->connect()){
|
||||
$info = $server->get_info();
|
||||
echo "<h4 style='text-align:center;'><img src='./img/success.png' height='20'/> Server is Online <img src='./img/success.png' height='20'/></h4>";
|
||||
//print_r($info);
|
||||
$players = $info['players'];
|
||||
//print_r($players);
|
||||
if(count($players)>0){
|
||||
echo "<p>Members Online Now: ";
|
||||
for($x=0; $x<count($players); $x++){
|
||||
if($x==(count($players)-1)){
|
||||
echo "<a href='https://namemc.com/profile/" . $players[$x] . "' target='_blank'>" . $players[$x] . "</a>";
|
||||
}
|
||||
else{
|
||||
echo "<a href='https://namemc.com/profile/" . $players[$x] . "' target='_blank'>" . $players[$x] . "</a>, ";
|
||||
}
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
else{
|
||||
echo "<p>There are currently no members online</p>";
|
||||
}
|
||||
}
|
||||
else{
|
||||
echo "<h4 style='text-align:center;'><img src='./img/error.png' height='20'/> Server is Offline <img src='./img/error.png' height='20'/></h4>";
|
||||
}
|
||||
}
|
||||
else{
|
||||
echo "<h4 style='text-align:center;'><img src='./img/warning.png' height='20'/> Server is Undergoing Maintence <img src='./img/warning.png' height='20'/></h4>";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<!--<p class="map">New! <a href="map/">View real-time map</a></p>-->
|
||||
<p style="text-align:center"><a style="border:none!important" href="http://skynet.ie"><img width="200px" src="img/CompSoc_Logo_PNG.png" /></a></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
245
lib/MinecraftPing.php
Normal file
245
lib/MinecraftPing.php
Normal file
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
namespace xPaw;
|
||||
|
||||
class MinecraftPing
|
||||
{
|
||||
/*
|
||||
* Queries Minecraft server
|
||||
* Returns array on success, false on failure.
|
||||
*
|
||||
* WARNING: This method was added in snapshot 13w41a (Minecraft 1.7)
|
||||
*
|
||||
* Written by xPaw
|
||||
*
|
||||
* Website: http://xpaw.me
|
||||
* GitHub: https://github.com/xPaw/PHP-Minecraft-Query
|
||||
*
|
||||
* ---------
|
||||
*
|
||||
* This method can be used to get server-icon.png too.
|
||||
* Something like this:
|
||||
*
|
||||
* $Server = new MinecraftPing( 'localhost' );
|
||||
* $Info = $Server->Query();
|
||||
* echo '<img width="64" height="64" src="' . Str_Replace( "\n", "", $Info[ 'favicon' ] ) . '">';
|
||||
*
|
||||
*/
|
||||
|
||||
private $Socket;
|
||||
private $ServerAddress;
|
||||
private $ServerPort;
|
||||
private $Timeout;
|
||||
|
||||
public function __construct( $Address, $Port = 25565, $Timeout = 2, $ResolveSRV = true )
|
||||
{
|
||||
$this->ServerAddress = $Address;
|
||||
$this->ServerPort = (int)$Port;
|
||||
$this->Timeout = (int)$Timeout;
|
||||
|
||||
if( $ResolveSRV )
|
||||
{
|
||||
$this->ResolveSRV();
|
||||
}
|
||||
|
||||
$this->Connect( );
|
||||
}
|
||||
|
||||
public function __destruct( )
|
||||
{
|
||||
$this->Close( );
|
||||
}
|
||||
|
||||
public function Close( )
|
||||
{
|
||||
if( $this->Socket !== null )
|
||||
{
|
||||
fclose( $this->Socket );
|
||||
|
||||
$this->Socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function Connect( )
|
||||
{
|
||||
$connectTimeout = $this->Timeout;
|
||||
$this->Socket = @fsockopen( $this->ServerAddress, $this->ServerPort, $errno, $errstr, $connectTimeout );
|
||||
|
||||
if( !$this->Socket )
|
||||
{
|
||||
$this->Socket = null;
|
||||
|
||||
throw new MinecraftPingException( "Failed to connect or create a socket: $errno ($errstr)" );
|
||||
}
|
||||
|
||||
// Set Read/Write timeout
|
||||
stream_set_timeout( $this->Socket, $this->Timeout );
|
||||
}
|
||||
|
||||
public function Query( )
|
||||
{
|
||||
$TimeStart = microtime(true); // for read timeout purposes
|
||||
|
||||
// See http://wiki.vg/Protocol (Status Ping)
|
||||
$Data = "\x00"; // packet ID = 0 (varint)
|
||||
|
||||
$Data .= "\x04"; // Protocol version (varint)
|
||||
$Data .= Pack( 'c', StrLen( $this->ServerAddress ) ) . $this->ServerAddress; // Server (varint len + UTF-8 addr)
|
||||
$Data .= Pack( 'n', $this->ServerPort ); // Server port (unsigned short)
|
||||
$Data .= "\x01"; // Next state: status (varint)
|
||||
|
||||
$Data = Pack( 'c', StrLen( $Data ) ) . $Data; // prepend length of packet ID + data
|
||||
|
||||
fwrite( $this->Socket, $Data ); // handshake
|
||||
fwrite( $this->Socket, "\x01\x00" ); // status ping
|
||||
|
||||
$Length = $this->ReadVarInt( ); // full packet length
|
||||
|
||||
if( $Length < 10 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
fgetc( $this->Socket ); // packet type, in server ping it's 0
|
||||
|
||||
$Length = $this->ReadVarInt( ); // string length
|
||||
|
||||
$Data = "";
|
||||
do
|
||||
{
|
||||
if (microtime(true) - $TimeStart > $this->Timeout)
|
||||
{
|
||||
throw new MinecraftPingException( 'Server read timed out' );
|
||||
}
|
||||
|
||||
$Remainder = $Length - StrLen( $Data );
|
||||
$block = fread( $this->Socket, $Remainder ); // and finally the json string
|
||||
// abort if there is no progress
|
||||
if (!$block)
|
||||
{
|
||||
throw new MinecraftPingException( 'Server returned too few data' );
|
||||
}
|
||||
|
||||
$Data .= $block;
|
||||
} while( StrLen($Data) < $Length );
|
||||
|
||||
if( $Data === FALSE )
|
||||
{
|
||||
throw new MinecraftPingException( 'Server didn\'t return any data' );
|
||||
}
|
||||
|
||||
$Data = JSON_Decode( $Data, true );
|
||||
|
||||
if( JSON_Last_Error( ) !== JSON_ERROR_NONE )
|
||||
{
|
||||
if( Function_Exists( 'json_last_error_msg' ) )
|
||||
{
|
||||
throw new MinecraftPingException( JSON_Last_Error_Msg( ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new MinecraftPingException( 'JSON parsing failed' );
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return $Data;
|
||||
}
|
||||
|
||||
public function QueryOldPre17( )
|
||||
{
|
||||
fwrite( $this->Socket, "\xFE\x01" );
|
||||
$Data = fread( $this->Socket, 512 );
|
||||
$Len = StrLen( $Data );
|
||||
|
||||
if( $Len < 4 || $Data[ 0 ] !== "\xFF" )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$Data = SubStr( $Data, 3 ); // Strip packet header (kick message packet and short length)
|
||||
$Data = iconv( 'UTF-16BE', 'UTF-8', $Data );
|
||||
|
||||
// Are we dealing with Minecraft 1.4+ server?
|
||||
if( $Data[ 1 ] === "\xA7" && $Data[ 2 ] === "\x31" )
|
||||
{
|
||||
$Data = Explode( "\x00", $Data );
|
||||
|
||||
return Array(
|
||||
'HostName' => $Data[ 3 ],
|
||||
'Players' => IntVal( $Data[ 4 ] ),
|
||||
'MaxPlayers' => IntVal( $Data[ 5 ] ),
|
||||
'Protocol' => IntVal( $Data[ 1 ] ),
|
||||
'Version' => $Data[ 2 ]
|
||||
);
|
||||
}
|
||||
|
||||
$Data = Explode( "\xA7", $Data );
|
||||
|
||||
return Array(
|
||||
'HostName' => SubStr( $Data[ 0 ], 0, -1 ),
|
||||
'Players' => isset( $Data[ 1 ] ) ? IntVal( $Data[ 1 ] ) : 0,
|
||||
'MaxPlayers' => isset( $Data[ 2 ] ) ? IntVal( $Data[ 2 ] ) : 0,
|
||||
'Protocol' => 0,
|
||||
'Version' => '1.3'
|
||||
);
|
||||
}
|
||||
|
||||
private function ReadVarInt( )
|
||||
{
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
|
||||
while( true )
|
||||
{
|
||||
$k = @fgetc( $this->Socket );
|
||||
|
||||
if( $k === FALSE )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$k = Ord( $k );
|
||||
|
||||
$i |= ( $k & 0x7F ) << $j++ * 7;
|
||||
|
||||
if( $j > 5 )
|
||||
{
|
||||
throw new MinecraftPingException( 'VarInt too big' );
|
||||
}
|
||||
|
||||
if( ( $k & 0x80 ) != 128 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
private function ResolveSRV()
|
||||
{
|
||||
if( ip2long( $this->ServerAddress ) !== false )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$Record = dns_get_record( '_minecraft._tcp.' . $this->ServerAddress, DNS_SRV );
|
||||
|
||||
if( empty( $Record ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( isset( $Record[ 0 ][ 'target' ] ) )
|
||||
{
|
||||
$this->ServerAddress = $Record[ 0 ][ 'target' ];
|
||||
}
|
||||
|
||||
if( isset( $Record[ 0 ][ 'port' ] ) )
|
||||
{
|
||||
$this->ServerPort = $Record[ 0 ][ 'port' ];
|
||||
}
|
||||
}
|
||||
}
|
8
lib/MinecraftPingException.php
Normal file
8
lib/MinecraftPingException.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace xPaw;
|
||||
|
||||
class MinecraftPingException extends \Exception
|
||||
{
|
||||
// Exception thrown by MinecraftPing class
|
||||
}
|
299
lib/MinecraftQuery.php
Normal file
299
lib/MinecraftQuery.php
Normal file
|
@ -0,0 +1,299 @@
|
|||
<?php
|
||||
|
||||
namespace xPaw;
|
||||
|
||||
class MinecraftQuery
|
||||
{
|
||||
/*
|
||||
* Class written by xPaw
|
||||
*
|
||||
* Website: http://xpaw.me
|
||||
* GitHub: https://github.com/xPaw/PHP-Minecraft-Query
|
||||
*/
|
||||
|
||||
const STATISTIC = 0x00;
|
||||
const HANDSHAKE = 0x09;
|
||||
|
||||
private $Socket;
|
||||
private $Players;
|
||||
private $Info;
|
||||
|
||||
public function Connect( $Ip, $Port = 25565, $Timeout = 3, $ResolveSRV = true )
|
||||
{
|
||||
if( !is_int( $Timeout ) || $Timeout < 0 )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'Timeout must be an integer.' );
|
||||
}
|
||||
|
||||
if( $ResolveSRV )
|
||||
{
|
||||
$this->ResolveSRV( $Ip, $Port );
|
||||
}
|
||||
|
||||
$this->Socket = @FSockOpen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, $Timeout );
|
||||
|
||||
if( $ErrNo || $this->Socket === false )
|
||||
{
|
||||
throw new MinecraftQueryException( 'Could not create socket: ' . $ErrStr );
|
||||
}
|
||||
|
||||
Stream_Set_Timeout( $this->Socket, $Timeout );
|
||||
Stream_Set_Blocking( $this->Socket, true );
|
||||
|
||||
try
|
||||
{
|
||||
$Challenge = $this->GetChallenge( );
|
||||
|
||||
$this->GetStatus( $Challenge );
|
||||
}
|
||||
finally
|
||||
{
|
||||
FClose( $this->Socket );
|
||||
}
|
||||
}
|
||||
|
||||
public function ConnectBedrock( $Ip, $Port = 19132, $Timeout = 3, $ResolveSRV = true )
|
||||
{
|
||||
if( !is_int( $Timeout ) || $Timeout < 0 )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'Timeout must be an integer.' );
|
||||
}
|
||||
|
||||
if( $ResolveSRV )
|
||||
{
|
||||
$this->ResolveSRV( $Ip, $Port );
|
||||
}
|
||||
|
||||
$this->Socket = @\fsockopen( 'udp://' . $Ip, (int)$Port, $ErrNo, $ErrStr, $Timeout );
|
||||
|
||||
if( $ErrNo || $this->Socket === false )
|
||||
{
|
||||
throw new MinecraftQueryException( 'Could not create socket: ' . $ErrStr );
|
||||
}
|
||||
|
||||
\stream_set_timeout( $this->Socket, $Timeout );
|
||||
\stream_set_blocking( $this->Socket, true );
|
||||
|
||||
try
|
||||
{
|
||||
$this->GetBedrockStatus();
|
||||
}
|
||||
finally
|
||||
{
|
||||
FClose( $this->Socket );
|
||||
}
|
||||
}
|
||||
|
||||
public function GetInfo( )
|
||||
{
|
||||
return isset( $this->Info ) ? $this->Info : false;
|
||||
}
|
||||
|
||||
public function GetPlayers( )
|
||||
{
|
||||
return isset( $this->Players ) ? $this->Players : false;
|
||||
}
|
||||
|
||||
private function GetChallenge( )
|
||||
{
|
||||
$Data = $this->WriteData( self :: HANDSHAKE );
|
||||
|
||||
if( $Data === false )
|
||||
{
|
||||
throw new MinecraftQueryException( 'Failed to receive challenge.' );
|
||||
}
|
||||
|
||||
return Pack( 'N', $Data );
|
||||
}
|
||||
|
||||
private function GetStatus( $Challenge )
|
||||
{
|
||||
$Data = $this->WriteData( self :: STATISTIC, $Challenge . Pack( 'c*', 0x00, 0x00, 0x00, 0x00 ) );
|
||||
|
||||
if( !$Data )
|
||||
{
|
||||
throw new MinecraftQueryException( 'Failed to receive status.' );
|
||||
}
|
||||
|
||||
$Last = '';
|
||||
$Info = Array( );
|
||||
|
||||
$Data = SubStr( $Data, 11 ); // splitnum + 2 int
|
||||
$Data = Explode( "\x00\x00\x01player_\x00\x00", $Data );
|
||||
|
||||
if( Count( $Data ) !== 2 )
|
||||
{
|
||||
throw new MinecraftQueryException( 'Failed to parse server\'s response.' );
|
||||
}
|
||||
|
||||
$Players = SubStr( $Data[ 1 ], 0, -2 );
|
||||
$Data = Explode( "\x00", $Data[ 0 ] );
|
||||
|
||||
// Array with known keys in order to validate the result
|
||||
// It can happen that server sends custom strings containing bad things (who can know!)
|
||||
$Keys = Array(
|
||||
'hostname' => 'HostName',
|
||||
'gametype' => 'GameType',
|
||||
'version' => 'Version',
|
||||
'plugins' => 'Plugins',
|
||||
'map' => 'Map',
|
||||
'numplayers' => 'Players',
|
||||
'maxplayers' => 'MaxPlayers',
|
||||
'hostport' => 'HostPort',
|
||||
'hostip' => 'HostIp',
|
||||
'game_id' => 'GameName'
|
||||
);
|
||||
|
||||
foreach( $Data as $Key => $Value )
|
||||
{
|
||||
if( ~$Key & 1 )
|
||||
{
|
||||
if( !Array_Key_Exists( $Value, $Keys ) )
|
||||
{
|
||||
$Last = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$Last = $Keys[ $Value ];
|
||||
$Info[ $Last ] = '';
|
||||
}
|
||||
else if( $Last != false )
|
||||
{
|
||||
$Info[ $Last ] = mb_convert_encoding( $Value, 'UTF-8' );
|
||||
}
|
||||
}
|
||||
|
||||
// Ints
|
||||
$Info[ 'Players' ] = IntVal( $Info[ 'Players' ] );
|
||||
$Info[ 'MaxPlayers' ] = IntVal( $Info[ 'MaxPlayers' ] );
|
||||
$Info[ 'HostPort' ] = IntVal( $Info[ 'HostPort' ] );
|
||||
|
||||
// Parse "plugins", if any
|
||||
if( $Info[ 'Plugins' ] )
|
||||
{
|
||||
$Data = Explode( ": ", $Info[ 'Plugins' ], 2 );
|
||||
|
||||
$Info[ 'RawPlugins' ] = $Info[ 'Plugins' ];
|
||||
$Info[ 'Software' ] = $Data[ 0 ];
|
||||
|
||||
if( Count( $Data ) == 2 )
|
||||
{
|
||||
$Info[ 'Plugins' ] = Explode( "; ", $Data[ 1 ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$Info[ 'Software' ] = 'Vanilla';
|
||||
}
|
||||
|
||||
$this->Info = $Info;
|
||||
|
||||
if( empty( $Players ) )
|
||||
{
|
||||
$this->Players = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->Players = Explode( "\x00", $Players );
|
||||
}
|
||||
}
|
||||
|
||||
private function GetBedrockStatus( )
|
||||
{
|
||||
// hardcoded magic https://github.com/facebookarchive/RakNet/blob/1a169895a900c9fc4841c556e16514182b75faf8/Source/RakPeer.cpp#L135
|
||||
$OFFLINE_MESSAGE_DATA_ID = \pack( 'c*', 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78 );
|
||||
|
||||
$Command = \pack( 'cQ', 0x01, time() ); // DefaultMessageIDTypes::ID_UNCONNECTED_PING + 64bit current time
|
||||
$Command .= $OFFLINE_MESSAGE_DATA_ID;
|
||||
$Command .= \pack( 'Q', 2 ); // 64bit guid
|
||||
$Length = \strlen( $Command );
|
||||
|
||||
if( $Length !== \fwrite( $this->Socket, $Command, $Length ) )
|
||||
{
|
||||
throw new MinecraftQueryException( "Failed to write on socket." );
|
||||
}
|
||||
|
||||
$Data = \fread( $this->Socket, 4096 );
|
||||
|
||||
if( $Data === false )
|
||||
{
|
||||
throw new MinecraftQueryException( "Failed to read from socket." );
|
||||
}
|
||||
|
||||
if( $Data[ 0 ] !== "\x1C" ) // DefaultMessageIDTypes::ID_UNCONNECTED_PONG
|
||||
{
|
||||
throw new MinecraftQueryException( "First byte is not ID_UNCONNECTED_PONG." );
|
||||
}
|
||||
|
||||
if( \substr( $Data, 17, 16 ) !== $OFFLINE_MESSAGE_DATA_ID )
|
||||
{
|
||||
throw new MinecraftQueryException( "Magic bytes do not match." );
|
||||
}
|
||||
|
||||
// TODO: What are the 2 bytes after the magic?
|
||||
$Data = \substr( $Data, 35 );
|
||||
|
||||
// TODO: If server-name contains a ';' it is not escaped, and will break this parsing
|
||||
$Data = \explode( ';', $Data );
|
||||
|
||||
$this->Info =
|
||||
[
|
||||
'GameName' => $Data[ 0 ],
|
||||
'HostName' => $Data[ 1 ],
|
||||
'Unknown1' => $Data[ 2 ], // TODO: What is this?
|
||||
'Version' => $Data[ 3 ],
|
||||
'Players' => $Data[ 4 ],
|
||||
'MaxPlayers' => $Data[ 5 ],
|
||||
'Unknown2' => $Data[ 6 ], // TODO: What is this?
|
||||
'Map' => $Data[ 7 ],
|
||||
'GameMode' => $Data[ 8 ],
|
||||
'Unknown3' => $Data[ 9 ], // TODO: What is this?
|
||||
];
|
||||
$this->Players = null;
|
||||
}
|
||||
|
||||
private function WriteData( $Command, $Append = "" )
|
||||
{
|
||||
$Command = Pack( 'c*', 0xFE, 0xFD, $Command, 0x01, 0x02, 0x03, 0x04 ) . $Append;
|
||||
$Length = StrLen( $Command );
|
||||
|
||||
if( $Length !== FWrite( $this->Socket, $Command, $Length ) )
|
||||
{
|
||||
throw new MinecraftQueryException( "Failed to write on socket." );
|
||||
}
|
||||
|
||||
$Data = FRead( $this->Socket, 4096 );
|
||||
|
||||
if( $Data === false )
|
||||
{
|
||||
throw new MinecraftQueryException( "Failed to read from socket." );
|
||||
}
|
||||
|
||||
if( StrLen( $Data ) < 5 || $Data[ 0 ] != $Command[ 2 ] )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SubStr( $Data, 5 );
|
||||
}
|
||||
|
||||
private function ResolveSRV( &$Address, &$Port )
|
||||
{
|
||||
if( ip2long( $Address ) !== false )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$Record = dns_get_record( '_minecraft._tcp.' . $Address, DNS_SRV );
|
||||
|
||||
if( empty( $Record ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( isset( $Record[ 0 ][ 'target' ] ) )
|
||||
{
|
||||
$Address = $Record[ 0 ][ 'target' ];
|
||||
}
|
||||
}
|
||||
}
|
8
lib/MinecraftQueryException.php
Normal file
8
lib/MinecraftQueryException.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace xPaw;
|
||||
|
||||
class MinecraftQueryException extends \Exception
|
||||
{
|
||||
// Exception thrown by MinecraftQuery class
|
||||
}
|
1
maintence.txt
Normal file
1
maintence.txt
Normal file
|
@ -0,0 +1 @@
|
|||
0
|
211
query.php
Normal file
211
query.php
Normal file
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
/**
|
||||
* Connects to a minecraft server using the [Gamespy?] Query protocol.
|
||||
* more info on the protocol can be found at
|
||||
* http://wiki.vg/Query
|
||||
*
|
||||
* @copyright 2013 Chris Churchwell
|
||||
*
|
||||
*/
|
||||
class Query {
|
||||
|
||||
private $host;
|
||||
private $port;
|
||||
private $timeout;
|
||||
|
||||
private $token = null;
|
||||
|
||||
private $socket;
|
||||
|
||||
private $errstr = "";
|
||||
|
||||
const SESSION_ID = 2;
|
||||
|
||||
const TYPE_HANDSHAKE = 0x09;
|
||||
const TYPE_STAT = 0x00;
|
||||
|
||||
public function __construct($host, $port=25565, $timeout=3, $auto_connect = false) {
|
||||
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
$this->timeout = $timeout;
|
||||
|
||||
if (is_array($host))
|
||||
{
|
||||
$this->host = $host['host'];
|
||||
$this->port = empty($host['port'])?$port:$host['port'];
|
||||
$this->timeout = empty($host['timeout'])?$timeout:$host['timeout'];
|
||||
$auto_connect = empty($host['auto_connect'])?$auto_connect:$host['auto_connect'];
|
||||
}
|
||||
|
||||
if ($auto_connect === true) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the last error produced.
|
||||
*
|
||||
* @return String - Last error string.
|
||||
*/
|
||||
public function get_error() {
|
||||
return $this->errstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the current connection is established.
|
||||
*
|
||||
* @return boolean - True if connected; false otherwise.
|
||||
*/
|
||||
public function is_connected() {
|
||||
if (empty($this->token)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects!
|
||||
* duh
|
||||
*/
|
||||
public function disconnect() {
|
||||
if ($this->socket) {
|
||||
fclose($this->socket);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the host via UDP with the provided credentials.
|
||||
* @return boolean - true if successful, false otherwise.
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
$this->socket = fsockopen( 'udp://' . $this->host, $this->port, $errno, $errstr, $this->timeout );
|
||||
|
||||
if (!$this->socket)
|
||||
{
|
||||
$this->errstr = $errstr;
|
||||
return false;
|
||||
}
|
||||
|
||||
stream_set_timeout( $this->socket, $this->timeout );
|
||||
stream_set_blocking( $this->socket, true );
|
||||
|
||||
return $this->get_challenge();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with the host server and saves the authentication token to a class var.
|
||||
*
|
||||
* @return boolean - True if succesfull; false otherwise.
|
||||
*/
|
||||
private function get_challenge()
|
||||
{
|
||||
if (!$this->socket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//build packet to get challenge.
|
||||
$packet = pack("c3N", 0xFE, 0xFD, Query::TYPE_HANDSHAKE, Query::SESSION_ID);
|
||||
|
||||
//write packet
|
||||
if ( fwrite($this->socket, $packet, strlen($packet)) === FALSE) {
|
||||
$this->errstr = "Unable to write to socket";
|
||||
return false;
|
||||
}
|
||||
|
||||
//read packet.
|
||||
$response = fread($this->socket, 2056);
|
||||
|
||||
if (empty($response)) {
|
||||
$this->errstr = "Unable to authenticate connection";
|
||||
return false;
|
||||
}
|
||||
|
||||
$response_data = unpack("c1type/N1id/a*token", $response);
|
||||
|
||||
if (!isset($response_data['token']) || empty($response_data['token'])) {
|
||||
$this->errstr = "Unable to authenticate connection.";
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->token = $response_data['token'];
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the info from the server.
|
||||
*
|
||||
* @return boolean|array - Returns the data in an array, or false if there was an error.
|
||||
*/
|
||||
public function get_info()
|
||||
{
|
||||
if (!$this->is_connected()) {
|
||||
$this->errstr = "Not connected to host";
|
||||
return false;
|
||||
}
|
||||
//build packet to get info
|
||||
$packet = pack("c3N2", 0xFE, 0xFD, Query::TYPE_STAT, Query::SESSION_ID, $this->token);
|
||||
|
||||
//add the full stat thingy.
|
||||
$packet = $packet . pack("c4", 0x00, 0x00, 0x00, 0x00);
|
||||
|
||||
//write packet
|
||||
if (!fwrite($this->socket, $packet, strlen($packet))) {
|
||||
$this->errstr = "Unable to write to socket.";
|
||||
return false;
|
||||
}
|
||||
|
||||
//read packet header
|
||||
$response = fread($this->socket, 16);
|
||||
//$response = stream_get_contents($this->socket);
|
||||
|
||||
// first byte is type. next 4 are id. dont know what the last 11 are for.
|
||||
$response_data = unpack("c1type/N1id", $response);
|
||||
|
||||
//read the rest of the stream.
|
||||
$response = fread($this->socket, 2056);
|
||||
|
||||
//split the response into 2 parts.
|
||||
$payload = explode ( "\x00\x01player_\x00\x00" , $response);
|
||||
|
||||
$info_raw = explode("\x00", rtrim($payload[0], "\x00"));
|
||||
|
||||
//extract key->value chunks from info
|
||||
$info = array();
|
||||
foreach (array_chunk($info_raw, 2) as $pair) {
|
||||
list($key, $value) = $pair;
|
||||
//strip possible color format codes from hostname
|
||||
if ($key == "hostname") {
|
||||
$value = $this->strip_color_codes($value);
|
||||
}
|
||||
$info[$key] = $value;
|
||||
}
|
||||
|
||||
//get player data.
|
||||
$players_raw = rtrim($payload[1], "\x00");
|
||||
$players = array();
|
||||
if (!empty($players_raw)) {
|
||||
$players = explode("\x00", $players_raw);
|
||||
}
|
||||
|
||||
//attach player data to info for simplicity
|
||||
$info['players'] = $players;
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears Minecraft color codes from a string.
|
||||
*
|
||||
* @param String $string - the string to remove the codes from
|
||||
* @return String - a clean string.
|
||||
*/
|
||||
public function strip_color_codes($string) {
|
||||
return preg_replace('/[\x00-\x1F\x80-\xFF]./', '', $string);
|
||||
}
|
||||
|
||||
}
|
25
test.php
Normal file
25
test.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
require __DIR__ . '/lib/MinecraftPing.php';
|
||||
require __DIR__ . '/lib/MinecraftPingException.php';
|
||||
|
||||
use xPaw\MinecraftPing;
|
||||
use xPaw\MinecraftPingException;
|
||||
|
||||
try
|
||||
{
|
||||
$Query = new MinecraftPing( 'localhost', 25555 );
|
||||
|
||||
print_r( $Query->Query() );
|
||||
}
|
||||
catch( MinecraftPingException $e )
|
||||
{
|
||||
echo $e->getMessage();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if( $Query )
|
||||
{
|
||||
$Query->Close();
|
||||
}
|
||||
}
|
||||
?>
|
15
test1.php
Normal file
15
test1.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
require_once('query.php');
|
||||
|
||||
$server = new Query('localhost', 25555);
|
||||
//or new Query('some.minecraftserver.com', $port, $timeout);
|
||||
|
||||
if ($server->connect()){
|
||||
$info = $server->get_info();
|
||||
print_r($info);
|
||||
}
|
||||
else{
|
||||
echo "Server Down";
|
||||
}
|
||||
?>
|
21
test2.php
Normal file
21
test2.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
require __DIR__ . '/libsrc/MinecraftQuery.php';
|
||||
require __DIR__ . '/lib/MinecraftQueryException.php';
|
||||
|
||||
use xPaw\MinecraftQuery;
|
||||
use xPaw\MinecraftQueryException;
|
||||
|
||||
$Query = new MinecraftQuery( );
|
||||
|
||||
try
|
||||
{
|
||||
$Query->Connect( 'localhost', 25555 );
|
||||
|
||||
print_r( $Query->GetInfo( ) );
|
||||
print_r( $Query->GetPlayers( ) );
|
||||
}
|
||||
catch( MinecraftQueryException $e )
|
||||
{
|
||||
echo $e->getMessage( );
|
||||
}
|
||||
?>
|
Loading…
Reference in a new issue