diff --git a/app/src/main/java/ru/kirillius/pf/sdn/App.java b/app/src/main/java/ru/kirillius/pf/sdn/App.java index eff90cf..f13273c 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -2,11 +2,7 @@ package ru.kirillius.pf.sdn; import lombok.Getter; import lombok.SneakyThrows; -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.Components.*; import ru.kirillius.pf.sdn.External.API.GitSubscription; import ru.kirillius.pf.sdn.External.API.HEInfoProvider; import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription; @@ -161,7 +157,7 @@ public class App implements Context, Closeable { .configFile(new File(getArgument("c", args))) .appLibrary(new File(getArgument("l", args))) .repository(getArgument("r", args)) - .availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class)).build())) { + .availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class, SRM.class)).build())) { Wait.when(app.running::get); if (app.shouldRestart.get()) { System.exit(42); diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/SRM.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/SRM.java new file mode 100644 index 0000000..b6da9fb --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/SRM.java @@ -0,0 +1,102 @@ +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 { + private final static String CTX = SRM.class.getSimpleName(); + private final static String HOOK_NAME = "SRM"; + private final EventListener updateEvent; + private final List lastSubnets = new ArrayList<>(); + private final AtomicLong lastUpdate = new AtomicLong(0); + private EventListener 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(); + 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; + } + } + +} diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/WebService.java b/app/src/main/java/ru/kirillius/pf/sdn/web/WebService.java index ab18306..77f8584 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/web/WebService.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/WebService.java @@ -1,6 +1,5 @@ package ru.kirillius.pf.sdn.web; - import lombok.Getter; import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; @@ -10,10 +9,7 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.core.AppService; import ru.kirillius.pf.sdn.core.Auth.AuthManager; import ru.kirillius.pf.sdn.core.Context; -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.*; import ru.kirillius.pf.sdn.web.RPC.System; import ru.kirillius.utils.logging.SystemLogger; @@ -28,7 +24,6 @@ import java.util.Set; */ public class WebService extends AppService { - /** * Stops the embedded HTTP server. */ @@ -73,7 +68,7 @@ public class WebService extends AppService { this.addConnector(connector); var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS); - + servletContext.addServlet(webhookServlet, WebhookServlet.CONTEXT_PATH); servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH); var holder = servletContext.addServlet(DefaultServlet.class, "/"); try { @@ -81,6 +76,7 @@ public class WebService extends AppService { } catch (MalformedURLException e) { throw new RuntimeException(e); } + this.setHandler(servletContext); start(); @@ -98,21 +94,18 @@ public class WebService extends AppService { } } - var isProtectedAccess = call.getMethod().getAnnotation(ProtectedMethod.class); if (isProtectedAccess != null) { if (!authorized) throw new SecurityException("Forbidden"); } }); - for (var handlerClass : RPCHandlerTypes) { var instance = RPC.instantiate(handlerClass, context); //noinspection unchecked JSONRPC.addTargetInstance((Class) handlerClass, instance); } - JSONRPC.getErrorHandler().add(throwable -> { SystemLogger.error("JRPC Request " + (throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) + @@ -122,12 +115,13 @@ public class WebService extends AppService { } } - private final static Set> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class); @Getter private final JSONRPCServlet JSONRPC = new JSONRPCServlet(); private final HTTPServer httpServer; + @Getter + private final WebhookServlet webhookServlet = new WebhookServlet(); /** * Starts the web service and publishes the JSON-RPC servlet. diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/WebhookServlet.java b/app/src/main/java/ru/kirillius/pf/sdn/web/WebhookServlet.java new file mode 100644 index 0000000..a8bc395 --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/WebhookServlet.java @@ -0,0 +1,55 @@ +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> 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 registerHook(String name, EventListener listener) { + if (!handlers.containsKey(name)) { + handlers.put(name, new ConcurrentEventHandler<>()); + } + return handlers.get(name).add(listener); + } + + public void unregisterHook(String name, EventListener listener) { + if (!handlers.containsKey(name)) { + return; + } + handlers.get(name).remove(listener); + } +}