Commit ce68df87 authored by Imran Hussain's avatar Imran Hussain

Merge branch 'API' into 'master'

Major Rebuild: AJAX and working game info

Changes the system to use AJAX to keep the user logged in (if javascript is enabled).
Still has noscript support.

Also correctly displays game information for Minecraft & Sauerbraten (as opposed to the palce holder data there now) and makes it easy to add more games

See merge request !1
parents 14d52337 6d4aebc1
SUCS Game Server Auth System v2
===============================
imranh@sucs.org
ripp_@sucs.org
What is it?
-----------
An authentication system to ensure only SUCS members and plus whoever we want
An authentication system to ensure only SUCS members and plus whoever we want
can connect and play games on the game server.
How's it work?
--------------
It's written in php and it's done in the style of a SPA. If a member wishes
to connect to a game, they visit games.sucs.org, enter their SUCS username +
It's written in php and it's done in the style of a SPA. If a member wishes
to connect to a game, they visit games.sucs.org, enter their SUCS username +
password, and they are then granted access to the server.
The page uses a HTTP Refresh: header with a timeout of 30 seconds to keep them
logged in. this timeout can be fiddeled with in index.php and in
gameauth-task.php
The page uses AJAX to keep them logged in (or a HTTP Refresh header if
javascript is disabled)
The timeout can be fiddeled with in index.php, refresh.js and gameauth-task.php
Every time the page is accessed, the member's entry in a sqlite db is updated
and a hole poked in the firewall on the game server for their IP (if there's
not already a hole there)
Every time the page is accessed (or AJAX posted to endpoint.php), the member's
entry in a sqlite db is updated and a hole poked in the firewall on the game
server for their IP (if there's not already a hole there)
How to add new game information?
--------------------------------
See the file in games for an idea of how it works.
How does it know when a user times out?
---------------------------------------
......@@ -42,8 +47,8 @@ disconnects, it won't affect the other user.
What if we want to open the server to the world for a special event?
--------------------------------------------------------------------
Currently we are restricting this to only allow Swansea University students,
rename the uni.deny file in /home/game-server to uni.allow to allow SUCS +
Swansea Univerity students and rename it back to uni.deny only allow SUCS
rename the uni.deny file in /home/game-server to uni.allow to allow SUCS +
Swansea Univerity students and rename it back to uni.deny only allow SUCS
members.
Why was it rewritten?
......@@ -52,4 +57,4 @@ The old system didn't work.
What is wrong with Apache LDAP Auth?
------------------------------------
It doesn't give us the ability to customise the login form.
\ No newline at end of file
It doesn't give us the ability to customise the login form.
<?php
/* Functions in this file:
login($username,$passoword) - checks the credentials aganist sucs & uni ldaps
authCheck($authd,$username) - checks the username and authd area again ban & allow flags
renew() - checks the players session and tries to renew if allowed
logout() - removes the session from the database and clears the cookie
Login Flow:
call login passing it the username and password
This will call the ldapAuth function included from ldap-auth.php
It when then call authCheck with the info to see what the user can do
If they are allowed access they are then inserted into the database
Renew Flow:
call renew, it has no arguments as it works off session_id
Firstly it gets the username and which ldap server they authd aganist from the database
Then it calls authCheck with the retrived data to check what they can still do
If they are still allowed acess the timeout their database entry is update
Otherwise they are removed from the database
Logout Flow:
call logout, it has no arguments as it works off session_id
It remvoes the user's database entry
Then destroys the session logging them out fully
*/
include('ldap-auth.php');
error_reporting(E_ERROR);
session_start();
$DB_PATH = "/tmp/gameauth.db";
$DB_CON;
if (!file_exists($DB_PATH)){
$DB_CON = new SQLite3($DB_PATH);
$DB_CON->exec("CREATE TABLE gamers
(
username TEXT PRIMARY KEY NOT NULL,
sessionid TEXT NOT NULL,
IP TEXT NOT NULL,
authd TEXT NOT NULL,
lastseen INT NOT NULL
)"
);
$DB_CON->exec("CREATE TABLE bans
(
username TEXT PRIMARY KEY NOT NULL,
reason TEXT
)"
);
} else {
$DB_CON = new SQLite3($DB_PATH);
}
function sqlite3Exists($table,$col,$val){
global $DB_CON;
$query = $DB_CON->prepare("SELECT 1 FROM $table WHERE $col = :val LIMIT 1");
$query->bindValue(':val', $val);
return (bool) $query->execute()->fetchArray();
}
function sqlite3Exec($query){
global $DB_CON;
return $DB_CON->query($query);
}
//Checks how authed the user is and returns an obejct describing it
function authCheck($authd,$username){
//Not a valid user
if ($authd != "sucs" && $authd != "uni"){
return [
level => "NO_LOGIN",
loginError => "BAD_LOGIN"
];
}
//Check if they are banned
if (sqlite3Exists("bans","username",$username)){
return [
level => "NO_GAMES",
loginError => "BANNED"
];
}
//if they are sucs they are always allowed on
//or if the uniAllowPath is there (since they will then be uni students)
if ($authd == "sucs" || file_exists($uniAllowFilePATH)) {
$accessLevel = "GAME_ACCESS";
} else {
//Otherwise they get no games.
$accessLevel = "NO_GAMES";
$failReason = "UNI_DISALLOWED";
}
return [
level => $accessLevel,
loginError => $failReason
];
}
function login($username,$password){
//Check to make sure we have a username and password
if ($username == "" || $password == "") {
return [
level => "NO_LOGIN",
loginError => "MISSING_USERNAME_OR_PASSWORD"
];
};
//Auth the user
$authd = ldapAuth($username,$password);
//If they logged in with a email we will detect it and string out username
if(filter_var($username, FILTER_VALIDATE_EMAIL)){
//Split the email using "@" as a delimiter
$s = explode("@",$username);
//Remove the domain (last element), then recombine it
array_pop($s);
$username = implode("@",$s);
}
$username = strtolower();
$authResult = authCheck($authd,$username);
//If they gave a good login
if($authResult["level"] == "GAME_ACCESS"){
//Add them into the database
$sessionid = session_id();
$cip = $_SERVER['REMOTE_ADDR'];
$time = time();
sqlite3Exec("DELET FROM gamers WHERE username='$username'");
sqlite3Exec("INSERT INTO gamers (username,sessionid,IP,authd,lastseen) VALUES ('$authdUser','$sessionid','$cip','$authd','$time')");
}
//Return the authResult
return $authResult;
}
function renew(){
$sessionid = session_id();
if (sqlite3Exists("gamers","sessionid",$sessionid)){
$query = sqlite3Exec("SELECT authd,username FROM gamers WHERE sessionid='$sessionid';");
$row = $query->fetchArray(SQLITE3_NUM);
$authd = $row[0];
$username = $row[1];
$authResult = authCheck($authd,$username);
//Check their login is still good and update if so
if($authResult["level"] == "GAME_ACCESS"){
$time = time();
sqlite3Exec("UPDATE gamers SET lastseen='$time' WHERE sessionid='$sessionid'");
} else {
//If it's bad (maybe they have been banned?) delete it and return an error.
sqlite3Exec("DELETE FROM gamers WHERE sessionid='$sessionid'");
return $authResult;
}
return $authResult;
} else {
return [
level => "NO_LOGIN",
loginError => "TIMEOUT"
];
}
}
function logout(){
$sessionid = session_id();
sqlite3Exec("DELETE FROM gamers WHERE sessionid='$sessionid'");
session_destroy();
return [
level => "NO_LOGIN",
loginError => null
];
}
function serverData(){
chdir('games');
include '_manager.php';
return getGameStatus();
}
?>
<?php
include 'controll_2.php';
$username = $_POST["username"];
$password = $_POST["password"];
$renew = $_POST["renew"];
$logout = $_POST["logout"];
$response;
if($renew){
$response = renew();
} else if ($logout){
$response = logout();
} else {
$response = login($username,$password);
}
chdir('games');
include '_manager.php';
$response["extraPayload"] = getGameStatus();
header('Content-Type: application/json');
echo json_encode($response);
?>
......@@ -19,7 +19,7 @@ if (!file_exists($dbPATH)){
username TEXT PRIMARY KEY NOT NULL,
sessionid TEXT NOT NULL,
IP TEXT NOT NULL,
type TEXT NOT NULL,
authd TEXT NOT NULL,
lastseen INT NOT NULL
)"
);
......
<?php
$GAMES_TO_INCLUDE = [
"minecraft",
"sauerbraten"
];
function getGameStatus($templateHeader = FALSE,$templateFooter = FALSE){
global $GAMES_TO_INCLUDE;
$gameInfo = [];
foreach ($GAMES_TO_INCLUDE as $game){
include "$game.php";
$a = "$game\\getInfo";
$thisGame = @$a();
$gameInfo[$game] = $thisGame;
if ($templateHeader !== FALSE){
echo $templateHeader;
echo "<div data-target=\"$game\">";
echo preg_replace_callback("/{{([^|]*)\|([^}]*)}}/",function ($matches) use ($thisGame){
$elem = $matches[1];
$key = $matches[2];
$val = $thisGame[$key];
if ($val === undefined) {$val="??";}
if ($key == "_online"){
if($val){
$val = "<span style='color:green'>Online</span>";
} else {
$val = "<span style='color:red'>Offline</span>";
}
} else if (is_array($val)){
$nVal = "";
foreach($val as $e){
$nVal .= "<li>$e</li>";
}
$val = $nVal;
}
return "<$elem data-target='$key'>$val</$elem>";
},file_get_contents("$game.html"));
echo "</div>";
echo $templateFooter;
}
}
return $gameInfo;
}
?>
<h2>Minecraft (Tekkit) - {{span|_online}}</h2>
<dl>
<dt>Players:</dt>
<dd>
{{span|players_on}}/{{span|players_max}}
</dd>
<dt>Description:</dt>
<dd>
{{span|description}}
</dd>
<dt>Version:</dt>
<dd>
{{span|version}}
</dd>
</dl>
<p>
<a target="_blank" href="http://games.sucs.org/tekkit-dynmap">Live Map</a>
</p>
<?php namespace minecraft;
function getInfo(){
$ADDRESS = "games";
$PORT = 25565;
if (($sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP)) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
if(socket_connect($sock,$ADDRESS,$PORT) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
//Documentation on the ping protcol: http://wiki.vg/Server_List_Ping
// Size Protcol Version Next State
// _|_ Id_ _|_ _Address____ _Port__ _|_ _Request Packet
$input = "\x0f\x00\x2f\x09127.0.0.1\x63\xdd\x01\x01\x00";
socket_write($sock,$input,strlen($input));
//Read an inital response from the socket
//This will let us get an initial size.
$out = socket_read($sock,8);
//Get the length of the packet
$t = readVarint($out);
$out = $t["data"];
$len = $t["res"];
//Keep reading from the socket
$toRead = $len - strlen($out);
while($toRead){
$out .= socket_read($sock,$toRead);
$toRead = $len - strlen($out);
}
socket_close($sock);
if ( substr($out,0,1) !== "\x00"){
return ["online"=>false,"error"=>"server sent unexpected result"];
}
$t = readVarint(substr($out,1));
$out = $t["data"];
$len = $t["res"];
$result = json_decode($out,$assoc=true);
if (!is_array($result)){
return ["online"=>true,"error"=>$result];
}
$players = [];
if ($result["players"]["sample"]){
foreach ($result["players"]["sample"] as $p){
$players[] = $p["name"];
}
}
return [
"_online"=>true,
"description"=>$result["description"],
"players_on"=>$result["players"]["online"],
"players_max"=>$result["players"]["max"],
"version"=>$result["version"]["name"]
];
}
function readVarint($data){
$result = 0;
$first = true;
for($i=0;$i<strlen($data);$i++){
$part = ord(substr($data,$i,1));
$result |= ($part & 0x7F) << 7 * $i;
if (($part & 0x80) == 0){
break;
}
}
return ["res"=>$result,"data"=>substr($data,$i+1)];
}
?>
0 <?php namespace openTTD;
function getInfo(){
$ADDRESS = "games.sucs.org";
$PORT = 3979;
if (($sock = socket_create(AF_INET,SOCK_DGRAM,0)) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
if(socket_connect($sock,$ADDRESS,$PORT) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
socket_write($sock,"\x03\x00\x00",3);
$out = socket_read($sock,2048);
socket_close($sock);
$length = unpack("v",substr($out,0,2))[0];
$info1 = unpack("CCC",substr($out,2,3));
$packetId = $info1[0];
if ($packetId != 1){
return ["online"=>false,"error"=>"server sent unexpected response"];
}
$protocolVersion = $info1[1];
if ($protocolVersion != 4) {
return ["online"=>false,"error"=>"server sent unsported packet version"];
}
$grfsCount = $info1[2];
$data = substr($out,5);
$grfs = readGrfs($data,$grfsCount);
$info2 = unpack("VVCCC",substr($data,0,7));
$gameDate = $info2[0];
$startDate = $info2[1];
$companiesMax = $info2[2];
$companiesOn = $info2[3];
$spectatorsMax = $info2[4];
$partsT = explode("\x00",substr($data,7));
$serverName = $partsT[0];
$serverRevision = $partsT[1];
$info3 = unpack("CCCCC",substr($partsT[2]));
$serverLang = $info3[0];
$passworded = $info3[1];
$clientsMax = $info3[2];
$clientsOn = $info3[3];
$spectatorsOn = $info3[4];
$mapName = $partsT[3];
$info4 = unpack("vvCC",$partsT[4]);
$mapWidth = $info4[0];
$mapHeight = $info4[1];
$mapSet = $info4[2];
$dedicated = $info4[3];
return [
"online"=>true,
"description"=>$serverName,
"map"=>$mapName,
"players"=>[
"current"=>$clientsOn,
"max"=>$clientsMax
]
];
}
function readGrfs(&$data,$number){
$rtn = [];
for($i=0;$i<number;$i++){
$rtn[] = unpack("VB128",substr($data,i*130,130));
}
$data = substr($data,$number*130);
return $rtn;
}
echo json_encode(getInfo());
?>
<h2>Sauerbraten - {{span|_online}}</h2>
<dl>
<dt>Players:</dt>
<dd>
{{span|players_on}}/{{span|players_max}}
</dd>
<dt>Game Mode:</dt>
<dd>
{{span|gameMode}}
</dd>
<dt>Map:</dt>
<dd>
{{span|map}}
</dd>
</dl>
<?php namespace sauerbraten;
function getInfo(){
$ADDRESS = "games.sucs.org";
$PORT = 28786;
if (($sock = socket_create(AF_INET,SOCK_DGRAM,0)) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
if(socket_connect($sock,$ADDRESS,$PORT) === false){
return ["online"=>false,"error"=>socket_strerror(socket_last_error($sock))];
}
$reqId = "GI\n";
socket_write($sock,$reqId,3);
$out = socket_read($sock,2048);
socket_close($sock);
//Check the response starts with the request Id
if ( substr($out,0,strlen($reqId)) !== $reqId){
return ["online"=>false,"error"=>"server sent unexpected response"];
}
//Chop off request Id
$out = substr($out,strlen($reqId));
//Read players & number of attributes
$numPlayers = readInt($out);
$numAttrs = readInt($out);
//Rad the basic version
$protcolVersion = readInt($out);
$gameMode = readInt($out);
$timeLeft = readInt($out);
$maxClients = readInt($out);
$masterMode = readInt($out);
//Check if we have 7 attributes
if ($numAttrs == 7){
//If we do we know if the game is paused and the speed
$gamePaused = readInt($out);
$gameSpeed = readInt($out);
} else {
$gamePaused = 0;
$gameSpeed = 100;
}
//Everything else is strings, take the reminder and explode on null
$tmp = explode("\x00",$out);
//The first part is the map, the second is the desc
$mapName = $tmp[0];
$serverDesc = $tmp[1];
//Conversion from game mode id to string
$nnn = [
"Free for all",
"Coop Edit",
"Teamplay",
"Instagib",
"Instagib Team",
"Efficiency",
"Efficiency team",
"Tactics",
"Tactics team",
"Capture",
"Regen capture",
"Capture the flag",
"Insta Capture the flag",
"Protect",
"Insta Protect",
"Hold",
"Insta Hold",
"Efficiency Capture the flag",
"Efficiency Protect",
"Efficiency Hold",
"Collect",
"Insta Collect",
"Efficiency Collect"
];
$gameMode = $nnn[$gameMode];
return [
"_online"=>true,
"description"=>$serverDesc,
"map"=>$mapName,
"players_on"=>$numPlayers,
"players_max"=>$maxClients,
"gameMode"=>$gameMode
];
}
function readInt(&$out){
$c = ord(substr($out,0,1));
if ($c == 128){ //Number that follows is a unsigned sort (2 bytes)
$c = unpack("v",substr($out,1,2))[1];
$out = substr($out,3);
} elseif ($c == -127){ //Number that follows is a unsigned int (4 bytes)
$c = unpack("V",substr($out,1,4))[1];
$out = substr($out,3);
} else { //Number is just a byte
$out = substr($out,1);
}
return $c;
}
#echo json_encode(sauerbraten());
?>
This diff is collapsed.
This diff is collapsed.
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
require('../../js/transition.js')
require('../../js/alert.js')
require('../../js/button.js')
require('../../js/carousel.js')
require('../../js/collapse.js')
require('../../js/dropdown.js')
require('../../js/modal.js')
require('../../js/tooltip.js')
require('../../js/popover.js')
require('../../js/scrollspy.js')
require('../../js/tab.js')
require('../../js/affix.js')
\ No newline at end of file
var ERR_MAP = {
"BAD_LOGIN":"You have entered invalid credentials.",
"MISSING_USERNAME_OR_PASSWORD":"Please enter a username and password.",
"BANNED":"Sorry you are banned. For more information contact games@sucs.org",
"ERR_UNKNOWN_AUTH_TYPE":"An unexpected error occoured - Bad Auth Type.",
"UNI_DISALLOWED":"Only SUCS members are currentlly allowed access."
},
SUCCESS = "You are now logged into the SUCS Game Server system, and can connect to any of the servers we have running by simply specifying the hostname/IP address 'games.sucs.org'. This page must be left open while you are playing. When you close this window, you will no longer have access to the games server, and will have to login again if you wish to play some more.",
SIGNUP_INFO = "Thank you for taking an interest in playing on the SUCS game server. Unfortunately the game server is currently only available to SUCS members, you can <a href=\"https://sucs.org/join\">sign up</a> to SUCS and get 24/7 access to the server plus all the other benefits that come with SUCS membership.";
function loginRefresh(){
$.post("endpoint.php",{renew:1},onPostResponse);
}
var REFRESH_ID;
function scheduleRefresh(){
REFRESH_ID = setTimeout(loginRefresh,30*1000);
}
function cancelRefresh(){
clearTimeout(REFRESH_ID);
}
function populateExtraData(data,domain){
if(domain === undefined){
domain = $("body");
}
$.each(data,function(key,value){
var target = domain.find("[data-target='"+key+"']");
if (target.length === 0){
console.warn("failed to find target",key,"under",domain);
return;
}
if (key == "_online"){
if (value){
target.css("color","green").text("Online");
} else {
target.css("color","red").text("Offline");
}
} else if (value === null){
target.empty();
} else if (value instanceof Array){
target.empty();
for(var i=0;i<value.length;i++){
$("<li>").text(value[i]).appendTo(target);
}
} else if (typeof value == "object") {
populateExtraData(value,target);
} else {
target.text(value);
}
});
}
function onPostResponse(response){
console.log(response);
//When this response comes back it will be 1 of 5 diffrent state we care about
//DEFAULT|BANNED|UNI-NO|GAME-ACCESS|BAD-CREDENTIALS
//Populate extra payload data
populateExtraData(response.extraPayload);
//if the response is AS_BEFORE nothing changes, just schedle a refresh
if (response.level == "AS_BEFORE") {
scheduleRefresh();
return;
}
//Show an error if there is one
if (response.loginError){
$("#loginErrorWrap").show();
$("#loginError").text(ERR_MAP[response.loginError]||response.loginError);
} else {
$("#loginErrorWrap").hide();
}
//Display username if we have it
if(response.username){
$("#username").show().text("Hello "+response.username);
} else {
$("#username").hide();
}
//Display login details if not logged in
if (response.level == "NO_LOGIN"){
$("#login, #signup").show();
$("#logout").hide();
} else {
$("#login, #signup").hide();
$("#logout").show();
}