Compare commits
No commits in common. "b815de23c0ef74f23a9ae683b93bcc23937a0d4d" and "c36d96677e4ee53f318b4634a1fab0f5078fc730" have entirely different histories.
b815de23c0
...
c36d96677e
|
|
@ -2,7 +2,11 @@ package ru.kirillius.pf.sdn;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.*;
|
import ru.kirillius.pf.sdn.External.API.Components.DNSMASQ;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.Components.ROS;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.Components.TDNS;
|
||||||
import ru.kirillius.pf.sdn.External.API.GitSubscription;
|
import ru.kirillius.pf.sdn.External.API.GitSubscription;
|
||||||
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||||
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
|
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
|
||||||
|
|
@ -157,7 +161,7 @@ public class App implements Context, Closeable {
|
||||||
.configFile(new File(getArgument("c", args)))
|
.configFile(new File(getArgument("c", args)))
|
||||||
.appLibrary(new File(getArgument("l", args)))
|
.appLibrary(new File(getArgument("l", args)))
|
||||||
.repository(getArgument("r", args))
|
.repository(getArgument("r", args))
|
||||||
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class, SRM.class)).build())) {
|
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class)).build())) {
|
||||||
Wait.when(app.running::get);
|
Wait.when(app.running::get);
|
||||||
if (app.shouldRestart.get()) {
|
if (app.shouldRestart.get()) {
|
||||||
System.exit(42);
|
System.exit(42);
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
package ru.kirillius.pf.sdn.External.API.Components;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import ru.kirillius.java.utils.events.EventListener;
|
|
||||||
import ru.kirillius.json.JSONUtility;
|
|
||||||
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
|
||||||
import ru.kirillius.pf.sdn.core.Context;
|
|
||||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
|
||||||
import ru.kirillius.pf.sdn.web.WebService;
|
|
||||||
import ru.kirillius.pf.sdn.web.WebhookServlet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component integrating with OpenVPN to expose management RPC and synchronize route exports.
|
|
||||||
*/
|
|
||||||
public final class SRM extends AbstractComponent<JSONObject> {
|
|
||||||
private final static String CTX = SRM.class.getSimpleName();
|
|
||||||
private final static String HOOK_NAME = "SRM";
|
|
||||||
private final EventListener<NetworkResourceBundle> updateEvent;
|
|
||||||
private final List<IPv4Subnet> lastSubnets = new ArrayList<>();
|
|
||||||
private final AtomicLong lastUpdate = new AtomicLong(0);
|
|
||||||
private EventListener<WebhookServlet.RequestContext> hookListener;
|
|
||||||
|
|
||||||
public SRM(Context context) {
|
|
||||||
super(context);
|
|
||||||
var eventsHandler = context.getEventsHandler();
|
|
||||||
|
|
||||||
updateEvent = eventsHandler.getNetworkManagerUpdateEvent().add(bundle -> {
|
|
||||||
var subnets = new HashSet<>(bundle.getSubnets());
|
|
||||||
synchronized (lastSubnets) {
|
|
||||||
if (lastSubnets.size() != subnets.size() || !subnets.containsAll(lastSubnets)) {
|
|
||||||
lastSubnets.clear();
|
|
||||||
lastSubnets.addAll(subnets);
|
|
||||||
lastUpdate.set(System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var webService = context.getServiceManager().getService(WebService.class);
|
|
||||||
var webhookServlet = webService.getWebhookServlet();
|
|
||||||
|
|
||||||
hookListener = webhookServlet.registerHook(HOOK_NAME, httpContext -> {
|
|
||||||
var resp = httpContext.response();
|
|
||||||
if (!httpContext.request().getMethod().equals("GET")) {
|
|
||||||
resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = httpContext.request().getRequestURI();
|
|
||||||
resp.setHeader("Cache-Control", "no-cache");
|
|
||||||
if (path.endsWith("/update")) {
|
|
||||||
resp.setHeader("Content-Type", "text/plain");
|
|
||||||
var writer = resp.getWriter();
|
|
||||||
writer.print(lastUpdate.get());
|
|
||||||
writer.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.endsWith("/subnets")) {
|
|
||||||
resp.setHeader("Content-Type", "application/json");
|
|
||||||
var subnets = new ArrayList<IPv4Subnet>();
|
|
||||||
synchronized (lastSubnets) {
|
|
||||||
//noinspection CollectionAddAllCanBeReplacedWithConstructor
|
|
||||||
subnets.addAll(lastSubnets);
|
|
||||||
subnets.sort(Comparator.comparingLong(IPv4Subnet::getLongAddress));
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = JSONUtility.serializeCollection(subnets, IPv4Subnet.class, null);
|
|
||||||
var writer = resp.getWriter();
|
|
||||||
writer.print(json.toString());
|
|
||||||
writer.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
var eventsHandler = context.getEventsHandler();
|
|
||||||
if (updateEvent != null) {
|
|
||||||
eventsHandler.getNetworkManagerUpdateEvent().remove(updateEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hookListener != null) {
|
|
||||||
var webService = context.getServiceManager().getService(WebService.class);
|
|
||||||
var webhookServlet = webService.getWebhookServlet();
|
|
||||||
webhookServlet.unregisterHook(HOOK_NAME, hookListener);
|
|
||||||
hookListener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ru.kirillius.pf.sdn.web;
|
package ru.kirillius.pf.sdn.web;
|
||||||
|
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
||||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||||
|
|
@ -9,7 +10,10 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||||
import ru.kirillius.pf.sdn.core.AppService;
|
import ru.kirillius.pf.sdn.core.AppService;
|
||||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||||
import ru.kirillius.pf.sdn.core.Context;
|
import ru.kirillius.pf.sdn.core.Context;
|
||||||
import ru.kirillius.pf.sdn.web.RPC.*;
|
import ru.kirillius.pf.sdn.web.RPC.Auth;
|
||||||
|
import ru.kirillius.pf.sdn.web.RPC.NetworkManager;
|
||||||
|
import ru.kirillius.pf.sdn.web.RPC.RPC;
|
||||||
|
import ru.kirillius.pf.sdn.web.RPC.SubscriptionManager;
|
||||||
import ru.kirillius.pf.sdn.web.RPC.System;
|
import ru.kirillius.pf.sdn.web.RPC.System;
|
||||||
import ru.kirillius.utils.logging.SystemLogger;
|
import ru.kirillius.utils.logging.SystemLogger;
|
||||||
|
|
||||||
|
|
@ -24,6 +28,7 @@ import java.util.Set;
|
||||||
*/
|
*/
|
||||||
public class WebService extends AppService {
|
public class WebService extends AppService {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the embedded HTTP server.
|
* Stops the embedded HTTP server.
|
||||||
*/
|
*/
|
||||||
|
|
@ -68,7 +73,7 @@ public class WebService extends AppService {
|
||||||
|
|
||||||
this.addConnector(connector);
|
this.addConnector(connector);
|
||||||
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
||||||
servletContext.addServlet(webhookServlet, WebhookServlet.CONTEXT_PATH);
|
|
||||||
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
||||||
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
||||||
try {
|
try {
|
||||||
|
|
@ -76,7 +81,6 @@ public class WebService extends AppService {
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setHandler(servletContext);
|
this.setHandler(servletContext);
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
|
@ -94,18 +98,21 @@ public class WebService extends AppService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var isProtectedAccess = call.getMethod().getAnnotation(ProtectedMethod.class);
|
var isProtectedAccess = call.getMethod().getAnnotation(ProtectedMethod.class);
|
||||||
if (isProtectedAccess != null) {
|
if (isProtectedAccess != null) {
|
||||||
if (!authorized) throw new SecurityException("Forbidden");
|
if (!authorized) throw new SecurityException("Forbidden");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
for (var handlerClass : RPCHandlerTypes) {
|
for (var handlerClass : RPCHandlerTypes) {
|
||||||
var instance = RPC.instantiate(handlerClass, context);
|
var instance = RPC.instantiate(handlerClass, context);
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
JSONRPC.addTargetInstance((Class<? super RPC>) handlerClass, instance);
|
JSONRPC.addTargetInstance((Class<? super RPC>) handlerClass, instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
JSONRPC.getErrorHandler().add(throwable -> {
|
JSONRPC.getErrorHandler().add(throwable -> {
|
||||||
SystemLogger.error("JRPC Request " +
|
SystemLogger.error("JRPC Request " +
|
||||||
(throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
|
(throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
|
||||||
|
|
@ -115,13 +122,12 @@ public class WebService extends AppService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final JSONRPCServlet JSONRPC = new JSONRPCServlet();
|
private final JSONRPCServlet JSONRPC = new JSONRPCServlet();
|
||||||
private final HTTPServer httpServer;
|
private final HTTPServer httpServer;
|
||||||
@Getter
|
|
||||||
private final WebhookServlet webhookServlet = new WebhookServlet();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the web service and publishes the JSON-RPC servlet.
|
* Starts the web service and publishes the JSON-RPC servlet.
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
package ru.kirillius.pf.sdn.web;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServlet;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
|
|
||||||
import ru.kirillius.java.utils.events.EventHandler;
|
|
||||||
import ru.kirillius.java.utils.events.EventListener;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class WebhookServlet extends HttpServlet {
|
|
||||||
public final static String PATH = "/webhook/";
|
|
||||||
public final static String CONTEXT_PATH = PATH + "*";
|
|
||||||
|
|
||||||
public record RequestContext(HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<String, EventHandler<RequestContext>> handlers = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
|
||||||
var path = req.getRequestURI();
|
|
||||||
if (path.startsWith(PATH)) {
|
|
||||||
var name = path.substring(PATH.length()).split(Pattern.quote("/"))[0];
|
|
||||||
if (handlers.containsKey(name)) {
|
|
||||||
try {
|
|
||||||
handlers.get(name).invoke(new RequestContext(req, resp));
|
|
||||||
return;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.service(req, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventListener<RequestContext> registerHook(String name, EventListener<RequestContext> listener) {
|
|
||||||
if (!handlers.containsKey(name)) {
|
|
||||||
handlers.put(name, new ConcurrentEventHandler<>());
|
|
||||||
}
|
|
||||||
return handlers.get(name).add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterHook(String name, EventListener<RequestContext> listener) {
|
|
||||||
if (!handlers.containsKey(name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlers.get(name).remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { TDNSConfig } from '../pages/TDNS.js';
|
||||||
import { FRRConfig } from '../pages/FRR.js';
|
import { FRRConfig } from '../pages/FRR.js';
|
||||||
import { ROSConfig } from '../pages/ROS.js';
|
import { ROSConfig } from '../pages/ROS.js';
|
||||||
import { DNSMASQConfig } from '../pages/DNSMASQ.js';
|
import { DNSMASQConfig } from '../pages/DNSMASQ.js';
|
||||||
import { SRMPage } from '../pages/SRM.js';
|
|
||||||
import { SettingsPage } from '../pages/Settings.js';
|
import { SettingsPage } from '../pages/Settings.js';
|
||||||
import { LogsPage } from '../pages/Logs.js';
|
import { LogsPage } from '../pages/Logs.js';
|
||||||
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
||||||
|
|
@ -35,7 +34,6 @@ const allMenuItems = [
|
||||||
{ label: 'Настройка FRR', path: 'frr', component: 'ru.kirillius.pf.sdn.External.API.Components.FRR' },
|
{ label: 'Настройка FRR', path: 'frr', component: 'ru.kirillius.pf.sdn.External.API.Components.FRR' },
|
||||||
{ label: 'Настройка ROS', path: 'ros', component: 'ru.kirillius.pf.sdn.External.API.Components.ROS' },
|
{ label: 'Настройка ROS', path: 'ros', component: 'ru.kirillius.pf.sdn.External.API.Components.ROS' },
|
||||||
{ label: 'Настройка DNSMASQ', path: 'dnsmasq', component: 'ru.kirillius.pf.sdn.External.API.Components.DNSMASQ' },
|
{ label: 'Настройка DNSMASQ', path: 'dnsmasq', component: 'ru.kirillius.pf.sdn.External.API.Components.DNSMASQ' },
|
||||||
{ label: 'SRM', path: 'srm', component: 'ru.kirillius.pf.sdn.External.API.Components.SRM' },
|
|
||||||
{ label: 'Журнал', path: 'logs', component: null },
|
{ label: 'Журнал', path: 'logs', component: null },
|
||||||
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
||||||
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
||||||
|
|
@ -94,11 +92,6 @@ const routes = {
|
||||||
mount: DNSMASQConfig.mount,
|
mount: DNSMASQConfig.mount,
|
||||||
unmount: DNSMASQConfig.unmount
|
unmount: DNSMASQConfig.unmount
|
||||||
},
|
},
|
||||||
'#srm': {
|
|
||||||
render: SRMPage.render,
|
|
||||||
mount: SRMPage.mount,
|
|
||||||
unmount: SRMPage.unmount
|
|
||||||
},
|
|
||||||
'#logs': {
|
'#logs': {
|
||||||
render: LogsPage.render,
|
render: LogsPage.render,
|
||||||
mount: LogsPage.mount,
|
mount: LogsPage.mount,
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
|
|
||||||
function getBaseUrl() {
|
|
||||||
return window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SRMPage = {
|
|
||||||
render: () => {
|
|
||||||
const baseUrl = getBaseUrl();
|
|
||||||
const updateUrl = `${baseUrl}/webhook/SRM/update`;
|
|
||||||
const subnetsUrl = `${baseUrl}/webhook/SRM/subnets`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<h1 class="page-title">SRM Webhooks</h1>
|
|
||||||
<div class="component-config-form">
|
|
||||||
<p class="hint-text" style="margin-bottom: 20px;">
|
|
||||||
SRM предоставляет HTTP webhook'ы для получения информации о текущем состоянии подсетей.
|
|
||||||
Настройка не требуется — компонент отслеживает изменения автоматически.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="srm-update-url">Последнее обновление</label>
|
|
||||||
<textarea id="srm-update-url" class="form-control" readonly rows="1"
|
|
||||||
style="font-family: monospace; resize: vertical; cursor: text;"
|
|
||||||
onclick="this.select();">${updateUrl}</textarea>
|
|
||||||
<small class="hint-text">Возвращает временную метку (unix timestamp) последнего обновления списка подсетей.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="margin-top: 20px;">
|
|
||||||
<label for="srm-subnets-url">Список подсетей</label>
|
|
||||||
<textarea id="srm-subnets-url" class="form-control" readonly rows="1"
|
|
||||||
style="font-family: monospace; resize: vertical; cursor: text;"
|
|
||||||
onclick="this.select();">${subnetsUrl}</textarea>
|
|
||||||
<small class="hint-text">Возвращает текущий список подсетей в формате JSON array.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
mount: async () => {},
|
|
||||||
unmount: () => {}
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue