From 1cb3e25bf904c5091f87ed005749967c94513f21 Mon Sep 17 00:00:00 2001 From: Shoghi Cervantes Pueyo Date: Mon, 22 Apr 2013 14:22:52 +0200 Subject: [PATCH] Added RCON protocol --- src/API/ConsoleAPI.php | 19 ++-- src/API/ServerAPI.php | 13 +++ src/utils/RCON.php | 193 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 src/utils/RCON.php diff --git a/src/API/ConsoleAPI.php b/src/API/ConsoleAPI.php index 86604c367..1e805ad02 100644 --- a/src/API/ConsoleAPI.php +++ b/src/API/ConsoleAPI.php @@ -37,8 +37,7 @@ class ConsoleAPI{ public function init(){ $this->event = $this->server->event("server.tick", array($this, "handle")); - $this->loop = new ConsoleLoop; - $this->loop->start(); + $this->loop = new ConsoleLoop(); $this->register("help", "[page|command name]", array($this, "defaultCommands")); $this->register("status", "", array($this, "defaultCommands")); $this->register("difficulty", "<0|1>", array($this, "defaultCommands")); @@ -53,6 +52,7 @@ class ConsoleAPI{ function __destruct(){ $this->server->deleteEvent($this->event); $this->loop->stop = true; + @fclose(STDIN); $this->loop->notify(); $this->loop->join(); } @@ -227,11 +227,6 @@ class ConsoleAPI{ } if($output != "" and ($issuer instanceof Player)){ $issuer->sendChat(trim($output)); - }elseif($output != "" and $issuer === "console"){ - $mes = explode("\n", trim($output)); - foreach($mes as $m){ - console("[CMD] ".$m); - } } return $output; } @@ -241,7 +236,13 @@ class ConsoleAPI{ if($this->loop->line !== false){ $line = trim($this->loop->line); $this->loop->line = false; - $this->run($line, "console"); + $output = $this->run($line, "console"); + if($output != ""){ + $mes = explode("\n", trim($output)); + foreach($mes as $m){ + console("[CMD] ".$m); + } + } }else{ $this->loop->notify(); } @@ -255,6 +256,7 @@ class ConsoleLoop extends Thread{ public function __construct(){ $this->line = false; $this->stop = false; + $this->start(); } public function run(){ @@ -264,6 +266,7 @@ class ConsoleLoop extends Thread{ $this->wait(); $this->line = false; } + @fclose($fp); exit(0); } } diff --git a/src/API/ServerAPI.php b/src/API/ServerAPI.php index bac55c983..089f7690a 100644 --- a/src/API/ServerAPI.php +++ b/src/API/ServerAPI.php @@ -33,6 +33,7 @@ class ServerAPI{ private $config; private $apiList = array(); private $asyncCnt = 0; + private $rcon; public static function request(){ return self::$serverRequest; @@ -113,6 +114,9 @@ class ServerAPI{ "generator-settings" => "", "level-name" => false, "server-id" => false, + "enable-rcon" => false, + "rcon.password" => substr(base64_encode(Utils::getRandomBytes(20, false)), 3, 10), + "rcon.port" => 19132, "upnp-forwarding" => false, "send-usage" => true, )); @@ -340,8 +344,17 @@ class ServerAPI{ $this->server->schedule(6000, array($this, "sendUsage")); //Send the info after 5 minutes have passed $this->sendUsage(); } + + if($this->getProperty("enable-rcon") === true){ + $this->rcon = new RCON($this->getProperty("rcon.password", ""), $this->getProperty("rcon.port", 19132)); + } + $this->server->init(); unregister_tick_function(array($this->server, "tick")); + $this->console->__destruct(); + if($this->rcon instanceof RCON){ + $this->rcon->stop(); + } $this->__destruct(); if($this->getProperty("upnp-forwarding") === true ){ console("[INFO] [UPnP] Removing port forward..."); diff --git a/src/utils/RCON.php b/src/utils/RCON.php new file mode 100644 index 000000000..d2e6c942c --- /dev/null +++ b/src/utils/RCON.php @@ -0,0 +1,193 @@ +workers = array(); + $this->password = (string) $password; + console("[INFO] Starting remote control listener"); + if($this->password === ""){ + console("[ERROR] RCON can't be started: Empty password"); + return; + } + $this->threads = (int) max(1, $threads); + $this->socket = socket_create_listen((int) $port); + if($this->socket === false){ + console("[ERROR] RCON can't be started: ".socket_strerror(socket_last_error())); + return; + } + @socket_set_nonblock($this->socket); + for($n = 0; $n < $this->threads; ++$n){ + $this->workers[$n] = new RCONInstance($this->socket, $this->password); + } + @socket_getsockname($this->socket, $addr, $port); + console("[INFO] RCON running on $addr:$port"); + ServerAPI::request()->schedule(2, array($this, "check"), array(), true); + } + + public function stop(){ + for($n = 0; $n < $this->threads; ++$n){ + $this->workers[$n]->close(); + $this->workers[$n]->join(); + } + @socket_close($this->socket); + $this->threads = 0; + } + + public function check(){ + for($n = 0; $n < $this->threads; ++$n){ + if($this->workers[$n]->isTerminated() === true){ + $this->workers[$n] = new RCONInstance($this->socket, $this->password); + }elseif($this->workers[$n]->isWaiting()){ + if($this->workers[$n]->response !== ""){ + console($this->workers[$n]->response); + $this->workers[$n]->notify(); + }else{ + $this->workers[$n]->response = ServerAPI::request()->api->console->run($this->workers[$n]->cmd, "console"); + $this->workers[$n]->notify(); + } + } + } + } + +} + +class RCONInstance extends Thread{ + public $stop; + public $cmd; + public $response; + private $socket; + private $password; + private $status; + private $client; + + public function __construct($socket, $password){ + $this->stop = false; + $this->cmd = ""; + $this->response = ""; + $this->socket = $socket; + $this->password = $password; + $this->status = 0; + $this->start(); + } + + private function writePacket($requestID, $packetType, $payload){ + return socket_write($this->client, Utils::writeLInt(strlen($payload)).Utils::writeLInt((int) $requestID).Utils::writeLInt((int) $packetType).($payload === "" ? "\x00":$payload)."\x00"); + } + + private function readPacket(&$size, &$requestID, &$packetType, &$payload){ + @socket_set_nonblock($this->client); + while(true){ + usleep(1); + $d = socket_read($this->client, 4); + if($this->stop === true){ + return false; + }elseif($d === false){ + continue; + }elseif($d === ""){ + return false; + } + break; + } + @socket_set_block($this->client); + $size = Utils::readLInt($d); + if($size < 0){ + return false; + } + $requestID = Utils::readLInt(socket_read($this->client, 4)); + $packetType = Utils::readLInt(socket_read($this->client, 4)); + $payload = rtrim(socket_read($this->client, $size + 2)); //Strip two null bytes + return true; + } + + public function close(){ + $this->stop = true; + $this->status = -1; + } + + public function run(){ + while($this->stop !== true){ + usleep(1000); + if(($this->client = socket_accept($this->socket)) !== false){ + socket_set_block($this->client); + socket_set_option($this->client, SOL_SOCKET, SO_KEEPALIVE, 1); + while($this->status !== -1 and $this->stop !== true){ + if($this->readPacket($size, $requestID, $packetType, $payload) === false){ + $this->status = -1; + break; + } + switch($packetType){ + case 3: //Login + if($this->status !== 0){ + $this->status = -1; + continue; + } + if($payload === $this->password){ + @socket_getpeername($this->client, $addr, $port); + $this->response = "[INFO] Successful Rcon connection from: /$addr:$port"; + $this->wait(); + $this->response = ""; + $this->writePacket($requestID, 2, ""); + $this->status = 1; + }else{ + $this->status = -1; + $this->writePacket(-1, 2, ""); + continue; + } + break; + case 2: //Command + if($this->status !== 1){ + $this->status = -1; + continue; + } + if(strlen($payload) > 0){ + $this->cmd = ltrim($payload); + $this->wait(); + $this->writePacket($requestID, 0, str_replace("\n", "\r\n", trim($this->response))); + $this->response = ""; + $this->cmd = ""; + } + break; + } + usleep(1); + } + @socket_set_option($this->client, SOL_SOCKET, SO_LINGER, array("l_onoff" => 1, "l_linger" => 1)); + @socket_shutdown($this->client, 2); + @socket_set_block($this->client); + @socket_read($this->client, 1); + @socket_close($this->client); + $this->status = 0; + } + } + unset($this->client, $this->socket, $this->cmd, $this->response, $this->stop, $this->status); + exit(0); + } +} \ No newline at end of file