Промежуточный коммит

This commit is contained in:
kirillius 2024-12-24 13:28:31 +03:00
parent 3c41ed288f
commit 3a3f6edaa0
21 changed files with 706 additions and 2 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/config.json

View File

@ -1,3 +1,8 @@
# protected-resources-list
# Protected Resources
Список ресурсов для настройки родительского контроля чтобы заблокировать доступ детям до запрещённых в РФ вражеских сервисов.
Список ресурсов для настройки родительского контроля чтобы заблокировать доступ детям до вражеских сервисов.
## networks
Каталог с файлами сервисов и их подсетей
## utils
Различные скрипты и утилиты

10
config.json.example Normal file
View File

@ -0,0 +1,10 @@
{
"password": {
"type": "plaintext",
"data": "admin"
},
"networks": [
"google"
],
"zebra_restart_cmd": "/etc/init.d/zebra restart"
}

21
networks/META.json Normal file
View File

@ -0,0 +1,21 @@
{
"domains": [],
"networks": [
"102.132.96.0/20",
"129.134.0.0/19",
"103.4.96.0/22",
"157.240.0.0/16",
"163.70.128.0/19",
"173.252.64.0/18",
"179.60.192.0/21",
"185.60.216.0/21",
"204.15.20.0/22",
"31.13.24.0/17",
"45.64.40.0/22",
"57.141.0.0/16",
"66.220.144.0/20",
"69.171.224.0/19",
"69.63.176.0/19",
"74.119.76.0/22"
]
}

6
networks/cura.json Normal file
View File

@ -0,0 +1,6 @@
{
"domains": [],
"networks": [
"188.114.98.0/23"
]
}

6
networks/flibusta.json Normal file
View File

@ -0,0 +1,6 @@
{
"domains": [],
"networks": [
"179.43.150.83/32"
]
}

24
networks/google.json Normal file
View File

@ -0,0 +1,24 @@
{
"domains": [],
"networks": [
"104.237.160.0/19",
"108.177.14.0/24",
"136.22.132.0/23",
"142.250.0.0/15",
"172.110.32.0/21",
"172.217.0.0/16",
"208.117.224.0/19",
"208.65.152.0/22",
"209.85.0.0/16",
"216.73.80.0/20",
"35.186.232.0/24",
"64.15.112.0/20",
"64.233.0.0/16",
"74.125.0.0/16",
"178.66.83.0/24",
"173.194.0.0/16",
"173.194.221.198/32",
"216.58.206.0/24",
"8.8.8.8/32"
]
}

7
networks/jetbrains.json Normal file
View File

@ -0,0 +1,7 @@
{
"domains": [],
"networks": [
"18.245.46.0/24",
"52.85.49.0/24"
]
}

7
networks/notion.json Normal file
View File

@ -0,0 +1,7 @@
{
"domains": [],
"networks": [
"104.18.39.102/32",
"172.64.148.154/32"
]
}

11
networks/rutracker.json Normal file
View File

@ -0,0 +1,11 @@
{
"domains": [],
"networks": [
"104.21.32.39/32",
"172.67.182.196/32",
"188.114.97.0/24",
"188.114.96.0/24",
"104.21.50.150/32",
"172.67.163.237/32"
]
}

114
utils/assets/index.html Normal file
View File

@ -0,0 +1,114 @@
<!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>
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(){
alert(await JSONRPC.__invoke("checkUpdates"));
})();
});
</script>
</body>
</html>

2
utils/assets/jquery-3.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

59
utils/assets/jrpc.js Normal file
View File

@ -0,0 +1,59 @@
export const JSONRPC = {
url: "/rpc",
__id: 1,
/**
*
* @param method
* @param params
* @returns Object
* @private
*/
__invoke: async function (method, params) {
if(params === undefined){
params = {};
}
const request = await JSONRPC.__performRequest(method, params);
if (!request.success) {
console.error(request.result);
throw new Error("Failed to invoke method " + method + " with params " + JSON.stringify(params));
}
return request.result;
},
__performRequest: async function (method, params) {
const __this = this;
const resp = await fetch(
__this.url,
{
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: JSON.stringify({
jsonrpc: '2.0',
method: method,
params: params,
id: __this.__id++
})
}
);
const success = resp.status === 200;
const result = (success ? (await resp.json()).result : {
"error": true,
"code": resp.status,
"status": resp.statusText,
"body": await resp.text()
});
return {
"result": result,
"success": success
};
}
};

57
utils/classes/Config.php Normal file
View File

@ -0,0 +1,57 @@
<?php
class Config implements ArrayAccess
{
private string $path;
public function asArray(): array
{
return $this->data;
}
public function fromArray($a)
{
$this->data = $a;
}
public function __construct()
{
$this->path = dirname(__DIR__, 2) . "/config.json";
}
public function read(): void
{
$this->data = @json_decode(@file_get_contents($this->path), true);
if ($this->data == null) {
throw new RuntimeException("Failed to read or parse config file");
}
}
public function save(): void
{
file_put_contents($this->path, json_encode($this->data));
}
private mixed $data = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->data[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->data[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->data[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->data[$offset]);
}
}

View File

@ -0,0 +1,32 @@
<?php
class NetworkConfigReader
{
private array $configs = [];
public function __construct()
{
$path = dirname(__DIR__, 2) . "/networks";
foreach (new IteratorIterator(new DirectoryIterator($path)) as $file) {
/**
* @var SplFileInfo $file
*/
if ($file->getExtension() === "json") {
$key = $file->getBasename(".json");
$value = @json_decode(@file_get_contents($file->getPathname()), true);
if ($value === null) {
throw new RuntimeException("Network file " . $file->getBasename() . " is invalid or cannot be read");
}
$this->configs[$key] = $value;
}
}
}
public function getConfigs(): array
{
return $this->configs;
}
}

View File

@ -0,0 +1,38 @@
<?php
class RoutingTableReader
{
private $routes = [];
/**
* @return array
*/
public function getRoutes(): array
{
return $this->routes;
}
public function __construct()
{
$result = @shell_exec("ip --json route show");
if (!$result) {
throw new RuntimeException("Failed to read routing table");
}
$this->routes = @json_decode($result, true);
if ($this->routes === null) {
throw new RuntimeException("Failed to parse json output");
}
foreach ($this->routes as $key => &$route) {
if ($route["dst"] === "default") {
$route["dst"] = "0.0.0.0/0";
} elseif (!str_contains($route["dst"], "/")) {
$route["dst"] .= "/32";
}
}
}
}

74
utils/common.inc.php Normal file
View File

@ -0,0 +1,74 @@
<?php
if (!function_exists('mime_content_type')) {
function mime_content_type($filename): bool|string
{
$mime_types = array(
'txt' => 'text/plain',
'htm' => 'text/html',
'html' => 'text/html',
'php' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'xml' => 'application/xml',
'swf' => 'application/x-shockwave-flash',
'flv' => 'video/x-flv',
// images
'png' => 'image/png',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'ico' => 'image/vnd.microsoft.icon',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
// archives
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
'exe' => 'application/x-msdownload',
'msi' => 'application/x-msdownload',
'cab' => 'application/vnd.ms-cab-compressed',
// audio/video
'mp3' => 'audio/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
// adobe
'pdf' => 'application/pdf',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
// ms office
'doc' => 'application/msword',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
// open office
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
);
$parts = explode('.', $filename);
$ext = strtolower(array_pop($parts));
if (array_key_exists($ext, $mime_types)) {
return $mime_types[$ext];
} elseif (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimetype;
} else {
return 'application/octet-stream';
}
}
}

6
utils/loader.php Normal file
View File

@ -0,0 +1,6 @@
#!/usr/bin/php
<?php
require_once __DIR__ . "/common.inc.php";
spl_autoload_register(function ($classname) {
require_once __DIR__ . "/classes/" . $classname . ".php";
});

155
utils/server.php Normal file
View File

@ -0,0 +1,155 @@
<?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 = shell_exec("git --no-pager fetch --dry-run --porcelain --verbose 2>&1");
return $data;
}
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();
}

2
utils/server.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
php -S 0.0.0.0:8000 server.php

67
utils/zebracfg.php Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/php
<?php
require_once __DIR__ . "/loader.php";
const CFGFILE = "/etc/quagga/zebra.conf";
const REM_PREFIX = "! routes from file ";
try {
$networks = (new NetworkConfigReader())->getConfigs();
$config = new Config();
$config->read();
$routeParser = new RoutingTableReader();
$routes = $routeParser->getRoutes();
$defGatewayInterface = "";
$defGateway = "";
foreach ($routes as $route) {
if ($route["dst"] === "0.0.0.0/0") {
$defGatewayInterface = $route["dev"];
$defGateway = $route["gateway"];
break;
}
}
if (!$defGatewayInterface) {
throw new RuntimeException("Failed to detect default gateway interface");
}
$contents = file_get_contents(CFGFILE);
$lines = explode("\n", $contents);
//remove existing routes
foreach ($lines as $key => $line) {
if (str_starts_with($line, REM_PREFIX) or str_starts_with($line, "ip route ") and str_contains($line . " ", $defGateway . " ")) {
unset($lines[$key]);
}
}
//add new routes
foreach ($config["networks"] as $key) {
$lines[] = REM_PREFIX . $key;
if (isset($networks[$key])) {
foreach ($networks[$key]["networks"] as $route) {
$lines[] = "ip route " . $route . " " . $defGateway;
}
}
}
foreach ($lines as $key => $line) {
if (trim($line) === "") {
unset($lines[$key]);
}
}
$backupFile = CFGFILE . ".sav";
unlink($backupFile);
rename(CFGFILE, $backupFile);
file_put_contents(CFGFILE, implode("\n", $lines));
//restart zebra
echo shell_exec($config["zebra_restart_cmd"]);
} catch (Exception $e) {
echo "\nError:" . $e->getMessage() . "\n";
exit(1);
}
exit(0);