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...
+
+
+ Selected networks:
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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...
-
-
-
-
Checking for updates...
-
- Selected networks:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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