total refactor WIP

This commit is contained in:
kirillius 2024-12-26 12:05:05 +03:00
parent 3b10da1adb
commit 7505b4d9e4
27 changed files with 520 additions and 308 deletions

View File

@ -5,4 +5,13 @@
Каталог с файлами сервисов и их подсетей
## utils
Различные скрипты и утилиты
Различные скрипты и утилиты
### Установка
```shell
apk add php php-session jq
```
```shell
chmod -R +x ./bin
```

89
assets/App.js Normal file
View File

@ -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};

36
assets/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Routing config</title>
<script src="jquery-3.7.1.min.js" type="text/javascript"></script>
</head>
<body>
<span id="loading">Loading...</span>
<div style="display: none" id="panel">
Selected networks:
<table id="net-table">
<tr>
<td><input type="checkbox"></td>
<td><span>Network name</span></td>
</tr>
</table>
<div id="buttons">
<button id="save">Save</button>
</div>
</div>
<script type="module">
import {App} from "./App.js";
$(document).ready(function () {
App.render();
});
</script>
</body>
</html>

13
bin/webui-server Executable file
View File

@ -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

View File

@ -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

13
classes/IPluggable.php Normal file
View File

@ -0,0 +1,13 @@
<?php
interface IPluggable
{
const PROTECTED_NAMES = ["enable", "disable", "render", "init"];
public function enable();
public function disable();
public function init();
}

View File

@ -1,12 +1,13 @@
<?php
class NetworkConfigReader
{
private array $configs = [];
public function __construct()
{
$path = dirname(__DIR__, 2) . "/networks";
$path = dirname(__DIR__) . "/networks";
foreach (new IteratorIterator(new DirectoryIterator($path)) as $file) {
/**
* @var SplFileInfo $file

142
classes/RPC.php Normal file
View File

@ -0,0 +1,142 @@
<?php
class RPC
{
private Config $config;
private array $plugins = [];
public function __construct()
{
$this->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);
}
}
}

View File

@ -1,5 +1,6 @@
<?php
class RoutingTableReader
{
private $routes = [];

85
classes/WebRouter.php Normal file
View File

@ -0,0 +1,85 @@
<?php
class WebRouter
{
private string $requestBody;
private string $requestedFile;
private string $URI;
public function __construct()
{
$this->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);
}
}

View File

@ -6,5 +6,9 @@
"networks": [
"google"
],
"zebra_restart_cmd": "/etc/init.d/zebra restart"
"web": {
"port":8000,
"host":"0.0.0.0"
},
"plugins":[]
}

View File

@ -1,5 +1,9 @@
#!/usr/bin/php
<?php
use classes\Config;
use classes\NetworkConfigReader;
if (!isset($argv[1])) {
exit(1);
}

View File

@ -0,0 +1,32 @@
<?php
/*
*
* public function restart_quagga()
{
return shell_exec("./zebracfg.php");
}
*/
class QuaggaPlugin implements IPluggable
{
public function restart()
{
return shell_exec("./zebracfg.php");
}
public function enable()
{
// TODO: Implement enable() method.
}
public function disable()
{
// TODO: Implement disable() method.
}
public function init()
{
// TODO: Implement init() method.
}
}

View File

@ -0,0 +1,9 @@
{
"class": "QuaggaPlugin",
"config": {
"quagga": {
"restart_cmd": "/etc/init.d/zebra restart",
"file": "/etc/quagga/zebra.conf"
}
}
}

1
plugins/quagga/plugin.js Normal file
View File

@ -0,0 +1 @@
//<button id="restart-quagga">Restart quagga</button>

View File

@ -0,0 +1,41 @@
<?php
class Updates implements IPluggable
{
public function enable()
{
}
public function disable()
{
}
public function render()
{
return file_get_contents(__DIR__ . "/plugin.js");
}
public function init()
{
}
public function check(): ?bool
{
$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 install(): string|bool|null
{
return @shell_exec("git --no-pager pull --verbose 2>&1");
}
}

View File

@ -0,0 +1,5 @@
{
"class": "Updates",
"config": {
}
}

22
plugins/updates/plugin.js Normal file
View File

@ -0,0 +1,22 @@
import {JSONRPC} from "/assets/jrpc.js";
(async function () {
$("#panel").prepend(`<div id="update-panel">Checking for updates...</div><hr>`);
let state = (await JSONRPC.__invoke("updates::check"));
if (state === null) {
$("#update-panel").html(`<span style="color:red;">Error checking updates</span>`);
} else if (state === false) {
$("#update-panel").html(`<span style="color:black;">There is no updates</span>`);
} else if (state === true) {
$("#update-panel").html(`<span style="color:green;">Some updates are available</span>&nbsp;<button>Update</button>`);
$("#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);
}
});
}
})();

4
server.php Normal file
View File

@ -0,0 +1,4 @@
<?php
require_once __DIR__ . "/loader.php";
$router = new WebRouter();
$router->handleRequest();

View File

@ -1,133 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Routing config</title>
<script src="jquery-3.7.1.min.js" type="text/javascript"></script>
</head>
<body>
<span id="loading">Loading...</span>
<div style="display: none" id="panel">
<div id="update-panel">Checking for updates...</div>
<hr>
Selected networks:
<table id="net-table">
<tr>
<td><input type="checkbox"></td>
<td><span>Network name</span></td>
</tr>
</table>
<div>
<button id="save">Save</button>
<button id="restart-quagga">Restart quagga</button>
</div>
</div>
<script type="module">
import {JSONRPC} from "./jrpc.js";
let config = {};
let networks = {};
async function auth() {
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);
}
config = await JSONRPC.__invoke("getConfig");
networks = await JSONRPC.__invoke("getNetworks");
$("#loading").hide();
fillNetworks();
$("#panel").show();
}
function fillNetworks() {
let proto = $("#net-table tr");
proto.detach();
for (const net in networks) {
let item = proto.clone();
item.find("input").prop('checked', config.networks.indexOf(net) !== -1).change(function () {
if ($(this).prop('checked')) {
config.networks.push(net);
} else {
config.networks = config.networks.filter(e => e !== net);
}
});
item.find("span").text(net);
$("#net-table").append(item);
}
}
$(document).ready(function () {
auth();
$("#save").click(function () {
const self = $(this);
self.prop("disabled", true);
(async function () {
await JSONRPC.__invoke("setConfig", 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);
}
})();
}
});
(async function () {
let state = (await JSONRPC.__invoke("checkUpdates"));
if (state === null) {
$("#update-panel").html(`<span style="color:red;">Error checking updates</span>`);
} else if (state === false) {
$("#update-panel").html(`<span style="color:black;">There is no updates</span>`);
} else if (state === true) {
$("#update-panel").html(`<span style="color:green;">Some updates are available</span>&nbsp;<button>Update</button>`);
$("#update-panel button").click(async function () {
$("#panel").hide();
$("#loading").show().text("Installing updates...");
try {
console.log(await JSONRPC.__invoke("installUpdates"));
} finally {
setTimeout(() => location.reload(), 10000);
}
});
}
})();
});
</script>
</body>
</html>

View File

@ -1,165 +0,0 @@
<?php
require_once __DIR__ . "/loader.php";
if (str_starts_with($_SERVER["REQUEST_URI"], "/assets")) {
$file = __DIR__ . $_SERVER["REQUEST_URI"];
if (!file_exists($file)) {
http_response_code(404);
echo "File not found: " . $_SERVER["REQUEST_URI"];
exit();
}
header("content-type: " . mime_content_type($file));
echo file_get_contents($file);
exit();
}
if (!str_starts_with($_SERVER["REQUEST_URI"], "/rpc")) {
http_response_code(302);
header("location: /assets/index.html");
exit();
}
$request = json_decode(@file_get_contents('php://input'), true);
try {
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");
}
}
/*
* {"jsonrpc":"2.0","method":"auth","params":{},"id":1}
*/
@session_start();
class RPC
{
private Config $config;
public function __construct()
{
$this->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();
}

View File

@ -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

View File

@ -1,5 +1,10 @@
#!/usr/bin/php
<?php
use classes\Config;
use classes\NetworkConfigReader;
use classes\RoutingTableReader;
require_once __DIR__ . "/loader.php";
const CFGFILE = "/etc/quagga/zebra.conf";
const REM_PREFIX = "! routes from file ";