diff --git a/app/pom.xml b/app/pom.xml
index 390f4d3..586ab55 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -6,17 +6,17 @@
ru.kirillius
pf-sdn
- 0.1.0.0
+ 1.0.1.5
- app
+ pf-sdn.app
ru.kirillius
- core
- 0.1.0.0
+ pf-sdn.core
+ ${project.parent.version}
compile
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 0fa7f4f..75fc31a 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/App.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java
@@ -2,33 +2,33 @@ package ru.kirillius.pf.sdn;
import lombok.Getter;
import lombok.SneakyThrows;
-import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
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.TDNS;
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
-import ru.kirillius.pf.sdn.core.Auth.AuthManager;
-import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
import ru.kirillius.pf.sdn.core.*;
-import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
-import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
-import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
+import ru.kirillius.pf.sdn.core.Auth.AuthManager;
+import ru.kirillius.pf.sdn.core.Auth.TokenService;
+import ru.kirillius.pf.sdn.core.Networking.BGPInfoService;
+import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
+import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.core.Util.Wait;
-import ru.kirillius.pf.sdn.web.HTTPServer;
+import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
+import static ru.kirillius.pf.sdn.core.Util.CommandLineUtils.getArgument;
+
+/**
+ * Entry point for the SDN control application that wires configuration, services, and shutdown handling.
+ */
public class App implements Context, Closeable {
- private final static File configFile = new File("config.json");
protected final static String CTX = App.class.getSimpleName();
static {
@@ -38,175 +38,101 @@ public class App implements Context, Closeable {
private final AtomicBoolean shouldRestart = new AtomicBoolean(false);
private final AtomicBoolean running = new AtomicBoolean(true);
-
- @Getter
- private final NetworkManager networkManager;
- @Getter
- private volatile Config config;
- @Getter
- private final AuthManager authManager;
- @Getter
- private final HTTPServer server;
- @Getter
- private final ASInfoService ASInfoService;
- @Getter
- private final SubscriptionManager subscriptionManager;
- @Getter
- private final UpdateManager updateManager;
- @Getter
- private final TokenStorage tokenStorage;
@Getter
private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
+ @Getter
+ private final ServiceManager serviceManager;
+ @Getter
+ private final LauncherConfig launcherConfig;
+ @Getter
+ private final Config config;
- private final List> loadedComponents = new ArrayList<>();
-
- @SneakyThrows
- public App(File configFile) {
+ /**
+ * Loads configuration from disk, creating a default file if missing.
+ */
+ private Config loadConfig() {
+ Config loadedConfig = null;
try {
- config = Config.load(configFile);
+ loadedConfig = Config.load(launcherConfig.getConfigFile());
} catch (IOException e) {
- config = new Config();
+ loadedConfig = new Config();
try {
- Config.store(config, configFile);
+ Config.store(loadedConfig, launcherConfig.getConfigFile());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
+ return loadedConfig;
+ }
- authManager = new AuthManager(this);
- ASInfoService = new ASInfoService();
- ASInfoService.setProvider(new HEInfoProvider(this));
- networkManager = new NetworkManager(this);
- networkManager.getInputResources().add(config.getCustomResources());
- subscriptionManager = new SubscriptionManager(this);
- updateManager = new UpdateManager(this);
- tokenStorage = new TokenStorage(this);
- subscribe();
- updateManager.start();
- initComponents();
- server = new HTTPServer(this);
+ /**
+ * Instantiates all application services and performs initial wiring.
+ */
+ private ServiceManager loadServiceManager() {
+ var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class));
+ manager.getService(BGPInfoService.class).setProvider(new HEInfoProvider());
+ manager.getService(ResourceUpdateService.class).start();
+ manager.getService(ComponentHandlerService.class).syncComponentsWithConfig();
+ return manager;
+ }
+ /**
+ * Ensures an admin password exists, defaulting to {@code admin} when missing.
+ */
+ private void checkDefaultPassword() {
if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) {
SystemLogger.error("There is no password for admin. Setting default password: admin", CTX);
- getAuthManager().updatePassword("admin");
+ getServiceManager().getService(AuthManager.class).updatePassword("admin");
}
- getSubscriptionManager().triggerUpdate();
}
+ /**
+ * Constructs the application binding to the provided launcher configuration.
+ */
+ @SneakyThrows
+ public App(LauncherConfig launcherConfig) {
+ this.launcherConfig = launcherConfig;
+ config = loadConfig();
+ serviceManager = loadServiceManager();
+ serviceManager.getService(SubscriptionService.class).triggerUpdate();
+ checkDefaultPassword();
+ }
+
+ /**
+ * Application entry point.
+ */
public static void main(String[] args) {
- var restart = false;
- do {
- try (var app = new App(configFile)) {
- Wait.when(app.running::get);
- restart = app.shouldRestart.get();
- } catch (Exception e) {
- SystemLogger.error("Unhandled error", CTX, e);
+ try (var app = new App(LauncherConfig.builder()
+ .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)).build())) {
+ Wait.when(app.running::get);
+ if (app.shouldRestart.get()) {
+ System.exit(303);
+ } else {
+ System.exit(0);
}
- } while (restart);
- }
-
- public void triggerRestart() {
- SystemLogger.message("Restarting app", CTX);
- running.set(false);
- shouldRestart.set(true);
- }
-
- public void triggerShutdown() {
- SystemLogger.message("Shutting down app", CTX);
- running.set(false);
- }
-
- public Collection>> getComponentClasses() {
- return List.of(FRR.class, OVPN.class, TDNS.class);
- }
-
- private void unloadComponent(Component> component) {
- SystemLogger.message("Unloading component: " + component.getClass().getSimpleName(), CTX);
- try {
- component.close();
- } catch (IOException e) {
- SystemLogger.error("Error on component unload", CTX, e);
- } finally {
- loadedComponents.remove(component);
+ } catch (Exception e) {
+ SystemLogger.error("Unhandled error", CTX, e);
+ System.exit(1);
}
}
- private void loadComponent(Class extends Component>> componentClass) {
- SystemLogger.message("Loading component: " + componentClass.getSimpleName(), CTX);
- var plugin = Component.loadPlugin(componentClass, this);
- loadedComponents.add(plugin);
- }
-
- public void initComponents() {
- var enabledPlugins = config.getEnabledComponents();
-
- (List.copyOf(loadedComponents)).forEach(plugin -> {
- if (!enabledPlugins.contains(plugin.getClass())) {
- unloadComponent(plugin);
- }
- });
- var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
- enabledPlugins.forEach(pluginClass -> {
- if (loadedClasses.contains(pluginClass)) {
- return;
- }
- loadComponent(pluginClass);
- });
- }
-
- @Override
- public void reloadComponents(Class extends Component>>... classes) {
- Arrays.stream(classes)
- .forEach(componentClass -> {
- loadedComponents.stream()
- .filter(component -> componentClass.equals(component.getClass()))
- .findFirst().ifPresent(this::unloadComponent);
- loadComponent(componentClass);
- }
- );
-
- }
-
- @Override
- public JSONRPCServlet getRPC() {
- if (server == null) {
- return null;
- }
- return server.getJSONRPC();
- }
-
- private void subscribe() {
- var eventsHandler = getEventsHandler();
- eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
- var manager = getNetworkManager();
- var inputResources = getNetworkManager().getInputResources();
- inputResources.clear();
- inputResources.add(config.getCustomResources());
- inputResources.add(bundle);
- manager.triggerUpdate(false);
- });
- }
-
- @Override
- public Component> getComponentInstance(Class extends Component>> pluginClass) {
- return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
+
+ /**
+ * Requests the application to exit, optionally restarting.
+ */
+ public void requestExit(boolean restart) {
+ running.set(false);
+ shouldRestart.set(restart);
}
+ /**
+ * Closes all managed services.
+ */
@Override
public void close() throws IOException {
- loadedComponents.forEach(plugin -> {
- try {
- plugin.close();
- } catch (IOException e) {
- SystemLogger.error("Error closing plugin", CTX, e);
- }
- });
- ASInfoService.close();
- networkManager.close();
- try {
- server.stop();
- } catch (Exception e) {
- SystemLogger.error("Error stopping server", CTX, e);
- }
+ serviceManager.close();
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java
index 8f6e810..8d09358 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java
@@ -18,17 +18,26 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
+/**
+ * Component that synchronises FRR routing instances with the aggregated subnet list.
+ */
public final class FRR extends AbstractComponent {
private final static String SUBNET_PATTERN = "{%subnet}";
private final static String GW_PATTERN = "{%gateway}";
private final static String CTX = FRR.class.getSimpleName();
private final EventListener subscription;
+ /**
+ * Binds the component to the application context and subscribes to network updates.
+ */
public FRR(Context context) {
super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
}
+ /**
+ * Synchronises FRR instances with the provided subnet list.
+ */
private void updateSubnets(List subnets) {
for (var entry : config.instances) {
SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
@@ -94,6 +103,9 @@ public final class FRR extends AbstractComponent {
}
}
+ /**
+ * Executes a batch of VTYSH commands optionally wrapping them in configuration mode.
+ */
private void executeVTYCommandBundle(List commands, boolean configMode, ShellExecutor shell, Consumer progressCallback) {
var buffer = new ArrayList();
@@ -122,6 +134,9 @@ public final class FRR extends AbstractComponent {
}
}
+ /**
+ * Executes a single VTYSH command and returns its stdout.
+ */
private String executeVTYCommand(String[] command, ShellExecutor shell) {
var buffer = new ArrayList();
buffer.add("vtysh");
@@ -133,11 +148,17 @@ public final class FRR extends AbstractComponent {
return shell.executeCommand(buffer.toArray(new String[0]));
}
+ /**
+ * Removes the subscription from the context event handler.
+ */
@Override
public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
}
+ /**
+ * Configuration describing FRR instances and how subnets should be rendered for them.
+ */
@JSONSerializable
public static class FRRConfig {
@@ -146,6 +167,9 @@ public final class FRR extends AbstractComponent {
@JSONArrayProperty(type = Entry.class)
private List instances = new ArrayList<>();
+ /**
+ * Declarative description of a single FRR instance managed by the component.
+ */
@Builder
@AllArgsConstructor
@NoArgsConstructor
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java
index e382c6f..77f0978 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java
@@ -12,19 +12,27 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
import ru.kirillius.pf.sdn.External.API.ShellExecutor;
import ru.kirillius.pf.sdn.core.AbstractComponent;
import ru.kirillius.pf.sdn.core.Context;
+import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
+import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger;
import java.io.IOException;
+/**
+ * Component integrating with OpenVPN to expose management RPC and synchronize route exports.
+ */
public final class OVPN extends AbstractComponent {
private final static String CTX = OVPN.class.getSimpleName();
private final EventListener subscription;
+ /**
+ * Registers the component with the JSON-RPC servlet or defers until it becomes available.
+ */
public OVPN(Context context) {
super(context);
- var RPC = context.getRPC();
+ var RPC = context.getServiceManager().getService(WebService.class).getJSONRPC();
if (RPC != null) {
RPC.addTargetInstance(OVPN.class, this);
subscription = null;
@@ -34,6 +42,9 @@ public final class OVPN extends AbstractComponent {
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
}
+ /**
+ * Executes the configured command to restart the OpenVPN service.
+ */
@JRPCMethod
@ProtectedMethod
public String restartSystemService() {
@@ -45,11 +56,14 @@ public final class OVPN extends AbstractComponent {
}
}
+ /**
+ * Returns a JSON array describing managed routes for OpenVPN clients.
+ */
@JRPCMethod
@ProtectedMethod
public JSONArray getManagedRoutes() {
var array = new JSONArray();
- context.getNetworkManager().getOutputResources().getSubnets().stream().map(subnet -> {
+ context.getServiceManager().getService(NetworkingService.class).getOutputResources().getSubnets().stream().map(subnet -> {
var json = new JSONObject();
json.put("address", subnet.getAddress());
json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength())));
@@ -59,6 +73,9 @@ public final class OVPN extends AbstractComponent {
}
+ /**
+ * Unregisters RPC listeners when the component is closed.
+ */
@Override
public void close() {
if (subscription != null) {
@@ -66,6 +83,9 @@ public final class OVPN extends AbstractComponent {
}
}
+ /**
+ * Configuration for OpenVPN shell interaction and service restart command.
+ */
@JSONSerializable
public static class OVPNConfig {
@Getter
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java
index 94eaf98..4c834ee 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java
@@ -16,16 +16,25 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
+/**
+ * Component that synchronises Technitium DNS forwarder zones with managed domain lists.
+ */
public final class TDNS extends AbstractComponent {
private final static String CTX = TDNS.class.getSimpleName();
private final EventListener subscription;
+ /**
+ * Subscribes to network updates to keep Technitium DNS forwarder zones in sync.
+ */
public TDNS(Context context) {
super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
}
+ /**
+ * Updates Technitium DNS servers to match the provided domain list.
+ */
private void updateSubnets(List domains) {
for (var instance : config.instances) {
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
@@ -56,12 +65,18 @@ public final class TDNS extends AbstractComponent {
}
+ /**
+ * Removes the event subscription when the component is closed.
+ */
@Override
public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
}
+ /**
+ * Configuration of Technitium DNS instances managed by the component.
+ */
@JSONSerializable
public static class TechnitiumConfig {
@@ -70,6 +85,9 @@ public final class TDNS extends AbstractComponent {
@JSONArrayProperty(type = Entry.class)
private List instances = new ArrayList<>();
+ /**
+ * Describes a single DNS server endpoint and its access credentials.
+ */
@Builder
@AllArgsConstructor
@NoArgsConstructor
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
index 6f233e9..665c842 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/GitSubscription.java
@@ -18,13 +18,22 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
+/**
+ * Subscription provider that pulls JSON resource bundles from a Git repository cache.
+ */
public class GitSubscription implements SubscriptionProvider {
private final Context context;
+ /**
+ * Creates the provider using the shared application context.
+ */
public GitSubscription(Context context) {
this.context = context;
}
+ /**
+ * Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
+ */
@Override
public Map getResources(RepositoryConfig config) {
try {
@@ -69,6 +78,9 @@ public class GitSubscription implements SubscriptionProvider {
}
+ /**
+ * Runs {@code git fetch} and {@code git pull} when remote updates are available.
+ */
private static void checkAndPullUpdates(Git git) throws GitAPIException {
@@ -102,6 +114,9 @@ public class GitSubscription implements SubscriptionProvider {
private final static String CTX = GitSubscription.class.getSimpleName();
+ /**
+ * Clones the repository into the given path.
+ */
private static Git cloneRepository(String REPO_URL, File path) throws GitAPIException {
SystemLogger.message("Cloning repository " + REPO_URL, CTX);
return Git.cloneRepository()
@@ -111,6 +126,9 @@ public class GitSubscription implements SubscriptionProvider {
.call();
}
+ /**
+ * Opens an existing repository located at the given directory.
+ */
private static Git openRepository(File repoDir) throws IOException {
var builder = new FileRepositoryBuilder();
var repository = builder.setGitDir(new File(repoDir, ".git"))
@@ -120,6 +138,9 @@ public class GitSubscription implements SubscriptionProvider {
return new Git(repository);
}
+ /**
+ * Returns whether the directory contains a Git repository.
+ */
private static boolean isGitRepository(File directory) {
if (!directory.exists() || !directory.isDirectory()) {
return false;
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/HEInfoProvider.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/HEInfoProvider.java
index 65fc689..0973bbd 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/HEInfoProvider.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/HEInfoProvider.java
@@ -4,7 +4,6 @@ import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import org.json.JSONTokener;
-import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.ASInfoProvider;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import ru.kirillius.utils.logging.SystemLogger;
@@ -18,14 +17,16 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+/**
+ * Retrieves ASN prefix information from Hurricane Electric's public API.
+ */
public class HEInfoProvider implements ASInfoProvider {
- private final Context context;
- public HEInfoProvider(Context context) {
- this.context = context;
- }
+ /**
+ * Fetches IPv4 prefixes announced by the specified autonomous system.
+ */
@Override
@SneakyThrows
public List getPrefixes(int as) {
@@ -43,6 +44,9 @@ public class HEInfoProvider implements ASInfoProvider {
}
}
+ /**
+ * Parses IPv4 prefix entries from the Hurricane Electric API response.
+ */
private static @NotNull ArrayList getIPv4Subnets(InputStream inputStream) {
var json = new JSONObject(new JSONTokener(inputStream));
var array = json.getJSONArray("prefixes");
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java
index 99b1013..1518df1 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java
@@ -15,15 +15,24 @@ import java.util.concurrent.TimeUnit;
import static net.schmizz.sshj.common.IOUtils.readFully;
+/**
+ * Executes commands either locally or over SSH based on the provided configuration.
+ */
public class ShellExecutor implements Closeable {
private final static String CTX = ShellExecutor.class.getSimpleName();
private final Config config;
private SSHClient sshClient;
+ /**
+ * Creates an executor with the supplied connection configuration.
+ */
public ShellExecutor(Config config) {
this.config = config;
}
+ /**
+ * Executes the given command, either locally or through SSH, returning the captured stdout.
+ */
public String executeCommand(String[] command) {
var buffer = new StringJoiner(" ");
Arrays.stream(command).forEach(e -> buffer.add('"' + e + '"'));
@@ -64,6 +73,9 @@ public class ShellExecutor implements Closeable {
}
}
+ /**
+ * Closes the underlying SSH client when one was created.
+ */
@Override
public void close() throws IOException {
@@ -77,6 +89,9 @@ public class ShellExecutor implements Closeable {
}
+ /**
+ * Connection and authentication options for shell command execution.
+ */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@@ -103,6 +118,9 @@ public class ShellExecutor implements Closeable {
@JSONProperty
private volatile String password = "securepassword";
+ /**
+ * Returns a human-readable description of the configured shell target.
+ */
@Override
public String toString() {
var builder = new StringBuilder();
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java
index 58d4a72..71eee72 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java
@@ -16,17 +16,26 @@ import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;
+/**
+ * Thin client for the Technitium DNS HTTP API used to manage forwarder zones.
+ */
public class TDNSAPI implements Closeable {
private final String server;
private final String authToken;
private final HttpClient httpClient;
+ /**
+ * Creates an API client targeting the specified Technitium server.
+ */
public TDNSAPI(String server, String authToken) {
this.server = server;
httpClient = HttpClient.newBuilder().build();
this.authToken = authToken;
}
+ /**
+ * Performs a GET request to the Technitium API with authentication parameters.
+ */
private JSONObject getRequest(String api, Map additionalParams) {
var params = new HashMap<>(additionalParams);
var joiner = new StringJoiner("&");
@@ -57,10 +66,16 @@ public class TDNSAPI implements Closeable {
}
}
+ /**
+ * Supported zone types returned by the API.
+ */
public enum ZoneType {
Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog
}
+ /**
+ * Transfer-object describing DNS zone metadata received from Technitium.
+ */
@JSONSerializable
public static class ZoneResponse {
@JSONProperty
@@ -92,11 +107,17 @@ public class TDNSAPI implements Closeable {
private Date lastModified;
}
+ /**
+ * Lists zones from the Technitium server.
+ */
public List getZones() {
var request = getRequest("/api/zones/list", Collections.emptyMap());
return JSONUtility.deserializeCollection(request.getJSONArray("zones"), ZoneResponse.class, null).toList();
}
+ /**
+ * Creates a forwarder zone pointing to the supplied upstream server.
+ */
public void createForwarderZone(String zoneName, String forwarder) {
var params = new HashMap();
params.put("zone", zoneName);
@@ -107,6 +128,9 @@ public class TDNSAPI implements Closeable {
+ /**
+ * Deletes the specified zone from the server.
+ */
public void deleteZone(String zoneName) {
var params = new HashMap();
params.put("zone", zoneName);
@@ -114,6 +138,9 @@ public class TDNSAPI implements Closeable {
}
+ /**
+ * Closes the underlying HTTP client.
+ */
@Override
public void close() throws IOException {
httpClient.close();
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java b/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java
index ced7a5e..71b28b7 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java
@@ -7,19 +7,31 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
+/**
+ * Maintains a rolling in-memory buffer of recent log messages for diagnostics exposure.
+ */
public class InMemoryLogHandler extends Handler {
private final static Queue queue = new ConcurrentLinkedQueue<>();
private final static DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US);
+ /**
+ * Formats a log record into a single-line message.
+ */
private String format(LogRecord logRecord) {
return "[" + dateFormat.format(new Date(logRecord.getMillis())) + "][" + logRecord.getLevel().getName() + "] " + logRecord.getMessage().trim();
}
+ /**
+ * Returns a read-only view of the buffered log messages.
+ */
public static Collection getMessages() {
return Collections.unmodifiableCollection(queue);
}
+ /**
+ * Stores the formatted message in the buffer, trimming old entries when full.
+ */
@Override
public void publish(LogRecord logRecord) {
if (queue.size() >= 1000) {
@@ -28,11 +40,17 @@ public class InMemoryLogHandler extends Handler {
queue.add(format(logRecord));
}
+ /**
+ * No-op flush implementation.
+ */
@Override
public void flush() {
}
+ /**
+ * No-op close implementation.
+ */
@Override
public void close() {
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java b/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java
deleted file mode 100644
index 8d64354..0000000
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package ru.kirillius.pf.sdn.web;
-
-
-import lombok.Getter;
-import org.eclipse.jetty.ee10.servlet.DefaultServlet;
-import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
-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.System;
-import ru.kirillius.utils.logging.SystemLogger;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Objects;
-import java.util.Set;
-
-public class HTTPServer extends Server {
-
- public final static String ANY_HOST = "0.0.0.0";
- private final static String LOG_CONTEXT = HTTPServer.class.getSimpleName();
- private final static String TOKEN_HEADER = "X-Auth-token";
-
- private String getResourceBase() throws MalformedURLException {
- var resourceFile = getClass().getClassLoader().getResource("htdocs/index.html");
- return new URL(Objects.requireNonNull(resourceFile).getProtocol(), resourceFile.getHost(), resourceFile.getPath()
- .substring(0, resourceFile.getPath().lastIndexOf("/")))
- .toString();
- }
-
- private final static Set> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
-
- @Getter
- private JSONRPCServlet JSONRPC = new JSONRPCServlet();
-
- public HTTPServer(Context appContext) {
- var config = appContext.getConfig();
- var connector = new ServerConnector(this);
- connector.setPort(config.getHttpPort());
- var host = config.getHost();
- if (host != null && !host.equals(ANY_HOST)) {
- connector.setHost(host);
- }
-
- this.addConnector(connector);
- var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
-
- servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
- var holder = servletContext.addServlet(DefaultServlet.class, "/");
- try {
- holder.setInitParameter("resourceBase", getResourceBase());
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- this.setHandler(servletContext);
-
- try {
- start();
- } catch (Exception e) {
- throw new RuntimeException("Error starting HTTPServer", e);
- }
-
- JSONRPC.addRequestHandler((request, response, call) -> {
- var authManager = appContext.getAuthManager();
- var authorized = authManager.getSessionAuthState(call.getContext().getSession());
- // Thread.sleep(100);//FIXME remove! debug only
-
- //auth by token
- if (!authorized) {
- var headerToken = request.getHeader(TOKEN_HEADER);
- if (headerToken != null) {
- authorized = authManager.validateToken(headerToken);
- authManager.setSessionAuthState(call.getContext().getSession(), authorized);
- }
- }
-
-
- 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, appContext);
- //noinspection unchecked
- JSONRPC.addTargetInstance((Class super RPC>) handlerClass, instance);
- }
-
-
- JSONRPC.getErrorHandler().add(throwable -> {
- SystemLogger.error("JRPC Request " +
- (throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
- " has failed with error", LOG_CONTEXT, throwable.getError());
- });
-
- try {
- appContext.getEventsHandler().getRPCInitEvent().invoke(JSONRPC);
- } catch (Exception e) {
- SystemLogger.error("Error on RPC init event", CTX, e);
- }
- }
-
- private final static String CTX = HTTPServer.class.getSimpleName();
-}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java b/app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java
index 0eef9f4..ae799e7 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java
@@ -5,6 +5,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+/**
+ * Marks RPC methods that require authenticated sessions or tokens.
+ */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ProtectedMethod {
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java
index b7223ac..ba635a1 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java
@@ -6,40 +6,60 @@ import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCContext;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.json.rpc.Servlet.CallContext;
+import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
+import ru.kirillius.pf.sdn.core.Auth.TokenService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.Date;
+/**
+ * JSON-RPC handler exposing authentication, session, and token management endpoints.
+ */
public class Auth implements RPC {
private final Context context;
+ /**
+ * Creates the handler bound to the shared application context.
+ */
public Auth(Context context) {
this.context = context;
}
+ /**
+ * Returns a JSON array containing all stored API tokens.
+ */
@ProtectedMethod
@JRPCMethod
public JSONArray listTokens() {
- return JSONUtility.serializeCollection(context.getTokenStorage().getTokens(), AuthToken.class, null);
+ return JSONUtility.serializeCollection(context.getServiceManager().getService(TokenService.class).getTokens(), AuthToken.class, null);
}
+ /**
+ * Removes a token identified by its raw value.
+ */
@ProtectedMethod
@JRPCMethod
public void removeToken(@JRPCArgument(name = "token") String token) {
- context.getTokenStorage().remove(new AuthToken(token));
+ context.getServiceManager().getService(TokenService.class).remove(new AuthToken(token));
}
+ /**
+ * Creates a new token with the provided description and returns its value.
+ */
@ProtectedMethod
@JRPCMethod
public String createAPIToken(@JRPCArgument(name = "description") String description) {
var token = new AuthToken();
token.setDescription(description);
- context.getTokenStorage().add(token);
+ context.getServiceManager().getService(TokenService.class).add(token);
return token.getToken();
}
+ /**
+ * Creates a session-bound token associated with the caller's user agent.
+ */
@ProtectedMethod
@JRPCMethod
public String createToken(@JRPCContext CallContext call) {
@@ -47,15 +67,18 @@ public class Auth implements RPC {
if (UA == null) {
UA = "Unknown user agent";
}
- var authManager = context.getAuthManager();
- var token = authManager.createToken(UA +" "+ new Date());
+ var authManager = context.getServiceManager().getService(AuthManager.class);
+ var token = authManager.createToken(UA + " " + new Date());
authManager.setSessionToken(call.getSession(), token);
return token.getToken();
}
+ /**
+ * Attempts to authenticate the session using a password.
+ */
@JRPCMethod
public boolean startSessionByPassword(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) {
- var authManager = context.getAuthManager();
+ var authManager = context.getServiceManager().getService(AuthManager.class);
if (authManager.validatePassword(password)) {
authManager.setSessionAuthState(call.getSession(), true);
return true;
@@ -63,9 +86,12 @@ public class Auth implements RPC {
return false;
}
+ /**
+ * Attempts to authenticate the session using an existing token.
+ */
@JRPCMethod
public boolean startSessionByToken(@JRPCArgument(name = "token") String token, @JRPCContext CallContext call) {
- var authManager = context.getAuthManager();
+ var authManager = context.getServiceManager().getService(AuthManager.class);
if (authManager.validateToken(token)) {
authManager.setSessionAuthState(call.getSession(), true);
return true;
@@ -73,16 +99,22 @@ public class Auth implements RPC {
return false;
}
+ /**
+ * Reports whether the current session is authenticated.
+ */
@JRPCMethod
public boolean isAuthenticated(@JRPCContext CallContext call) {
- var authManager = context.getAuthManager();
+ var authManager = context.getServiceManager().getService(AuthManager.class);
return authManager.getSessionAuthState(call.getSession());
}
+ /**
+ * Logs out the current session and invalidates its token when present.
+ */
@JRPCMethod
public void logout(@JRPCContext CallContext call) {
- var authManager = context.getAuthManager();
+ var authManager = context.getServiceManager().getService(AuthManager.class);
authManager.setSessionAuthState(call.getSession(), false);
var token = authManager.getSessionToken(call.getSession());
if (token != null) {
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java
index 60e6e9a..b059628 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java
@@ -5,30 +5,46 @@ import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.core.Context;
+import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
+/**
+ * JSON-RPC handler exposing operations on the network aggregation service.
+ */
public class NetworkManager implements RPC {
private final Context context;
+ /**
+ * Creates the handler bound to the application context.
+ */
public NetworkManager(Context context) {
this.context = context;
}
+ /**
+ * Indicates whether the networking service is currently recomputing resources.
+ */
@JRPCMethod
@ProtectedMethod
public boolean isUpdating() {
- return context.getNetworkManager().isUpdatingNow();
+ return context.getServiceManager().getService(NetworkingService.class).isUpdatingNow();
}
+ /**
+ * Triggers an update of networking resources.
+ */
@JRPCMethod
@ProtectedMethod
public void triggerUpdate(@JRPCArgument(name = "ignoreCache") boolean ignoreCache) {
- context.getNetworkManager().triggerUpdate(ignoreCache);
+ context.getServiceManager().getService(NetworkingService.class).triggerUpdate(ignoreCache);
}
+ /**
+ * Returns the latest aggregated network resource bundle.
+ */
@JRPCMethod
@ProtectedMethod
public JSONObject getOutputResources() {
- return JSONUtility.serializeStructure(context.getNetworkManager().getOutputResources());
+ return JSONUtility.serializeStructure(context.getServiceManager().getService(NetworkingService.class).getOutputResources());
}
}
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java
index 2d2272e..8a0ec7e 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java
@@ -4,7 +4,13 @@ import ru.kirillius.pf.sdn.core.Context;
import java.lang.reflect.InvocationTargetException;
+/**
+ * Marker interface for JSON-RPC handlers with a helper to instantiate them via the application context.
+ */
public interface RPC {
+ /**
+ * Instantiates an RPC handler using its context-aware constructor.
+ */
static T instantiate(Class type, Context context) {
try {
return type.getConstructor(Context.class).newInstance(context);
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java
index 95709a9..091294f 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java
@@ -7,41 +7,63 @@ import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
+import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.stream.Collectors;
+/**
+ * JSON-RPC handler for subscription lifecycle and resource selection operations.
+ */
public class SubscriptionManager implements RPC {
private final Context context;
+ /**
+ * Creates the handler bound to the application context.
+ */
public SubscriptionManager(Context context) {
this.context = context;
}
+ /**
+ * Indicates whether the subscription service is updating.
+ */
@JRPCMethod
@ProtectedMethod
public boolean isUpdating() {
- return context.getSubscriptionManager().isUpdatingNow();
+ return context.getServiceManager().getService(SubscriptionService.class).isUpdatingNow();
}
+ /**
+ * Requests the subscription service to refresh repositories.
+ */
@JRPCMethod
@ProtectedMethod
public void triggerUpdate() {
- context.getSubscriptionManager().triggerUpdate();
+ context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
}
+ /**
+ * Returns the list of resource identifiers currently subscribed to.
+ */
@JRPCMethod
@ProtectedMethod
public JSONArray getSubscribedResources() {
return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null);
}
+ /**
+ * Returns all available resources grouped by repository.
+ */
@JRPCMethod
@ProtectedMethod
public JSONObject getAvailableResources() {
- return JSONUtility.serializeMap(context.getSubscriptionManager().getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
+ return JSONUtility.serializeMap(context.getServiceManager().getService(SubscriptionService.class).getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
}
+ /**
+ * Persists the new selection of subscribed resources and triggers an update.
+ */
@JRPCMethod
@ProtectedMethod
public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) {
diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java
index b94e989..e7b9f26 100644
--- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java
@@ -5,50 +5,95 @@ import org.json.JSONObject;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
+import ru.kirillius.pf.sdn.core.AppUpdateService;
import ru.kirillius.pf.sdn.core.Component;
+import ru.kirillius.pf.sdn.core.ComponentHandlerService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.stream.Collectors;
+/**
+ * JSON-RPC handler for system control operations and component configuration management.
+ */
public class System implements RPC {
private final Context context;
+ /**
+ * Creates the handler bound to the application context.
+ */
public System(Context context) {
this.context = context;
}
-
+ /**
+ * Requests an application restart.
+ */
@ProtectedMethod
@JRPCMethod
public void restart() {
- context.triggerRestart();
+ context.requestExit(true);
}
+ /**
+ * Requests an application shutdown.
+ */
@ProtectedMethod
@JRPCMethod
public void shutdown() {
- context.triggerShutdown();
+ context.requestExit(false);
}
+ /**
+ * Returns the current configuration as JSON.
+ */
@ProtectedMethod
@JRPCMethod
public JSONObject getConfig() {
return JSONUtility.serializeStructure(context.getConfig());
}
+ /**
+ * Indicates whether the configuration has unsaved changes.
+ */
@ProtectedMethod
@JRPCMethod
public boolean isConfigChanged() {
return context.getConfig().isModified();
}
+ /**
+ * Retrieves the latest version available for update.
+ */
@ProtectedMethod
@JRPCMethod
- public boolean hasUpdates() {
- return true;
+ public String getVersionForUpdate() {
+ return context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
+ }
+
+ /**
+ * Returns the currently installed application version.
+ */
+ @ProtectedMethod
+ @JRPCMethod
+ public String getAppVersion() {
+ return context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
+ }
+
+ /**
+ * Provides additional metadata about available versions.
+ */
+ @ProtectedMethod
+ @JRPCMethod
+ public JSONObject getVersionInfo() {
+ var available = context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
+
+ return null;
}
+ /**
+ * Returns the list of enabled component class names.
+ */
@ProtectedMethod
@JRPCMethod
public JSONArray getEnabledComponents() {
@@ -56,6 +101,9 @@ public class System implements RPC {
}
+ /**
+ * Updates the set of enabled components according to the provided class names.
+ */
@SuppressWarnings("unchecked")
@ProtectedMethod
@JRPCMethod
@@ -67,16 +115,22 @@ public class System implements RPC {
throw new RuntimeException("Unable to load Component class " + s, e);
}
}).collect(Collectors.toList()));
- context.initComponents();
+ context.getServiceManager().getService(ComponentHandlerService.class).syncComponentsWithConfig();
}
+ /**
+ * Returns the list of component classes available to the launcher.
+ */
@ProtectedMethod
@JRPCMethod
public JSONArray getAvailableComponents() {
- return JSONUtility.serializeCollection(context.getComponentClasses().stream().map(Class::getName).toList(), String.class, null);
+ return JSONUtility.serializeCollection(context.getLauncherConfig().getAvailableComponentClasses().stream().map(Class::getName).toList(), String.class, null);
}
+ /**
+ * Returns the configuration object for the specified component.
+ */
@SuppressWarnings({"unchecked", "rawtypes"})
@ProtectedMethod
@JRPCMethod
@@ -86,6 +140,9 @@ public class System implements RPC {
}
+ /**
+ Updates the configuration of the specified component and reloads it.
+ */
@SuppressWarnings({"rawtypes", "unchecked"})
@ProtectedMethod
@JRPCMethod
@@ -93,8 +150,7 @@ public class System implements RPC {
var cls = (Class) Class.forName(componentName);
var configClass = Component.getConfigClass(cls);
context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass));
- context.reloadComponents(cls);
- Object config1 = context.getConfig().getComponentsConfig().getConfig(cls);
- return;
+ context.getServiceManager().getService(ComponentHandlerService.class).reloadComponents(cls);
+ context.getConfig().getComponentsConfig().getConfig(cls);
}
}
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
new file mode 100644
index 0000000..70fa616
--- /dev/null
+++ b/app/src/main/java/ru/kirillius/pf/sdn/web/WebService.java
@@ -0,0 +1,152 @@
+package ru.kirillius.pf.sdn.web;
+
+
+import lombok.Getter;
+import org.eclipse.jetty.ee10.servlet.DefaultServlet;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+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.System;
+import ru.kirillius.utils.logging.SystemLogger;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Hosts the web UI and JSON-RPC API backed by an embedded Jetty server.
+ */
+public class WebService extends AppService {
+
+
+ /**
+ * Stops the embedded HTTP server.
+ */
+ @Override
+ public void close() throws IOException {
+ try {
+ httpServer.stop();
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Embedded Jetty server configured with static resources and RPC servlet.
+ */
+ private class HTTPServer extends Server {
+ public final static String ANY_HOST = "0.0.0.0";
+ private final static String LOG_CONTEXT = WebService.class.getSimpleName();
+ private final static String TOKEN_HEADER = "X-Auth-token";
+
+ /**
+ * Resolves the base directory for static resources.
+ */
+ private String getResourceBase() throws MalformedURLException {
+ var resourceFile = getClass().getClassLoader().getResource("htdocs/index.html");
+ return new URL(Objects.requireNonNull(resourceFile).getProtocol(), resourceFile.getHost(), resourceFile.getPath()
+ .substring(0, resourceFile.getPath().lastIndexOf("/")))
+ .toString();
+ }
+
+ /**
+ * Configures connectors, servlets, and authentication hooks.
+ */
+ public HTTPServer() throws Exception {
+ var config = context.getConfig();
+ var connector = new ServerConnector(this);
+ connector.setPort(config.getHttpPort());
+ var host = config.getHost();
+ if (host != null && !host.equals(ANY_HOST)) {
+ connector.setHost(host);
+ }
+
+ this.addConnector(connector);
+ var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
+
+ servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
+ var holder = servletContext.addServlet(DefaultServlet.class, "/");
+ try {
+ holder.setInitParameter("resourceBase", getResourceBase());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ this.setHandler(servletContext);
+ start();
+
+ JSONRPC.addRequestHandler((request, response, call) -> {
+ var authManager = context.getServiceManager().getService(AuthManager.class);
+ var authorized = authManager.getSessionAuthState(call.getContext().getSession());
+ // Thread.sleep(100);//FIXME remove! debug only
+
+ //auth by token
+ if (!authorized) {
+ var headerToken = request.getHeader(TOKEN_HEADER);
+ if (headerToken != null) {
+ authorized = authManager.validateToken(headerToken);
+ authManager.setSessionAuthState(call.getContext().getSession(), authorized);
+ }
+ }
+
+
+ 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 super RPC>) handlerClass, instance);
+ }
+
+
+ JSONRPC.getErrorHandler().add(throwable -> {
+ SystemLogger.error("JRPC Request " +
+ (throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
+ " has failed with error", LOG_CONTEXT, throwable.getError());
+ });
+
+ }
+ }
+
+
+ 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;
+
+ /**
+ * Starts the web service and publishes the JSON-RPC servlet.
+ */
+ public WebService(Context context) {
+ super(context);
+
+ try {
+ httpServer = new HTTPServer();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to start web server", e);
+ }
+
+ try {
+ context.getEventsHandler().getRPCInitEvent().invoke(JSONRPC);
+ } catch (Exception e) {
+ SystemLogger.error("Error on RPC init event", CTX, e);
+ }
+ }
+
+ private final static String CTX = WebService.class.getSimpleName();
+}
diff --git a/core/pom.xml b/core/pom.xml
index 3a9be39..9f5b195 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -6,10 +6,11 @@
ru.kirillius
pf-sdn
- 0.1.0.0
+ 1.0.1.5
- core
+
+ pf-sdn.core
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java
index f13a9eb..1cc7577 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java
@@ -1,9 +1,15 @@
package ru.kirillius.pf.sdn.core;
+/**
+ * Convenience base implementation that wires component configuration and context dependencies.
+ */
public abstract class AbstractComponent implements Component {
protected final CT config;
protected final Context context;
+ /**
+ * Loads the component configuration from the shared storage and stores the context reference.
+ */
@SuppressWarnings({"unchecked", "rawtypes"})
public AbstractComponent(Context context) {
config = (CT) context.getConfig().getComponentsConfig().getConfig((Class) getClass());
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AppService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AppService.java
new file mode 100644
index 0000000..f868095
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AppService.java
@@ -0,0 +1,17 @@
+package ru.kirillius.pf.sdn.core;
+
+import java.io.Closeable;
+
+/**
+ * Base class for long-lived application services bound to a shared {@link Context}.
+ */
+public abstract class AppService implements Closeable {
+ protected final Context context;
+
+ /**
+ * Binds the service to the hosting application context.
+ */
+ public AppService(Context context) {
+ this.context = context;
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java
new file mode 100644
index 0000000..c5c53f3
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java
@@ -0,0 +1,304 @@
+package ru.kirillius.pf.sdn.core;
+
+import org.w3c.dom.Element;
+import ru.kirillius.utils.logging.SystemLogger;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.time.Duration;
+import java.util.regex.Pattern;
+
+/**
+ * Handles application update discovery by polling a repository and downloading new packages.
+ */
+public class AppUpdateService extends AppService {
+ private static final String CTX = AppUpdateService.class.getSimpleName();
+ private static final Pattern VERSION_LINK_PATTERN = Pattern.compile("]*href=\"([0-9]+(?:\\.[0-9]+)*\\.pfapp)\"", Pattern.CASE_INSENSITIVE);
+
+ private final String repository;
+ private final Path appLibraryPath;
+ private final Class> anchorClass;
+ private final HttpClient httpClient;
+ private volatile String cachedLatestVersion;
+
+ /**
+ * Creates the service bound to the provided context and initialises HTTP access helpers.
+ */
+ AppUpdateService(Context context) {
+ super(context);
+ this.repository = context.getLauncherConfig().getRepository();
+ this.appLibraryPath = context.getLauncherConfig().getAppLibrary().toPath();
+ this.anchorClass = context.getClass();
+ this.httpClient = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+
+ /**
+ * Determines the current application version using the nearest {@code pom.xml}.
+ *
+ * @return semantic version string or {@code "unknown"} when unavailable.
+ */
+ public String getAppVersion() {
+ var pomPath = findPomFile();
+ if (pomPath == null) {
+ SystemLogger.error("Unable to locate pom.xml to determine application version", CTX);
+ return "unknown";
+ }
+ try (var input = Files.newInputStream(pomPath)) {
+ var factory = DocumentBuilderFactory.newInstance();
+ try {
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+ } catch (Exception ignored) {
+ }
+ var builder = factory.newDocumentBuilder();
+ var document = builder.parse(input);
+ document.getDocumentElement().normalize();
+
+ var projectElement = document.getDocumentElement();
+ var projectVersion = extractVersion(projectElement);
+ if (projectVersion != null) {
+ return projectVersion;
+ }
+
+ var parentNodes = projectElement.getElementsByTagName("parent");
+ if (parentNodes.getLength() > 0) {
+ var parentElement = (Element) parentNodes.item(0);
+ var parentVersion = extractVersion(parentElement);
+ if (parentVersion != null) {
+ return parentVersion;
+ }
+ }
+ } catch (Exception e) {
+ SystemLogger.error("Failed to read application version from pom.xml", CTX, e);
+ }
+ return "unknown";
+ }
+
+ /**
+ * Fetches the latest available version and caches the result for reuse.
+ *
+ * @return newest version string or the previously cached value when fetch fails.
+ */
+ public synchronized String checkVersionForUpdate() {
+ var latest = fetchLatestVersion();
+ if (latest != null) {
+ cachedLatestVersion = latest;
+ return latest;
+ }
+ return cachedLatestVersion;
+ }
+
+ /**
+ * Downloads the application package corresponding to the latest known version.
+ */
+ public void updateApp() {
+ var version = cachedLatestVersion;
+ if (version == null || version.isBlank()) {
+ version = checkVersionForUpdate();
+ }
+ if (version == null || version.isBlank()) {
+ SystemLogger.error("No version available for update", CTX);
+ return;
+ }
+ if (repository == null || repository.isBlank()) {
+ SystemLogger.error("Repository URL is not configured", CTX);
+ return;
+ }
+ if (appLibraryPath == null) {
+ SystemLogger.error("Application library path is not configured", CTX);
+ return;
+ }
+
+ var fileName = version + ".pfapp";
+ var targetDirectory = appLibraryPath;
+ var tempFile = targetDirectory.resolve(fileName + ".download");
+ var targetFile = targetDirectory.resolve(fileName);
+
+ try {
+ Files.createDirectories(targetDirectory);
+ Files.deleteIfExists(tempFile);
+ var request = HttpRequest.newBuilder()
+ .uri(buildVersionUri(version))
+ .timeout(Duration.ofMinutes(2))
+ .GET()
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(tempFile));
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
+ SystemLogger.error("Unexpected response code when downloading update: " + response.statusCode(), CTX);
+ return;
+ }
+ Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
+ cachedLatestVersion = version;
+ } catch (Exception e) {
+ SystemLogger.error("Failed to download update", CTX, e);
+ } finally {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {
+ SystemLogger.error("Failed to clean up temporary update file", CTX, e);
+ }
+ }
+ }
+
+ /**
+ * Retrieves the newest version identifier from the repository index.
+ */
+ private String fetchLatestVersion() {
+ if (repository == null || repository.isBlank()) {
+ return null;
+ }
+ try {
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create(repository))
+ .timeout(Duration.ofSeconds(20))
+ .GET()
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
+ SystemLogger.error("Unexpected response code when checking updates: " + response.statusCode(), CTX);
+ return null;
+ }
+ return extractLatestVersion(response.body());
+ } catch (Exception e) {
+ SystemLogger.error("Failed to check version for update", CTX, e);
+ return null;
+ }
+ }
+
+ /**
+ * Builds the absolute URI to the package file for the provided version.
+ */
+ private URI buildVersionUri(String version) {
+ var base = repository.endsWith("/") ? repository : repository + "/";
+ return URI.create(base + version + ".pfapp");
+ }
+
+ /**
+ * Locates the closest {@code pom.xml} to infer the running version.
+ */
+ private Path findPomFile() {
+ if (anchorClass != null) {
+ try {
+ var codeSource = anchorClass.getProtectionDomain().getCodeSource();
+ if (codeSource != null) {
+ var location = Paths.get(codeSource.getLocation().toURI());
+ var directory = Files.isDirectory(location) ? location : location.getParent();
+ while (directory != null) {
+ var candidate = directory.resolve("pom.xml");
+ if (Files.exists(candidate)) {
+ return candidate;
+ }
+ directory = directory.getParent();
+ }
+ }
+ } catch (Exception e) {
+ SystemLogger.error("Failed to resolve pom.xml location", CTX, e);
+ }
+ }
+
+ var workingDirCandidate = Paths.get("pom.xml");
+ if (Files.exists(workingDirCandidate)) {
+ return workingDirCandidate;
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts a version value from the given XML element if present.
+ */
+ private static String extractVersion(Element element) {
+ if (element == null) {
+ return null;
+ }
+ var nodes = element.getElementsByTagName("version");
+ if (nodes.getLength() == 0) {
+ return null;
+ }
+ var value = nodes.item(0).getTextContent();
+ if (value == null) {
+ return null;
+ }
+ var trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+
+ /**
+ * Parses the repository index HTML and returns the highest {@code .pfapp} version found.
+ */
+ private static String extractLatestVersion(String html) {
+ if (html == null || html.isBlank()) {
+ return null;
+ }
+ String latest = null;
+ var matcher = VERSION_LINK_PATTERN.matcher(html);
+ while (matcher.find()) {
+ var href = matcher.group(1);
+ if (href == null || href.isBlank()) {
+ continue;
+ }
+ var version = href.endsWith(".pfapp") ? href.substring(0, href.length() - 6) : href;
+ if (version.isEmpty()) {
+ continue;
+ }
+ if (latest == null || compareVersions(version, latest) > 0) {
+ latest = version;
+ }
+ }
+ return latest;
+ }
+
+ /**
+ * Compares two semantic version strings using numeric components.
+ */
+ private static int compareVersions(String first, String second) {
+ if (first.equals(second)) {
+ return 0;
+ }
+ var aParts = first.split("\\.");
+ var bParts = second.split("\\.");
+ var length = Math.max(aParts.length, bParts.length);
+ for (int i = 0; i < length; i++) {
+ var a = i < aParts.length ? parseVersionPart(aParts[i]) : 0;
+ var b = i < bParts.length ? parseVersionPart(bParts[i]) : 0;
+ var cmp = Integer.compare(a, b);
+ if (cmp != 0) {
+ return cmp;
+ }
+ }
+ return Integer.compare(aParts.length, bParts.length);
+ }
+
+ /**
+ * Converts a version segment into an integer, defaulting to zero when invalid.
+ */
+ private static int parseVersionPart(String part) {
+ try {
+ return Integer.parseInt(part);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Shuts down the underlying HTTP client.
+ */
+ @Override
+ public void close() throws IOException {
+ httpClient.close();
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java
index da7610d..c7aac3e 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java
@@ -1,6 +1,7 @@
package ru.kirillius.pf.sdn.core.Auth;
import jakarta.servlet.http.HttpSession;
+import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Util.HashUtil;
import ru.kirillius.utils.logging.SystemLogger;
@@ -9,21 +10,27 @@ import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
-public class AuthManager {
+/**
+ * Coordinates authentication flows, password hashing, and token lifecycle management.
+ */
+public class AuthManager extends AppService {
public final static String SESSION_AUTH_KEY = "auth";
public final static String SESSION_TOKEN = "token";
private final static String CTX = AuthManager.class.getSimpleName();
- private final Context context;
- public AuthManager(Context context) {
- this.context = context;
- }
+
+ /**
+ * Verifies the provided password against the stored hash.
+ */
public boolean validatePassword(String pass) {
var config = context.getConfig();
return HashUtil.hash(pass, config.getPasswordSalt()).equals(config.getPasswordHash());
}
+ /**
+ * Updates the stored password hash and generates a new salt.
+ */
public void updatePassword(String pass) {
var config = context.getConfig();
config.setPasswordSalt(UUID.randomUUID().toString());
@@ -32,10 +39,13 @@ public class AuthManager {
);
}
+ /**
+ * Creates a new authentication token with the provided description and persists it.
+ */
public AuthToken createToken(String description) {
var token = new AuthToken();
token.setDescription(description);
- var tokenStorage = context.getTokenStorage();
+ var tokenStorage = context.getServiceManager().getService(TokenService.class);
tokenStorage.add(token);
try {
tokenStorage.store();
@@ -45,28 +55,46 @@ public class AuthManager {
return token;
}
+ /**
+ * Checks whether the storage contains the specified token.
+ */
public boolean validateToken(AuthToken token) {
- return context.getTokenStorage().contains(token);
+ return context.getServiceManager().getService(TokenService.class).contains(token);
}
+ /**
+ * Stores the authentication flag in the HTTP session.
+ */
public void setSessionAuthState(HttpSession session, boolean state) {
session.setAttribute(SESSION_AUTH_KEY, state);
}
+ /**
+ * Associates the given token with the HTTP session.
+ */
public void setSessionToken(HttpSession session, AuthToken token) {
session.setAttribute(SESSION_TOKEN, token);
}
+ /**
+ * Retrieves the token associated with the HTTP session.
+ */
public AuthToken getSessionToken(HttpSession session) {
return (AuthToken) session.getAttribute(SESSION_TOKEN);
}
+ /**
+ * Returns whether the HTTP session is marked as authenticated.
+ */
public boolean getSessionAuthState(HttpSession session) {
return Objects.equals(session.getAttribute(SESSION_AUTH_KEY), Boolean.TRUE);
}
+ /**
+ * Removes the provided token from storage if present.
+ */
public void invalidateToken(AuthToken token) {
- var tokenStorage = context.getTokenStorage();
+ var tokenStorage = context.getServiceManager().getService(TokenService.class);
if (tokenStorage.contains(token)) {
tokenStorage.remove(token);
try {
@@ -78,11 +106,32 @@ public class AuthManager {
}
+ /**
+ * Checks token validity using its raw string value.
+ */
public boolean validateToken(String token) {
return validateToken(new AuthToken(token));
}
+ /**
+ * Invalidates a token identified by its string representation.
+ */
public void invalidateToken(String token) {
invalidateToken(new AuthToken(token));
}
+
+ /**
+ * Creates the manager bound to the shared context.
+ */
+ public AuthManager(Context context) {
+ super(context);
+ }
+
+ /**
+ * No-op close implementation.
+ */
+ @Override
+ public void close() throws IOException {
+ //no-op
+ }
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java
index e1bcebb..13d62da 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java
@@ -9,9 +9,15 @@ import ru.kirillius.json.JSONSerializable;
import java.util.Objects;
import java.util.UUID;
+/**
+ * Represents a persistent authentication token with metadata for API access.
+ */
@NoArgsConstructor
@JSONSerializable
public class AuthToken {
+ /**
+ * Creates a token wrapper for the specified raw token string.
+ */
public AuthToken(String token) {
this.token = token;
}
@@ -21,6 +27,9 @@ public class AuthToken {
@JSONProperty
private String description = "untitled";
+ /**
+ * Tokens are equal when their token strings match.
+ */
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
@@ -28,6 +37,9 @@ public class AuthToken {
return Objects.equals(token, authToken.token);
}
+ /**
+ * Produces a hash code derived from the token string.
+ */
@Override
public int hashCode() {
return Objects.hashCode(token);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenService.java
similarity index 69%
rename from core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java
rename to core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenService.java
index 1332341..56f8b45 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenService.java
@@ -3,6 +3,7 @@ package ru.kirillius.pf.sdn.core.Auth;
import org.json.JSONArray;
import org.json.JSONTokener;
import ru.kirillius.json.JSONUtility;
+import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context;
import java.io.*;
@@ -10,10 +11,17 @@ import java.util.*;
import java.util.stream.Collectors;
-public class TokenStorage {
+/**
+ * Persists and manages authentication tokens on disk for API consumers.
+ */
+public class TokenService extends AppService {
private final File file;
- public TokenStorage(Context context) {
+ /**
+ * Loads existing tokens from disk and prepares the storage file.
+ */
+ public TokenService(Context context) {
+ super(context);
file = new File(context.getConfig().getCacheDirectory(), "tokens.json");
if (file.exists()) {
try (var stream = new FileInputStream(file)) {
@@ -29,30 +37,45 @@ public class TokenStorage {
private final List tokens;
+ /**
+ * Returns an immutable view over the stored tokens.
+ */
public Collection getTokens() {
synchronized (tokens) {
return Collections.unmodifiableList(tokens);
}
}
+ /**
+ * Adds the provided tokens to the storage.
+ */
public void add(AuthToken... what) {
synchronized (tokens) {
Collections.addAll(tokens, what);
}
}
+ /**
+ * Checks whether the specified token is present in storage.
+ */
public boolean contains(AuthToken what) {
synchronized (tokens) {
return tokens.contains(what);
}
}
+ /**
+ * Removes the provided tokens from storage.
+ */
public void remove(AuthToken... what) {
synchronized (tokens) {
Arrays.stream(what).forEach(tokens::remove);
}
}
+ /**
+ * Writes the in-memory token list to disk.
+ */
public synchronized void store() throws IOException {
try (var fileInputStream = new FileOutputStream(file)) {
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
@@ -61,4 +84,12 @@ public class TokenStorage {
}
}
}
+
+ /**
+ * No-op close implementation.
+ */
+ @Override
+ public void close() throws IOException {
+ //no-op
+ }
}
\ No newline at end of file
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java
index 8e5eb41..4c68394 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java
@@ -5,7 +5,13 @@ import lombok.SneakyThrows;
import java.io.Closeable;
import java.lang.reflect.ParameterizedType;
+/**
+ * Defines the contract for pluggable application components with type-safe configuration access.
+ */
public interface Component extends Closeable {
+ /**
+ * Resolves the configuration type parameter for a component implementation.
+ */
@SuppressWarnings("unchecked")
static Class getConfigClass(Class extends Component> pluginClass) {
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
@@ -13,6 +19,9 @@ public interface Component extends Closeable {
return (Class) typeArguments[0];
}
+ /**
+ * Instantiates a component using its context-aware constructor.
+ */
@SneakyThrows
static > T loadPlugin(Class pluginClass, Context context) {
return pluginClass.getConstructor(Context.class).newInstance(context);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentConfigStorage.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentConfigStorage.java
index 3b96c4d..75bdc16 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentConfigStorage.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentConfigStorage.java
@@ -11,12 +11,21 @@ import ru.kirillius.json.SerializationException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+/**
+ * Stores per-component configuration instances and serializes them as JSON.
+ */
@NoArgsConstructor
@JSONSerializable(ComponentConfigStorage.Serializer.class)
public class ComponentConfigStorage {
+ /**
+ * JSON serializer that materializes component configuration maps.
+ */
public final static class Serializer implements JSONSerializer {
+ /**
+ * Converts the stored configurations into a JSON object keyed by class name.
+ */
@Override
public Object serialize(ComponentConfigStorage componentConfigStorage) throws SerializationException {
var json = new JSONObject();
@@ -26,6 +35,9 @@ public class ComponentConfigStorage {
return json;
}
+ /**
+ * Restores component configuration instances from the provided JSON payload.
+ */
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public ComponentConfigStorage deserialize(Object o, Class> aClass) throws SerializationException {
@@ -48,6 +60,9 @@ public class ComponentConfigStorage {
private final Map>, Object> configs = new ConcurrentHashMap<>();
+ /**
+ * Returns (and instantiates if necessary) the configuration object for the given component class.
+ */
@SuppressWarnings("unchecked")
@SneakyThrows
public CT getConfig(Class extends Component> componentClass) {
@@ -59,6 +74,9 @@ public class ComponentConfigStorage {
return (CT) configs.get(componentClass);
}
+ /**
+ * Stores the provided configuration instance for the specified component class.
+ */
@SneakyThrows
public void setConfig(Class extends Component> componentClass, CT config) {
var configClass = Component.getConfigClass(componentClass);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentHandlerService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentHandlerService.java
new file mode 100644
index 0000000..bc35e18
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ComponentHandlerService.java
@@ -0,0 +1,111 @@
+package ru.kirillius.pf.sdn.core;
+
+import ru.kirillius.utils.logging.SystemLogger;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Manages lifecycle of dynamically configurable components, keeping them in sync with application settings.
+ */
+public final class ComponentHandlerService extends AppService {
+ private final static String CTX = ComponentHandlerService.class.getSimpleName();
+
+ private final List> loadedComponents = new ArrayList<>();
+
+ /**
+ * Constructs the service and captures the hosting context.
+ */
+ public ComponentHandlerService(Context context) {
+ super(context);
+ }
+
+
+
+ /**
+ * Unloads all currently managed components.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ unloadComponents(loadedComponents.toArray(Component[]::new));
+ }
+
+ /**
+ * Retrieves a loaded component instance by class.
+ */
+ public synchronized Component> getComponentInstance(Class extends Component>> pluginClass) {
+ return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
+ }
+
+ /**
+ * Reloads the specified component classes by unloading then loading them again.
+ */
+ @SafeVarargs
+ public final synchronized void reloadComponents(Class extends Component>>... classes) {
+ Arrays.stream(classes)
+ .forEach(componentClass -> {
+ loadedComponents.stream()
+ .filter(component -> componentClass.equals(component.getClass()))
+ .findFirst().ifPresent(this::unloadComponents);
+ loadComponents(componentClass);
+ }
+ );
+
+ }
+
+ /**
+ * Loads the specified component classes if not already present.
+ */
+ @SafeVarargs
+ public final synchronized void loadComponents(Class extends Component>>... componentClasses) {
+ for (var componentClass : componentClasses) {
+ if (loadedComponents.stream().map(Component::getClass).anyMatch(componentClass::equals)) {
+ SystemLogger.warning("Unable to load component " + componentClass.getSimpleName() + " because it is loaded already", CTX);
+ continue;
+ }
+ SystemLogger.message("Loading component: " + componentClass.getSimpleName(), CTX);
+ var plugin = Component.loadPlugin(componentClass, context);
+ loadedComponents.add(plugin);
+ }
+ }
+
+ /**
+ * Unloads the given component instances and closes their resources.
+ */
+ public final synchronized void unloadComponents(Component>... components) {
+ for (var component : components) {
+ SystemLogger.message("Unloading component: " + component.getClass().getSimpleName(), CTX);
+ try {
+ component.close();
+ } catch (IOException e) {
+ SystemLogger.error("Error on component unload", CTX, e);
+ } finally {
+ loadedComponents.remove(component);
+ }
+ }
+ }
+
+ /**
+ * Aligns loaded components with the set defined in configuration.
+ */
+ public final synchronized void syncComponentsWithConfig() {
+ var config = context.getConfig();
+ var enabledPlugins = config.getEnabledComponents();
+
+ (List.copyOf(loadedComponents)).forEach(plugin -> {
+ if (!enabledPlugins.contains(plugin.getClass())) {
+ unloadComponents(plugin);
+ }
+ });
+ var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
+ enabledPlugins.forEach(pluginClass -> {
+ if (loadedClasses.contains(pluginClass)) {
+ return;
+ }
+ loadComponents(pluginClass);
+ });
+ }
+
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
index 73c18b0..e368062 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java
@@ -18,6 +18,9 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
+/**
+ * Represents persisted application settings and component configuration bundles.
+ */
@NoArgsConstructor
@JSONSerializable
public class Config {
@@ -104,6 +107,9 @@ public class Config {
@JSONProperty
private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle();
+ /**
+ * Persists the given configuration into the supplied file as JSON.
+ */
public static void store(Config config, File file) throws IOException {
try (var fileInputStream = new FileOutputStream(file)) {
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
@@ -113,14 +119,23 @@ public class Config {
}
}
+ /**
+ * Serialises the configuration into a JSONObject representation.
+ */
public static JSONObject serialize(Config config) {
return JSONUtility.serializeStructure(config);
}
+ /**
+ * Reconstructs a configuration instance from JSON data.
+ */
public static Config deserialize(JSONObject object) {
return JSONUtility.deserializeStructure(object, Config.class);
}
+ /**
+ * Loads a configuration from disk and tracks the original JSON for modification detection.
+ */
public static Config load(File file) throws IOException {
try (var stream = new FileInputStream(file)) {
var json = new JSONObject(new JSONTokener(stream));
@@ -133,6 +148,9 @@ public class Config {
private JSONObject initialJSON = new JSONObject();
+ /**
+ * Indicates whether the in-memory configuration diverges from the initially loaded snapshot.
+ */
public boolean isModified() {
return !initialJSON.equals(serialize(this));
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
index 292c32c..572d926 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java
@@ -1,42 +1,29 @@
package ru.kirillius.pf.sdn.core;
-import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
-import ru.kirillius.pf.sdn.core.Auth.AuthManager;
-import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
-import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
-import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
-import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
-
-import java.util.Collection;
-
+/**
+ * Provides access to core services and configuration for application components.
+ */
public interface Context {
+ /**
+ * Returns immutable launcher parameters shared across services.
+ */
+ LauncherConfig getLauncherConfig();
+ /**
+ * Provides access to the service registry for retrieving components.
+ */
+ ServiceManager getServiceManager();
+ /**
+ * Supplies the mutable runtime configuration.
+ */
Config getConfig();
-
- AuthManager getAuthManager();
-
- ASInfoService getASInfoService();
-
- NetworkManager getNetworkManager();
-
+ /**
+ * Exposes event hooks for subscribers interested in runtime changes.
+ */
ContextEventsHandler getEventsHandler();
-
- SubscriptionManager getSubscriptionManager();
-
- UpdateManager getUpdateManager();
-
- Component> getComponentInstance(Class extends Component>> pluginClass);
-
- void triggerRestart();
-
- void triggerShutdown();
-
- TokenStorage getTokenStorage();
-
- void initComponents();
-
- void reloadComponents(Class extends Component>>... classes);
-
- JSONRPCServlet getRPC();
-
- Collection>> getComponentClasses();
+ /**
+ * Signals the application to exit, optionally requesting a restart.
+ *
+ * @param shouldRestart {@code true} to exit with restart intent, {@code false} to shut down.
+ */
+ void requestExit(boolean shouldRestart);
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java
index 9bd0586..680ba2a 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java
@@ -6,11 +6,23 @@ import ru.kirillius.java.utils.events.EventHandler;
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
+/**
+ * Aggregates event handlers used to notify the application about runtime state changes.
+ */
public final class ContextEventsHandler {
+ /**
+ * Event fired when network resource bundles are recalculated.
+ */
@Getter
private final EventHandler networkManagerUpdateEvent = new ConcurrentEventHandler<>();
+ /**
+ * Event fired when subscription data has been refreshed.
+ */
@Getter
private final EventHandler subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
+ /**
+ * Event fired after the RPC servlet is initialised and ready.
+ */
@Getter
private final EventHandler RPCInitEvent = new ConcurrentEventHandler<>();
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/LauncherConfig.java b/core/src/main/java/ru/kirillius/pf/sdn/core/LauncherConfig.java
new file mode 100644
index 0000000..a7d89a0
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/LauncherConfig.java
@@ -0,0 +1,26 @@
+package ru.kirillius.pf.sdn.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Immutable configuration passed to the application launcher, describing runtime resources.
+ */
+@Builder
+@AllArgsConstructor
+public class LauncherConfig {
+ @Getter
+ private final File configFile;
+ @Getter
+ private final File appLibrary;
+ @Getter
+ private final String repository;
+ @Getter
+ private final Collection>> availableComponentClasses;
+
+
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoProvider.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoProvider.java
index 6a7c17b..af6316b 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoProvider.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoProvider.java
@@ -5,9 +5,18 @@ import ru.kirillius.pf.sdn.core.Context;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
+/**
+ * Abstraction for retrieving prefixes announced by a specific autonomous system.
+ */
public interface ASInfoProvider {
+ /**
+ * Returns IPv4 subnets originated by the provided autonomous system number.
+ */
List getPrefixes(int as);
+ /**
+ * Instantiates a provider class using the context-aware constructor.
+ */
static ASInfoProvider instantiate(Class extends ASInfoProvider> providerClass, Context context) {
try {
var constructor = providerClass.getConstructor(Context.class);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/BGPInfoService.java
similarity index 50%
rename from core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoService.java
rename to core/src/main/java/ru/kirillius/pf/sdn/core/Networking/BGPInfoService.java
index 5a5bc2a..3bead9b 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/ASInfoService.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/BGPInfoService.java
@@ -1,30 +1,46 @@
package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
-import lombok.NoArgsConstructor;
import lombok.Setter;
+import ru.kirillius.pf.sdn.core.AppService;
+import ru.kirillius.pf.sdn.core.Context;
-import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-@NoArgsConstructor
-public class ASInfoService implements Closeable {
+
+/**
+ * Delegates asynchronous retrieval of BGP prefix data through a configurable provider.
+ */
+public class BGPInfoService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Getter
@Setter
- private ASInfoProvider provider = null;
+ private ASInfoProvider provider;
+
+ /**
+ * Creates the service tied to the shared context.
+ */
+ public BGPInfoService(Context context) {
+ super(context);
+ }
+ /**
+ * Submits a request to obtain prefixes for the provided autonomous system number.
+ */
public Future> getPrefixes(int as) {
return executor.submit(() -> provider.getPrefixes(as));
}
+ /**
+ * Shuts down the background executor.
+ */
@Override
public void close() throws IOException {
executor.shutdown();
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
index e098d5e..a0d125e 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/IPv4Subnet.java
@@ -9,16 +9,28 @@ import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import java.util.Objects;
import java.util.regex.Pattern;
+/**
+ * Immutable representation of an IPv4 subnet with utility helpers for serialization.
+ */
@JSONSerializable(IPv4Subnet.Serializer.class)
public class IPv4Subnet {
+ /**
+ * Serializes subnets to CIDR notation strings for JSON persistence.
+ */
public final static class Serializer implements JSONSerializer {
+ /**
+ * Writes the subnet as a string using CIDR notation.
+ */
@Override
public Object serialize(IPv4Subnet subnet) throws SerializationException {
return subnet.toString();
}
+ /**
+ * Parses a CIDR notation string and returns the corresponding subnet instance.
+ */
@Override
public IPv4Subnet deserialize(Object o, Class> aClass) throws SerializationException {
return new IPv4Subnet((String) o);
@@ -31,6 +43,9 @@ public class IPv4Subnet {
private final int prefixLength;
+ /**
+ * Compares two subnets for equality based on address and prefix length.
+ */
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
@@ -38,11 +53,17 @@ public class IPv4Subnet {
return longAddress == that.longAddress && prefixLength == that.prefixLength;
}
+ /**
+ * Computes a hash code using address and prefix length.
+ */
@Override
public int hashCode() {
return Objects.hash(longAddress, prefixLength);
}
+ /**
+ * Parses a subnet from textual CIDR notation.
+ */
public IPv4Subnet(String subnet) {
var split = subnet.split(Pattern.quote("/"));
if (split.length != 2) {
@@ -55,11 +76,17 @@ public class IPv4Subnet {
prefixLength = prefix;
}
+ /**
+ * Creates a subnet from a numeric address and prefix length.
+ */
public IPv4Subnet(long longAddress, int prefixLength) {
this.longAddress = longAddress;
this.prefixLength = prefixLength;
}
+ /**
+ * Creates a subnet from a dotted address string and prefix length.
+ */
public IPv4Subnet(String address, int prefixLength) {
IPv4Util.validatePrefix(prefixLength);
@@ -67,19 +94,31 @@ public class IPv4Subnet {
this.prefixLength = prefixLength;
}
+ /**
+ * Calculates the number of addresses within the subnet.
+ */
public long count() {
return IPv4Util.calculateCountForPrefixLength(prefixLength);
}
+ /**
+ * Returns the dotted-decimal representation of the subnet's network address.
+ */
public String getAddress() {
return IPv4Util.longToIpAddress(longAddress);
}
+ /**
+ * Formats the subnet using CIDR notation.
+ */
@Override
public String toString() {
return getAddress() + '/' + prefixLength;
}
+ /**
+ * Determines whether this subnet overlaps with another.
+ */
public boolean overlaps(IPv4Subnet subnet) {
var minPrefixLength = Math.min(prefixLength, subnet.prefixLength);
var commonMask = IPv4Util.calculateMask(minPrefixLength);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
index 37a89e3..d4f2cbf 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java
@@ -8,6 +8,9 @@ import ru.kirillius.json.JSONSerializable;
import java.util.ArrayList;
import java.util.List;
+/**
+ * Container for grouped network identifiers used to configure filtering and subscriptions.
+ */
@AllArgsConstructor
@NoArgsConstructor
@Builder
@@ -30,12 +33,18 @@ public class NetworkResourceBundle {
@JSONArrayProperty(type = String.class)
private List domains = new ArrayList<>();
+ /**
+ * Clears all stored network identifiers.
+ */
public void clear() {
ASN.clear();
subnets.clear();
domains.clear();
}
+ /**
+ * Adds all resources from the provided bundle into this bundle.
+ */
public void add(NetworkResourceBundle networkResourceBundle) {
ASN.addAll(networkResourceBundle.getASN());
subnets.addAll(networkResourceBundle.getSubnets());
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java
similarity index 78%
rename from core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java
rename to core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java
index 0f25405..8580185 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java
@@ -3,7 +3,9 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
import org.json.JSONObject;
import org.json.JSONTokener;
+import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONUtility;
+import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.utils.logging.SystemLogger;
@@ -13,14 +15,30 @@ import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
-public class NetworkManager implements Closeable {
+/**
+ * Builds the effective set of network resources by combining subscriptions, caches, and filters.
+ */
+public class NetworkingService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
- private final static String CTX = NetworkManager.class.getSimpleName();
- private final Context context;
- private final File cacheFile;
+ private final static String CTX = NetworkingService.class.getSimpleName();
- public NetworkManager(Context context) {
- this.context = context;
+ private final File cacheFile;
+ private final EventListener subscription;
+
+ /**
+ * Creates the networking service, wiring subscriptions and restoring cached state.
+ */
+ public NetworkingService(Context context) {
+ super(context);
+ inputResources.clear();
+ inputResources.add(context.getConfig().getCustomResources());
+ subscription = context.getEventsHandler().getSubscriptionsUpdateEvent().add(bundle -> {
+ var config = context.getConfig();
+ inputResources.clear();
+ inputResources.add(config.getCustomResources());
+ inputResources.add(bundle);
+ triggerUpdate(false);
+ });
cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
if (cacheFile.exists() && context.getConfig().isCachingAS()) {
SystemLogger.message("Loading as cache file", CTX);
@@ -42,6 +60,9 @@ public class NetworkManager implements Closeable {
@Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
+ /**
+ * Indicates whether an update job is currently executing.
+ */
public boolean isUpdatingNow() {
var future = updateProcess.get();
return future != null && !future.isDone() && !future.isCancelled();
@@ -49,6 +70,9 @@ public class NetworkManager implements Closeable {
private final Map> prefixCache = new ConcurrentHashMap<>();
+ /**
+ * Schedules an update of network resources, optionally ignoring cached prefixes.
+ */
public void triggerUpdate(boolean ignoreCache) {
if (isUpdatingNow()) {
return;
@@ -129,8 +153,11 @@ public class NetworkManager implements Closeable {
}
+ /**
+ * Fetches prefixes for the given autonomous systems and stores them in the cache.
+ */
private void fetchPrefixes(List systems) {
- var service = context.getASInfoService();
+ var service = context.getServiceManager().getService(BGPInfoService.class);
systems.forEach(as -> {
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
var future = service.getPrefixes(as);
@@ -149,8 +176,12 @@ public class NetworkManager implements Closeable {
}
+ /**
+ * Removes event subscriptions and shuts down the executor.
+ */
@Override
public void close() throws IOException {
+ context.getEventsHandler().getSubscriptionsUpdateEvent().remove(subscription);
executor.shutdown();
}
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java
similarity index 52%
rename from core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java
rename to core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java
index ad2cc53..5c7f0d5 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java
@@ -1,36 +1,58 @@
package ru.kirillius.pf.sdn.core;
import lombok.SneakyThrows;
+import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
+import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.core.Util.Wait;
import ru.kirillius.utils.logging.SystemLogger;
-import java.io.Closeable;
import java.io.IOException;
import java.time.Duration;
-public class UpdateManager implements Closeable {
+/**
+ * Periodically refreshes subscription content and network data according to runtime settings.
+ */
+public class ResourceUpdateService extends AppService {
private final Thread updateThread;
- public UpdateManager(Context context) {
- this.context = context;
+ /**
+ * Creates the service and prepares the background update worker.
+ */
+ public ResourceUpdateService(Context context) {
+ super(context);
+
updateThread = new Thread(new ThreadWorker());
}
+ /**
+ * Starts the update thread; can only be invoked once.
+ */
public void start() {
+ if (updateThread.isAlive()) {
+ throw new IllegalStateException("Started already");
+ }
updateThread.start();
}
- private final Context context;
+ /**
+ * Interrupts the update thread and stops scheduling tasks.
+ */
@Override
public void close() throws IOException {
updateThread.interrupt();
}
- private final static String CTX = UpdateManager.class.getSimpleName();
+ private final static String CTX = ResourceUpdateService.class.getSimpleName();
+ /**
+ * Background runner that performs scheduled resource maintenance tasks.
+ */
private class ThreadWorker implements Runnable {
+ /**
+ * Performs periodic subscription and network updates based on configuration intervals.
+ */
@SneakyThrows
@Override
public void run() {
@@ -41,9 +63,9 @@ public class UpdateManager implements Closeable {
uptime++;
- if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval()*60L) == 0) {
+ if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval() * 60L) == 0) {
SystemLogger.message("Updating subscriptions", CTX);
- var subscriptionManager = context.getSubscriptionManager();
+ var subscriptionManager = context.getServiceManager().getService(SubscriptionService.class);
subscriptionManager.triggerUpdate();
Wait.until(subscriptionManager::isUpdatingNow);
Wait.when(subscriptionManager::isUpdatingNow);
@@ -51,7 +73,7 @@ public class UpdateManager implements Closeable {
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
SystemLogger.message("Updating cached AS", CTX);
- var networkManager = context.getNetworkManager();
+ var networkManager = context.getServiceManager().getService(NetworkingService.class);
networkManager.triggerUpdate(true);
}
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ServiceManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ServiceManager.java
new file mode 100644
index 0000000..1daae14
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ServiceManager.java
@@ -0,0 +1,100 @@
+package ru.kirillius.pf.sdn.core;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Instantiates and manages the lifecycle of registered application services.
+ */
+public class ServiceManager implements Closeable {
+ private final Map, AppService> services = new LinkedHashMap<>();
+
+ /**
+ * Creates a manager and eagerly instantiates the provided service classes.
+ */
+ public ServiceManager(Context context, Collection> serviceClasses) {
+ Objects.requireNonNull(context, "context");
+ if (serviceClasses == null) {
+ return;
+ }
+ for (var serviceClass : serviceClasses) {
+ if (serviceClass == null) {
+ continue;
+ }
+ services.put(serviceClass, instantiateService(context, serviceClass));
+ }
+ }
+
+ /**
+ * Retrieves a registered service by type.
+ *
+ * @param serviceClass target service class.
+ * @param service subtype.
+ * @return instance if available, otherwise {@code null}.
+ */
+ public S getService(Class serviceClass) {
+ var service = services.get(serviceClass);
+ if (service == null) {
+ return null;
+ }
+ return serviceClass.cast(service);
+ }
+
+ /**
+ * Closes all managed services, propagating the first {@link IOException} encountered.
+ */
+ @Override
+ public void close() throws IOException {
+ IOException failure = null;
+ for (var service : services.values()) {
+ try {
+ service.close();
+ } catch (IOException e) {
+ if (failure == null) {
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
+ }
+ }
+ }
+ if (failure != null) {
+ throw failure;
+ }
+ }
+
+ /**
+ * Creates a service instance using either context-aware or default constructor.
+ */
+ private AppService instantiateService(Context context, Class extends AppService> serviceClass) {
+ try {
+ var constructor = resolveConstructor(serviceClass);
+ constructor.setAccessible(true);
+ return constructor.getParameterCount() == 1
+ ? constructor.newInstance(context)
+ : constructor.newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Failed to instantiate service: " + serviceClass.getName(), e);
+ }
+ }
+
+ /**
+ * Resolves a usable constructor favouring context-aware signatures.
+ */
+ private Constructor extends AppService> resolveConstructor(Class extends AppService> serviceClass) {
+ try {
+ return serviceClass.getDeclaredConstructor(Context.class);
+ } catch (NoSuchMethodException ignored) {
+ try {
+ return serviceClass.getDeclaredConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("No suitable constructor found for service: " + serviceClass.getName(), e);
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/RepositoryConfig.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/RepositoryConfig.java
index 40cce90..dfc06c6 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/RepositoryConfig.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/RepositoryConfig.java
@@ -7,6 +7,9 @@ import lombok.Setter;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
+/**
+ * Describes a subscription source with its name, implementation type, and origin descriptor.
+ */
@NoArgsConstructor
@AllArgsConstructor
@JSONSerializable
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionProvider.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionProvider.java
index 1a9e83f..f37aa64 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionProvider.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionProvider.java
@@ -6,6 +6,9 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
+/**
+ * Loads network resource bundles from a repository described by {@link RepositoryConfig}.
+ */
public interface SubscriptionProvider {
Map getResources(RepositoryConfig config);
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionService.java
similarity index 83%
rename from core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java
rename to core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionService.java
index ec76156..fa277bf 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionService.java
@@ -1,11 +1,11 @@
package ru.kirillius.pf.sdn.core.Subscription;
import lombok.Getter;
+import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.utils.logging.SystemLogger;
-import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@@ -15,22 +15,30 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
-public class SubscriptionManager implements Closeable {
+/**
+ * Resolves subscription repositories, builds aggregated resource bundles, and publishes update events.
+ */
+public class SubscriptionService extends AppService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
- private final Context context;
+
private final Map, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
- public SubscriptionManager(Context context) {
- this.context = context;
- }
+
private final AtomicReference> updateProcess = new AtomicReference<>();
@Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
+ public SubscriptionService(Context context) {
+ super(context);
+ }
+
+ /**
+ * Indicates whether a subscription update job is currently running.
+ */
public boolean isUpdatingNow() {
var future = updateProcess.get();
return future != null && !future.isDone() && !future.isCancelled();
@@ -40,6 +48,9 @@ public class SubscriptionManager implements Closeable {
private final Map availableResources = new ConcurrentHashMap<>();
+ /**
+ * Starts a background task that refreshes subscription repositories and aggregates resources.
+ */
public synchronized void triggerUpdate() {
if (isUpdatingNow()) {
return;
@@ -84,9 +95,12 @@ public class SubscriptionManager implements Closeable {
}));
}
- private final static String CTX = SubscriptionManager.class.getSimpleName();
+ private final static String CTX = SubscriptionService.class.getSimpleName();
+ /**
+ * Shuts down the executor used for update tasks.
+ */
@Override
public void close() throws IOException {
executor.shutdown();
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java
new file mode 100644
index 0000000..f5afdfe
--- /dev/null
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java
@@ -0,0 +1,22 @@
+package ru.kirillius.pf.sdn.core.Util;
+
+import java.util.Arrays;
+
+/**
+ * Utility methods for parsing command-line arguments used by the launcher.
+ */
+public final class CommandLineUtils {
+ private CommandLineUtils() {
+ }
+
+ /**
+ * Returns the first command-line argument starting with the given flag name.
+ */
+ public static String getArgument(String argname, String[] args) {
+ var first = Arrays.stream(args).filter(arg -> arg.startsWith("-" + argname)).findFirst();
+ if (first.isEmpty()) {
+ throw new IllegalArgumentException("Missing required argument: -" + argname);
+ }
+ return first.get();
+ }
+}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/HashUtil.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/HashUtil.java
index 5490fbc..a7c2a47 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/HashUtil.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/HashUtil.java
@@ -4,10 +4,16 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+/**
+ * Provides hashing helpers for password management and compatibility utilities.
+ */
public final class HashUtil {
private HashUtil() {
}
+ /**
+ * Computes the MD5 digest for the supplied input string.
+ */
public static String md5(String input) {
try {
var md = MessageDigest.getInstance("MD5");
@@ -29,6 +35,9 @@ public final class HashUtil {
}
}
+ /**
+ * Computes a salted SHA-512 hash, returning the hexadecimal representation.
+ */
public static String hash(String data, String salt) {
String generatedPassword = null;
MessageDigest md = null;
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
index 37418b6..2b23e58 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java
@@ -12,6 +12,9 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
+/**
+ * Helper methods for IPv4 address manipulation and subnet aggregation logic.
+ */
public class IPv4Util {
private IPv4Util() {
@@ -19,12 +22,18 @@ public class IPv4Util {
private static final Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
+ /**
+ * Ensures the supplied string is a valid IPv4 address.
+ */
public static void validateAddress(String address) {
if (!pattern.matcher(address).matches()) {
throw new IllegalArgumentException("Invalid IPv4 address: " + address);
}
}
+ /**
+ * Ensures the prefix length is within the allowed range 0..32.
+ */
public static void validatePrefix(int prefix) {
if (prefix < 0 || prefix > 32) {
throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
@@ -32,6 +41,9 @@ public class IPv4Util {
}
+ /**
+ * Converts a dotted IPv4 address into its numeric representation.
+ */
@SneakyThrows
public static long ipAddressToLong(String address) {
validateAddress(address);
@@ -44,12 +56,18 @@ public class IPv4Util {
return result;
}
+ /**
+ * Returns a bit mask representing the provided prefix length.
+ */
public static long calculateMask(int prefixLength) {
validatePrefix(prefixLength);
return 0xFFFFFFFFL << (32 - prefixLength);
}
+ /**
+ * Converts a numeric IPv4 address into dotted notation.
+ */
public static String longToIpAddress(long ipLong) {
if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
throw new IllegalArgumentException("Address number should be in range 0 - 4294967295");
@@ -57,6 +75,9 @@ public class IPv4Util {
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
}
+ /**
+ * Formats a numeric mask into dotted notation.
+ */
public static String maskToString(long maskLong) {
return String.format("%d.%d.%d.%d",
(maskLong >> 24) & 0xff,
@@ -65,12 +86,24 @@ public class IPv4Util {
maskLong & 0xff);
}
+ /**
+ * Result contract for subnet summarisation operations.
+ */
public interface SummarisationResult {
+ /**
+ * Returns the resulting set of subnets after summarisation.
+ */
List getResult();
+ /**
+ * Returns the original subnets that were merged during summarisation.
+ */
Set getMergedSubnets();
}
+ /**
+ * Performs subnet merging strategies based on overlap and utilisation heuristics.
+ */
private static class SubnetSummaryUtility implements SummarisationResult {
@Getter
@@ -80,6 +113,9 @@ public class IPv4Util {
private final Set mergedSubnets = new HashSet<>();
+ /**
+ * Builds the summarisation utility with the supplied subnets and usage threshold.
+ */
public SubnetSummaryUtility(Collection subnets, int usePercentage) {
source = subnets;
result = new ArrayList<>(subnets);
@@ -90,6 +126,9 @@ public class IPv4Util {
result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
}
+ /**
+ * Removes redundant subnets wholly covered by other subnets.
+ */
private void summaryOverlapped() {
if (result.size() < 2) {
return;
@@ -113,6 +152,9 @@ public class IPv4Util {
overlapped.forEach(result::remove);
}
+ /**
+ * Attempts to merge adjacent subnets into larger ones while preserving coverage.
+ */
private void mergeNeighbours() {
if (result.size() < 2) {
return;
@@ -155,22 +197,37 @@ public class IPv4Util {
}
}
+ /**
+ * Returns the smallest prefix length currently present in the result set.
+ */
private int findMinPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
}
+ /**
+ * Returns the largest prefix length currently present in the result set.
+ */
private int findMaxPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
}
+ /**
+ * Returns the smallest network address found in the result set.
+ */
private long findMinAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
}
+ /**
+ * Returns the largest network address found in the result set.
+ */
private long findMaxAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
}
+ /**
+ * Produces candidate subnets capable of covering the current set at the given prefix length.
+ */
private List findMergeCandidatesForPrefixLength(int prefixLength) {
//создаём подсети-кандидаты, которые покроют наш список
var maxAddress = findMaxAddress();
@@ -201,6 +258,9 @@ public class IPv4Util {
return candidates;
}
+ /**
+ * Tests whether the candidate subnet meets utilisation requirements and performs the merge.
+ */
private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
if (result.contains(candidate)) {
return false;
@@ -234,6 +294,9 @@ public class IPv4Util {
return false;
}
+ /**
+ * Iteratively merges subnets that satisfy the utilisation threshold.
+ */
private void summaryWithUsage(int usePercentage) {
if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
return;
@@ -266,14 +329,23 @@ public class IPv4Util {
}
}
+ /**
+ * Summaries the provided subnets using merging heuristics and utilisation thresholds.
+ */
public static SummarisationResult summarySubnets(Collection subnets, int usePercentage) {
return new SubnetSummaryUtility(subnets, usePercentage);
}
+ /**
+ * Creates a subnet covering the given address at the specified prefix length.
+ */
private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
}
+ /**
+ * Returns the number of addresses represented by a prefix length.
+ */
public static long calculateCountForPrefixLength(long prefixLength) {
return 1L << (32L - prefixLength);
}
diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java
index 414faec..02bf5eb 100644
--- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java
+++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java
@@ -3,11 +3,17 @@ package ru.kirillius.pf.sdn.core.Util;
import java.time.Duration;
import java.util.function.Supplier;
+/**
+ * Provides blocking wait utilities for polling boolean conditions with interruption support.
+ */
public final class Wait {
private Wait() {
throw new AssertionError();
}
+ /**
+ * Blocks until the supplied condition becomes true or the thread is interrupted.
+ */
public static void until(Supplier condition) throws InterruptedException {
if (condition == null) {
return;
@@ -18,6 +24,9 @@ public final class Wait {
}
}
+ /**
+ * Blocks while the supplied condition remains true or until interrupted.
+ */
public static void when(Supplier condition) throws InterruptedException {
if (condition == null) {
return;
diff --git a/launcher/pom.xml b/launcher/pom.xml
new file mode 100644
index 0000000..1901290
--- /dev/null
+++ b/launcher/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ ru.kirillius
+ pf-sdn
+ 1.0.1.5
+
+
+ launcher
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ clean-root-target
+ clean
+ false
+
+ run
+
+
+
+
+
+
+
+
+ post-build-assembly
+ verify
+ false
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ovpn-connector/pom.xml b/ovpn-connector/pom.xml
index 1b856ad..05288aa 100644
--- a/ovpn-connector/pom.xml
+++ b/ovpn-connector/pom.xml
@@ -6,10 +6,10 @@
ru.kirillius
pf-sdn
- 0.1.0.0
+ 1.0.1.5
- ovpn-connector
+ pf-sdn.ovpn-connector
diff --git a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java
index 6d7af1d..adb72a7 100644
--- a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java
+++ b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java
@@ -13,7 +13,13 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
+/**
+ * CLI utility that requests managed routes from the SDN API and writes them into an OpenVPN push file.
+ */
public class App {
+ /**
+ * Loads connector configuration from {@code ovpn-connector.json}.
+ */
private static Config loadConfig() {
var configFile = new File("ovpn-connector.json");
if (!configFile.exists()) {
@@ -26,6 +32,9 @@ public class App {
}
}
+ /**
+ * Writes the received routes to the OpenVPN push file.
+ */
private static void pushRoutes(JSONArray routes, File pushFile) {
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) {
for (var i = 0; i < routes.length(); i++) {
@@ -39,6 +48,9 @@ public class App {
}
}
+ /**
+ * Application entry point.
+ */
public static void main(String[] args) {
try {
var config = loadConfig();
@@ -56,6 +68,9 @@ public class App {
}
}
+ /**
+ * Performs the JSON-RPC request to the SDN API and returns the route list.
+ */
private static JSONArray sendRequest(Config config) throws IOException, InterruptedException {
var json = new JSONObject();
json.put("jsonrpc", "2.0");
diff --git a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java
index 1ed55ed..d566b4f 100644
--- a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java
+++ b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java
@@ -5,6 +5,9 @@ import lombok.Setter;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
+/**
+ * Configuration for the OpenVPN connector utility, including API endpoint and token.
+ */
@JSONSerializable
@Getter
@Setter
diff --git a/pom.xml b/pom.xml
index b74b3f0..7fe6d53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,12 +6,15 @@
ru.kirillius
pf-sdn
- 0.1.0.0
+ 1.0.1.5
+
+
pom
core
app
ovpn-connector
+ launcher
@@ -42,62 +45,6 @@
-
-
-
- org.apache.maven.plugins
- maven-antrun-plugin
- 3.1.0
-
-
- clean-root-target
- clean
- false
-
- run
-
-
-
-
-
-
-
-
- post-build-assembly
- verify
- false
-
- run
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -116,7 +63,6 @@
-
org.junit.jupiter