28 Nov 2018

MC Server Whitelist

Hey guys!

A while ago I had set up a Minecraft server for myself and recently I decided to open it for use for the members of the guild that I am a member.

Now, me being me (read: lazy), I didn't want to set up a permissions system like most public servers as I trust the members of my guild to not do stupid things so instead I decided to implement the built-in whitelist system that Minecraft has.

This decision produced another issue: who is going to whitelist the new players when I'm offline?

The answer was to make a webpage where a person can enter in their Minecraft username, as well as a pre-determined password to allow said person to add themselves to the whitelist.

This turned out to be stupidly easy, as the init script I use for my Minecraft servers allow you to send commands to the console via the script in the linux shell.

So, enter phpseclib! Phpseclib includes an SSH2 library which allows you to do a whole host of things as you normally would via SSH.

Anyways, here's the gist of how I put this page together.

First, I pulled the theme setup for all the pages of my site (standardisation FTW!) and put them into a php file. Next, I set the includes for both the phpseclib SSH2 library as well as a config file for all of those lovely variables, just so that if I ever want to set this up for another server, just copy-pasta the whole thing and change what's required in the config file.

So the top of my index.php now looks like this:

<?php

    set_include_path("./inc/");

    include("Net/SSH2.php");
    include("config.inc.php");

    $ssh = new Net_SSH2($GLOBALS["ssh_hostname"]);
    if (!$ssh->login($GLOBALS["ssh_username"], $GLOBALS["ssh_password"])) {
        exit("Login Failed");
    }

Next up, I put in what happens when the form on the page is submitted via POST.

    //Form action - do ALL THE THINGS
    if(isset($_POST["mcusername"])) {
        if($_POST["formpassword"] == $GLOBALS["formpassword"]) {
            $mcuuidraw = file_get_contents("http://api.mcusername.net/playertouuid/" . $_POST["mcusername"]);
            if($mcuuidraw == "Not Premium") {
                $msg = "Bad MC Username";
                $msgtype = "danger";
                } else {
                $mcuuidst1 = substr_replace($mcuuidraw, '-', 8, 0);
                $mcuuidst2 = substr_replace($mcuuidst1, '-', 13, 0);
                $mcuuidst3 = substr_replace($mcuuidst2, '-', 18, 0);                    
                $mcuuid = substr_replace($mcuuidst3, '-', 23, 0);
                $ssh->exec($GLOBALS["ssh_init"] . " command whitelist add " . $_POST["mcusername"]);
                $ssh->exec($GLOBALS["ssh_init"] . " command whitelist reload");
                $msg = "Successfully  added <strong>" . $_POST["mcusername"] . "</strong> to the whitelist with UUID of <strong>" . $mcuuid . "</strong>";
                $msgtype = "success";
            }
            } else {
            $msg = "Bad whitelist password";
            $msgtype = "danger";
        }
    }

To explain what's happening here, first it checks to see if the form field mcusername was submitted and if it was then it checks the pre-set password in formpassword. If the form password is good, then it goes on to check if the Minecraft username entered is both correct and a paid account, as my server (all of my servers, actually) run on online mode. If everything checks out, it then goes on to grab the UUID of the entered Minecraft username and formats it into readable form for use in a bootstrap alert div later on - well pretty much if the $msg variable is set, it will generate the alert div, colour it based on $msgtype and print the message contained in $msg.

Just to show what the alert div looks like:

        <div class="container">
            <?php if( isset($msg) ):
                echo ('<div class="alert alert-'.$msgtype.'"><center>'.$msg.'</center></div>');
            endif; ?>
        </div>

Now here's the form code:

        <div class="container">
            <form class="form-horizontal" method="post" name="whitelist_add" action="">
                <fieldset>
                    <legend>MC Server Whitelist Control - Add User</legend>
                    <div class="form-group">
                        <label for="mcusername" class="col-lg-2 control-label">MC Username</label>
                        <div class="col-lg-10">
                            <input type="text" class="form-control" id="mcusername" name="mcusername" placeholder="Your MC Username" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="formpassword" class="col-lg-2 control-label">Whitelist Password</label>
                        <div class="col-lg-10">
                            <input type="password" class="form-control" id="formpassword" name="formpassword" placeholder="Whitelist password" required>
                        </div>
                    </div>                    
                    <div class="form-group">
                        <div class="col-lg-10 col-lg-offset-2">
                            <button type="submit" class="btn btn-primary">Add to whitelist</button>
                        </div>
                    </div>
                </fieldset>
            </form>
        </div>

At this stage, I was pretty much done - I was now able to enter in a MC username as well as the pre-set form password and it went through and added the user to the whitelist.

However, I now wanted to take it a step further by showing who was already added to the whitelist - both as a show-off type thing but also as a way to see if you need to fill the form out or not.

This was solved by phpseclib again, the SFTP library.

So first up, include the SFTP library, as well as the other required bits for it and the top of my index.php now looks like this:

<?php

    set_include_path("./inc/");

    include("Net/SSH2.php");
    include("Net/SFTP.php");
    include("config.inc.php");

    $ssh = new Net_SSH2($GLOBALS["ssh_hostname"]);
    if (!$ssh->login($GLOBALS["ssh_username"], $GLOBALS["ssh_password"])) {
        exit("Login Failed");
    }

    $sftp = new Net_SFTP($GLOBALS["ssh_hostname"]);
    if (!$sftp->login($GLOBALS["ssh_username"], $GLOBALS["ssh_password"])) {
        exit("Login Failed");
    }

Now let's grab the whitelist.json and pull all the names into an array:

    //Get currently whitelisted peoples
    $whitelistfile = $sftp->get($GLOBALS["ssh_dir"]."whitelist.json");
    if($whitelistfile == "[]") {
        exit("No users whitelisted");
    } else {
        $whitelistraw = json_decode($whitelistfile);
        foreach($whitelistraw as $obj) {
            $whitelisted[] = $obj->name;
        }
    }

Then create our table:

        <div class="container">
            <legend>Currently Whitelisted Users</legend>
            <table class="table table-striped">
                <tbody>
                    <?php if(isset($whitelisted)) { ?>
                    <?php foreach($whitelisted as $a): ?>
                        <tr>
                            <td class="vert-align">
                                <img src="https://minotar.net/helm/<?php echo($a); ?>/30.png">
                                <?php 
                                    echo($a);
                                ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    <?php } else { ?>
                        <tr>
                            <td class="vert-align">
                                No whitelisted users found.
                            </td>
                        </tr>
                    <?php } ?>
                </tbody>
            </table>
        </div>

Now whenever the page loads, it will pull the whitelist from the Minecraft server, grab all the names and display them in a table.

So we're done, right? Well, not quite - as there was going to eventually be multiple OPs on the server, I wanted to be able to identify in the displayed whitelisted users who was an OP.

This bit was trivially easy compared to the rest of this project.

First, grab the ops.json (this is underneath where we grab the whitelist.json):

    //Get Operators
    $opsfile = $sftp->get($GLOBALS["ssh_dir"]."ops.json");
    if($opsfile == "[]") {
        exit("No OPS defined");
    } else {
        $opsraw = json_decode($opsfile);
        foreach($opsraw as $obj) {
            $ops[] = $obj->name;
        }
    }

Then compare the names in the $ops to the names in $whitelisted:

        <div class="container">
            <legend>Currently Whitelisted Users</legend>
            <table class="table table-striped">
                <tbody>
                    <?php if(isset($whitelisted)) { ?>
                    <?php foreach($whitelisted as $a): ?>
                        <tr>
                            <td class="vert-align">
                                <img src="https://minotar.net/helm/<?php echo($a); ?>/30.png">
                                <?php 
                                    echo($a);
                                    foreach($ops as $b) {
                                        if($b == $a) {
                                            echo(" <span class='glyphicon glyphicon-certificate' />");
                                        } else {
                                        }
                                    }
                                ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    <?php } else { ?>
                        <tr>
                            <td class="vert-align">
                                No whitelisted users found.
                            </td>
                        </tr>
                    <?php } ?>
                </tbody>
            </table>
        </div>

As a finisher (for this post anyways), here is what the config.inc.php file looks like sans production values (change to whatever you need):

<?php   // /inc/config.inc.php

    $GLOBALS["ssh_hostname"] = "IP/HOSTNAME";
    $GLOBALS["ssh_username"] = "LINUX USERNAME";
    $GLOBALS["ssh_password"] = "LINUX PASSWORD";

    $user_dir = "/home/".$GLOBALS["ssh_username"]."/";
    $mc_dir = $user_dir . "SERVERDIR/";

    $GLOBALS["ssh_init"] = $mc_dir . "init.sh";
    $GLOBALS["ssh_dir"] = $mc_dir;
    $GLOBALS["formpassword"] = "FORM PASSWORD";

?>

Before you fly off the handle saying "YOU'VE STORED THE PASSWORD IN PLAIN TEXT!!!!"- yes I have, as this was mainly an experiment in can I do this whole shebang. I may end up using SHA or something of like later down the track, but for all intents and purposes I've finshed up with this project.

Here's the full index.php:

<?php

    set_include_path("./inc/");

    include("Net/SSH2.php");
    include("Net/SFTP.php");
    include("config.inc.php");

    $ssh = new Net_SSH2($GLOBALS["ssh_hostname"]);
    if (!$ssh->login($GLOBALS["ssh_username"], $GLOBALS["ssh_password"])) {
        exit("Login Failed");
    }

    $sftp = new Net_SFTP($GLOBALS["ssh_hostname"]);
    if (!$sftp->login($GLOBALS["ssh_username"], $GLOBALS["ssh_password"])) {
        exit("Login Failed");
    }

    //Form action - do ALL THE THINGS
    if(isset($_POST["mcusername"])) {
        if($_POST["formpassword"] == $GLOBALS["formpassword"]) {
            $mcuuidraw = file_get_contents("http://api.mcusername.net/playertouuid/" . $_POST["mcusername"]);
            if($mcuuidraw == "Not Premium") {
                $msg = "Bad MC Username";
                $msgtype = "danger";
                } else {
                $mcuuidst1 = substr_replace($mcuuidraw, '-', 8, 0);
                $mcuuidst2 = substr_replace($mcuuidst1, '-', 13, 0);
                $mcuuidst3 = substr_replace($mcuuidst2, '-', 18, 0);                    
                $mcuuid = substr_replace($mcuuidst3, '-', 23, 0);
                $ssh->exec($GLOBALS["ssh_init"] . " command whitelist add " . $_POST["mcusername"]);
                $ssh->exec($GLOBALS["ssh_init"] . " command whitelist reload");
                $msg = "Successfully  added <strong>" . $_POST["mcusername"] . "</strong> to the whitelist with UUID of <strong>" . $mcuuid . "</strong>";
                $msgtype = "success";
            }
            } else {
            $msg = "Bad whitelist password";
            $msgtype = "danger";
        }
    }

    //Get currently whitelisted peoples
    $whitelistfile = $sftp->get($GLOBALS["ssh_dir"]."whitelist.json");
    if($whitelistfile == "[]") {
        exit("No users whitelisted");
    } else {
        $whitelistraw = json_decode($whitelistfile);
        foreach($whitelistraw as $obj) {
            $whitelisted[] = $obj->name;
        }
    }

    //Get Operators
    $opsfile = $sftp->get($GLOBALS["ssh_dir"]."ops.json");
    if($opsfile == "[]") {
        exit("No OPS defined");
    } else {
        $opsraw = json_decode($opsfile);
        foreach($opsraw as $obj) {
            $ops[] = $obj->name;
        }
    }    
?>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Wok's Place | MC Whitelist</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <link rel="stylesheet" href="./css/main.css" media="screen">
        <link rel="stylesheet" href="./css/custom.min.css">
        <link rel="icon" type="image/ico" href="./img/favicon.ico">
    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="http://mcw.wok.wtf">Wok's Place | MC Whitelist</a>
                </div>

                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">

                        <li><a target="_blank" href="http://l.wok.wtf/y"><img height="16" width="16" src="http://wok.wtf/img/youtube.ico"> YouTube</a></li>
                        <li class="divider"></li>                                
                        <li><a target="_blank" href="http://l.wok.wtf/s"><img height="16" width="16" src="http://wok.wtf/img/steam.ico"> Steam</a></li>
                        <li class="divider"></li>                                
                        <li><a target="_blank" href="http://l.wok.wtf/t"><img height="16" width="16" src="http://wok.wtf/img/twit.ico"> Twitter</a></li>
                        <li><a target="_blank" href="http://l.wok.wtf/p"><img height="16" width="16" src="http://wok.wtf/img/player.ico"> Player.ME</a></li>
                        <li><a target="_blank" href="http://l.wok.wtf/g"><img height="16" width="16" src="http://wok.wtf/img/gp.ico"> Google+</a></li>
                        <li><a target="_blank" href="http://l.wok.wtf/pb"><img height="16" width="16" src="http://wok.wtf/img/pastebin.png"> Pastebin</a></li>                        

                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        <li class=""><a href="http://wok.wtf">Home</a></li>
                        <li class=""><a href="http://wok.wtf/blog">Blog</a></li>
                        <li class=""><a href="http://files.wok.wtf">Files</a></li>
                        <li class=""><a target="_blank" href="http://git.wok.wtf/u/Wok">Git</a></li>
                    </ul>
                </div>
            </div>
        </nav>

        <div class="container">
            <?php if( isset($msg) ):
                echo ('<div class="alert alert-'.$msgtype.'"><center>'.$msg.'</center></div>');
            endif; ?>
        </div>
        <br>
        <div class="container"> 
            <h1>WokPack Server Whitelist Control</h1><br>
            Here you will be able to add your username to the whitelist for the WokPack server, as long as you have been given the "Whitelist Password".<br><br>
            Please make sure you aren't already added to the list before attempting to add yourself.<br><br>
        </div>
        <div class="container">
            <form class="form-horizontal" method="post" name="whitelist_add" action="">
                <fieldset>
                    <legend>MC Server Whitelist Control - Add User</legend>
                    <div class="form-group">
                        <label for="mcusername" class="col-lg-2 control-label">MC Username</label>
                        <div class="col-lg-10">
                            <input type="text" class="form-control" id="mcusername" name="mcusername" placeholder="Your MC Username" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="formpassword" class="col-lg-2 control-label">Whitelist Password</label>
                        <div class="col-lg-10">
                            <input type="password" class="form-control" id="formpassword" name="formpassword" placeholder="Whitelist password" required>
                        </div>
                    </div>                    
                    <div class="form-group">
                        <div class="col-lg-10 col-lg-offset-2">
                            <button type="submit" class="btn btn-primary">Add to whitelist</button>
                        </div>
                    </div>
                </fieldset>
            </form>
        </div>
        <br>
        <div class="container">
            <legend>Currently Whitelisted Users</legend>
            <table class="table table-striped">
                <tbody>
                    <?php if(isset($whitelisted)) { ?>
                    <?php foreach($whitelisted as $a): ?>
                        <tr>
                            <td class="vert-align">
                                <img src="https://minotar.net/helm/<?php echo($a); ?>/30.png">
                                <?php 
                                    echo($a);
                                    foreach($ops as $b) {
                                        if($b == $a) {
                                            echo(" <span class='glyphicon glyphicon-certificate' />");
                                        } else {
                                        }
                                    }
                                ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    <?php } else { ?>
                        <tr>
                            <td class="vert-align">
                                No whitelisted users found.
                            </td>
                        </tr>
                    <?php } ?>
                </tbody>
            </table>
        </div>        
        <footer>
            <div class="container">
                <hr>
                <p class="text-muted">&copy; 2016 Warrick (Wok) Harding</p>
            </div>
        </footer>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
        <script src="./js/main.js"></script>
        <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
        <script src="./js/ie10-viewport-bug-workaround.js"></script>
    </body>
</html>

EDIT: As an afterthought, here's the init script I use for my Minecraft servers:

#!/bin/bash

### BEGIN INIT INFO
# Provides:   MinecraftServer
# Required-Start: $local_fs $remote_fs
# Required-Stop:  $local_fs $remote_fs
# Should-Start:   $network
# Should-Stop:    $network
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description:    Minecraft Server
# Description:    Minecraft Server
### END INIT INFO

#Settings
SERVICE='server-name.jar'
OPTIONS='nogui'
USERNAME='linux-username'
SCREENNAME='screen-name'
WORLD='world'
MCPATH='/home/linux-username/server-name'
BACKUPPATH='/home/linux-username/server-name-Backups'
MAXHEAP=4096
MINHEAP=1024
CPU_COUNT=8
INVOCATION="java -server -Xms${MINHEAP}M -Xmx${MAXHEAP}M -d64 -XX:UseSSE=4 -XX:+UseParNewGC -XX:+CMSIncrementalPacing -XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=${CPU_COUNT} -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10 -jar $SERVICE $OPTIONS"

ME=`whoami`
as_user() {
  if [ $ME == $USERNAME ] ; then
    bash -c "$1"
  else
    su - $USERNAME -c "$1"
  fi
}

mc_start() {
  if  pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    echo "$SERVICE is already running!"
  else
    echo "Starting $SERVICE..."
    cd $MCPATH
    as_user "cd $MCPATH && screen -h $HISTORY -dmSL $SCREENNAME $INVOCATION"
    sleep 7
    if pgrep -u $USERNAME -f $SERVICE > /dev/null
    then
      echo "$SERVICE is now running."
    else
      echo "Error! Could not start $SERVICE!"
    fi
  fi
}

mc_saveoff() {
  if pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    echo "$SERVICE is running... suspending saves"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"say SERVER BACKUP STARTING. Server going readonly...\"\015'"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"save-off\"\015'"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"save-all\"\015'"
    sync
    sleep 10
  else
    echo "$SERVICE is not running. Not suspending saves."
  fi
}

mc_saveon() {
  if pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    echo "$SERVICE is running... re-enabling saves"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"save-on\"\015'"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"say SERVER BACKUP ENDED. Server going read-write...\"\015'"
  else
    echo "$SERVICE is not running. Not resuming saves."
  fi
}

mc_stop() {
  if pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    echo "Stopping $SERVICE"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"say SERVER SHUTTING DOWN IN 10 SECONDS. Saving map...\"\015'"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"save-all\"\015'"
    sleep 10
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"stop\"\015'"
    sleep 7
  else
    echo "$SERVICE was not running."
  fi
  if pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    echo "Error! $SERVICE could not be stopped."
  else
    echo "$SERVICE is stopped."
  fi
}

mc_backup() {
   echo "Disabling saves..."
   mc_saveoff
   NOW=`date "+%Y-%m-%d_%Hh%M"`
   BACKUP_FILE="$BACKUPPATH/${WORLD}_${NOW}.tar"
   as_user "cd $MCPATH && tar -cf $BACKUP_FILE $MCPATH"
   echo "Backing up and compressing..."
   mc_saveon
   echo "Enabling saves..."
   echo "Done."
}

mc_command() {
  command="$1";
  if pgrep -u $USERNAME -f $SERVICE > /dev/null
  then
    pre_log_len=`wc -l "$MCPATH/logs/latest.log" | awk '{print $1}'`
    echo "$SERVICE is running... executing command"
    as_user "screen -p 0 -S $SCREENNAME -X eval 'stuff \"$command\"\015'"
    sleep .1 # assumes that the command will run and print to the log file in less than .1 seconds
    # print output
    tail -n $[`wc -l "$MCPATH/logs/latest.log" | awk '{print $1}'`-$pre_log_len] "$MCPATH/logs/latest.log"
  fi
}

#Start-Stop here
case "$1" in
  start)
    mc_start
    ;;
  stop)
    mc_stop
    ;;
  restart)
    mc_stop
    mc_start
    ;;
  backup)
    mc_backup
    ;;
  status)
    if pgrep -u $USERNAME -f $SERVICE > /dev/null
    then
      echo "$SERVICE is running."
    else
      echo "$SERVICE is not running."
    fi
    ;;
  command)
    if [ $# -gt 1 ]; then
      shift
      mc_command "$*"
    else
      echo "Must specify server command (try 'help'?)"
    fi
    ;;

  *)
  echo "Usage: $0 {start|stop|backup|status|restart|command \"server command\"}"
  exit 1
  ;;
esac

exit 0

Anyways guys, I hope you learned something from this,

--Wok

Previous Post