diff --git a/README.md b/README.md index c06a02d..b7d935e 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,5 @@ netcontrol ========== An in-browser interface for popular tools like RCON, Quake Server queries, Minecraft server queries, etc. + +All credits for jquery.ba-hashchange.min.js go to https://github.com/cowboy/jquery-hashchange \ No newline at end of file diff --git a/commands/crcon.php b/commands/crcon.php new file mode 100644 index 0000000..2bcb71d --- /dev/null +++ b/commands/crcon.php @@ -0,0 +1,84 @@ +parseFlaggedParameters($requestParameters, $parameters); + $missing = $this->missingKeys($parameters, $requestParameters); + + if ($missing === false) + { + $this->help(); + return; + } + + $parsedURL = parse_url($parameters['h']); + $ip = '127.0.0.1'; + $port = 28960; + + if (array_key_exists('host', $parsedURL)) + { + if (array_key_exists('port', $parsedURL)) + { + $port = $parsedURL['port']; + } + + $ip = $parsedURL['host']; + } + else + { + $ip = $parsedURL['path']; + } + + if ($result = $this->color($this->query($ip, $port, $parameters['p'], $parameters['r']))) + { + $terminal->writeLine($result); + return; + } + + $terminal->writeLine('Failed to connect to server.'); + } + + function help() + { + global $terminal; + $terminal->writeLine('Call of Duty RCON Utility'); + $terminal->writeLine('Example: crcon -h 127.0.0.1:28960 -p password -r serverinfo'); + } + + function color($string) + { + global $settings; + return $this->nonClosingColor($string, $settings['colors']['cod4'], '^'); + } + + function query($ip, $port, $rcon_pass, $command) + { + $socket = fsockopen('udp://'.$ip, $port, $errno, $errstr, 5); + socket_set_timeout($socket, 5); + + if (!$socket) + { + return false; + } + + $query = "\xFF\xFF\xFF\xFFrcon \"".$rcon_pass."\" ".$command; + fwrite($socket, $query); + + $data = ''; + + while ($d = fread ($socket, 10000)) + { + $data .= $d; + } + + fclose($socket); + $data = preg_replace("/....print\n/", '', $data); + return $data; + } +} +?> \ No newline at end of file diff --git a/commands/mping.php b/commands/mping.php new file mode 100644 index 0000000..369c1ef --- /dev/null +++ b/commands/mping.php @@ -0,0 +1,78 @@ +help(); + return; + } + + $parsedURL = parse_url($parameters[0]); + $result = false; + + if (array_key_exists('host', $parsedURL)) + { + if (array_key_exists('port', $parsedURL)) + { + $result = $this->ping($parsedURL['host'], $parsedURL['port']); + } + else + { + $result = $this->ping($parsedURL['host']); + } + } + else + { + $result = $this->ping($parsedURL['path']); + } + + if ($result !== false) + { + $terminal->writeLine($result['motd']); + $terminal->writeLine($result['players'].'/'.$result['max_players'].' players'); + return; + } + + $terminal->writeLine('The server could not be reached.'); + } + + function help() + { + global $terminal; + $terminal->writeLine('Minecraft Ping Utility'); + $terminal->writeLine('Example: mping 127.0.0.1:25565'); + } + + function ping($host, $port = 25565, $timeout = 5) + { + $server = @fsockopen($host, $port, $errno, $errstr, $timeout); + + if (!$server) + { + return false; + } + + fwrite($server, "\xFE"); + $result = fread($server, 256); + + if ($result[0] != "\xFF") + { + return false; + } + + $result = substr($result, 3); + $result = mb_convert_encoding($result, 'auto', 'UCS-2'); + $result = explode("\xA7", $result); + fclose($server); + return array('motd' => $result[0], + 'players' => intval($result[1]), + 'max_players' => intval($result[2])); + } +} +?> \ No newline at end of file diff --git a/commands/qquery.php b/commands/qquery.php new file mode 100644 index 0000000..0eac8e7 --- /dev/null +++ b/commands/qquery.php @@ -0,0 +1,71 @@ +help(); + return; + } + + $parsedURL = parse_url($parameters[0]); + $quake = new Quake3(); + $result = false; + + if (array_key_exists('host', $parsedURL)) + { + if (array_key_exists('port', $parsedURL)) + { + $result = $quake->queryServerInfo($parsedURL['host'], $parsedURL['port']); + } + else + { + $result = $quake->queryServerInfo($parsedURL['host'], 28960); + } + } + else + { + $result = $quake->queryServerInfo($parsedURL['path'], 28960); + } + + if ($result !== false) + { + $terminal->writeLine($this->color($this->arr2String(': ', $result))); + return; + } + + $terminal->writeLine('The server could not be reached.'); + } + + function arr2String($glue, $array) + { + $toReturn = ''; + + foreach ($array as $key => $value) + { + $toReturn .= $key.$glue.$value."\n"; + } + + return $toReturn; + } + + function help() + { + global $terminal; + $terminal->writeLine('Quake 3 Server Query Utility'); + $terminal->writeLine('Example: qquery 127.0.0.1:28960'); + } + + function color($string) + { + global $settings; + return $this->nonClosingColor($string, $settings['colors']['cod4'], '^'); + } +} +?> \ No newline at end of file diff --git a/commands/rcon.php b/commands/rcon.php new file mode 100644 index 0000000..7a10c95 --- /dev/null +++ b/commands/rcon.php @@ -0,0 +1,83 @@ +parseFlaggedParameters($requestParameters, $parameters); + $missing = $this->missingKeys($parameters, $requestParameters); + + if ($missing === false) + { + $this->help(); + return; + } + + $parsedURL = parse_url($parameters['h']); + $rcon = null; + + if (array_key_exists('host', $parsedURL)) + { + if (array_key_exists('port', $parsedURL)) + { + $rcon = new RCONSocket($parsedURL['host'], $parsedURL['port']); + } + else + { + $rcon = new RCONSocket($parsedURL['host']); + } + } + else + { + $rcon = new RCONSocket($parsedURL['path']); + } + + //$rcon = new RCONSocket($parsedURL['host'], $parsedURL['port']); + + if (!$rcon->connect()) + { + $terminal->writeLine('Failed to connect. '.$rcon->getErrorString()); + return; + } + + if ($rcon->login($parameters['p'])) + { + $result = $rcon->commandGetResponse($parameters['r']); + + if ($result === false) + { + $terminal->writeLine('Failed to communicate. '.$rcon->getErrorString()); + return; + } + + $terminal->writeLine($this->color($result['s1'])); + } + else + { + $terminal->writeLine('Failed to communicate with server or wrong password.'); + } + + $rcon->disconnect(); + } + + function help() + { + global $terminal; + $terminal->writeLine('Valve RCON Utility (Minecraft, Counter-Strike, Half-Life, etc.)'); + $terminal->writeLine('Example: rcon -h 127.0.0.1:25565 -p password -r help'); + } + + function color($string) + { + global $settings; + return $this->nonClosingColor($string, $settings['rcon']['colors'], '§'); + } + + const RCON_AUTH = 3; + const RCON_ECOMMAND = 2; +} +?> \ No newline at end of file diff --git a/config.php b/config.php new file mode 100644 index 0000000..619c27a --- /dev/null +++ b/config.php @@ -0,0 +1,35 @@ + 'commands/rcon.php', + 'MPing' => 'commands/mping.php', + 'CRCON' => 'commands/crcon.php', + 'QQuery' => 'commands/qquery.php'); + +/* COLOR SETTINGS */ +$settings['colors']['minecraft'] = array('0' => '000000', '1' => '0000aa', '2' => '00aa00', '3' => '00aaaa', + '4' => 'aa0000', '5' => 'aa00aa', '6' => 'ffaa00', '7' => 'aaaaaa', + '8' => '555550', '9' => '5555ff', 'a' => '55ff55', 'b' => '55ffff', + 'c' => 'ff5555', 'd' => 'ff55ff', 'e' => 'ffff55', 'f' => 'ffffff'); +$settings['colors']['cod4'] = array('1' => 'f15757', '2' => '00fb00', '3' => 'e8e803', '4' => '0000fe', + '5' => '02e5e5', '6' => 'ff5cff', '7' => 'aaaaaa', '8' => '000000', + '9' => '000000', '0' => '000000'); + +/* RCON SETTINGS */ +$settings['rcon']['quirks'] = false; // quirks mode off by default, turn on if you get errors. +$settings['rcon']['colors'] = $settings['colors']['minecraft']; // reference to the color array +$settings['rcon']['timeout'] = 3; // in seconds, the timeout for an rcon connection + +/* MASTER SERVERS */ +$settings['masters']['cod4'] = array('cod4master.activision.com', 'cod4authorize.activision.com', + 'cod4master.infinityward.com', 'cod4update.activision.com', + 'master.gamespy.com:28960', 'master0.gamespy.com', + 'master1.gamespy.com', 'clanservers.net'); +$settings['masters']['cod5'] = array('cod5master.activision.com', 'cod5authorize.activision.com', + 'cod5master.infinityward.com', 'cod5update.activision.com', + 'master.gamespy.com:28960', 'master0.gamespy.com', + 'master1.gamespy.com', 'clanservers.net'); +?> \ No newline at end of file diff --git a/images/application-terminal-icon.png b/images/application-terminal-icon.png new file mode 100644 index 0000000..de569e1 Binary files /dev/null and b/images/application-terminal-icon.png differ diff --git a/images/cmd.png b/images/cmd.png new file mode 100644 index 0000000..00c7661 Binary files /dev/null and b/images/cmd.png differ diff --git a/includes/class_quake3.php b/includes/class_quake3.php new file mode 100644 index 0000000..7589670 --- /dev/null +++ b/includes/class_quake3.php @@ -0,0 +1,129 @@ +queryServer($host,$port, $query)) + { + return false; + } + + $data = str_replace(array('getserversResponse', 'EOT'), '', $data); + $data = explode('\\', $data); + + if(is_array($data) && ($count = count($data)) > 0) + { + $master_info = array(); + + for($i = 0,$j = 0; $i < $count; $i++) + { + if(strlen($data[$i]) === 6) + { + $master_info[$j]['ip'] = ord($data[$i][0]).'.'.ord($data[$i][1]).'.'.ord($data[$i][2]).'.'.ord($data[$i][3]); + $tmp_port = unpack('nint', substr($data[$i], 4, 2)); + $master_info[$j]['port'] = (int)$tmp_port['int']; + $j++; + } + } + + return $master_info; + } + + return false; + } + + function queryServerStatus($host, $port) + { + if(!$status['response'] = $this->queryServer($host, $port, 'getstatus')) + { + return false; + } + + $data = explode("\n",$status['response']); + + if(is_array($data) && ($count_d = count($data)) > 1) + { + $arr_info = array_chunk(explode("\\", substr($data[1], 1)), 2); + $count_i = count($arr_info); + + for($i = 0; $i < $count_i; $i++) + { + $status['info'][htmlspecialchars(strtolower($arr_info[$i][0]))] = htmlspecialchars($arr_info[$i][1]); + } + + for($i = 2,$j = 0; $i < $count_d; $i++) + { + if(!empty($data[$i]) && is_array($arr = explode(' ', $data[$i]))) + { + $status['players'][$j]['score'] = (isset($arr[0])) ? $arr[0] : '-1'; + $status['players'][$j]['ping'] = (isset($arr[1])) ? $arr[1] : '-1'; + + if(isset($status['info']['protocol']) && $status['info']['protocol']==69) + { + $status['players'][$j]['team'] = (isset($arr[2])) ? $arr[2] : ''; // 0=free, 1=red, 2=blue, 3=spectator + $status['players'][$j]['isbot'] = (isset($arr[3])) ? $arr[3] : ''; + } + + $status['players'][$j]['name'] = substr($data[$i], strpos($data[$i], '"') + 1, (strrpos($data[$i], '"') - strpos($data[$i], '"')) - 1); + $j++; + } + } + + return $status; + } + + return false; + } + + function queryServerInfo($host, $port) + { + if(!$info_response = $this->queryServer($host, $port, 'getinfo')) + { + return false; + } + + if(is_array($data = explode("\x0a", $info_response)) && count($data) > 1) + { + $arr = array_chunk(explode("\\", substr($data[1], 1)), 2); + $count = count($arr); + + for($i = 0; $i < $count; $i++) + { + $info[htmlspecialchars(strtolower($arr[$i][0]))] = htmlspecialchars($arr[$i][1]); + } + + return $info; + } + + return false; + } +} +?> \ No newline at end of file diff --git a/includes/class_rcon.php b/includes/class_rcon.php new file mode 100644 index 0000000..c1654b7 --- /dev/null +++ b/includes/class_rcon.php @@ -0,0 +1,203 @@ +host = $host; + $this->port = $port; + $this->timeout = time(); + } + + function connect() + { + $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $connected = @socket_connect($this->socket, $this->host, $this->port); + + if ($connected) + { + socket_set_nonblock($this->socket); + socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 1, 'usec' => 1)); + } + + return $connected; + } + + function login($password = '') + { + if ($this->write(RCON_AUTH, $password) === false) + { + return false; + } + + $response = $this->readPackets(); + + if ($response === false) + { + return false; + } + + return !isset($response[-1]); + } + + function write($command, $arg1 = '') + { + global $settings; + $id = ++$this->id; // data is in the format of id, command, argument, null char, nothing, null char + $data = pack('VV', $id, $command).$arg1.chr(0).chr(0); + $data = pack('V', strlen($data)).$data; // pack again for the size appended at the start + @socket_set_block($this->socket); + + if (@socket_send($this->socket, $data, strlen($data), 0) === false) + { + return false; + } + + @socket_set_nonblock($this->socket); + $this->timeout = time(); + return $id; + } + + function readPackets($block = true) + { + global $settings; + $toReturn = array(); + $found = false; + $size = ''; + + while (!$found) + { + if ($size = @socket_read($this->socket, 4)) + { + $found = true; + break; + } + + if (time() - $this->timeout >= $settings['rcon']['timeout']) + { + return true; + } + + if (!$block && ($size == '' || $size === false)) + { + return false; + } + } + + $size = unpack('V1size', $size); + + if ($size['size'] > 4096) + { + for ($i = 0; $i < 8; $i++) + { + $packet .= chr(0); + } + + $result = @socket_read($this->socket, 4096); + + if ($result === false) + { + return false; + } + + $packet .= $result; + } + else + { + $packet = @socket_read($this->socket, $size['size']); + + if ($packet === false) + { + return false; + } + } + + $this->timeout = time(); + $unpacked = unpack("V1id/V1response/a*s1/a*s2", $packet); + $toReturn[$unpacked['id']] = $unpacked; + return $toReturn; + } + + function read() + { + $toReturn = array(); + $block = true; + + while (($packets = $this->readPackets($block)) !== false) + { + if ($packets === true) + { + return false; + } + + foreach ($packets as $packet) + { + if (isset($toReturn[$packet['id']])) + { + $toReturn[$packet['id']]['s1'] .= $packet['s1']; + $toReturn[$packet['id']]['s2'] .= $packet['s2']; + } + else + { + $toReturn[$packet['id']] = $packet; + } + } + + if (!empty($packets)) + { + $block = false; + } + } + + return $toReturn; + } + + function command($argument) + { + return $this->write(RCON_ECOMMAND, $argument); + } + + function commandGetResponse($argument) + { + global $settings; + $commandID = $this->command($argument); + + if ($commandID === false) + { + return false; + } + + $toReturn = $this->read(); + + if ($toReturn === false) + { + return false; + } + + return ($settings['rcom']['quirks'] ? $toReturn[0] : $toReturn[$commandID]); + } + + function disconnect() + { + @socket_close($this->socket); + } + + function getError() + { + return @socket_last_error($this->socket); + } + + function getErrorString() + { + return @socket_strerror($this->getError()); + } +} +?> \ No newline at end of file diff --git a/includes/command.php b/includes/command.php new file mode 100644 index 0000000..5a9816a --- /dev/null +++ b/includes/command.php @@ -0,0 +1,135 @@ +'; + $open = false; + } + } + else if ($section) + { + $open = true; + $section = false; + $newLine .= ''; + } + else + { + $newLine .= $line{$i}; + } + } + + if ($open) + { + $newLine .= ''; + $open = false; + } + + $newData[] = $newLine; + } + + return implode("\n", $newData); + } + + function missingKeys($result, $compareArray) + { + $missing = array(); + $compLen = count($compareArray); + + for ($i = 0; $i < $compLen; $i++) + { + if ($compareArray[$i]{0} == '-') + { + $compareArray[$i] = substr($compareArray[$i], 1); + } + } + + foreach ($result as $oKey => $oVal) + { + foreach ($compareArray as $cVal) + { + if (strcasecmp($oKey, $cVal) == 0) + { + break; + } + } + + $missing[] = $oKey; + } + + if (count($missing) == 0) + { + return false; + } + + return $missing; + } + + function parseFlaggedParameters($noopt = array(), $params) + { + $result = array(); + reset($params); + + while (list($tmp, $p) = each($params)) + { + if ($p{0} == '-') + { + $pname = substr($p, 1); + $value = true; + + if ($pname{0} == '-') + { + $pname = substr($pname, 1); + + if (strpos($p, '=') !== false) + { + list($pname, $value) = explode('=', substr($p, 2), 2); + } + } + + $nextparm = current($params); + + if (!in_array($pname, $noopt) && $value === true + && $nextparm !== false && $nextparm{0} != '-') + { + list($tmp, $value) = each($params); + } + + $result[$pname] = $value; + } + else + { + $result[] = $p; + } + } + + return $result; + } +} +?> \ No newline at end of file diff --git a/includes/terminal.php b/includes/terminal.php new file mode 100644 index 0000000..1f8208c --- /dev/null +++ b/includes/terminal.php @@ -0,0 +1,72 @@ + ''); + protected $arguments = array(); + protected $command = ''; + + function Terminal($command) + { + $this->arguments = preg_split('/"([^"]*)"|\s/', $command, + NULL, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $this->command = array_shift($this->arguments); + } + + function arrayToString($glue, $array) + { + $toReturn = ''; + + foreach ($array as $key => $value) + { + $toReturn .= $key.$glue.$value."\n"; + } + + return $toReturn; + } + + function createLink($to, $title) + { + $this->write(''.$title.''); + } + + function write($data) + { + $this->output['response'] .= $data; + } + + function writeLine($line) + { + $this->write($line."\n"); + } + + function setValue($key, $value) + { + $this->output[$key] = $value; + } + + function nl2br($key) + { + $this->output[$key] = nl2br(rtrim($this->output[$key])); + } + + function getCommand() + { + return $this->command; + } + + function getArguments() + { + return $this->arguments; + } + + function getJSONOutput() + { + return json_encode($this->getOutput()); + } + + function getOutput() + { + return $this->output; + } +} +?> \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3a4f193 --- /dev/null +++ b/index.html @@ -0,0 +1,167 @@ + + + + Console + + + + + + + + + + + + +
+
+ Terminal +
+
+ + +
>
+
+
+ + \ No newline at end of file diff --git a/init.php b/init.php new file mode 100644 index 0000000..f778a06 --- /dev/null +++ b/init.php @@ -0,0 +1,71 @@ +getCommand() != '' && $settings['terminal']['enabled']) +{ + $command = array_key_exists_nc($terminal->getCommand(), $settings['terminal']['commands']); + + if ($command !== false) + { + require_once($settings['terminal']['commands'][$command]); + $arguments = $terminal->getArguments(); + $cl = new $command(); + + if (@$arguments[0] == 'help') + { + $cl->help(); + } + else + { + $cl->run($arguments); + } + + $terminal->setValue('command', $command); + } + else + { + $terminal->writeLine('Command not found.'); + } +} + +if (!$settings['terminal']['enabled']) +{ + $terminal->writeLine('Terminal has been disabled by an administrator.'); +} + +$terminal->setValue('ob', ob_get_clean()); +$terminal->nl2br('response'); +echo($terminal->getJSONOutput()); +ob_end_flush(); + +function array_key_exists_nc($key, $search) +{ + if (array_key_exists($key, $search)) + { + return $key; + } + + if (!(is_string($key) && is_array($search) && count($search))) + { + return false; + } + + $key = strtolower($key); + + foreach ($search as $k => $v) + { + if (strtolower($k) == $key) + { + return $k; + } + } + return false; +} +?> \ No newline at end of file diff --git a/jquery.ba-hashchange.min.js b/jquery.ba-hashchange.min.js new file mode 100644 index 0000000..3c607ba --- /dev/null +++ b/jquery.ba-hashchange.min.js @@ -0,0 +1,9 @@ +/* + * jQuery hashchange event - v1.3 - 7/21/2010 + * http://benalman.com/projects/jquery-hashchange-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,b){var c="hashchange",h=document,f,g=$.event.special,i=h.documentMode,d="on"+c in e&&(i===b||i>7);function a(j){j=j||location.href;return"#"+j.replace(/^[^#]*#?(.*)$/,"$1")}$.fn[c]=function(j){return j?this.bind(c,j):this.trigger(c)};$.fn[c].delay=50;g[c]=$.extend(g[c],{setup:function(){if(d){return false}$(f.start)},teardown:function(){if(d){return false}$(f.stop)}});f=(function(){var j={},p,m=a(),k=function(q){return q},l=k,o=k;j.start=function(){p||n()};j.stop=function(){p&&clearTimeout(p);p=b};function n(){var r=a(),q=o(m);if(r!==m){l(m=r,q);$(e).trigger(c)}else{if(q!==m){location.href=location.href.replace(/#.*/,"")+q}}p=setTimeout(n,$.fn[c].delay)}$.browser.msie&&!d&&(function(){var q,r;j.start=function(){if(!q){r=$.fn[c].src;r=r&&r+a();q=$('