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