diff --git a/README.md b/README.md index 1fea14e..922380d 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,13 @@ Каталог с файлами сервисов и их подсетей ## utils -Различные скрипты и утилиты \ No newline at end of file +Различные скрипты и утилиты + +### Установка +```shell +apk add php php-session jq +``` + +```shell +chmod -R +x ./bin +``` \ No newline at end of file diff --git a/assets/App.js b/assets/App.js new file mode 100644 index 0000000..8a785b4 --- /dev/null +++ b/assets/App.js @@ -0,0 +1,89 @@ +import {JSONRPC} from "./jrpc.js"; + +const App = { + config: {}, + networks: {}, + RPC: JSONRPC +}; + + +App.auth = async function () { + let authorized = await JSONRPC.__invoke("auth"); + if (!authorized) { + do { + let pass = prompt("Password"); + authorized = await JSONRPC.__invoke("auth", { + "password": pass + }); + + if (!authorized) { + alert("Wrong password"); + } + } while (!authorized); + } + + this.config = await JSONRPC.__invoke("getConfig"); + this.networks = await JSONRPC.__invoke("getNetworks"); + + + for (const key of this.config.plugins) { + $("body").append("<" + "script type='module' src='/plugins/" + key + "/plugin.js'><" + "/script>"); + } + + $("#loading").hide(); + + this.fillNetworks(); + + $("#panel").show(); +} + +App.fillNetworks = function () { + const that = this; + let proto = $("#net-table tr"); + proto.detach(); + + for (const net in this.networks) { + let item = proto.clone(); + item.find("input").prop('checked', this.config.networks.indexOf(net) !== -1).change(function () { + if ($(this).prop('checked')) { + that.config.networks.push(net); + } else { + that.config.networks = that.config.networks.filter(e => e !== net); + } + }); + item.find("span").text(net); + $("#net-table").append(item); + } +} + +App.render = async function () { + await this.auth(); + $("#save").click(function () { + const self = $(this); + self.prop("disabled", true); + (async function () { + await JSONRPC.__invoke("setConfig", App.config); + alert("Config saved!"); + self.prop("disabled", false); + })(); + }); + $("#restart-quagga").click(function () { + if (confirm("Are you sure?")) { + const self = $(this); + self.prop("disabled", true); + (async function () { + try { + alert(await JSONRPC.__invoke("restart_quagga")); + } finally { + setTimeout(() => { + self.prop("disabled", false); + }, 5000); + } + + })(); + } + }); +} + + +export {App}; \ No newline at end of file diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..bf02845 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,36 @@ + + + + + Routing config + + + +Loading... + + + + + + + + \ No newline at end of file diff --git a/utils/assets/jquery-3.7.1.min.js b/assets/jquery-3.7.1.min.js similarity index 100% rename from utils/assets/jquery-3.7.1.min.js rename to assets/jquery-3.7.1.min.js diff --git a/utils/assets/jrpc.js b/assets/jrpc.js similarity index 100% rename from utils/assets/jrpc.js rename to assets/jrpc.js diff --git a/bin/webui-server b/bin/webui-server new file mode 100755 index 0000000..3ffd8e4 --- /dev/null +++ b/bin/webui-server @@ -0,0 +1,13 @@ +#!/bin/sh +SELFDIR=`dirname $0` +ROOT=`realpath $SELFDIR/..` +CFGFILE=$ROOT/config.json + +if ! test -f "$CFGFILE"; then + echo Config file $CFGFILE not found + exit 1 +fi + +HOST=`jq -r .web.host $CFGFILE` +PORT=`jq -r .web.port $CFGFILE` +php -S $HOST:$PORT $ROOT/server.php \ No newline at end of file diff --git a/utils/classes/Config.php b/classes/Config.php similarity index 94% rename from utils/classes/Config.php rename to classes/Config.php index 6ce33ea..e8598c4 100644 --- a/utils/classes/Config.php +++ b/classes/Config.php @@ -16,7 +16,7 @@ class Config implements ArrayAccess public function __construct() { - $this->path = dirname(__DIR__, 2) . "/config.json"; + $this->path = dirname(__DIR__) . "/config.json"; } public function read(): void diff --git a/classes/IPluggable.php b/classes/IPluggable.php new file mode 100644 index 0000000..330d27e --- /dev/null +++ b/classes/IPluggable.php @@ -0,0 +1,13 @@ +config = new Config(); + $this->config->read(); + + foreach ($this->config["plugins"] as $plugin) { + try { + $meta = $this->getPluginMetadata($plugin); + $this->loadPlugin($plugin, $meta["class"]); + } catch (Error $e) { + continue; + } + } + } + + private function getPluginMetadata($name): array + { + $root = dirname(__DIR__) . "/plugins/" . $name; + if (!file_exists($root)) { + throw new RuntimeException("Plugin $name dir not found"); + } + $metafile = $root . "/metadata.json"; + if (!file_exists($metafile)) { + throw new RuntimeException("Plugin $name metadata not found"); + } + + $meta = @json_decode(@file_get_contents($metafile), true); + + if ($meta === null) { + throw new RuntimeException("Unable to parse $name plugin metadata"); + } + return $meta; + } + + private function loadPlugin($name, $classname): void + { + $file = dirname(__DIR__) . "/plugins/" . $name . "/" . $classname . ".php"; + if (!file_exists($file)) { + throw new RuntimeException("Plugin $name class $classname not found"); + } + require_once $file; + + $instance = new $classname(); + if (!($instance instanceof IPluggable)) { + throw new RuntimeException("Class $classname have to implement IPluggable"); + } + + $this->plugins[$name] = $instance; + } + + public function getConfig(): array + { + $this->checkAuth(); + return $this->config->asArray(); + } + + public function setConfig($config): bool + { + $this->config->fromArray($config); + $this->config->save(); + return true; + } + + private function checkAuth(): void + { + $auth = $_SESSION["auth"] ?? false; + if (!$auth) { + throw new RuntimeException("Unauthorized"); + } + } + + public function getNetworks(): array + { + $this->checkAuth(); + return (new NetworkConfigReader())->getConfigs(); + + } + + + public function logout(): void + { + $_SESSION["auth"] = false; + } + + public function auth($params): bool + { + if (isset($params["password"])) { + if ($this->comparePassword($params["password"])) { + return $_SESSION["auth"] = true; + } else { + return false; + } + } + return $_SESSION["auth"] ?? false; + } + + private function comparePassword($passwd): bool + { + $pass = $this->config["password"]; + if ($pass["type"] == "plaintext") { + return $pass["data"] == $passwd; + } else if ($pass["type"] == "hash") { + return $this->hash($passwd) == $pass["data"]; + } + return false; + } + + private function hash($what): string + { + return md5(sha1($what) . md5($what)); + } + + public function __invoke($method, $args) + { + $parts = explode("::", $method); + $isPlugin = count($parts) > 1; + + if ($isPlugin) { + $this->checkAuth(); + $plugin = $this->plugins[$parts[0]]; + $methodname = $parts[1]; + if (in_array($methodname, IPluggable::PROTECTED_NAMES)) { + throw new RuntimeException("Unable to invoke internal methods"); + } + return call_user_func([$plugin, $methodname], $args); + } else { + $cls = new ReflectionClass(__CLASS__); + $method = $cls->getMethod($method); + if (!$method or !$method->isPublic()) { + throw new RuntimeException("Unable to find method"); + } + return $method->invoke($this, $args); + } + } +} \ No newline at end of file diff --git a/utils/classes/RoutingTableReader.php b/classes/RoutingTableReader.php similarity index 99% rename from utils/classes/RoutingTableReader.php rename to classes/RoutingTableReader.php index ad0bfdb..6f2c814 100644 --- a/utils/classes/RoutingTableReader.php +++ b/classes/RoutingTableReader.php @@ -1,5 +1,6 @@ requestBody = @file_get_contents('php://input'); + $this->requestedFile = dirname(__DIR__) . "/" . $_SERVER["REQUEST_URI"]; + $this->URI = $_SERVER["REQUEST_URI"]; + } + + public function handleRequest(): void + { + @session_start(); + try { + if (str_starts_with($this->URI, "/assets") or str_starts_with($this->URI, "/plugins") and str_ends_with($this->URI, ".js")) { + $this->handleAsset(); + } elseif (!str_starts_with($this->URI, "/rpc")) { + $this->redirect("/assets/index.html"); + } else { + $this->handleJRPC(); + } + } finally { + session_write_close(); + } + } + + private function handleJRPC(): void + { + try { + $request = @json_decode($this->requestBody, true); + if ($request === null) { + throw new RuntimeException("Failed to parse JRPC"); + } + + foreach (["id", "jsonrpc", "method", "params"] as $param) { + if (!isset($request[$param])) { + throw new RuntimeException("Bad JRPC structure"); + } + } + + $rpc = new RPC(); + $response = $rpc($request["method"], $request["params"]); + + header("content-type: application/json"); + + echo json_encode([ + "jsonrpc" => "2.0", + "id" => $request["id"] ?? 0, + "result" => $response + ]); + + } catch (Error $e) { + http_response_code(500); + echo json_encode([ + "jsonrpc" => "2.0", + "id" => $request["id"], + "error" => $e->getMessage() + ]); + } + + } + + private function handleAsset(): void + { + if (!file_exists($this->requestedFile)) { + http_response_code(404); + echo "File not found: " . $this->URI; + } else { + header("content-type: " . mime_content_type($this->requestedFile)); + echo file_get_contents($this->requestedFile); + } + + } + + private function redirect($where): void + { + http_response_code(302); + header("location: " . $where); + } +} \ No newline at end of file diff --git a/utils/common.inc.php b/common.inc.php similarity index 100% rename from utils/common.inc.php rename to common.inc.php diff --git a/config.json.example b/config.json.example index 0d7b025..b239a16 100644 --- a/config.json.example +++ b/config.json.example @@ -6,5 +6,9 @@ "networks": [ "google" ], - "zebra_restart_cmd": "/etc/init.d/zebra restart" + "web": { + "port":8000, + "host":"0.0.0.0" + }, + "plugins":[] } \ No newline at end of file diff --git a/utils/loader.php b/loader.php similarity index 100% rename from utils/loader.php rename to loader.php diff --git a/utils/ovpn-connect.php b/ovpn-connect.php similarity index 93% rename from utils/ovpn-connect.php rename to ovpn-connect.php index 449348c..bad6f29 100755 --- a/utils/ovpn-connect.php +++ b/ovpn-connect.php @@ -1,5 +1,9 @@ #!/usr/bin/php Restart quagga \ No newline at end of file diff --git a/plugins/updates/Updates.php b/plugins/updates/Updates.php new file mode 100644 index 0000000..9b1097c --- /dev/null +++ b/plugins/updates/Updates.php @@ -0,0 +1,41 @@ +&1 | grep refs")); + $parts = explode(" ", trim($data)); + if (count($parts) < 3) { + return null; + } + + return $parts[0] != $parts[1]; + } + + public function install(): string|bool|null + { + return @shell_exec("git --no-pager pull --verbose 2>&1"); + } +} \ No newline at end of file diff --git a/plugins/updates/metadata.json b/plugins/updates/metadata.json new file mode 100644 index 0000000..58df75b --- /dev/null +++ b/plugins/updates/metadata.json @@ -0,0 +1,5 @@ +{ + "class": "Updates", + "config": { + } +} \ No newline at end of file diff --git a/plugins/updates/plugin.js b/plugins/updates/plugin.js new file mode 100644 index 0000000..49aec8f --- /dev/null +++ b/plugins/updates/plugin.js @@ -0,0 +1,22 @@ +import {JSONRPC} from "/assets/jrpc.js"; + +(async function () { + $("#panel").prepend(`
Checking for updates...

`); + let state = (await JSONRPC.__invoke("updates::check")); + if (state === null) { + $("#update-panel").html(`Error checking updates`); + } else if (state === false) { + $("#update-panel").html(`There is no updates`); + } else if (state === true) { + $("#update-panel").html(`Some updates are available `); + $("#update-panel button").click(async function () { + $("#panel").hide(); + $("#loading").show().text("Installing updates..."); + try { + console.log(await JSONRPC.__invoke("updates::install")); + } finally { + setTimeout(() => location.reload(), 10000); + } + }); + } +})(); \ No newline at end of file diff --git a/server.php b/server.php new file mode 100644 index 0000000..1e1d967 --- /dev/null +++ b/server.php @@ -0,0 +1,4 @@ +handleRequest(); \ No newline at end of file diff --git a/utils/assets/index.html b/utils/assets/index.html deleted file mode 100644 index 431ad0d..0000000 --- a/utils/assets/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - Routing config - - - -Loading... - - - - - - - - \ No newline at end of file diff --git a/utils/server.php b/utils/server.php deleted file mode 100644 index 7e237c5..0000000 --- a/utils/server.php +++ /dev/null @@ -1,165 +0,0 @@ -config = new Config(); - $this->config->read(); - } - - private function hash($what): string - { - return md5(sha1($what) . md5($what)); - } - - public function getConfig(): array - { - $this->checkAuth(); - return $this->config->asArray(); - } - - - public function restart_quagga() - { - return shell_exec("./zebracfg.php"); - } - - public function checkUpdates() - { - $this->checkAuth(); - $data = str_replace("=","",@shell_exec("git --no-pager fetch --dry-run --porcelain --verbose 2>&1 | grep refs")); - $parts = explode(" ", trim($data)); - if(count($parts) < 3){ - return null; - } - - return $parts[0] != $parts[1]; - } - - public function installUpdates() - { - return @shell_exec("git --no-pager pull --verbose 2>&1"); - } - - - - public function getNetworks(): array - { - $this->checkAuth(); - return (new NetworkConfigReader())->getConfigs(); - - } - - - - public function setConfig($config): bool - { - $this->config->fromArray($config); - $this->config->save(); - return true; - } - - private function checkAuth(): void - { - $auth = $_SESSION["auth"] ?? false; - if (!$auth) { - throw new RuntimeException("Unauthorized"); - } - } - - private function comparePassword($passwd): bool - { - $pass = $this->config["password"]; - if ($pass["type"] == "plaintext") { - return $pass["data"] == $passwd; - } else if ($pass["type"] == "hash") { - return $this->hash($passwd) == $pass["data"]; - } - return false; - } - - public function logout(): void - { - $_SESSION["auth"] = false; - } - - public function auth($params): bool - { - if (isset($params["password"])) { - if ($this->comparePassword($params["password"])) { - return $_SESSION["auth"] = true; - } else { - return false; - } - } - return $_SESSION["auth"] ?? false; - } - - public function __invoke($method, $args) - { - $cls = new ReflectionClass(__CLASS__); - $method = $cls->getMethod($method); - if (!$method or !$method->isPublic()) { - throw new RuntimeException("Unable to find method"); - } - return $method->invoke($this, $args); - } - } - - $rpc = new RPC(); - $response = $rpc($request["method"], $request["params"]); - - header("content-type: application/json"); - @session_write_close(); - - echo json_encode([ - "jsonrpc" => "2.0", - "id" => $request["id"], - "result" => $response - ]); - -} catch (Exception $e) { - http_response_code(500); - echo $e->getMessage(); -} diff --git a/utils/server.sh b/utils/server.sh deleted file mode 100755 index e48752b..0000000 --- a/utils/server.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -SELF=`dirname $0` -DIR=`realpath $SELF` -cd $DIR -killall php -php -S 0.0.0.0:8000 server.php \ No newline at end of file diff --git a/utils/zebracfg.php b/zebracfg.php similarity index 95% rename from utils/zebracfg.php rename to zebracfg.php index d935b31..6b13307 100644 --- a/utils/zebracfg.php +++ b/zebracfg.php @@ -1,5 +1,10 @@ #!/usr/bin/php