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