Рефакторинг бэкэнда

This commit is contained in:
kirillius 2025-10-04 20:41:56 +03:00
parent 28d37f0dfb
commit 95d09e3c79
51 changed files with 1685 additions and 438 deletions

View File

@ -6,17 +6,17 @@
<parent> <parent>
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId> <artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version> <version>1.0.1.5</version>
</parent> </parent>
<artifactId>app</artifactId> <artifactId>pf-sdn.app</artifactId>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>core</artifactId> <artifactId>pf-sdn.core</artifactId>
<version>0.1.0.0</version> <version>${project.parent.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>

View File

@ -2,33 +2,33 @@ package ru.kirillius.pf.sdn;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; 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.FRR;
import ru.kirillius.pf.sdn.External.API.Components.OVPN; 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.Components.TDNS;
import ru.kirillius.pf.sdn.External.API.HEInfoProvider; 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.*;
import ru.kirillius.pf.sdn.core.Networking.ASInfoService; import ru.kirillius.pf.sdn.core.Auth.AuthManager;
import ru.kirillius.pf.sdn.core.Networking.NetworkManager; import ru.kirillius.pf.sdn.core.Auth.TokenService;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager; 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.core.Util.Wait;
import ru.kirillius.pf.sdn.web.HTTPServer; import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; 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 { public class App implements Context, Closeable {
private final static File configFile = new File("config.json");
protected final static String CTX = App.class.getSimpleName(); protected final static String CTX = App.class.getSimpleName();
static { static {
@ -38,175 +38,101 @@ public class App implements Context, Closeable {
private final AtomicBoolean shouldRestart = new AtomicBoolean(false); private final AtomicBoolean shouldRestart = new AtomicBoolean(false);
private final AtomicBoolean running = new AtomicBoolean(true); 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 @Getter
private final ContextEventsHandler EventsHandler = new ContextEventsHandler(); private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
@Getter
private final ServiceManager serviceManager;
@Getter
private final LauncherConfig launcherConfig;
@Getter
private final Config config;
private final List<Component<?>> loadedComponents = new ArrayList<>(); /**
* Loads configuration from disk, creating a default file if missing.
@SneakyThrows */
public App(File configFile) { private Config loadConfig() {
Config loadedConfig = null;
try { try {
config = Config.load(configFile); loadedConfig = Config.load(launcherConfig.getConfigFile());
} catch (IOException e) { } catch (IOException e) {
config = new Config(); loadedConfig = new Config();
try { try {
Config.store(config, configFile); Config.store(loadedConfig, launcherConfig.getConfigFile());
} catch (IOException ex) { } catch (IOException ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);
} }
} }
return loadedConfig;
}
authManager = new AuthManager(this); /**
ASInfoService = new ASInfoService(); * Instantiates all application services and performs initial wiring.
ASInfoService.setProvider(new HEInfoProvider(this)); */
networkManager = new NetworkManager(this); private ServiceManager loadServiceManager() {
networkManager.getInputResources().add(config.getCustomResources()); 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));
subscriptionManager = new SubscriptionManager(this); manager.getService(BGPInfoService.class).setProvider(new HEInfoProvider());
updateManager = new UpdateManager(this); manager.getService(ResourceUpdateService.class).start();
tokenStorage = new TokenStorage(this); manager.getService(ComponentHandlerService.class).syncComponentsWithConfig();
subscribe(); return manager;
updateManager.start(); }
initComponents();
server = new HTTPServer(this);
/**
* Ensures an admin password exists, defaulting to {@code admin} when missing.
*/
private void checkDefaultPassword() {
if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) { if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) {
SystemLogger.error("There is no password for admin. Setting default password: admin", CTX); 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) { public static void main(String[] args) {
var restart = false; try (var app = new App(LauncherConfig.builder()
do { .configFile(new File(getArgument("c", args)))
try (var app = new App(configFile)) { .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); Wait.when(app.running::get);
restart = app.shouldRestart.get(); if (app.shouldRestart.get()) {
System.exit(303);
} else {
System.exit(0);
}
} catch (Exception e) { } catch (Exception e) {
SystemLogger.error("Unhandled error", CTX, e); SystemLogger.error("Unhandled error", CTX, e);
System.exit(1);
} }
} while (restart);
} }
public void triggerRestart() {
SystemLogger.message("Restarting app", CTX); /**
* Requests the application to exit, optionally restarting.
*/
public void requestExit(boolean restart) {
running.set(false); running.set(false);
shouldRestart.set(true); shouldRestart.set(restart);
}
public void triggerShutdown() {
SystemLogger.message("Shutting down app", CTX);
running.set(false);
}
public Collection<Class<? extends Component<?>>> 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);
}
}
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);
} }
/**
* Closes all managed services.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
loadedComponents.forEach(plugin -> { serviceManager.close();
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);
}
} }
} }

View File

@ -18,17 +18,26 @@ import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/**
* Component that synchronises FRR routing instances with the aggregated subnet list.
*/
public final class FRR extends AbstractComponent<FRR.FRRConfig> { public final class FRR extends AbstractComponent<FRR.FRRConfig> {
private final static String SUBNET_PATTERN = "{%subnet}"; private final static String SUBNET_PATTERN = "{%subnet}";
private final static String GW_PATTERN = "{%gateway}"; private final static String GW_PATTERN = "{%gateway}";
private final static String CTX = FRR.class.getSimpleName(); private final static String CTX = FRR.class.getSimpleName();
private final EventListener<NetworkResourceBundle> subscription; private final EventListener<NetworkResourceBundle> subscription;
/**
* Binds the component to the application context and subscribes to network updates.
*/
public FRR(Context context) { public FRR(Context context) {
super(context); super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets())); subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
} }
/**
* Synchronises FRR instances with the provided subnet list.
*/
private void updateSubnets(List<IPv4Subnet> subnets) { private void updateSubnets(List<IPv4Subnet> subnets) {
for (var entry : config.instances) { for (var entry : config.instances) {
SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX); SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
@ -94,6 +103,9 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
} }
} }
/**
* Executes a batch of VTYSH commands optionally wrapping them in configuration mode.
*/
private void executeVTYCommandBundle(List<String> commands, boolean configMode, ShellExecutor shell, Consumer<Integer> progressCallback) { private void executeVTYCommandBundle(List<String> commands, boolean configMode, ShellExecutor shell, Consumer<Integer> progressCallback) {
var buffer = new ArrayList<String>(); var buffer = new ArrayList<String>();
@ -122,6 +134,9 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
} }
} }
/**
* Executes a single VTYSH command and returns its stdout.
*/
private String executeVTYCommand(String[] command, ShellExecutor shell) { private String executeVTYCommand(String[] command, ShellExecutor shell) {
var buffer = new ArrayList<String>(); var buffer = new ArrayList<String>();
buffer.add("vtysh"); buffer.add("vtysh");
@ -133,11 +148,17 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
return shell.executeCommand(buffer.toArray(new String[0])); return shell.executeCommand(buffer.toArray(new String[0]));
} }
/**
* Removes the subscription from the context event handler.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription); context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
} }
/**
* Configuration describing FRR instances and how subnets should be rendered for them.
*/
@JSONSerializable @JSONSerializable
public static class FRRConfig { public static class FRRConfig {
@ -146,6 +167,9 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
@JSONArrayProperty(type = Entry.class) @JSONArrayProperty(type = Entry.class)
private List<Entry> instances = new ArrayList<>(); private List<Entry> instances = new ArrayList<>();
/**
* Declarative description of a single FRR instance managed by the component.
*/
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor

View File

@ -12,19 +12,27 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
import ru.kirillius.pf.sdn.External.API.ShellExecutor; import ru.kirillius.pf.sdn.External.API.ShellExecutor;
import ru.kirillius.pf.sdn.core.AbstractComponent; import ru.kirillius.pf.sdn.core.AbstractComponent;
import ru.kirillius.pf.sdn.core.Context; 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.core.Util.IPv4Util;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
import ru.kirillius.pf.sdn.web.WebService;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.IOException; import java.io.IOException;
/**
* Component integrating with OpenVPN to expose management RPC and synchronize route exports.
*/
public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> { public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
private final static String CTX = OVPN.class.getSimpleName(); private final static String CTX = OVPN.class.getSimpleName();
private final EventListener<JSONRPCServlet> subscription; private final EventListener<JSONRPCServlet> subscription;
/**
* Registers the component with the JSON-RPC servlet or defers until it becomes available.
*/
public OVPN(Context context) { public OVPN(Context context) {
super(context); super(context);
var RPC = context.getRPC(); var RPC = context.getServiceManager().getService(WebService.class).getJSONRPC();
if (RPC != null) { if (RPC != null) {
RPC.addTargetInstance(OVPN.class, this); RPC.addTargetInstance(OVPN.class, this);
subscription = null; subscription = null;
@ -34,6 +42,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this)); .add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
} }
/**
* Executes the configured command to restart the OpenVPN service.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public String restartSystemService() { public String restartSystemService() {
@ -45,11 +56,14 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
} }
} }
/**
* Returns a JSON array describing managed routes for OpenVPN clients.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public JSONArray getManagedRoutes() { public JSONArray getManagedRoutes() {
var array = new JSONArray(); 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(); var json = new JSONObject();
json.put("address", subnet.getAddress()); json.put("address", subnet.getAddress());
json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength()))); json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength())));
@ -59,6 +73,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
} }
/**
* Unregisters RPC listeners when the component is closed.
*/
@Override @Override
public void close() { public void close() {
if (subscription != null) { if (subscription != null) {
@ -66,6 +83,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
} }
} }
/**
* Configuration for OpenVPN shell interaction and service restart command.
*/
@JSONSerializable @JSONSerializable
public static class OVPNConfig { public static class OVPNConfig {
@Getter @Getter

View File

@ -16,16 +16,25 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Component that synchronises Technitium DNS forwarder zones with managed domain lists.
*/
public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> { public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
private final static String CTX = TDNS.class.getSimpleName(); private final static String CTX = TDNS.class.getSimpleName();
private final EventListener<NetworkResourceBundle> subscription; private final EventListener<NetworkResourceBundle> subscription;
/**
* Subscribes to network updates to keep Technitium DNS forwarder zones in sync.
*/
public TDNS(Context context) { public TDNS(Context context) {
super(context); super(context);
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains())); subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
} }
/**
* Updates Technitium DNS servers to match the provided domain list.
*/
private void updateSubnets(List<String> domains) { private void updateSubnets(List<String> domains) {
for (var instance : config.instances) { for (var instance : config.instances) {
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX); SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
@ -56,12 +65,18 @@ public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
} }
/**
* Removes the event subscription when the component is closed.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription); context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
} }
/**
* Configuration of Technitium DNS instances managed by the component.
*/
@JSONSerializable @JSONSerializable
public static class TechnitiumConfig { public static class TechnitiumConfig {
@ -70,6 +85,9 @@ public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
@JSONArrayProperty(type = Entry.class) @JSONArrayProperty(type = Entry.class)
private List<Entry> instances = new ArrayList<>(); private List<Entry> instances = new ArrayList<>();
/**
* Describes a single DNS server endpoint and its access credentials.
*/
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor

View File

@ -18,13 +18,22 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
/**
* Subscription provider that pulls JSON resource bundles from a Git repository cache.
*/
public class GitSubscription implements SubscriptionProvider { public class GitSubscription implements SubscriptionProvider {
private final Context context; private final Context context;
/**
* Creates the provider using the shared application context.
*/
public GitSubscription(Context context) { public GitSubscription(Context context) {
this.context = context; this.context = context;
} }
/**
* Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
*/
@Override @Override
public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) { public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) {
try { 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 { 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(); 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 { private static Git cloneRepository(String REPO_URL, File path) throws GitAPIException {
SystemLogger.message("Cloning repository " + REPO_URL, CTX); SystemLogger.message("Cloning repository " + REPO_URL, CTX);
return Git.cloneRepository() return Git.cloneRepository()
@ -111,6 +126,9 @@ public class GitSubscription implements SubscriptionProvider {
.call(); .call();
} }
/**
* Opens an existing repository located at the given directory.
*/
private static Git openRepository(File repoDir) throws IOException { private static Git openRepository(File repoDir) throws IOException {
var builder = new FileRepositoryBuilder(); var builder = new FileRepositoryBuilder();
var repository = builder.setGitDir(new File(repoDir, ".git")) var repository = builder.setGitDir(new File(repoDir, ".git"))
@ -120,6 +138,9 @@ public class GitSubscription implements SubscriptionProvider {
return new Git(repository); return new Git(repository);
} }
/**
* Returns whether the directory contains a Git repository.
*/
private static boolean isGitRepository(File directory) { private static boolean isGitRepository(File directory) {
if (!directory.exists() || !directory.isDirectory()) { if (!directory.exists() || !directory.isDirectory()) {
return false; return false;

View File

@ -4,7 +4,6 @@ import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; 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.ASInfoProvider;
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet; import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
@ -18,14 +17,16 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/**
* Retrieves ASN prefix information from Hurricane Electric's public API.
*/
public class HEInfoProvider implements ASInfoProvider { 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 @Override
@SneakyThrows @SneakyThrows
public List<IPv4Subnet> getPrefixes(int as) { public List<IPv4Subnet> 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<IPv4Subnet> getIPv4Subnets(InputStream inputStream) { private static @NotNull ArrayList<IPv4Subnet> getIPv4Subnets(InputStream inputStream) {
var json = new JSONObject(new JSONTokener(inputStream)); var json = new JSONObject(new JSONTokener(inputStream));
var array = json.getJSONArray("prefixes"); var array = json.getJSONArray("prefixes");

View File

@ -15,15 +15,24 @@ import java.util.concurrent.TimeUnit;
import static net.schmizz.sshj.common.IOUtils.readFully; 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 { public class ShellExecutor implements Closeable {
private final static String CTX = ShellExecutor.class.getSimpleName(); private final static String CTX = ShellExecutor.class.getSimpleName();
private final Config config; private final Config config;
private SSHClient sshClient; private SSHClient sshClient;
/**
* Creates an executor with the supplied connection configuration.
*/
public ShellExecutor(Config config) { public ShellExecutor(Config config) {
this.config = config; this.config = config;
} }
/**
* Executes the given command, either locally or through SSH, returning the captured stdout.
*/
public String executeCommand(String[] command) { public String executeCommand(String[] command) {
var buffer = new StringJoiner(" "); var buffer = new StringJoiner(" ");
Arrays.stream(command).forEach(e -> buffer.add('"' + e + '"')); 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 @Override
public void close() throws IOException { public void close() throws IOException {
@ -77,6 +89,9 @@ public class ShellExecutor implements Closeable {
} }
/**
* Connection and authentication options for shell command execution.
*/
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@ -103,6 +118,9 @@ public class ShellExecutor implements Closeable {
@JSONProperty @JSONProperty
private volatile String password = "securepassword"; private volatile String password = "securepassword";
/**
* Returns a human-readable description of the configured shell target.
*/
@Override @Override
public String toString() { public String toString() {
var builder = new StringBuilder(); var builder = new StringBuilder();

View File

@ -16,17 +16,26 @@ import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
/**
* Thin client for the Technitium DNS HTTP API used to manage forwarder zones.
*/
public class TDNSAPI implements Closeable { public class TDNSAPI implements Closeable {
private final String server; private final String server;
private final String authToken; private final String authToken;
private final HttpClient httpClient; private final HttpClient httpClient;
/**
* Creates an API client targeting the specified Technitium server.
*/
public TDNSAPI(String server, String authToken) { public TDNSAPI(String server, String authToken) {
this.server = server; this.server = server;
httpClient = HttpClient.newBuilder().build(); httpClient = HttpClient.newBuilder().build();
this.authToken = authToken; this.authToken = authToken;
} }
/**
* Performs a GET request to the Technitium API with authentication parameters.
*/
private JSONObject getRequest(String api, Map<String, String> additionalParams) { private JSONObject getRequest(String api, Map<String, String> additionalParams) {
var params = new HashMap<>(additionalParams); var params = new HashMap<>(additionalParams);
var joiner = new StringJoiner("&"); var joiner = new StringJoiner("&");
@ -57,10 +66,16 @@ public class TDNSAPI implements Closeable {
} }
} }
/**
* Supported zone types returned by the API.
*/
public enum ZoneType { public enum ZoneType {
Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog
} }
/**
* Transfer-object describing DNS zone metadata received from Technitium.
*/
@JSONSerializable @JSONSerializable
public static class ZoneResponse { public static class ZoneResponse {
@JSONProperty @JSONProperty
@ -92,11 +107,17 @@ public class TDNSAPI implements Closeable {
private Date lastModified; private Date lastModified;
} }
/**
* Lists zones from the Technitium server.
*/
public List<ZoneResponse> getZones() { public List<ZoneResponse> getZones() {
var request = getRequest("/api/zones/list", Collections.emptyMap()); var request = getRequest("/api/zones/list", Collections.emptyMap());
return JSONUtility.deserializeCollection(request.getJSONArray("zones"), ZoneResponse.class, null).toList(); 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) { public void createForwarderZone(String zoneName, String forwarder) {
var params = new HashMap<String, String>(); var params = new HashMap<String, String>();
params.put("zone", zoneName); 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) { public void deleteZone(String zoneName) {
var params = new HashMap<String, String>(); var params = new HashMap<String, String>();
params.put("zone", zoneName); params.put("zone", zoneName);
@ -114,6 +138,9 @@ public class TDNSAPI implements Closeable {
} }
/**
* Closes the underlying HTTP client.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
httpClient.close(); httpClient.close();

View File

@ -7,19 +7,31 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Handler; import java.util.logging.Handler;
import java.util.logging.LogRecord; import java.util.logging.LogRecord;
/**
* Maintains a rolling in-memory buffer of recent log messages for diagnostics exposure.
*/
public class InMemoryLogHandler extends Handler { public class InMemoryLogHandler extends Handler {
private final static Queue<String> queue = new ConcurrentLinkedQueue<>(); private final static Queue<String> queue = new ConcurrentLinkedQueue<>();
private final static DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US); 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) { private String format(LogRecord logRecord) {
return "[" + dateFormat.format(new Date(logRecord.getMillis())) + "][" + logRecord.getLevel().getName() + "] " + logRecord.getMessage().trim(); 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<String> getMessages() { public static Collection<String> getMessages() {
return Collections.unmodifiableCollection(queue); return Collections.unmodifiableCollection(queue);
} }
/**
* Stores the formatted message in the buffer, trimming old entries when full.
*/
@Override @Override
public void publish(LogRecord logRecord) { public void publish(LogRecord logRecord) {
if (queue.size() >= 1000) { if (queue.size() >= 1000) {
@ -28,11 +40,17 @@ public class InMemoryLogHandler extends Handler {
queue.add(format(logRecord)); queue.add(format(logRecord));
} }
/**
* No-op flush implementation.
*/
@Override @Override
public void flush() { public void flush() {
} }
/**
* No-op close implementation.
*/
@Override @Override
public void close() { public void close() {

View File

@ -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<Class<? extends RPC>> 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();
}

View File

@ -5,6 +5,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/**
* Marks RPC methods that require authenticated sessions or tokens.
*/
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
public @interface ProtectedMethod { public @interface ProtectedMethod {

View File

@ -6,40 +6,60 @@ import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCContext; import ru.kirillius.json.rpc.Annotations.JRPCContext;
import ru.kirillius.json.rpc.Annotations.JRPCMethod; import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.json.rpc.Servlet.CallContext; 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.AuthToken;
import ru.kirillius.pf.sdn.core.Auth.TokenService;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.Date; import java.util.Date;
/**
* JSON-RPC handler exposing authentication, session, and token management endpoints.
*/
public class Auth implements RPC { public class Auth implements RPC {
private final Context context; private final Context context;
/**
* Creates the handler bound to the shared application context.
*/
public Auth(Context context) { public Auth(Context context) {
this.context = context; this.context = context;
} }
/**
* Returns a JSON array containing all stored API tokens.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public JSONArray listTokens() { 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 @ProtectedMethod
@JRPCMethod @JRPCMethod
public void removeToken(@JRPCArgument(name = "token") String token) { 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 @ProtectedMethod
@JRPCMethod @JRPCMethod
public String createAPIToken(@JRPCArgument(name = "description") String description) { public String createAPIToken(@JRPCArgument(name = "description") String description) {
var token = new AuthToken(); var token = new AuthToken();
token.setDescription(description); token.setDescription(description);
context.getTokenStorage().add(token); context.getServiceManager().getService(TokenService.class).add(token);
return token.getToken(); return token.getToken();
} }
/**
* Creates a session-bound token associated with the caller's user agent.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public String createToken(@JRPCContext CallContext call) { public String createToken(@JRPCContext CallContext call) {
@ -47,15 +67,18 @@ public class Auth implements RPC {
if (UA == null) { if (UA == null) {
UA = "Unknown user agent"; UA = "Unknown user agent";
} }
var authManager = context.getAuthManager(); var authManager = context.getServiceManager().getService(AuthManager.class);
var token = authManager.createToken(UA + " " + new Date()); var token = authManager.createToken(UA + " " + new Date());
authManager.setSessionToken(call.getSession(), token); authManager.setSessionToken(call.getSession(), token);
return token.getToken(); return token.getToken();
} }
/**
* Attempts to authenticate the session using a password.
*/
@JRPCMethod @JRPCMethod
public boolean startSessionByPassword(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) { 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)) { if (authManager.validatePassword(password)) {
authManager.setSessionAuthState(call.getSession(), true); authManager.setSessionAuthState(call.getSession(), true);
return true; return true;
@ -63,9 +86,12 @@ public class Auth implements RPC {
return false; return false;
} }
/**
* Attempts to authenticate the session using an existing token.
*/
@JRPCMethod @JRPCMethod
public boolean startSessionByToken(@JRPCArgument(name = "token") String token, @JRPCContext CallContext call) { 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)) { if (authManager.validateToken(token)) {
authManager.setSessionAuthState(call.getSession(), true); authManager.setSessionAuthState(call.getSession(), true);
return true; return true;
@ -73,16 +99,22 @@ public class Auth implements RPC {
return false; return false;
} }
/**
* Reports whether the current session is authenticated.
*/
@JRPCMethod @JRPCMethod
public boolean isAuthenticated(@JRPCContext CallContext call) { public boolean isAuthenticated(@JRPCContext CallContext call) {
var authManager = context.getAuthManager(); var authManager = context.getServiceManager().getService(AuthManager.class);
return authManager.getSessionAuthState(call.getSession()); return authManager.getSessionAuthState(call.getSession());
} }
/**
* Logs out the current session and invalidates its token when present.
*/
@JRPCMethod @JRPCMethod
public void logout(@JRPCContext CallContext call) { public void logout(@JRPCContext CallContext call) {
var authManager = context.getAuthManager(); var authManager = context.getServiceManager().getService(AuthManager.class);
authManager.setSessionAuthState(call.getSession(), false); authManager.setSessionAuthState(call.getSession(), false);
var token = authManager.getSessionToken(call.getSession()); var token = authManager.getSessionToken(call.getSession());
if (token != null) { if (token != null) {

View File

@ -5,30 +5,46 @@ import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument; import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod; import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
/**
* JSON-RPC handler exposing operations on the network aggregation service.
*/
public class NetworkManager implements RPC { public class NetworkManager implements RPC {
private final Context context; private final Context context;
/**
* Creates the handler bound to the application context.
*/
public NetworkManager(Context context) { public NetworkManager(Context context) {
this.context = context; this.context = context;
} }
/**
* Indicates whether the networking service is currently recomputing resources.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public boolean isUpdating() { public boolean isUpdating() {
return context.getNetworkManager().isUpdatingNow(); return context.getServiceManager().getService(NetworkingService.class).isUpdatingNow();
} }
/**
* Triggers an update of networking resources.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public void triggerUpdate(@JRPCArgument(name = "ignoreCache") boolean ignoreCache) { 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 @JRPCMethod
@ProtectedMethod @ProtectedMethod
public JSONObject getOutputResources() { public JSONObject getOutputResources() {
return JSONUtility.serializeStructure(context.getNetworkManager().getOutputResources()); return JSONUtility.serializeStructure(context.getServiceManager().getService(NetworkingService.class).getOutputResources());
} }
} }

View File

@ -4,7 +4,13 @@ import ru.kirillius.pf.sdn.core.Context;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
/**
* Marker interface for JSON-RPC handlers with a helper to instantiate them via the application context.
*/
public interface RPC { public interface RPC {
/**
* Instantiates an RPC handler using its context-aware constructor.
*/
static <T extends RPC> T instantiate(Class<T> type, Context context) { static <T extends RPC> T instantiate(Class<T> type, Context context) {
try { try {
return type.getConstructor(Context.class).newInstance(context); return type.getConstructor(Context.class).newInstance(context);

View File

@ -7,41 +7,63 @@ import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod; import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* JSON-RPC handler for subscription lifecycle and resource selection operations.
*/
public class SubscriptionManager implements RPC { public class SubscriptionManager implements RPC {
private final Context context; private final Context context;
/**
* Creates the handler bound to the application context.
*/
public SubscriptionManager(Context context) { public SubscriptionManager(Context context) {
this.context = context; this.context = context;
} }
/**
* Indicates whether the subscription service is updating.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public boolean isUpdating() { public boolean isUpdating() {
return context.getSubscriptionManager().isUpdatingNow(); return context.getServiceManager().getService(SubscriptionService.class).isUpdatingNow();
} }
/**
* Requests the subscription service to refresh repositories.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public void triggerUpdate() { public void triggerUpdate() {
context.getSubscriptionManager().triggerUpdate(); context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
} }
/**
* Returns the list of resource identifiers currently subscribed to.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public JSONArray getSubscribedResources() { public JSONArray getSubscribedResources() {
return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null); return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null);
} }
/**
* Returns all available resources grouped by repository.
*/
@JRPCMethod @JRPCMethod
@ProtectedMethod @ProtectedMethod
public JSONObject getAvailableResources() { 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 @JRPCMethod
@ProtectedMethod @ProtectedMethod
public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) { public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) {

View File

@ -5,50 +5,95 @@ import org.json.JSONObject;
import ru.kirillius.json.JSONUtility; import ru.kirillius.json.JSONUtility;
import ru.kirillius.json.rpc.Annotations.JRPCArgument; import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCMethod; 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.Component;
import ru.kirillius.pf.sdn.core.ComponentHandlerService;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.web.ProtectedMethod; import ru.kirillius.pf.sdn.web.ProtectedMethod;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* JSON-RPC handler for system control operations and component configuration management.
*/
public class System implements RPC { public class System implements RPC {
private final Context context; private final Context context;
/**
* Creates the handler bound to the application context.
*/
public System(Context context) { public System(Context context) {
this.context = context; this.context = context;
} }
/**
* Requests an application restart.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public void restart() { public void restart() {
context.triggerRestart(); context.requestExit(true);
} }
/**
* Requests an application shutdown.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public void shutdown() { public void shutdown() {
context.triggerShutdown(); context.requestExit(false);
} }
/**
* Returns the current configuration as JSON.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public JSONObject getConfig() { public JSONObject getConfig() {
return JSONUtility.serializeStructure(context.getConfig()); return JSONUtility.serializeStructure(context.getConfig());
} }
/**
* Indicates whether the configuration has unsaved changes.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public boolean isConfigChanged() { public boolean isConfigChanged() {
return context.getConfig().isModified(); return context.getConfig().isModified();
} }
/**
* Retrieves the latest version available for update.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public boolean hasUpdates() { public String getVersionForUpdate() {
return true; 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 @ProtectedMethod
@JRPCMethod @JRPCMethod
public JSONArray getEnabledComponents() { 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") @SuppressWarnings("unchecked")
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
@ -67,16 +115,22 @@ public class System implements RPC {
throw new RuntimeException("Unable to load Component class " + s, e); throw new RuntimeException("Unable to load Component class " + s, e);
} }
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
context.initComponents(); context.getServiceManager().getService(ComponentHandlerService.class).syncComponentsWithConfig();
} }
/**
* Returns the list of component classes available to the launcher.
*/
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
public JSONArray getAvailableComponents() { 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"}) @SuppressWarnings({"unchecked", "rawtypes"})
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
@ -86,6 +140,9 @@ public class System implements RPC {
} }
/**
Updates the configuration of the specified component and reloads it.
*/
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@ProtectedMethod @ProtectedMethod
@JRPCMethod @JRPCMethod
@ -93,8 +150,7 @@ public class System implements RPC {
var cls = (Class) Class.forName(componentName); var cls = (Class) Class.forName(componentName);
var configClass = Component.getConfigClass(cls); var configClass = Component.getConfigClass(cls);
context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass)); context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass));
context.reloadComponents(cls); context.getServiceManager().getService(ComponentHandlerService.class).reloadComponents(cls);
Object config1 = context.getConfig().getComponentsConfig().getConfig(cls); context.getConfig().getComponentsConfig().getConfig(cls);
return;
} }
} }

View File

@ -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<Class<? extends RPC>> 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();
}

View File

@ -6,10 +6,11 @@
<parent> <parent>
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId> <artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version> <version>1.0.1.5</version>
</parent> </parent>
<artifactId>core</artifactId>
<artifactId>pf-sdn.core</artifactId>
<dependencies> <dependencies>
<dependency> <dependency>

View File

@ -1,9 +1,15 @@
package ru.kirillius.pf.sdn.core; package ru.kirillius.pf.sdn.core;
/**
* Convenience base implementation that wires component configuration and context dependencies.
*/
public abstract class AbstractComponent<CT> implements Component<CT> { public abstract class AbstractComponent<CT> implements Component<CT> {
protected final CT config; protected final CT config;
protected final Context context; protected final Context context;
/**
* Loads the component configuration from the shared storage and stores the context reference.
*/
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
public AbstractComponent(Context context) { public AbstractComponent(Context context) {
config = (CT) context.getConfig().getComponentsConfig().getConfig((Class) getClass()); config = (CT) context.getConfig().getComponentsConfig().getConfig((Class) getClass());

View File

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

View File

@ -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("<a\\s+[^>]*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();
}
}

View File

@ -1,6 +1,7 @@
package ru.kirillius.pf.sdn.core.Auth; package ru.kirillius.pf.sdn.core.Auth;
import jakarta.servlet.http.HttpSession; 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.Context;
import ru.kirillius.pf.sdn.core.Util.HashUtil; import ru.kirillius.pf.sdn.core.Util.HashUtil;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
@ -9,21 +10,27 @@ import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; 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_AUTH_KEY = "auth";
public final static String SESSION_TOKEN = "token"; public final static String SESSION_TOKEN = "token";
private final static String CTX = AuthManager.class.getSimpleName(); 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) { public boolean validatePassword(String pass) {
var config = context.getConfig(); var config = context.getConfig();
return HashUtil.hash(pass, config.getPasswordSalt()).equals(config.getPasswordHash()); return HashUtil.hash(pass, config.getPasswordSalt()).equals(config.getPasswordHash());
} }
/**
* Updates the stored password hash and generates a new salt.
*/
public void updatePassword(String pass) { public void updatePassword(String pass) {
var config = context.getConfig(); var config = context.getConfig();
config.setPasswordSalt(UUID.randomUUID().toString()); 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) { public AuthToken createToken(String description) {
var token = new AuthToken(); var token = new AuthToken();
token.setDescription(description); token.setDescription(description);
var tokenStorage = context.getTokenStorage(); var tokenStorage = context.getServiceManager().getService(TokenService.class);
tokenStorage.add(token); tokenStorage.add(token);
try { try {
tokenStorage.store(); tokenStorage.store();
@ -45,28 +55,46 @@ public class AuthManager {
return token; return token;
} }
/**
* Checks whether the storage contains the specified token.
*/
public boolean validateToken(AuthToken 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) { public void setSessionAuthState(HttpSession session, boolean state) {
session.setAttribute(SESSION_AUTH_KEY, state); session.setAttribute(SESSION_AUTH_KEY, state);
} }
/**
* Associates the given token with the HTTP session.
*/
public void setSessionToken(HttpSession session, AuthToken token) { public void setSessionToken(HttpSession session, AuthToken token) {
session.setAttribute(SESSION_TOKEN, token); session.setAttribute(SESSION_TOKEN, token);
} }
/**
* Retrieves the token associated with the HTTP session.
*/
public AuthToken getSessionToken(HttpSession session) { public AuthToken getSessionToken(HttpSession session) {
return (AuthToken) session.getAttribute(SESSION_TOKEN); return (AuthToken) session.getAttribute(SESSION_TOKEN);
} }
/**
* Returns whether the HTTP session is marked as authenticated.
*/
public boolean getSessionAuthState(HttpSession session) { public boolean getSessionAuthState(HttpSession session) {
return Objects.equals(session.getAttribute(SESSION_AUTH_KEY), Boolean.TRUE); return Objects.equals(session.getAttribute(SESSION_AUTH_KEY), Boolean.TRUE);
} }
/**
* Removes the provided token from storage if present.
*/
public void invalidateToken(AuthToken token) { public void invalidateToken(AuthToken token) {
var tokenStorage = context.getTokenStorage(); var tokenStorage = context.getServiceManager().getService(TokenService.class);
if (tokenStorage.contains(token)) { if (tokenStorage.contains(token)) {
tokenStorage.remove(token); tokenStorage.remove(token);
try { try {
@ -78,11 +106,32 @@ public class AuthManager {
} }
/**
* Checks token validity using its raw string value.
*/
public boolean validateToken(String token) { public boolean validateToken(String token) {
return validateToken(new AuthToken(token)); return validateToken(new AuthToken(token));
} }
/**
* Invalidates a token identified by its string representation.
*/
public void invalidateToken(String token) { public void invalidateToken(String token) {
invalidateToken(new AuthToken(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
}
} }

View File

@ -9,9 +9,15 @@ import ru.kirillius.json.JSONSerializable;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
/**
* Represents a persistent authentication token with metadata for API access.
*/
@NoArgsConstructor @NoArgsConstructor
@JSONSerializable @JSONSerializable
public class AuthToken { public class AuthToken {
/**
* Creates a token wrapper for the specified raw token string.
*/
public AuthToken(String token) { public AuthToken(String token) {
this.token = token; this.token = token;
} }
@ -21,6 +27,9 @@ public class AuthToken {
@JSONProperty @JSONProperty
private String description = "untitled"; private String description = "untitled";
/**
* Tokens are equal when their token strings match.
*/
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
@ -28,6 +37,9 @@ public class AuthToken {
return Objects.equals(token, authToken.token); return Objects.equals(token, authToken.token);
} }
/**
* Produces a hash code derived from the token string.
*/
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(token); return Objects.hashCode(token);

View File

@ -3,6 +3,7 @@ package ru.kirillius.pf.sdn.core.Auth;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONTokener; import org.json.JSONTokener;
import ru.kirillius.json.JSONUtility; 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.Context;
import java.io.*; import java.io.*;
@ -10,10 +11,17 @@ import java.util.*;
import java.util.stream.Collectors; 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; 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"); file = new File(context.getConfig().getCacheDirectory(), "tokens.json");
if (file.exists()) { if (file.exists()) {
try (var stream = new FileInputStream(file)) { try (var stream = new FileInputStream(file)) {
@ -29,30 +37,45 @@ public class TokenStorage {
private final List<AuthToken> tokens; private final List<AuthToken> tokens;
/**
* Returns an immutable view over the stored tokens.
*/
public Collection<AuthToken> getTokens() { public Collection<AuthToken> getTokens() {
synchronized (tokens) { synchronized (tokens) {
return Collections.unmodifiableList(tokens); return Collections.unmodifiableList(tokens);
} }
} }
/**
* Adds the provided tokens to the storage.
*/
public void add(AuthToken... what) { public void add(AuthToken... what) {
synchronized (tokens) { synchronized (tokens) {
Collections.addAll(tokens, what); Collections.addAll(tokens, what);
} }
} }
/**
* Checks whether the specified token is present in storage.
*/
public boolean contains(AuthToken what) { public boolean contains(AuthToken what) {
synchronized (tokens) { synchronized (tokens) {
return tokens.contains(what); return tokens.contains(what);
} }
} }
/**
* Removes the provided tokens from storage.
*/
public void remove(AuthToken... what) { public void remove(AuthToken... what) {
synchronized (tokens) { synchronized (tokens) {
Arrays.stream(what).forEach(tokens::remove); Arrays.stream(what).forEach(tokens::remove);
} }
} }
/**
* Writes the in-memory token list to disk.
*/
public synchronized void store() throws IOException { public synchronized void store() throws IOException {
try (var fileInputStream = new FileOutputStream(file)) { try (var fileInputStream = new FileOutputStream(file)) {
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) { 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
}
} }

View File

@ -5,7 +5,13 @@ import lombok.SneakyThrows;
import java.io.Closeable; import java.io.Closeable;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
/**
* Defines the contract for pluggable application components with type-safe configuration access.
*/
public interface Component<CT> extends Closeable { public interface Component<CT> extends Closeable {
/**
* Resolves the configuration type parameter for a component implementation.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static <T> Class<T> getConfigClass(Class<? extends Component<T>> pluginClass) { static <T> Class<T> getConfigClass(Class<? extends Component<T>> pluginClass) {
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass(); var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
@ -13,6 +19,9 @@ public interface Component<CT> extends Closeable {
return (Class<T>) typeArguments[0]; return (Class<T>) typeArguments[0];
} }
/**
* Instantiates a component using its context-aware constructor.
*/
@SneakyThrows @SneakyThrows
static <T extends Component<?>> T loadPlugin(Class<T> pluginClass, Context context) { static <T extends Component<?>> T loadPlugin(Class<T> pluginClass, Context context) {
return pluginClass.getConstructor(Context.class).newInstance(context); return pluginClass.getConstructor(Context.class).newInstance(context);

View File

@ -11,12 +11,21 @@ import ru.kirillius.json.SerializationException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Stores per-component configuration instances and serializes them as JSON.
*/
@NoArgsConstructor @NoArgsConstructor
@JSONSerializable(ComponentConfigStorage.Serializer.class) @JSONSerializable(ComponentConfigStorage.Serializer.class)
public class ComponentConfigStorage { public class ComponentConfigStorage {
/**
* JSON serializer that materializes component configuration maps.
*/
public final static class Serializer implements JSONSerializer<ComponentConfigStorage> { public final static class Serializer implements JSONSerializer<ComponentConfigStorage> {
/**
* Converts the stored configurations into a JSON object keyed by class name.
*/
@Override @Override
public Object serialize(ComponentConfigStorage componentConfigStorage) throws SerializationException { public Object serialize(ComponentConfigStorage componentConfigStorage) throws SerializationException {
var json = new JSONObject(); var json = new JSONObject();
@ -26,6 +35,9 @@ public class ComponentConfigStorage {
return json; return json;
} }
/**
* Restores component configuration instances from the provided JSON payload.
*/
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@Override @Override
public ComponentConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException { public ComponentConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
@ -48,6 +60,9 @@ public class ComponentConfigStorage {
private final Map<Class<? extends Component<?>>, Object> configs = new ConcurrentHashMap<>(); private final Map<Class<? extends Component<?>>, Object> configs = new ConcurrentHashMap<>();
/**
* Returns (and instantiates if necessary) the configuration object for the given component class.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@SneakyThrows @SneakyThrows
public <CT> CT getConfig(Class<? extends Component<CT>> componentClass) { public <CT> CT getConfig(Class<? extends Component<CT>> componentClass) {
@ -59,6 +74,9 @@ public class ComponentConfigStorage {
return (CT) configs.get(componentClass); return (CT) configs.get(componentClass);
} }
/**
* Stores the provided configuration instance for the specified component class.
*/
@SneakyThrows @SneakyThrows
public <CT> void setConfig(Class<? extends Component<CT>> componentClass, CT config) { public <CT> void setConfig(Class<? extends Component<CT>> componentClass, CT config) {
var configClass = Component.getConfigClass(componentClass); var configClass = Component.getConfigClass(componentClass);

View File

@ -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<Component<?>> 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);
});
}
}

View File

@ -18,6 +18,9 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Represents persisted application settings and component configuration bundles.
*/
@NoArgsConstructor @NoArgsConstructor
@JSONSerializable @JSONSerializable
public class Config { public class Config {
@ -104,6 +107,9 @@ public class Config {
@JSONProperty @JSONProperty
private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle(); 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 { public static void store(Config config, File file) throws IOException {
try (var fileInputStream = new FileOutputStream(file)) { try (var fileInputStream = new FileOutputStream(file)) {
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) { 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) { public static JSONObject serialize(Config config) {
return JSONUtility.serializeStructure(config); return JSONUtility.serializeStructure(config);
} }
/**
* Reconstructs a configuration instance from JSON data.
*/
public static Config deserialize(JSONObject object) { public static Config deserialize(JSONObject object) {
return JSONUtility.deserializeStructure(object, Config.class); 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 { public static Config load(File file) throws IOException {
try (var stream = new FileInputStream(file)) { try (var stream = new FileInputStream(file)) {
var json = new JSONObject(new JSONTokener(stream)); var json = new JSONObject(new JSONTokener(stream));
@ -133,6 +148,9 @@ public class Config {
private JSONObject initialJSON = new JSONObject(); private JSONObject initialJSON = new JSONObject();
/**
* Indicates whether the in-memory configuration diverges from the initially loaded snapshot.
*/
public boolean isModified() { public boolean isModified() {
return !initialJSON.equals(serialize(this)); return !initialJSON.equals(serialize(this));
} }

View File

@ -1,42 +1,29 @@
package ru.kirillius.pf.sdn.core; package ru.kirillius.pf.sdn.core;
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; /**
import ru.kirillius.pf.sdn.core.Auth.AuthManager; * Provides access to core services and configuration for application components.
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;
public interface Context { 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(); Config getConfig();
/**
AuthManager getAuthManager(); * Exposes event hooks for subscribers interested in runtime changes.
*/
ASInfoService getASInfoService();
NetworkManager getNetworkManager();
ContextEventsHandler getEventsHandler(); ContextEventsHandler getEventsHandler();
/**
SubscriptionManager getSubscriptionManager(); * Signals the application to exit, optionally requesting a restart.
*
UpdateManager getUpdateManager(); * @param shouldRestart {@code true} to exit with restart intent, {@code false} to shut down.
*/
Component<?> getComponentInstance(Class<? extends Component<?>> pluginClass); void requestExit(boolean shouldRestart);
void triggerRestart();
void triggerShutdown();
TokenStorage getTokenStorage();
void initComponents();
void reloadComponents(Class<? extends Component<?>>... classes);
JSONRPCServlet getRPC();
Collection<Class<? extends Component<?>>> getComponentClasses();
} }

View File

@ -6,11 +6,23 @@ import ru.kirillius.java.utils.events.EventHandler;
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
/**
* Aggregates event handlers used to notify the application about runtime state changes.
*/
public final class ContextEventsHandler { public final class ContextEventsHandler {
/**
* Event fired when network resource bundles are recalculated.
*/
@Getter @Getter
private final EventHandler<NetworkResourceBundle> networkManagerUpdateEvent = new ConcurrentEventHandler<>(); private final EventHandler<NetworkResourceBundle> networkManagerUpdateEvent = new ConcurrentEventHandler<>();
/**
* Event fired when subscription data has been refreshed.
*/
@Getter @Getter
private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>(); private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
/**
* Event fired after the RPC servlet is initialised and ready.
*/
@Getter @Getter
private final EventHandler<JSONRPCServlet> RPCInitEvent = new ConcurrentEventHandler<>(); private final EventHandler<JSONRPCServlet> RPCInitEvent = new ConcurrentEventHandler<>();
} }

View File

@ -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<Class<? extends Component<?>>> availableComponentClasses;
}

View File

@ -5,9 +5,18 @@ import ru.kirillius.pf.sdn.core.Context;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.List; import java.util.List;
/**
* Abstraction for retrieving prefixes announced by a specific autonomous system.
*/
public interface ASInfoProvider { public interface ASInfoProvider {
/**
* Returns IPv4 subnets originated by the provided autonomous system number.
*/
List<IPv4Subnet> getPrefixes(int as); List<IPv4Subnet> getPrefixes(int as);
/**
* Instantiates a provider class using the context-aware constructor.
*/
static ASInfoProvider instantiate(Class<? extends ASInfoProvider> providerClass, Context context) { static ASInfoProvider instantiate(Class<? extends ASInfoProvider> providerClass, Context context) {
try { try {
var constructor = providerClass.getConstructor(Context.class); var constructor = providerClass.getConstructor(Context.class);

View File

@ -1,30 +1,46 @@
package ru.kirillius.pf.sdn.core.Networking; package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; 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.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; 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(); private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Getter @Getter
@Setter @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<List<IPv4Subnet>> getPrefixes(int as) { public Future<List<IPv4Subnet>> getPrefixes(int as) {
return executor.submit(() -> provider.getPrefixes(as)); return executor.submit(() -> provider.getPrefixes(as));
} }
/**
* Shuts down the background executor.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
executor.shutdown(); executor.shutdown();

View File

@ -9,16 +9,28 @@ import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/**
* Immutable representation of an IPv4 subnet with utility helpers for serialization.
*/
@JSONSerializable(IPv4Subnet.Serializer.class) @JSONSerializable(IPv4Subnet.Serializer.class)
public class IPv4Subnet { public class IPv4Subnet {
/**
* Serializes subnets to CIDR notation strings for JSON persistence.
*/
public final static class Serializer implements JSONSerializer<IPv4Subnet> { public final static class Serializer implements JSONSerializer<IPv4Subnet> {
/**
* Writes the subnet as a string using CIDR notation.
*/
@Override @Override
public Object serialize(IPv4Subnet subnet) throws SerializationException { public Object serialize(IPv4Subnet subnet) throws SerializationException {
return subnet.toString(); return subnet.toString();
} }
/**
* Parses a CIDR notation string and returns the corresponding subnet instance.
*/
@Override @Override
public IPv4Subnet deserialize(Object o, Class<?> aClass) throws SerializationException { public IPv4Subnet deserialize(Object o, Class<?> aClass) throws SerializationException {
return new IPv4Subnet((String) o); return new IPv4Subnet((String) o);
@ -31,6 +43,9 @@ public class IPv4Subnet {
private final int prefixLength; private final int prefixLength;
/**
* Compares two subnets for equality based on address and prefix length.
*/
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
@ -38,11 +53,17 @@ public class IPv4Subnet {
return longAddress == that.longAddress && prefixLength == that.prefixLength; return longAddress == that.longAddress && prefixLength == that.prefixLength;
} }
/**
* Computes a hash code using address and prefix length.
*/
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(longAddress, prefixLength); return Objects.hash(longAddress, prefixLength);
} }
/**
* Parses a subnet from textual CIDR notation.
*/
public IPv4Subnet(String subnet) { public IPv4Subnet(String subnet) {
var split = subnet.split(Pattern.quote("/")); var split = subnet.split(Pattern.quote("/"));
if (split.length != 2) { if (split.length != 2) {
@ -55,11 +76,17 @@ public class IPv4Subnet {
prefixLength = prefix; prefixLength = prefix;
} }
/**
* Creates a subnet from a numeric address and prefix length.
*/
public IPv4Subnet(long longAddress, int prefixLength) { public IPv4Subnet(long longAddress, int prefixLength) {
this.longAddress = longAddress; this.longAddress = longAddress;
this.prefixLength = prefixLength; this.prefixLength = prefixLength;
} }
/**
* Creates a subnet from a dotted address string and prefix length.
*/
public IPv4Subnet(String address, int prefixLength) { public IPv4Subnet(String address, int prefixLength) {
IPv4Util.validatePrefix(prefixLength); IPv4Util.validatePrefix(prefixLength);
@ -67,19 +94,31 @@ public class IPv4Subnet {
this.prefixLength = prefixLength; this.prefixLength = prefixLength;
} }
/**
* Calculates the number of addresses within the subnet.
*/
public long count() { public long count() {
return IPv4Util.calculateCountForPrefixLength(prefixLength); return IPv4Util.calculateCountForPrefixLength(prefixLength);
} }
/**
* Returns the dotted-decimal representation of the subnet's network address.
*/
public String getAddress() { public String getAddress() {
return IPv4Util.longToIpAddress(longAddress); return IPv4Util.longToIpAddress(longAddress);
} }
/**
* Formats the subnet using CIDR notation.
*/
@Override @Override
public String toString() { public String toString() {
return getAddress() + '/' + prefixLength; return getAddress() + '/' + prefixLength;
} }
/**
* Determines whether this subnet overlaps with another.
*/
public boolean overlaps(IPv4Subnet subnet) { public boolean overlaps(IPv4Subnet subnet) {
var minPrefixLength = Math.min(prefixLength, subnet.prefixLength); var minPrefixLength = Math.min(prefixLength, subnet.prefixLength);
var commonMask = IPv4Util.calculateMask(minPrefixLength); var commonMask = IPv4Util.calculateMask(minPrefixLength);

View File

@ -8,6 +8,9 @@ import ru.kirillius.json.JSONSerializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* Container for grouped network identifiers used to configure filtering and subscriptions.
*/
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
@ -30,12 +33,18 @@ public class NetworkResourceBundle {
@JSONArrayProperty(type = String.class) @JSONArrayProperty(type = String.class)
private List<String> domains = new ArrayList<>(); private List<String> domains = new ArrayList<>();
/**
* Clears all stored network identifiers.
*/
public void clear() { public void clear() {
ASN.clear(); ASN.clear();
subnets.clear(); subnets.clear();
domains.clear(); domains.clear();
} }
/**
* Adds all resources from the provided bundle into this bundle.
*/
public void add(NetworkResourceBundle networkResourceBundle) { public void add(NetworkResourceBundle networkResourceBundle) {
ASN.addAll(networkResourceBundle.getASN()); ASN.addAll(networkResourceBundle.getASN());
subnets.addAll(networkResourceBundle.getSubnets()); subnets.addAll(networkResourceBundle.getSubnets());

View File

@ -3,7 +3,9 @@ package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter; import lombok.Getter;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; import org.json.JSONTokener;
import ru.kirillius.java.utils.events.EventListener;
import ru.kirillius.json.JSONUtility; 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.Context;
import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
@ -13,14 +15,30 @@ import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference; 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 ExecutorService executor = Executors.newSingleThreadExecutor();
private final static String CTX = NetworkManager.class.getSimpleName(); private final static String CTX = NetworkingService.class.getSimpleName();
private final Context context;
private final File cacheFile;
public NetworkManager(Context context) { private final File cacheFile;
this.context = context; private final EventListener<NetworkResourceBundle> 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"); cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
if (cacheFile.exists() && context.getConfig().isCachingAS()) { if (cacheFile.exists() && context.getConfig().isCachingAS()) {
SystemLogger.message("Loading as cache file", CTX); SystemLogger.message("Loading as cache file", CTX);
@ -42,6 +60,9 @@ public class NetworkManager implements Closeable {
@Getter @Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle(); private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
/**
* Indicates whether an update job is currently executing.
*/
public boolean isUpdatingNow() { public boolean isUpdatingNow() {
var future = updateProcess.get(); var future = updateProcess.get();
return future != null && !future.isDone() && !future.isCancelled(); return future != null && !future.isDone() && !future.isCancelled();
@ -49,6 +70,9 @@ public class NetworkManager implements Closeable {
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>(); private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
/**
* Schedules an update of network resources, optionally ignoring cached prefixes.
*/
public void triggerUpdate(boolean ignoreCache) { public void triggerUpdate(boolean ignoreCache) {
if (isUpdatingNow()) { if (isUpdatingNow()) {
return; 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<Integer> systems) { private void fetchPrefixes(List<Integer> systems) {
var service = context.getASInfoService(); var service = context.getServiceManager().getService(BGPInfoService.class);
systems.forEach(as -> { systems.forEach(as -> {
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX); SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
var future = service.getPrefixes(as); var future = service.getPrefixes(as);
@ -149,8 +176,12 @@ public class NetworkManager implements Closeable {
} }
/**
* Removes event subscriptions and shuts down the executor.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
context.getEventsHandler().getSubscriptionsUpdateEvent().remove(subscription);
executor.shutdown(); executor.shutdown();
} }
} }

View File

@ -1,36 +1,58 @@
package ru.kirillius.pf.sdn.core; package ru.kirillius.pf.sdn.core;
import lombok.SneakyThrows; 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.pf.sdn.core.Util.Wait;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; 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; 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()); updateThread = new Thread(new ThreadWorker());
} }
/**
* Starts the update thread; can only be invoked once.
*/
public void start() { public void start() {
if (updateThread.isAlive()) {
throw new IllegalStateException("Started already");
}
updateThread.start(); updateThread.start();
} }
private final Context context;
/**
* Interrupts the update thread and stops scheduling tasks.
*/
@Override @Override
public void close() throws IOException { public void close() throws IOException {
updateThread.interrupt(); 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 { private class ThreadWorker implements Runnable {
/**
* Performs periodic subscription and network updates based on configuration intervals.
*/
@SneakyThrows @SneakyThrows
@Override @Override
public void run() { public void run() {
@ -43,7 +65,7 @@ public class UpdateManager implements Closeable {
if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval() * 60L) == 0) { if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval() * 60L) == 0) {
SystemLogger.message("Updating subscriptions", CTX); SystemLogger.message("Updating subscriptions", CTX);
var subscriptionManager = context.getSubscriptionManager(); var subscriptionManager = context.getServiceManager().getService(SubscriptionService.class);
subscriptionManager.triggerUpdate(); subscriptionManager.triggerUpdate();
Wait.until(subscriptionManager::isUpdatingNow); Wait.until(subscriptionManager::isUpdatingNow);
Wait.when(subscriptionManager::isUpdatingNow); Wait.when(subscriptionManager::isUpdatingNow);
@ -51,7 +73,7 @@ public class UpdateManager implements Closeable {
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) { if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
SystemLogger.message("Updating cached AS", CTX); SystemLogger.message("Updating cached AS", CTX);
var networkManager = context.getNetworkManager(); var networkManager = context.getServiceManager().getService(NetworkingService.class);
networkManager.triggerUpdate(true); networkManager.triggerUpdate(true);
} }
} }

View File

@ -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<Class<? extends AppService>, AppService> services = new LinkedHashMap<>();
/**
* Creates a manager and eagerly instantiates the provided service classes.
*/
public ServiceManager(Context context, Collection<Class<? extends AppService>> 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 <S> service subtype.
* @return instance if available, otherwise {@code null}.
*/
public <S extends AppService> S getService(Class<S> 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);
}
}
}
}

View File

@ -7,6 +7,9 @@ import lombok.Setter;
import ru.kirillius.json.JSONProperty; import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable; import ru.kirillius.json.JSONSerializable;
/**
* Describes a subscription source with its name, implementation type, and origin descriptor.
*/
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@JSONSerializable @JSONSerializable

View File

@ -6,6 +6,9 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.Map; import java.util.Map;
/**
* Loads network resource bundles from a repository described by {@link RepositoryConfig}.
*/
public interface SubscriptionProvider { public interface SubscriptionProvider {
Map<String, NetworkResourceBundle> getResources(RepositoryConfig config); Map<String, NetworkResourceBundle> getResources(RepositoryConfig config);

View File

@ -1,11 +1,11 @@
package ru.kirillius.pf.sdn.core.Subscription; package ru.kirillius.pf.sdn.core.Subscription;
import lombok.Getter; import lombok.Getter;
import ru.kirillius.pf.sdn.core.AppService;
import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
import ru.kirillius.utils.logging.SystemLogger; import ru.kirillius.utils.logging.SystemLogger;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -15,22 +15,30 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference; 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 ExecutorService executor = Executors.newSingleThreadExecutor();
private final Context context;
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>(); private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
public SubscriptionManager(Context context) {
this.context = context;
}
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>(); private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
@Getter @Getter
private final NetworkResourceBundle outputResources = new NetworkResourceBundle(); private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
public SubscriptionService(Context context) {
super(context);
}
/**
* Indicates whether a subscription update job is currently running.
*/
public boolean isUpdatingNow() { public boolean isUpdatingNow() {
var future = updateProcess.get(); var future = updateProcess.get();
return future != null && !future.isDone() && !future.isCancelled(); return future != null && !future.isDone() && !future.isCancelled();
@ -40,6 +48,9 @@ public class SubscriptionManager implements Closeable {
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>(); private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
/**
* Starts a background task that refreshes subscription repositories and aggregates resources.
*/
public synchronized void triggerUpdate() { public synchronized void triggerUpdate() {
if (isUpdatingNow()) { if (isUpdatingNow()) {
return; 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 @Override
public void close() throws IOException { public void close() throws IOException {
executor.shutdown(); executor.shutdown();

View File

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

View File

@ -4,10 +4,16 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
/**
* Provides hashing helpers for password management and compatibility utilities.
*/
public final class HashUtil { public final class HashUtil {
private HashUtil() { private HashUtil() {
} }
/**
* Computes the MD5 digest for the supplied input string.
*/
public static String md5(String input) { public static String md5(String input) {
try { try {
var md = MessageDigest.getInstance("MD5"); 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) { public static String hash(String data, String salt) {
String generatedPassword = null; String generatedPassword = null;
MessageDigest md = null; MessageDigest md = null;

View File

@ -12,6 +12,9 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Helper methods for IPv4 address manipulation and subnet aggregation logic.
*/
public class IPv4Util { public class IPv4Util {
private 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]?)$"); 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) { public static void validateAddress(String address) {
if (!pattern.matcher(address).matches()) { if (!pattern.matcher(address).matches()) {
throw new IllegalArgumentException("Invalid IPv4 address: " + address); throw new IllegalArgumentException("Invalid IPv4 address: " + address);
} }
} }
/**
* Ensures the prefix length is within the allowed range 0..32.
*/
public static void validatePrefix(int prefix) { public static void validatePrefix(int prefix) {
if (prefix < 0 || prefix > 32) { if (prefix < 0 || prefix > 32) {
throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix); throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
@ -32,6 +41,9 @@ public class IPv4Util {
} }
/**
* Converts a dotted IPv4 address into its numeric representation.
*/
@SneakyThrows @SneakyThrows
public static long ipAddressToLong(String address) { public static long ipAddressToLong(String address) {
validateAddress(address); validateAddress(address);
@ -44,12 +56,18 @@ public class IPv4Util {
return result; return result;
} }
/**
* Returns a bit mask representing the provided prefix length.
*/
public static long calculateMask(int prefixLength) { public static long calculateMask(int prefixLength) {
validatePrefix(prefixLength); validatePrefix(prefixLength);
return 0xFFFFFFFFL << (32 - prefixLength); return 0xFFFFFFFFL << (32 - prefixLength);
} }
/**
* Converts a numeric IPv4 address into dotted notation.
*/
public static String longToIpAddress(long ipLong) { public static String longToIpAddress(long ipLong) {
if (ipLong < 0 || ipLong > 0xFFFFFFFFL) { if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
throw new IllegalArgumentException("Address number should be in range 0 - 4294967295"); 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); 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) { public static String maskToString(long maskLong) {
return String.format("%d.%d.%d.%d", return String.format("%d.%d.%d.%d",
(maskLong >> 24) & 0xff, (maskLong >> 24) & 0xff,
@ -65,12 +86,24 @@ public class IPv4Util {
maskLong & 0xff); maskLong & 0xff);
} }
/**
* Result contract for subnet summarisation operations.
*/
public interface SummarisationResult { public interface SummarisationResult {
/**
* Returns the resulting set of subnets after summarisation.
*/
List<IPv4Subnet> getResult(); List<IPv4Subnet> getResult();
/**
* Returns the original subnets that were merged during summarisation.
*/
Set<IPv4Subnet> getMergedSubnets(); Set<IPv4Subnet> getMergedSubnets();
} }
/**
* Performs subnet merging strategies based on overlap and utilisation heuristics.
*/
private static class SubnetSummaryUtility implements SummarisationResult { private static class SubnetSummaryUtility implements SummarisationResult {
@Getter @Getter
@ -80,6 +113,9 @@ public class IPv4Util {
private final Set<IPv4Subnet> mergedSubnets = new HashSet<>(); private final Set<IPv4Subnet> mergedSubnets = new HashSet<>();
/**
* Builds the summarisation utility with the supplied subnets and usage threshold.
*/
public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) { public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) {
source = subnets; source = subnets;
result = new ArrayList<>(subnets); result = new ArrayList<>(subnets);
@ -90,6 +126,9 @@ public class IPv4Util {
result.sort(Comparator.comparing(IPv4Subnet::getLongAddress)); result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
} }
/**
* Removes redundant subnets wholly covered by other subnets.
*/
private void summaryOverlapped() { private void summaryOverlapped() {
if (result.size() < 2) { if (result.size() < 2) {
return; return;
@ -113,6 +152,9 @@ public class IPv4Util {
overlapped.forEach(result::remove); overlapped.forEach(result::remove);
} }
/**
* Attempts to merge adjacent subnets into larger ones while preserving coverage.
*/
private void mergeNeighbours() { private void mergeNeighbours() {
if (result.size() < 2) { if (result.size() < 2) {
return; return;
@ -155,22 +197,37 @@ public class IPv4Util {
} }
} }
/**
* Returns the smallest prefix length currently present in the result set.
*/
private int findMinPrefixLength() { private int findMinPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt(); return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
} }
/**
* Returns the largest prefix length currently present in the result set.
*/
private int findMaxPrefixLength() { private int findMaxPrefixLength() {
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt(); return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
} }
/**
* Returns the smallest network address found in the result set.
*/
private long findMinAddress() { private long findMinAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong(); return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
} }
/**
* Returns the largest network address found in the result set.
*/
private long findMaxAddress() { private long findMaxAddress() {
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong(); return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
} }
/**
* Produces candidate subnets capable of covering the current set at the given prefix length.
*/
private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) { private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) {
//создаём подсети-кандидаты, которые покроют наш список //создаём подсети-кандидаты, которые покроют наш список
var maxAddress = findMaxAddress(); var maxAddress = findMaxAddress();
@ -201,6 +258,9 @@ public class IPv4Util {
return candidates; return candidates;
} }
/**
* Tests whether the candidate subnet meets utilisation requirements and performs the merge.
*/
private boolean testCandidate(IPv4Subnet candidate, int usePercentage) { private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
if (result.contains(candidate)) { if (result.contains(candidate)) {
return false; return false;
@ -234,6 +294,9 @@ public class IPv4Util {
return false; return false;
} }
/**
* Iteratively merges subnets that satisfy the utilisation threshold.
*/
private void summaryWithUsage(int usePercentage) { private void summaryWithUsage(int usePercentage) {
if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) { if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
return; return;
@ -266,14 +329,23 @@ public class IPv4Util {
} }
} }
/**
* Summaries the provided subnets using merging heuristics and utilisation thresholds.
*/
public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) { public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
return new SubnetSummaryUtility(subnets, 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) { private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), 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) { public static long calculateCountForPrefixLength(long prefixLength) {
return 1L << (32L - prefixLength); return 1L << (32L - prefixLength);
} }

View File

@ -3,11 +3,17 @@ package ru.kirillius.pf.sdn.core.Util;
import java.time.Duration; import java.time.Duration;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* Provides blocking wait utilities for polling boolean conditions with interruption support.
*/
public final class Wait { public final class Wait {
private Wait() { private Wait() {
throw new AssertionError(); throw new AssertionError();
} }
/**
* Blocks until the supplied condition becomes true or the thread is interrupted.
*/
public static void until(Supplier<Boolean> condition) throws InterruptedException { public static void until(Supplier<Boolean> condition) throws InterruptedException {
if (condition == null) { if (condition == null) {
return; return;
@ -18,6 +24,9 @@ public final class Wait {
} }
} }
/**
* Blocks while the supplied condition remains true or until interrupted.
*/
public static void when(Supplier<Boolean> condition) throws InterruptedException { public static void when(Supplier<Boolean> condition) throws InterruptedException {
if (condition == null) { if (condition == null) {
return; return;

72
launcher/pom.xml Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId>
<version>1.0.1.5</version>
</parent>
<artifactId>launcher</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>clean-root-target</id>
<phase>clean</phase>
<inherited>false</inherited>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete dir="${project.build.directory}"/>
</target>
</configuration>
</execution>
<execution>
<id>post-build-assembly</id>
<phase>verify</phase> <!-- выполнится после сборки всех модулей -->
<inherited>false</inherited>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<!-- 1. Создать папку target (для root) -->
<mkdir dir="${project.build.directory}"/>
<!-- 2. Копировать app/target/app-$VERSION-shaded.jar -> target/$VERSION.pfapp -->
<copy file="${basedir}/../app/target/pf-sdn.app-${project.version}-shaded.jar"
tofile="${project.build.directory}/${project.version}.pfapp"
overwrite="true"/>
<!-- 3. Копировать launcher.sh -> target/pfsdnd и сделать исполняемым -->
<copy file="${basedir}/../launcher.sh"
tofile="${project.build.directory}/pfsdnd"
overwrite="true"/>
<!-- Делаем файл исполняемым (для Unix/Linux) -->
<chmod file="${project.build.directory}/pfsdnd" perm="755"/>
<!-- 4. Копировать ovpn-connector/target/ovpn-pfsdn-bind -> target/ -->
<copy file="${basedir}/../ovpn-connector/target/ovpn-pfsdn-bind"
tofile="${project.build.directory}/ovpn-pfsdn-bind"
overwrite="true"/>
<!-- Также делаем исполняемым -->
<chmod file="${project.build.directory}/ovpn-pfsdn-bind" perm="755"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,10 +6,10 @@
<parent> <parent>
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId> <artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version> <version>1.0.1.5</version>
</parent> </parent>
<artifactId>ovpn-connector</artifactId> <artifactId>pf-sdn.ovpn-connector</artifactId>

View File

@ -13,7 +13,13 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; 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 { public class App {
/**
* Loads connector configuration from {@code ovpn-connector.json}.
*/
private static Config loadConfig() { private static Config loadConfig() {
var configFile = new File("ovpn-connector.json"); var configFile = new File("ovpn-connector.json");
if (!configFile.exists()) { 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) { private static void pushRoutes(JSONArray routes, File pushFile) {
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) { try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) {
for (var i = 0; i < routes.length(); i++) { for (var i = 0; i < routes.length(); i++) {
@ -39,6 +48,9 @@ public class App {
} }
} }
/**
* Application entry point.
*/
public static void main(String[] args) { public static void main(String[] args) {
try { try {
var config = loadConfig(); 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 { private static JSONArray sendRequest(Config config) throws IOException, InterruptedException {
var json = new JSONObject(); var json = new JSONObject();
json.put("jsonrpc", "2.0"); json.put("jsonrpc", "2.0");

View File

@ -5,6 +5,9 @@ import lombok.Setter;
import ru.kirillius.json.JSONProperty; import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable; import ru.kirillius.json.JSONSerializable;
/**
* Configuration for the OpenVPN connector utility, including API endpoint and token.
*/
@JSONSerializable @JSONSerializable
@Getter @Getter
@Setter @Setter

62
pom.xml
View File

@ -6,12 +6,15 @@
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId> <artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version> <version>1.0.1.5</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>core</module> <module>core</module>
<module>app</module> <module>app</module>
<module>ovpn-connector</module> <module>ovpn-connector</module>
<module>launcher</module>
</modules> </modules>
<properties> <properties>
@ -42,62 +45,6 @@
</annotationProcessorPaths> </annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>
<!-- ФИНАЛЬНОЕ ИСПРАВЛЕНИЕ: Antrun Plugin вынесен в основной build.
runOnlyAtExecutionRoot гарантирует, что он будет выполнен ТОЛЬКО в корневом POM. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>clean-root-target</id>
<phase>clean</phase>
<inherited>false</inherited>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete dir="${project.build.directory}"/>
</target>
</configuration>
</execution>
<execution>
<id>post-build-assembly</id>
<phase>verify</phase> <!-- выполнится после сборки всех модулей -->
<inherited>false</inherited>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<!-- 1. Создать папку target (для root) -->
<mkdir dir="${project.build.directory}"/>
<!-- 2. Копировать app/target/app-$VERSION-shaded.jar -> target/$VERSION.pfapp -->
<copy file="${basedir}/app/target/app-${project.version}-shaded.jar"
tofile="${project.build.directory}/${project.version}.pfapp"
overwrite="true"/>
<!-- 3. Копировать launcher.sh -> target/pfsdnd и сделать исполняемым -->
<copy file="${basedir}/launcher.sh"
tofile="${project.build.directory}/pfsdnd"
overwrite="true"/>
<!-- Делаем файл исполняемым (для Unix/Linux) -->
<chmod file="${project.build.directory}/pfsdnd" perm="755"/>
<!-- 4. Копировать ovpn-connector/target/ovpn-pfsdn-bind -> target/ -->
<copy file="${basedir}/ovpn-connector/target/ovpn-pfsdn-bind"
tofile="${project.build.directory}/ovpn-pfsdn-bind"
overwrite="true"/>
<!-- Также делаем исполняемым -->
<chmod file="${project.build.directory}/ovpn-pfsdn-bind" perm="755"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
@ -116,7 +63,6 @@
</repository> </repository>
</repositories> </repositories>
<dependencies> <dependencies>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>