<?php


/*
[tcp_query]
enabled = 1
ip = "0.0.0.0"
port = 1337
logintimeout = 30
defaulaction = "say" ;rcon, say, none
sayprefix = "^3TCP-Admin (<ADMIN>): ^7"
admingroups = "master,admin"
disallowedcommands = "vote,yes,no"
maxconnections = 3
*/

define("MT_CONSOLE", 0);
define("MT_RCONSAY", 1);
define("MT_CHAT",    2);
define("MT_PM",      3);
define("MT_INFO",    4);


$mod->setDefaultCV("tcp_query", "enabled",  0);
$mod->setDefaultCV("tcp_query", "ip",  "0.0.0.0");
$mod->setDefaultCV("tcp_query", "port",  1337);
$mod->setDefaultCV("tcp_query", "logintimeout",  30);
$mod->setDefaultCV("tcp_query", "defaulaction",  "say");
$mod->setDefaultCV("tcp_query", "sayprefix",  "^3TCP-Admin (<ADMIN>): ^7");
$mod->setDefaultCV("tcp_query", "admingroups",  "master,admin");
$mod->setDefaultCV("tcp_query", "disallowedcommands",  "");
$mod->setDefaultCV("tcp_query", "maxconnections",  5);

$tcp_query = new tcp_query();

$mod->registerEvent("everyTime", "everyTime", $tcp_query);
$mod->registerEvent("rconSay", "eventRconSay", $tcp_query);
$mod->registerEvent("logAction", "eventLogAction", $tcp_query);
$mod->registerEvent("nextMap", "eventNextMap", $tcp_query);
$mod->registerEvent("parseConfig", "eventReadConfig", $tcp_query);
$mod->registerEvent("playerJoined", "eventJoin", $tcp_query);
$mod->registerEvent("playerQuit", "eventQuit", $tcp_query);

class tcp_query {
    private $socket = false;
    private $connections = array();
    private $enabled = false;
    private $mod;
    private $logging;
    private $players;
    private $userDB = array();

    public function __construct() {
        $this->mod     =& $GLOBALS["mod"];
        $this->logging =& $GLOBALS["logging"];
        $this->players =& $GLOBALS["players"];


        $this->enabled = (bool)(int)$this->mod->getCV("tcp_query", "enabled");

        if (!$this->enabled) return;

        $this->initSocket();
        $this->readUserDB();
    }

    public function eventReadConfig() {
        if ($this->enabled && !$this->mod->getCV("tcp_query", "enabled")) {
            $this->writeAll("You have been disconnected: TCP-Interface has been disabled by admin\n");

            foreach (array_keys($this->connections) as $conection) {
                unset ($this->connections[$conection]);
            }
        }
        elseif (!$this->enabled && $this->mod->getCV("tcp_query", "enabled")) {
            $this->enabled = true;
            $this->initSocket();
            $this->readUserDB();
        }
        elseif ($this->enabled) {
            $this->readUserDB();
        }
    }

    private function readUserDB() {
        $this->userDB = array();
        $db = file($this->mod->getConfigDir() . "/plugins/tcp_user.db");
        if ($db === false) {
            $this->logging->write(MOD_ERROR, "TCP-Query: Could not open user database (" . $this->mod->getConfigDir() . "/plugins/tcp_user.db)");
            return;
        }
        foreach ($db as $lineno => $line) {
            //Comments
            $line = preg_replace('|;.*|', "", trim($line));

            if (empty($line)) {
                continue;
            }
            elseif (!preg_match('|^([a-z0-9_]+):([a-z0-9_]+):([a-f0-9]{40})$|i', $line, $subpatterns)) {
                $this->logging->write(MOD_WARNING, "TCP-Query: Error: Syntax error in tcp_user.db on line " . ($lineno + 1));
            }
            else {
                list ($dummy, $user, $group, $pw) = $subpatterns;
                $group = strtolower($group);
                $pw = strtolower($pw);
                if (!array_key_exists($group, $this->mod->getGroups())) {
                    $this->logging->write(MOD_WARNING, "TCP-Query: Error: Group '$group' does not exist in tcp_user.db on line " . ($lineno + 1));
                    continue;
                }
                if (array_i_key_exists($user, $this->userDB)) {
                    $this->logging->write(MOD_WARNING, "TCP-Query: Error: User '$user' already exists in tcp_user.db on line " . ($lineno + 1));
                    continue;
                }
                $this->userDB[$user] = array("group"=>$group, "password" =>$pw);
            }
        }

    }

    public function isValidLogin(&$user, $password, &$group) {
        $user = strtolower($user);
        //if (!array_key_exists($user, $this->userDB)) return false;

        foreach ($this->userDB as $username => $userdata) {
            if (strtolower($username) == strtolower($user)) {
                if ($userdata["password"] == sha1($password)) {
                    $user = $username;
                    $group = $this->userDB[$user]["group"];
                    return true;
                }
                else {
                    return false;
                }
            }
        }

        return false;
    }

    public function everyTime() {
        if (!$this->enabled) return;

        $this->checkForConnections();

        foreach (array_keys($this->connections) as $conection) {
            //Vorsorge gegen Fehlermeldungen wenn die Verbindungen während der Schleifenausführung gelöscht wurden
            if (!array_key_exists($conection, $this->connections)) continue;

            $this->connections[$conection]->everyTime();
            if ($this->connections[$conection]->closed) {
                unset ($this->connections[$conection]);
            }
        }
    }

    private function initSocket() {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$this->socket) {
            $this->socketError();
            return;
        }
        if (!socket_bind($this->socket, $this->mod->getCV("tcp_query", "ip"), $this->mod->getCV("tcp_query", "port"))) {
            $this->socketError($this->socket);
            return;
        }
        if (!socket_listen($this->socket)) {
            $this->socketError($this->socket);
            return;
        }
        if (!socket_set_nonblock($this->socket)) {
            $this->socketError($this->socket);
            return;
        }
    }

    private function socketError($socket = NULL) {
        $this->enabled = false;
        $err = socket_last_error($socket);
        $this->logging->write(MOD_WARNING, "TCP-Query: Socket-Error: " . socket_strerror($err));
    }

    private function checkForConnections() {
        $connection = @socket_accept($this->socket);
        if (!$connection) return false;
        if (count($this->connections) >= $this->mod->getCV("tcp_query", "maxconnections")) {
            socket_write($connection, chr(MT_CONSOLE).chr(0)."Maximum connections reached, you'll be disconnected\n");
            socket_close($connection);
            return;
        }

        socket_set_nonblock($connection);
        $this->connections[] = new tcp_query_connection($connection);
    }

    public function eventRconSay($msg) {
        if (!$this->enabled) return;

        $this->writeAll($msg, MT_RCONSAY);
    }

    public function eventLogAction($line) {
        if (!$this->enabled) return;

        if ($line["action"] == "say" || $line["action"] == "sayteam") {
            $parts = $line["parsed"];
            $this->writeAll($parts[2] . ": " . $parts[3] . "\n", MT_CHAT);
        }
        return;
    }

    public function eventNextMap($dvars) {
        if (!$this->enabled) return;

        $this->writeAll("Next map / map restart: " . $this->mod->getLongMapName($dvars["mapname"]) . " (" . $this->mod->getLongGameType($dvars["g_gametype"]). ")\n");
    }

    private function writeAll($msg, $type = MT_CONSOLE) {
        foreach (array_keys($this->connections) as $connection) {
            if (!$this->connections[$connection]->loggedin()) continue;
            $this->connections[$connection]->write($msg, $type);
        }
    }

    public function kick($id) {
        if (!$this->enabled) return;

        if (array_key_exists($id, $this->connections)) {
            $this->connections[$id]->kick();
            return true;
        }
        return false;
    }

    public function getConnections() {
        if (!$this->enabled) return false;

        $return = array();
        foreach (array_keys($this->connections) as $connection) {
            $return[$connection] = array(
                "id" => $connection,
                "ip" => $this->connections[$connection]->getIP(),
                "port" => $this->connections[$connection]->getPort(),
                "user" => $this->connections[$connection]->getUser(),

            );
        }
        return $return;
    }

    public function eventJoin($guid) {
        if (!$this->enabled) return;
        $name = $this->players[$guid]->getName();

        $this->writeAll("Player joined: '$name'\n", MT_INFO);
    }

    public function eventQuit($guid) {
        if (!$this->enabled) return;
        $name = $this->players[$guid]->getName();

        $this->writeAll("Player quit: '$name'\n", MT_INFO);
    }
}


class tcp_query_connection {
    private $con = false;
    private $created = 0;
    private $loginStarted = 0;
    private $lastcommand = false;
    private $status = "loggedout";
    private $user = false;
    private $ip;
    private $port;
    private $buffer = "";
    private $mod = false;
    private $logging = false;
    private $group = false;
    private $players = array();
    public $closed = false;
    public $dummyplayer;
    private $sent = 0;
    private $received = 0;
    private $isResult = false;


    public function __construct($socket) {
        $this->mod     =& $GLOBALS["mod"];
        $this->logging =& $GLOBALS["logging"];
        $this->players =& $GLOBALS["players"];

        $this->con = $socket;
        $this->created = time();
        $this->loginStarted = time();
        socket_getpeername($this->con, $this->ip, $this->port);

        $this->logging->write(MOD_NOTICE, "TCP-Query: New connection from $this->ip on port $this->port");

        $this->welcome();
        $this->getUserName();
    }

    public function __destruct() {
        if (is_resource($this->con)) {
            $this->close();
        }
    }

    private function welcome() {
        $timeout = $this->mod->getCV("tcp_query", "logintimeout");
        $msg  = "Welcome to Manu-Admin-Mod TCP-Interface\n";
        $msg .= "Your IP is $this->ip and will be logged for security reasons\n";
        $msg .= "You have $timeout seconds to authentificate...\n";
        $this->write($msg);
    }

    private function read() {
        $read = @socket_read($this->con, 9999, PHP_BINARY_READ);

        $this->received += strlen($read);
                                                                          //WIN  , LINUX, FreeBSD
        if ($read === '' || !in_array(@socket_last_error($this->con), array(10035, 11,    35))) {
            $this->close();
            return;
        }

        $this->buffer .= $read;

        if (strpos($this->buffer, "\n") !== false) {
            $parts = explode("\n", $this->buffer, 2);
            $cmd = trim($parts[0]);
            if (!empty($cmd)) {
                $this->input($cmd);
            }
            $this->buffer = "";
        }
    }

    public function write($msg, $type = MT_CONSOLE)  {


        $isCmd = ($this->isResult) ? chr(1) : chr(0);
        $msg =  chr($type) . $isCmd . $msg;

        @socket_write($this->con, $msg, strlen($msg));
        $this->sent += strlen($msg);
    }

    public function everyTime() {
        $this->read();

        $timeout = $this->mod->getCV("tcp_query", "logintimeout");
        if ($this->status == "loggedout" && time() - $this->loginStarted > $timeout) {
            $this->write("Your $timeout seconds are over, please try again\n");
            $this->close();
        }

    }

    private function close() {
        if ($this->closed)return;
        $this->write("Bye, see you later :D\n");
        $this->closed = true;
        if ($this->status == "loggedout") {
            $this->user = "NOBODY";
        }
        else {
            $this->status = "loggedout";
            $this->mod->rconSay("TCP-Admin ($this->user) has disconnected");
        }
        $this->logging->write(MOD_NOTICE, "TCP-Query: Connection closed: $this->user ($this->ip:$this->port)");
        socket_close($this->con);
    }

    private function input($input) {
        if ($this->status == "loggedout") {
            if ($this->user == false) {
                $this->user = $input;
                $this->getPassword();
            }
            else {
                if ($this->login($this->user, $input)) {
                    $this->write("You have been successfully logged in as $this->user\nHave fun :D\n");
                }
                else {
                    $this->write("Login failed, please try again...\n");
                    $this->user = false;
                    $this->getUserName();
                }
            }
        }
        else {
            $this->isResult = true;
            //quit command
            if (strtolower($input) == "quit" || strtolower($input) == "exit") {
                $this->close();
            }
            //logout command
            elseif (strtolower($input) == "logout") {
                $this->logout();
            }
            elseif (strtolower($input) == "info") {
                $this->connectionInfo();
            }
            //admin commands
            elseif (strtolower($input) == "lc" && in_array($this->group, explode(",", $this->mod->getCV("tcp_query", "admingroups")))) {
                $connections = $GLOBALS['tcp_query']->getConnections();
                $msg = "";
                foreach ($connections as $connection) {
                    $msg .= str_pad($connection["id"], 3, " ", STR_PAD_LEFT) . ": ";
                    $msg .= str_pad($connection["ip"] . ":" . $connection["port"], 21, " ", STR_PAD_RIGHT) . " ";
                    $msg .= $connection["user"] . "\n";
                }
                $this->write($msg);
            }
            //kick command
            elseif (substr(strtolower($input), 0, 5) == "kick " && in_array($this->group, explode(",", $this->mod->getCV("tcp_query", "admingroups")))) {
                $id = explode(" ", $input);
                $this->logging->write(MOD_NOTICE, "User '$this->user' used command kick");
                if ($GLOBALS['tcp_query']->kick($id[1])) {
                    $this->write("User has been kicked\n");
                }
                else {
                    $this->write("Error...\n");
                }
            }
            //getinfo command
            elseif (preg_match('|^getinfo([^\w].*)?$|', $input)) {
                if (strlen($input) <= 8) {
                     $this->write("Possible: players, maps");
                }
                else {
                    switch (substr($input, 8)) {
                        case "players":
                            $this->writePlayers();
                            break;
                        case "maps":
                            $this->writeMaps();
                            break;
                        default:
                            $this->write("error");
                    }
                }
            }
            elseif (substr(strtolower($input), 0, 5) == "rcon " && $this->dummyplayer->isAllowedToExec("rcon")) {
                $rcon = substr($input, 5);
                $return = $this->mod->rconRcon($rcon);
                $this->write($return);
            }
            //normal commands
            else {
                if ($input{0} == $this->mod->getCV("main", "prefix")) {
                    $this->players["TCPUSER"] =& $this->dummyplayer;
                    $this->mod->executeCommand($input, "TCPUSER", $executed);
                    if (!$executed && !$this->mod->getCV("main", "responsefailcmds")) {
                        $this->write("Unrecognized command: " . $input . "\n");
                    }
                    unset($this->players["TCPUSER"]);

                }
                else {
                    if ($this->mod->getCV("tcp_query", "defaulaction") == "say") {
                        $msg = str_replace("<ADMIN>", $this->user, $this->mod->getCV("tcp_query", "sayprefix")) . $input;
                        $this->logging->write(MOD_NOTICE, "TCP-Query: User '$this->user' says: '$input' ($this->ip:$this->port)");
                        $this->mod->rconSay($msg);
                    }
                    elseif($this->mod->getCV("tcp_query", "defaulaction") == "rcon") {
                        $return = $this->mod->rconRcon($input);
                        $this->logging->write(MOD_NOTICE, "TCP-Query: User '$this->user' executed RCON command: '$input' ($this->ip:$this->port)");
                        $this->write($return);
                    }
                }
            }
            $this->isResult = false;
        }
    }

    private function getUserName() {
        $this->write("User: \n");
    }

    private function getPassword() {
        $this->write("Password: \n");
    }

    private function login($user, $password) {
        if ($GLOBALS['tcp_query']->isValidLogin($user, $password, $group)) {
            $this->group = $group;  //Is changed by function isValidLogin
            $this->user = $user;    //Is changed by function isValidLogin
            $this->status = "loggedin";
            $this->logging->write(MOD_NOTICE, "TCP-Query: User logged in: $this->user ($this->ip:$this->port)");
            $this->mod->rconSay("TCP-Admin ($this->user) has connected");
            $this->dummyplayer = new tcp_query_playerDummy($this->user, $this->group, $this);
            return true;
        }
        return false;
    }

    private function logout() {
        $this->loginStarted = time();
        $this->status = "loggedout";;
        $this->write("You have been successfully logged out!\n\n");
        $this->logging->write(MOD_NOTICE, "TCP-Query: User logged out: $this->user ($this->ip:$this->port)");
        $this->mod->rconSay("TCP-Admin ($this->user) has disconnected");
        $this->user = false;
        $this->group = "";
        $this->welcome();
        $this->getUserName();
    }

    public function loggedin() {
        if ($this->status == "loggedin") return true;
        return false;
    }

    public function getIP() {
        return $this->ip;
    }

    public function getPort() {
        return $this->port;
    }

    public function getUser() {
        if ($this->loggedin()) return $this->user;
        return "NOT LOGGED IN";
    }

    public function kick() {
        $this->write("You have been kicked by Admin\n");
        $this->logging->write(MOD_NOTICE, "TCP-Query: Connection of '" . $this->getUser() . "' ($this->ip:$this->port) got kicked by admin");
        $this->close();
    }

    private function connectionInfo() {
        $ip = $this->getIP();
        $port = $this->getPort();
        $user = $this->getUser();
        $group = $this->mod->getLongGroupName($this->group);
        $time = makeuptime2(time() - $this->created);

        $text = "Connection info:\n================\n";
        $text .= "Username:        $user ($group)\n";
        $text .= "Address:         $ip:$port\n";
        $text .= "Connection time: $time\n";
        $text .= "Bytes read:      ".size($this->received)."\n";
        $text .= "Bytes sent:      ".size($this->sent)."\n";


        $this->write($text);
    }

    private function writeMaps() {
        $str = "Game: ".$this->mod->getGame() . "\n";
        $lastmaps = $this->mod->getLastMaps(1);
        $str .= "Current: " . $this->mod->getLongMapName($lastmaps[0]);
        $str .= " (".$this->mod->getLongGametype($this->mod->getCurrentGametype()).")\n";
        $maps = $this->mod->getMaps();
        sort($maps);
        foreach ($maps as $short) {
            $str .= "$short\t";
            $str .= $this->mod->getLongMapName($short) . "\n";
        }
        $str = trim($str);

        $this->write($str);
    }

    private function writePlayers() {
        $str = "";
        foreach (array_keys($this->players) as $guid) {
            $str .= $this->players[$guid]->getPID();
            $str .= "\t" . $this->players[$guid]->getName();
            $str .= "\t";
            $str .= $this->mod->getLongGroupName($this->players[$guid]->getGroup());
            $str .= "\t$guid\t0\n";
        }
        $this->write(trim($str));
    }
}

class tcp_query_playerDummy {
	private $mod;
	private $rcon;
	private $players;
	private $name;
	private $group;
	private $connection;
	public $joined = 0;

	public function __construct($name, $group, &$connection) {
		$this->mod = & $GLOBALS['mod'];
		$this->rcon = & $GLOBALS['rcon'];
		$this->name = $name;
		$this->group = $group;
		$this->connection =& $connection;
		$this->joined = time();
	}

	public function __set  ($name, $value) {}
	public function __get  ($name) {return 0;}
	public function __call ($name, $arguments) {
	    return 0;
	}

	public function getGroup() {
		return $this->group;
	}

	public function getName() {
		return "TCP-Admin ($this->name)";
	}

	public function getPID() {
		return "X";
	}

	public function getGuid() {
		return "TCPUSER";
	}

	public function isAllowedToExec($command) {
	    if (in_array($command, explode(",", $this->mod->getCV("tcp_query", "disallowedcommands")))) return false;
		return in_array($command, $this->mod->getGroupCommands($this->getGroup()));
	}

	public function getAllowedCommands() {
	    return $this->mod->getGroupCommands($this->getGroup());
	}

	public function setGroup($group) {
	    return false;
	}

	public function say($msg) {
        $msg = str_replace("{{br}}", "\n", $msg) . "\n";
	    $this->connection->write($msg, MT_PM);
	}
}

?>