Рефакторинг бэкэнда
This commit is contained in:
parent
28d37f0dfb
commit
95d09e3c79
|
|
@ -6,17 +6,17 @@
|
|||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>app</artifactId>
|
||||
<artifactId>pf-sdn.app</artifactId>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<artifactId>pf-sdn.core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,33 +2,33 @@ package ru.kirillius.pf.sdn;
|
|||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
||||
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
||||
import ru.kirillius.pf.sdn.External.API.Components.TDNS;
|
||||
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||
import ru.kirillius.pf.sdn.core.*;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.BGPInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
||||
import ru.kirillius.pf.sdn.core.Util.Wait;
|
||||
import ru.kirillius.pf.sdn.web.HTTPServer;
|
||||
import ru.kirillius.pf.sdn.web.WebService;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static ru.kirillius.pf.sdn.core.Util.CommandLineUtils.getArgument;
|
||||
|
||||
/**
|
||||
* Entry point for the SDN control application that wires configuration, services, and shutdown handling.
|
||||
*/
|
||||
public class App implements Context, Closeable {
|
||||
private final static File configFile = new File("config.json");
|
||||
protected final static String CTX = App.class.getSimpleName();
|
||||
|
||||
static {
|
||||
|
|
@ -38,175 +38,101 @@ public class App implements Context, Closeable {
|
|||
|
||||
private final AtomicBoolean shouldRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||
|
||||
@Getter
|
||||
private final NetworkManager networkManager;
|
||||
@Getter
|
||||
private volatile Config config;
|
||||
@Getter
|
||||
private final AuthManager authManager;
|
||||
@Getter
|
||||
private final HTTPServer server;
|
||||
@Getter
|
||||
private final ASInfoService ASInfoService;
|
||||
@Getter
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
@Getter
|
||||
private final UpdateManager updateManager;
|
||||
@Getter
|
||||
private final TokenStorage tokenStorage;
|
||||
@Getter
|
||||
private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
|
||||
@Getter
|
||||
private final ServiceManager serviceManager;
|
||||
@Getter
|
||||
private final LauncherConfig launcherConfig;
|
||||
@Getter
|
||||
private final Config config;
|
||||
|
||||
private final List<Component<?>> loadedComponents = new ArrayList<>();
|
||||
|
||||
@SneakyThrows
|
||||
public App(File configFile) {
|
||||
/**
|
||||
* Loads configuration from disk, creating a default file if missing.
|
||||
*/
|
||||
private Config loadConfig() {
|
||||
Config loadedConfig = null;
|
||||
try {
|
||||
config = Config.load(configFile);
|
||||
loadedConfig = Config.load(launcherConfig.getConfigFile());
|
||||
} catch (IOException e) {
|
||||
config = new Config();
|
||||
loadedConfig = new Config();
|
||||
try {
|
||||
Config.store(config, configFile);
|
||||
Config.store(loadedConfig, launcherConfig.getConfigFile());
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
return loadedConfig;
|
||||
}
|
||||
|
||||
authManager = new AuthManager(this);
|
||||
ASInfoService = new ASInfoService();
|
||||
ASInfoService.setProvider(new HEInfoProvider(this));
|
||||
networkManager = new NetworkManager(this);
|
||||
networkManager.getInputResources().add(config.getCustomResources());
|
||||
subscriptionManager = new SubscriptionManager(this);
|
||||
updateManager = new UpdateManager(this);
|
||||
tokenStorage = new TokenStorage(this);
|
||||
subscribe();
|
||||
updateManager.start();
|
||||
initComponents();
|
||||
server = new HTTPServer(this);
|
||||
/**
|
||||
* Instantiates all application services and performs initial wiring.
|
||||
*/
|
||||
private ServiceManager loadServiceManager() {
|
||||
var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class));
|
||||
manager.getService(BGPInfoService.class).setProvider(new HEInfoProvider());
|
||||
manager.getService(ResourceUpdateService.class).start();
|
||||
manager.getService(ComponentHandlerService.class).syncComponentsWithConfig();
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures an admin password exists, defaulting to {@code admin} when missing.
|
||||
*/
|
||||
private void checkDefaultPassword() {
|
||||
if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) {
|
||||
SystemLogger.error("There is no password for admin. Setting default password: admin", CTX);
|
||||
getAuthManager().updatePassword("admin");
|
||||
getServiceManager().getService(AuthManager.class).updatePassword("admin");
|
||||
}
|
||||
getSubscriptionManager().triggerUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the application binding to the provided launcher configuration.
|
||||
*/
|
||||
@SneakyThrows
|
||||
public App(LauncherConfig launcherConfig) {
|
||||
this.launcherConfig = launcherConfig;
|
||||
config = loadConfig();
|
||||
serviceManager = loadServiceManager();
|
||||
serviceManager.getService(SubscriptionService.class).triggerUpdate();
|
||||
checkDefaultPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* Application entry point.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
var restart = false;
|
||||
do {
|
||||
try (var app = new App(configFile)) {
|
||||
try (var app = new App(LauncherConfig.builder()
|
||||
.configFile(new File(getArgument("c", args)))
|
||||
.appLibrary(new File(getArgument("l", args)))
|
||||
.repository(getArgument("r", args))
|
||||
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class)).build())) {
|
||||
Wait.when(app.running::get);
|
||||
restart = app.shouldRestart.get();
|
||||
if (app.shouldRestart.get()) {
|
||||
System.exit(303);
|
||||
} else {
|
||||
System.exit(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Unhandled error", CTX, e);
|
||||
System.exit(1);
|
||||
}
|
||||
} while (restart);
|
||||
}
|
||||
|
||||
public void triggerRestart() {
|
||||
SystemLogger.message("Restarting app", CTX);
|
||||
|
||||
/**
|
||||
* Requests the application to exit, optionally restarting.
|
||||
*/
|
||||
public void requestExit(boolean restart) {
|
||||
running.set(false);
|
||||
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);
|
||||
loadedComponents.add(plugin);
|
||||
}
|
||||
|
||||
public void initComponents() {
|
||||
var enabledPlugins = config.getEnabledComponents();
|
||||
|
||||
(List.copyOf(loadedComponents)).forEach(plugin -> {
|
||||
if (!enabledPlugins.contains(plugin.getClass())) {
|
||||
unloadComponent(plugin);
|
||||
}
|
||||
});
|
||||
var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
|
||||
enabledPlugins.forEach(pluginClass -> {
|
||||
if (loadedClasses.contains(pluginClass)) {
|
||||
return;
|
||||
}
|
||||
loadComponent(pluginClass);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadComponents(Class<? extends Component<?>>... classes) {
|
||||
Arrays.stream(classes)
|
||||
.forEach(componentClass -> {
|
||||
loadedComponents.stream()
|
||||
.filter(component -> componentClass.equals(component.getClass()))
|
||||
.findFirst().ifPresent(this::unloadComponent);
|
||||
loadComponent(componentClass);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONRPCServlet getRPC() {
|
||||
if (server == null) {
|
||||
return null;
|
||||
}
|
||||
return server.getJSONRPC();
|
||||
}
|
||||
|
||||
private void subscribe() {
|
||||
var eventsHandler = getEventsHandler();
|
||||
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
||||
var manager = getNetworkManager();
|
||||
var inputResources = getNetworkManager().getInputResources();
|
||||
inputResources.clear();
|
||||
inputResources.add(config.getCustomResources());
|
||||
inputResources.add(bundle);
|
||||
manager.triggerUpdate(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component<?> getComponentInstance(Class<? extends Component<?>> pluginClass) {
|
||||
return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
|
||||
shouldRestart.set(restart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all managed services.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
loadedComponents.forEach(plugin -> {
|
||||
try {
|
||||
plugin.close();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error closing plugin", CTX, e);
|
||||
}
|
||||
});
|
||||
ASInfoService.close();
|
||||
networkManager.close();
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Error stopping server", CTX, e);
|
||||
}
|
||||
serviceManager.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,26 @@ import java.util.List;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Component that synchronises FRR routing instances with the aggregated subnet list.
|
||||
*/
|
||||
public final class FRR extends AbstractComponent<FRR.FRRConfig> {
|
||||
private final static String SUBNET_PATTERN = "{%subnet}";
|
||||
private final static String GW_PATTERN = "{%gateway}";
|
||||
private final static String CTX = FRR.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
/**
|
||||
* Binds the component to the application context and subscribes to network updates.
|
||||
*/
|
||||
public FRR(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronises FRR instances with the provided subnet list.
|
||||
*/
|
||||
private void updateSubnets(List<IPv4Subnet> subnets) {
|
||||
for (var entry : config.instances) {
|
||||
SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
|
||||
|
|
@ -94,6 +103,9 @@ public final class FRR extends AbstractComponent<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) {
|
||||
|
||||
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) {
|
||||
var buffer = new ArrayList<String>();
|
||||
buffer.add("vtysh");
|
||||
|
|
@ -133,11 +148,17 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
|
|||
return shell.executeCommand(buffer.toArray(new String[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the subscription from the context event handler.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration describing FRR instances and how subnets should be rendered for them.
|
||||
*/
|
||||
@JSONSerializable
|
||||
public static class FRRConfig {
|
||||
|
||||
|
|
@ -146,6 +167,9 @@ public final class FRR extends AbstractComponent<FRR.FRRConfig> {
|
|||
@JSONArrayProperty(type = Entry.class)
|
||||
private List<Entry> instances = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Declarative description of a single FRR instance managed by the component.
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@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.core.AbstractComponent;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
import ru.kirillius.pf.sdn.web.WebService;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Component integrating with OpenVPN to expose management RPC and synchronize route exports.
|
||||
*/
|
||||
public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
||||
private final static String CTX = OVPN.class.getSimpleName();
|
||||
private final EventListener<JSONRPCServlet> subscription;
|
||||
|
||||
/**
|
||||
* Registers the component with the JSON-RPC servlet or defers until it becomes available.
|
||||
*/
|
||||
public OVPN(Context context) {
|
||||
super(context);
|
||||
var RPC = context.getRPC();
|
||||
var RPC = context.getServiceManager().getService(WebService.class).getJSONRPC();
|
||||
if (RPC != null) {
|
||||
RPC.addTargetInstance(OVPN.class, this);
|
||||
subscription = null;
|
||||
|
|
@ -34,6 +42,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
|||
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the configured command to restart the OpenVPN service.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public String restartSystemService() {
|
||||
|
|
@ -45,11 +56,14 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON array describing managed routes for OpenVPN clients.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONArray getManagedRoutes() {
|
||||
var array = new JSONArray();
|
||||
context.getNetworkManager().getOutputResources().getSubnets().stream().map(subnet -> {
|
||||
context.getServiceManager().getService(NetworkingService.class).getOutputResources().getSubnets().stream().map(subnet -> {
|
||||
var json = new JSONObject();
|
||||
json.put("address", subnet.getAddress());
|
||||
json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength())));
|
||||
|
|
@ -59,6 +73,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unregisters RPC listeners when the component is closed.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (subscription != null) {
|
||||
|
|
@ -66,6 +83,9 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for OpenVPN shell interaction and service restart command.
|
||||
*/
|
||||
@JSONSerializable
|
||||
public static class OVPNConfig {
|
||||
@Getter
|
||||
|
|
|
|||
|
|
@ -16,16 +16,25 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Component that synchronises Technitium DNS forwarder zones with managed domain lists.
|
||||
*/
|
||||
public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
|
||||
|
||||
private final static String CTX = TDNS.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
/**
|
||||
* Subscribes to network updates to keep Technitium DNS forwarder zones in sync.
|
||||
*/
|
||||
public TDNS(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Technitium DNS servers to match the provided domain list.
|
||||
*/
|
||||
private void updateSubnets(List<String> domains) {
|
||||
for (var instance : config.instances) {
|
||||
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
|
||||
|
|
@ -56,12 +65,18 @@ public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the event subscription when the component is closed.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration of Technitium DNS instances managed by the component.
|
||||
*/
|
||||
@JSONSerializable
|
||||
public static class TechnitiumConfig {
|
||||
|
||||
|
|
@ -70,6 +85,9 @@ public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
|
|||
@JSONArrayProperty(type = Entry.class)
|
||||
private List<Entry> instances = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Describes a single DNS server endpoint and its access credentials.
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
|
|
|
|||
|
|
@ -18,13 +18,22 @@ import java.io.FileInputStream;
|
|||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Subscription provider that pulls JSON resource bundles from a Git repository cache.
|
||||
*/
|
||||
public class GitSubscription implements SubscriptionProvider {
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Creates the provider using the shared application context.
|
||||
*/
|
||||
public GitSubscription(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones or updates the configured repository and loads resource bundles from the {@code resources} directory.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, NetworkResourceBundle> getResources(RepositoryConfig config) {
|
||||
try {
|
||||
|
|
@ -69,6 +78,9 @@ public class GitSubscription implements SubscriptionProvider {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs {@code git fetch} and {@code git pull} when remote updates are available.
|
||||
*/
|
||||
private static void checkAndPullUpdates(Git git) throws GitAPIException {
|
||||
|
||||
|
||||
|
|
@ -102,6 +114,9 @@ public class GitSubscription implements SubscriptionProvider {
|
|||
|
||||
private final static String CTX = GitSubscription.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Clones the repository into the given path.
|
||||
*/
|
||||
private static Git cloneRepository(String REPO_URL, File path) throws GitAPIException {
|
||||
SystemLogger.message("Cloning repository " + REPO_URL, CTX);
|
||||
return Git.cloneRepository()
|
||||
|
|
@ -111,6 +126,9 @@ public class GitSubscription implements SubscriptionProvider {
|
|||
.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an existing repository located at the given directory.
|
||||
*/
|
||||
private static Git openRepository(File repoDir) throws IOException {
|
||||
var builder = new FileRepositoryBuilder();
|
||||
var repository = builder.setGitDir(new File(repoDir, ".git"))
|
||||
|
|
@ -120,6 +138,9 @@ public class GitSubscription implements SubscriptionProvider {
|
|||
return new Git(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the directory contains a Git repository.
|
||||
*/
|
||||
private static boolean isGitRepository(File directory) {
|
||||
if (!directory.exists() || !directory.isDirectory()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import lombok.SneakyThrows;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoProvider;
|
||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
|
@ -18,14 +17,16 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Retrieves ASN prefix information from Hurricane Electric's public API.
|
||||
*/
|
||||
public class HEInfoProvider implements ASInfoProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public HEInfoProvider(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches IPv4 prefixes announced by the specified autonomous system.
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public List<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) {
|
||||
var json = new JSONObject(new JSONTokener(inputStream));
|
||||
var array = json.getJSONArray("prefixes");
|
||||
|
|
|
|||
|
|
@ -15,15 +15,24 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
import static net.schmizz.sshj.common.IOUtils.readFully;
|
||||
|
||||
/**
|
||||
* Executes commands either locally or over SSH based on the provided configuration.
|
||||
*/
|
||||
public class ShellExecutor implements Closeable {
|
||||
private final static String CTX = ShellExecutor.class.getSimpleName();
|
||||
private final Config config;
|
||||
private SSHClient sshClient;
|
||||
|
||||
/**
|
||||
* Creates an executor with the supplied connection configuration.
|
||||
*/
|
||||
public ShellExecutor(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given command, either locally or through SSH, returning the captured stdout.
|
||||
*/
|
||||
public String executeCommand(String[] command) {
|
||||
var buffer = new StringJoiner(" ");
|
||||
Arrays.stream(command).forEach(e -> buffer.add('"' + e + '"'));
|
||||
|
|
@ -64,6 +73,9 @@ public class ShellExecutor implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the underlying SSH client when one was created.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
|
||||
|
|
@ -77,6 +89,9 @@ public class ShellExecutor implements Closeable {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection and authentication options for shell command execution.
|
||||
*/
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
|
|
@ -103,6 +118,9 @@ public class ShellExecutor implements Closeable {
|
|||
@JSONProperty
|
||||
private volatile String password = "securepassword";
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of the configured shell target.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
var builder = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -16,17 +16,26 @@ import java.net.http.HttpResponse;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Thin client for the Technitium DNS HTTP API used to manage forwarder zones.
|
||||
*/
|
||||
public class TDNSAPI implements Closeable {
|
||||
private final String server;
|
||||
private final String authToken;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Creates an API client targeting the specified Technitium server.
|
||||
*/
|
||||
public TDNSAPI(String server, String authToken) {
|
||||
this.server = server;
|
||||
httpClient = HttpClient.newBuilder().build();
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request to the Technitium API with authentication parameters.
|
||||
*/
|
||||
private JSONObject getRequest(String api, Map<String, String> additionalParams) {
|
||||
var params = new HashMap<>(additionalParams);
|
||||
var joiner = new StringJoiner("&");
|
||||
|
|
@ -57,10 +66,16 @@ public class TDNSAPI implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported zone types returned by the API.
|
||||
*/
|
||||
public enum ZoneType {
|
||||
Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer-object describing DNS zone metadata received from Technitium.
|
||||
*/
|
||||
@JSONSerializable
|
||||
public static class ZoneResponse {
|
||||
@JSONProperty
|
||||
|
|
@ -92,11 +107,17 @@ public class TDNSAPI implements Closeable {
|
|||
private Date lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists zones from the Technitium server.
|
||||
*/
|
||||
public List<ZoneResponse> getZones() {
|
||||
var request = getRequest("/api/zones/list", Collections.emptyMap());
|
||||
return JSONUtility.deserializeCollection(request.getJSONArray("zones"), ZoneResponse.class, null).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a forwarder zone pointing to the supplied upstream server.
|
||||
*/
|
||||
public void createForwarderZone(String zoneName, String forwarder) {
|
||||
var params = new HashMap<String, String>();
|
||||
params.put("zone", zoneName);
|
||||
|
|
@ -107,6 +128,9 @@ public class TDNSAPI implements Closeable {
|
|||
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the specified zone from the server.
|
||||
*/
|
||||
public void deleteZone(String zoneName) {
|
||||
var params = new HashMap<String, String>();
|
||||
params.put("zone", zoneName);
|
||||
|
|
@ -114,6 +138,9 @@ public class TDNSAPI implements Closeable {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the underlying HTTP client.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
httpClient.close();
|
||||
|
|
|
|||
|
|
@ -7,19 +7,31 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/**
|
||||
* Maintains a rolling in-memory buffer of recent log messages for diagnostics exposure.
|
||||
*/
|
||||
public class InMemoryLogHandler extends Handler {
|
||||
private final static Queue<String> queue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final static DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US);
|
||||
|
||||
/**
|
||||
* Formats a log record into a single-line message.
|
||||
*/
|
||||
private String format(LogRecord logRecord) {
|
||||
return "[" + dateFormat.format(new Date(logRecord.getMillis())) + "][" + logRecord.getLevel().getName() + "] " + logRecord.getMessage().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a read-only view of the buffered log messages.
|
||||
*/
|
||||
public static Collection<String> getMessages() {
|
||||
return Collections.unmodifiableCollection(queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the formatted message in the buffer, trimming old entries when full.
|
||||
*/
|
||||
@Override
|
||||
public void publish(LogRecord logRecord) {
|
||||
if (queue.size() >= 1000) {
|
||||
|
|
@ -28,11 +40,17 @@ public class InMemoryLogHandler extends Handler {
|
|||
queue.add(format(logRecord));
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op flush implementation.
|
||||
*/
|
||||
@Override
|
||||
public void flush() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op close implementation.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
|||
|
|
@ -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.Target;
|
||||
|
||||
/**
|
||||
* Marks RPC methods that require authenticated sessions or tokens.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
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.JRPCMethod;
|
||||
import ru.kirillius.json.rpc.Servlet.CallContext;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JSON-RPC handler exposing authentication, session, and token management endpoints.
|
||||
*/
|
||||
public class Auth implements RPC {
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Creates the handler bound to the shared application context.
|
||||
*/
|
||||
public Auth(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON array containing all stored API tokens.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray listTokens() {
|
||||
return JSONUtility.serializeCollection(context.getTokenStorage().getTokens(), AuthToken.class, null);
|
||||
return JSONUtility.serializeCollection(context.getServiceManager().getService(TokenService.class).getTokens(), AuthToken.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a token identified by its raw value.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void removeToken(@JRPCArgument(name = "token") String token) {
|
||||
context.getTokenStorage().remove(new AuthToken(token));
|
||||
context.getServiceManager().getService(TokenService.class).remove(new AuthToken(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new token with the provided description and returns its value.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public String createAPIToken(@JRPCArgument(name = "description") String description) {
|
||||
var token = new AuthToken();
|
||||
token.setDescription(description);
|
||||
context.getTokenStorage().add(token);
|
||||
context.getServiceManager().getService(TokenService.class).add(token);
|
||||
return token.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session-bound token associated with the caller's user agent.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public String createToken(@JRPCContext CallContext call) {
|
||||
|
|
@ -47,15 +67,18 @@ public class Auth implements RPC {
|
|||
if (UA == null) {
|
||||
UA = "Unknown user agent";
|
||||
}
|
||||
var authManager = context.getAuthManager();
|
||||
var authManager = context.getServiceManager().getService(AuthManager.class);
|
||||
var token = authManager.createToken(UA + " " + new Date());
|
||||
authManager.setSessionToken(call.getSession(), token);
|
||||
return token.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the session using a password.
|
||||
*/
|
||||
@JRPCMethod
|
||||
public boolean startSessionByPassword(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
var authManager = context.getServiceManager().getService(AuthManager.class);
|
||||
if (authManager.validatePassword(password)) {
|
||||
authManager.setSessionAuthState(call.getSession(), true);
|
||||
return true;
|
||||
|
|
@ -63,9 +86,12 @@ public class Auth implements RPC {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the session using an existing token.
|
||||
*/
|
||||
@JRPCMethod
|
||||
public boolean startSessionByToken(@JRPCArgument(name = "token") String token, @JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
var authManager = context.getServiceManager().getService(AuthManager.class);
|
||||
if (authManager.validateToken(token)) {
|
||||
authManager.setSessionAuthState(call.getSession(), true);
|
||||
return true;
|
||||
|
|
@ -73,16 +99,22 @@ public class Auth implements RPC {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the current session is authenticated.
|
||||
*/
|
||||
@JRPCMethod
|
||||
public boolean isAuthenticated(@JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
var authManager = context.getServiceManager().getService(AuthManager.class);
|
||||
return authManager.getSessionAuthState(call.getSession());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs out the current session and invalidates its token when present.
|
||||
*/
|
||||
@JRPCMethod
|
||||
public void logout(@JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
var authManager = context.getServiceManager().getService(AuthManager.class);
|
||||
authManager.setSessionAuthState(call.getSession(), false);
|
||||
var token = authManager.getSessionToken(call.getSession());
|
||||
if (token != null) {
|
||||
|
|
|
|||
|
|
@ -5,30 +5,46 @@ import ru.kirillius.json.JSONUtility;
|
|||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
/**
|
||||
* JSON-RPC handler exposing operations on the network aggregation service.
|
||||
*/
|
||||
public class NetworkManager implements RPC {
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Creates the handler bound to the application context.
|
||||
*/
|
||||
public NetworkManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the networking service is currently recomputing resources.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public boolean isUpdating() {
|
||||
return context.getNetworkManager().isUpdatingNow();
|
||||
return context.getServiceManager().getService(NetworkingService.class).isUpdatingNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an update of networking resources.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void triggerUpdate(@JRPCArgument(name = "ignoreCache") boolean ignoreCache) {
|
||||
context.getNetworkManager().triggerUpdate(ignoreCache);
|
||||
context.getServiceManager().getService(NetworkingService.class).triggerUpdate(ignoreCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest aggregated network resource bundle.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONObject getOutputResources() {
|
||||
return JSONUtility.serializeStructure(context.getNetworkManager().getOutputResources());
|
||||
return JSONUtility.serializeStructure(context.getServiceManager().getService(NetworkingService.class).getOutputResources());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import ru.kirillius.pf.sdn.core.Context;
|
|||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
/**
|
||||
* Marker interface for JSON-RPC handlers with a helper to instantiate them via the application context.
|
||||
*/
|
||||
public interface RPC {
|
||||
/**
|
||||
* Instantiates an RPC handler using its context-aware constructor.
|
||||
*/
|
||||
static <T extends RPC> T instantiate(Class<T> type, Context context) {
|
||||
try {
|
||||
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.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JSON-RPC handler for subscription lifecycle and resource selection operations.
|
||||
*/
|
||||
public class SubscriptionManager implements RPC {
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Creates the handler bound to the application context.
|
||||
*/
|
||||
public SubscriptionManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the subscription service is updating.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public boolean isUpdating() {
|
||||
return context.getSubscriptionManager().isUpdatingNow();
|
||||
return context.getServiceManager().getService(SubscriptionService.class).isUpdatingNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the subscription service to refresh repositories.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void triggerUpdate() {
|
||||
context.getSubscriptionManager().triggerUpdate();
|
||||
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of resource identifiers currently subscribed to.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONArray getSubscribedResources() {
|
||||
return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available resources grouped by repository.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONObject getAvailableResources() {
|
||||
return JSONUtility.serializeMap(context.getSubscriptionManager().getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
|
||||
return JSONUtility.serializeMap(context.getServiceManager().getService(SubscriptionService.class).getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the new selection of subscribed resources and triggers an update.
|
||||
*/
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) {
|
||||
|
|
|
|||
|
|
@ -5,50 +5,95 @@ import org.json.JSONObject;
|
|||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.pf.sdn.core.AppUpdateService;
|
||||
import ru.kirillius.pf.sdn.core.Component;
|
||||
import ru.kirillius.pf.sdn.core.ComponentHandlerService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JSON-RPC handler for system control operations and component configuration management.
|
||||
*/
|
||||
public class System implements RPC {
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Creates the handler bound to the application context.
|
||||
*/
|
||||
public System(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an application restart.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void restart() {
|
||||
context.triggerRestart();
|
||||
context.requestExit(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an application shutdown.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void shutdown() {
|
||||
context.triggerShutdown();
|
||||
context.requestExit(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current configuration as JSON.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONObject getConfig() {
|
||||
return JSONUtility.serializeStructure(context.getConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the configuration has unsaved changes.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public boolean isConfigChanged() {
|
||||
return context.getConfig().isModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the latest version available for update.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public boolean hasUpdates() {
|
||||
return true;
|
||||
public String getVersionForUpdate() {
|
||||
return context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently installed application version.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public String getAppVersion() {
|
||||
return context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides additional metadata about available versions.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONObject getVersionInfo() {
|
||||
var available = context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the list of enabled component class names.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray getEnabledComponents() {
|
||||
|
|
@ -56,6 +101,9 @@ public class System implements RPC {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the set of enabled components according to the provided class names.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
|
|
@ -67,16 +115,22 @@ public class System implements RPC {
|
|||
throw new RuntimeException("Unable to load Component class " + s, e);
|
||||
}
|
||||
}).collect(Collectors.toList()));
|
||||
context.initComponents();
|
||||
context.getServiceManager().getService(ComponentHandlerService.class).syncComponentsWithConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of component classes available to the launcher.
|
||||
*/
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray getAvailableComponents() {
|
||||
return JSONUtility.serializeCollection(context.getComponentClasses().stream().map(Class::getName).toList(), String.class, null);
|
||||
return JSONUtility.serializeCollection(context.getLauncherConfig().getAvailableComponentClasses().stream().map(Class::getName).toList(), String.class, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the configuration object for the specified component.
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
|
|
@ -86,6 +140,9 @@ public class System implements RPC {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
Updates the configuration of the specified component and reloads it.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
|
|
@ -93,8 +150,7 @@ public class System implements RPC {
|
|||
var cls = (Class) Class.forName(componentName);
|
||||
var configClass = Component.getConfigClass(cls);
|
||||
context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass));
|
||||
context.reloadComponents(cls);
|
||||
Object config1 = context.getConfig().getComponentsConfig().getConfig(cls);
|
||||
return;
|
||||
context.getServiceManager().getService(ComponentHandlerService.class).reloadComponents(cls);
|
||||
context.getConfig().getComponentsConfig().getConfig(cls);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>core</artifactId>
|
||||
|
||||
<artifactId>pf-sdn.core</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
/**
|
||||
* Convenience base implementation that wires component configuration and context dependencies.
|
||||
*/
|
||||
public abstract class AbstractComponent<CT> implements Component<CT> {
|
||||
protected final CT config;
|
||||
protected final Context context;
|
||||
|
||||
/**
|
||||
* Loads the component configuration from the shared storage and stores the context reference.
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public AbstractComponent(Context context) {
|
||||
config = (CT) context.getConfig().getComponentsConfig().getConfig((Class) getClass());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.HashUtil;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
|
@ -9,21 +10,27 @@ import java.io.IOException;
|
|||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AuthManager {
|
||||
/**
|
||||
* Coordinates authentication flows, password hashing, and token lifecycle management.
|
||||
*/
|
||||
public class AuthManager extends AppService {
|
||||
public final static String SESSION_AUTH_KEY = "auth";
|
||||
public final static String SESSION_TOKEN = "token";
|
||||
private final static String CTX = AuthManager.class.getSimpleName();
|
||||
private final Context context;
|
||||
|
||||
public AuthManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verifies the provided password against the stored hash.
|
||||
*/
|
||||
public boolean validatePassword(String pass) {
|
||||
var config = context.getConfig();
|
||||
return HashUtil.hash(pass, config.getPasswordSalt()).equals(config.getPasswordHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stored password hash and generates a new salt.
|
||||
*/
|
||||
public void updatePassword(String pass) {
|
||||
var config = context.getConfig();
|
||||
config.setPasswordSalt(UUID.randomUUID().toString());
|
||||
|
|
@ -32,10 +39,13 @@ public class AuthManager {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new authentication token with the provided description and persists it.
|
||||
*/
|
||||
public AuthToken createToken(String description) {
|
||||
var token = new AuthToken();
|
||||
token.setDescription(description);
|
||||
var tokenStorage = context.getTokenStorage();
|
||||
var tokenStorage = context.getServiceManager().getService(TokenService.class);
|
||||
tokenStorage.add(token);
|
||||
try {
|
||||
tokenStorage.store();
|
||||
|
|
@ -45,28 +55,46 @@ public class AuthManager {
|
|||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the storage contains the specified token.
|
||||
*/
|
||||
public boolean validateToken(AuthToken token) {
|
||||
return context.getTokenStorage().contains(token);
|
||||
return context.getServiceManager().getService(TokenService.class).contains(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication flag in the HTTP session.
|
||||
*/
|
||||
public void setSessionAuthState(HttpSession session, boolean state) {
|
||||
session.setAttribute(SESSION_AUTH_KEY, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the given token with the HTTP session.
|
||||
*/
|
||||
public void setSessionToken(HttpSession session, AuthToken token) {
|
||||
session.setAttribute(SESSION_TOKEN, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the token associated with the HTTP session.
|
||||
*/
|
||||
public AuthToken getSessionToken(HttpSession session) {
|
||||
return (AuthToken) session.getAttribute(SESSION_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the HTTP session is marked as authenticated.
|
||||
*/
|
||||
public boolean getSessionAuthState(HttpSession session) {
|
||||
return Objects.equals(session.getAttribute(SESSION_AUTH_KEY), Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided token from storage if present.
|
||||
*/
|
||||
public void invalidateToken(AuthToken token) {
|
||||
var tokenStorage = context.getTokenStorage();
|
||||
var tokenStorage = context.getServiceManager().getService(TokenService.class);
|
||||
if (tokenStorage.contains(token)) {
|
||||
tokenStorage.remove(token);
|
||||
try {
|
||||
|
|
@ -78,11 +106,32 @@ public class AuthManager {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks token validity using its raw string value.
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
return validateToken(new AuthToken(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates a token identified by its string representation.
|
||||
*/
|
||||
public void invalidateToken(String token) {
|
||||
invalidateToken(new AuthToken(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the manager bound to the shared context.
|
||||
*/
|
||||
public AuthManager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op close implementation.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@ import ru.kirillius.json.JSONSerializable;
|
|||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a persistent authentication token with metadata for API access.
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable
|
||||
public class AuthToken {
|
||||
/**
|
||||
* Creates a token wrapper for the specified raw token string.
|
||||
*/
|
||||
public AuthToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
|
@ -21,6 +27,9 @@ public class AuthToken {
|
|||
@JSONProperty
|
||||
private String description = "untitled";
|
||||
|
||||
/**
|
||||
* Tokens are equal when their token strings match.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
|
@ -28,6 +37,9 @@ public class AuthToken {
|
|||
return Objects.equals(token, authToken.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a hash code derived from the token string.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(token);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ru.kirillius.pf.sdn.core.Auth;
|
|||
import org.json.JSONArray;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
|
||||
import java.io.*;
|
||||
|
|
@ -10,10 +11,17 @@ import java.util.*;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class TokenStorage {
|
||||
/**
|
||||
* Persists and manages authentication tokens on disk for API consumers.
|
||||
*/
|
||||
public class TokenService extends AppService {
|
||||
private final File file;
|
||||
|
||||
public TokenStorage(Context context) {
|
||||
/**
|
||||
* Loads existing tokens from disk and prepares the storage file.
|
||||
*/
|
||||
public TokenService(Context context) {
|
||||
super(context);
|
||||
file = new File(context.getConfig().getCacheDirectory(), "tokens.json");
|
||||
if (file.exists()) {
|
||||
try (var stream = new FileInputStream(file)) {
|
||||
|
|
@ -29,30 +37,45 @@ public class TokenStorage {
|
|||
|
||||
private final List<AuthToken> tokens;
|
||||
|
||||
/**
|
||||
* Returns an immutable view over the stored tokens.
|
||||
*/
|
||||
public Collection<AuthToken> getTokens() {
|
||||
synchronized (tokens) {
|
||||
return Collections.unmodifiableList(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided tokens to the storage.
|
||||
*/
|
||||
public void add(AuthToken... what) {
|
||||
synchronized (tokens) {
|
||||
Collections.addAll(tokens, what);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified token is present in storage.
|
||||
*/
|
||||
public boolean contains(AuthToken what) {
|
||||
synchronized (tokens) {
|
||||
return tokens.contains(what);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided tokens from storage.
|
||||
*/
|
||||
public void remove(AuthToken... what) {
|
||||
synchronized (tokens) {
|
||||
Arrays.stream(what).forEach(tokens::remove);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the in-memory token list to disk.
|
||||
*/
|
||||
public synchronized void store() throws IOException {
|
||||
try (var fileInputStream = new FileOutputStream(file)) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
|
||||
|
|
@ -61,4 +84,12 @@ public class TokenStorage {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op close implementation.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,13 @@ import lombok.SneakyThrows;
|
|||
import java.io.Closeable;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
/**
|
||||
* Defines the contract for pluggable application components with type-safe configuration access.
|
||||
*/
|
||||
public interface Component<CT> extends Closeable {
|
||||
/**
|
||||
* Resolves the configuration type parameter for a component implementation.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Class<T> getConfigClass(Class<? extends Component<T>> pluginClass) {
|
||||
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
|
||||
|
|
@ -13,6 +19,9 @@ public interface Component<CT> extends Closeable {
|
|||
return (Class<T>) typeArguments[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a component using its context-aware constructor.
|
||||
*/
|
||||
@SneakyThrows
|
||||
static <T extends Component<?>> T loadPlugin(Class<T> pluginClass, Context context) {
|
||||
return pluginClass.getConstructor(Context.class).newInstance(context);
|
||||
|
|
|
|||
|
|
@ -11,12 +11,21 @@ import ru.kirillius.json.SerializationException;
|
|||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Stores per-component configuration instances and serializes them as JSON.
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable(ComponentConfigStorage.Serializer.class)
|
||||
public class ComponentConfigStorage {
|
||||
|
||||
/**
|
||||
* JSON serializer that materializes component configuration maps.
|
||||
*/
|
||||
public final static class Serializer implements JSONSerializer<ComponentConfigStorage> {
|
||||
|
||||
/**
|
||||
* Converts the stored configurations into a JSON object keyed by class name.
|
||||
*/
|
||||
@Override
|
||||
public Object serialize(ComponentConfigStorage componentConfigStorage) throws SerializationException {
|
||||
var json = new JSONObject();
|
||||
|
|
@ -26,6 +35,9 @@ public class ComponentConfigStorage {
|
|||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores component configuration instances from the provided JSON payload.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Override
|
||||
public ComponentConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
|
||||
|
|
@ -48,6 +60,9 @@ public class ComponentConfigStorage {
|
|||
|
||||
private final Map<Class<? extends Component<?>>, Object> configs = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Returns (and instantiates if necessary) the configuration object for the given component class.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@SneakyThrows
|
||||
public <CT> CT getConfig(Class<? extends Component<CT>> componentClass) {
|
||||
|
|
@ -59,6 +74,9 @@ public class ComponentConfigStorage {
|
|||
return (CT) configs.get(componentClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the provided configuration instance for the specified component class.
|
||||
*/
|
||||
@SneakyThrows
|
||||
public <CT> void setConfig(Class<? extends Component<CT>> componentClass, CT config) {
|
||||
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.UUID;
|
||||
|
||||
/**
|
||||
* Represents persisted application settings and component configuration bundles.
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable
|
||||
public class Config {
|
||||
|
|
@ -104,6 +107,9 @@ public class Config {
|
|||
@JSONProperty
|
||||
private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle();
|
||||
|
||||
/**
|
||||
* Persists the given configuration into the supplied file as JSON.
|
||||
*/
|
||||
public static void store(Config config, File file) throws IOException {
|
||||
try (var fileInputStream = new FileOutputStream(file)) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
|
||||
|
|
@ -113,14 +119,23 @@ public class Config {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialises the configuration into a JSONObject representation.
|
||||
*/
|
||||
public static JSONObject serialize(Config config) {
|
||||
return JSONUtility.serializeStructure(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a configuration instance from JSON data.
|
||||
*/
|
||||
public static Config deserialize(JSONObject object) {
|
||||
return JSONUtility.deserializeStructure(object, Config.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a configuration from disk and tracks the original JSON for modification detection.
|
||||
*/
|
||||
public static Config load(File file) throws IOException {
|
||||
try (var stream = new FileInputStream(file)) {
|
||||
var json = new JSONObject(new JSONTokener(stream));
|
||||
|
|
@ -133,6 +148,9 @@ public class Config {
|
|||
|
||||
private JSONObject initialJSON = new JSONObject();
|
||||
|
||||
/**
|
||||
* Indicates whether the in-memory configuration diverges from the initially loaded snapshot.
|
||||
*/
|
||||
public boolean isModified() {
|
||||
return !initialJSON.equals(serialize(this));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,29 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Provides access to core services and configuration for application components.
|
||||
*/
|
||||
public interface Context {
|
||||
/**
|
||||
* Returns immutable launcher parameters shared across services.
|
||||
*/
|
||||
LauncherConfig getLauncherConfig();
|
||||
/**
|
||||
* Provides access to the service registry for retrieving components.
|
||||
*/
|
||||
ServiceManager getServiceManager();
|
||||
/**
|
||||
* Supplies the mutable runtime configuration.
|
||||
*/
|
||||
Config getConfig();
|
||||
|
||||
AuthManager getAuthManager();
|
||||
|
||||
ASInfoService getASInfoService();
|
||||
|
||||
NetworkManager getNetworkManager();
|
||||
|
||||
/**
|
||||
* Exposes event hooks for subscribers interested in runtime changes.
|
||||
*/
|
||||
ContextEventsHandler getEventsHandler();
|
||||
|
||||
SubscriptionManager getSubscriptionManager();
|
||||
|
||||
UpdateManager getUpdateManager();
|
||||
|
||||
Component<?> getComponentInstance(Class<? extends Component<?>> pluginClass);
|
||||
|
||||
void triggerRestart();
|
||||
|
||||
void triggerShutdown();
|
||||
|
||||
TokenStorage getTokenStorage();
|
||||
|
||||
void initComponents();
|
||||
|
||||
void reloadComponents(Class<? extends Component<?>>... classes);
|
||||
|
||||
JSONRPCServlet getRPC();
|
||||
|
||||
Collection<Class<? extends Component<?>>> getComponentClasses();
|
||||
/**
|
||||
* Signals the application to exit, optionally requesting a restart.
|
||||
*
|
||||
* @param shouldRestart {@code true} to exit with restart intent, {@code false} to shut down.
|
||||
*/
|
||||
void requestExit(boolean shouldRestart);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,23 @@ import ru.kirillius.java.utils.events.EventHandler;
|
|||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
|
||||
/**
|
||||
* Aggregates event handlers used to notify the application about runtime state changes.
|
||||
*/
|
||||
public final class ContextEventsHandler {
|
||||
/**
|
||||
* Event fired when network resource bundles are recalculated.
|
||||
*/
|
||||
@Getter
|
||||
private final EventHandler<NetworkResourceBundle> networkManagerUpdateEvent = new ConcurrentEventHandler<>();
|
||||
/**
|
||||
* Event fired when subscription data has been refreshed.
|
||||
*/
|
||||
@Getter
|
||||
private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
|
||||
/**
|
||||
* Event fired after the RPC servlet is initialised and ready.
|
||||
*/
|
||||
@Getter
|
||||
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.util.List;
|
||||
|
||||
/**
|
||||
* Abstraction for retrieving prefixes announced by a specific autonomous system.
|
||||
*/
|
||||
public interface ASInfoProvider {
|
||||
/**
|
||||
* Returns IPv4 subnets originated by the provided autonomous system number.
|
||||
*/
|
||||
List<IPv4Subnet> getPrefixes(int as);
|
||||
|
||||
/**
|
||||
* Instantiates a provider class using the context-aware constructor.
|
||||
*/
|
||||
static ASInfoProvider instantiate(Class<? extends ASInfoProvider> providerClass, Context context) {
|
||||
try {
|
||||
var constructor = providerClass.getConstructor(Context.class);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,46 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class ASInfoService implements Closeable {
|
||||
|
||||
/**
|
||||
* Delegates asynchronous retrieval of BGP prefix data through a configurable provider.
|
||||
*/
|
||||
public class BGPInfoService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private ASInfoProvider provider = null;
|
||||
private ASInfoProvider provider;
|
||||
|
||||
/**
|
||||
* Creates the service tied to the shared context.
|
||||
*/
|
||||
public BGPInfoService(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Submits a request to obtain prefixes for the provided autonomous system number.
|
||||
*/
|
||||
public Future<List<IPv4Subnet>> getPrefixes(int as) {
|
||||
return executor.submit(() -> provider.getPrefixes(as));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the background executor.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
executor.shutdown();
|
||||
|
|
@ -9,16 +9,28 @@ import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
|||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Immutable representation of an IPv4 subnet with utility helpers for serialization.
|
||||
*/
|
||||
@JSONSerializable(IPv4Subnet.Serializer.class)
|
||||
public class IPv4Subnet {
|
||||
|
||||
/**
|
||||
* Serializes subnets to CIDR notation strings for JSON persistence.
|
||||
*/
|
||||
public final static class Serializer implements JSONSerializer<IPv4Subnet> {
|
||||
|
||||
/**
|
||||
* Writes the subnet as a string using CIDR notation.
|
||||
*/
|
||||
@Override
|
||||
public Object serialize(IPv4Subnet subnet) throws SerializationException {
|
||||
return subnet.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CIDR notation string and returns the corresponding subnet instance.
|
||||
*/
|
||||
@Override
|
||||
public IPv4Subnet deserialize(Object o, Class<?> aClass) throws SerializationException {
|
||||
return new IPv4Subnet((String) o);
|
||||
|
|
@ -31,6 +43,9 @@ public class IPv4Subnet {
|
|||
private final int prefixLength;
|
||||
|
||||
|
||||
/**
|
||||
* Compares two subnets for equality based on address and prefix length.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
|
@ -38,11 +53,17 @@ public class IPv4Subnet {
|
|||
return longAddress == that.longAddress && prefixLength == that.prefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash code using address and prefix length.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(longAddress, prefixLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a subnet from textual CIDR notation.
|
||||
*/
|
||||
public IPv4Subnet(String subnet) {
|
||||
var split = subnet.split(Pattern.quote("/"));
|
||||
if (split.length != 2) {
|
||||
|
|
@ -55,11 +76,17 @@ public class IPv4Subnet {
|
|||
prefixLength = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subnet from a numeric address and prefix length.
|
||||
*/
|
||||
public IPv4Subnet(long longAddress, int prefixLength) {
|
||||
this.longAddress = longAddress;
|
||||
this.prefixLength = prefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subnet from a dotted address string and prefix length.
|
||||
*/
|
||||
public IPv4Subnet(String address, int prefixLength) {
|
||||
IPv4Util.validatePrefix(prefixLength);
|
||||
|
||||
|
|
@ -67,19 +94,31 @@ public class IPv4Subnet {
|
|||
this.prefixLength = prefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of addresses within the subnet.
|
||||
*/
|
||||
public long count() {
|
||||
return IPv4Util.calculateCountForPrefixLength(prefixLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dotted-decimal representation of the subnet's network address.
|
||||
*/
|
||||
public String getAddress() {
|
||||
return IPv4Util.longToIpAddress(longAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the subnet using CIDR notation.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAddress() + '/' + prefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this subnet overlaps with another.
|
||||
*/
|
||||
public boolean overlaps(IPv4Subnet subnet) {
|
||||
var minPrefixLength = Math.min(prefixLength, subnet.prefixLength);
|
||||
var commonMask = IPv4Util.calculateMask(minPrefixLength);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import ru.kirillius.json.JSONSerializable;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Container for grouped network identifiers used to configure filtering and subscriptions.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
|
|
@ -30,12 +33,18 @@ public class NetworkResourceBundle {
|
|||
@JSONArrayProperty(type = String.class)
|
||||
private List<String> domains = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Clears all stored network identifiers.
|
||||
*/
|
||||
public void clear() {
|
||||
ASN.clear();
|
||||
subnets.clear();
|
||||
domains.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all resources from the provided bundle into this bundle.
|
||||
*/
|
||||
public void add(NetworkResourceBundle networkResourceBundle) {
|
||||
ASN.addAll(networkResourceBundle.getASN());
|
||||
subnets.addAll(networkResourceBundle.getSubnets());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package ru.kirillius.pf.sdn.core.Networking;
|
|||
import lombok.Getter;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
|
@ -13,14 +15,30 @@ import java.util.*;
|
|||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class NetworkManager implements Closeable {
|
||||
/**
|
||||
* Builds the effective set of network resources by combining subscriptions, caches, and filters.
|
||||
*/
|
||||
public class NetworkingService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final static String CTX = NetworkManager.class.getSimpleName();
|
||||
private final Context context;
|
||||
private final File cacheFile;
|
||||
private final static String CTX = NetworkingService.class.getSimpleName();
|
||||
|
||||
public NetworkManager(Context context) {
|
||||
this.context = context;
|
||||
private final File cacheFile;
|
||||
private final EventListener<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");
|
||||
if (cacheFile.exists() && context.getConfig().isCachingAS()) {
|
||||
SystemLogger.message("Loading as cache file", CTX);
|
||||
|
|
@ -42,6 +60,9 @@ public class NetworkManager implements Closeable {
|
|||
@Getter
|
||||
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
|
||||
|
||||
/**
|
||||
* Indicates whether an update job is currently executing.
|
||||
*/
|
||||
public boolean isUpdatingNow() {
|
||||
var future = updateProcess.get();
|
||||
return future != null && !future.isDone() && !future.isCancelled();
|
||||
|
|
@ -49,6 +70,9 @@ public class NetworkManager implements Closeable {
|
|||
|
||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Schedules an update of network resources, optionally ignoring cached prefixes.
|
||||
*/
|
||||
public void triggerUpdate(boolean ignoreCache) {
|
||||
if (isUpdatingNow()) {
|
||||
return;
|
||||
|
|
@ -129,8 +153,11 @@ public class NetworkManager implements Closeable {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches prefixes for the given autonomous systems and stores them in the cache.
|
||||
*/
|
||||
private void fetchPrefixes(List<Integer> systems) {
|
||||
var service = context.getASInfoService();
|
||||
var service = context.getServiceManager().getService(BGPInfoService.class);
|
||||
systems.forEach(as -> {
|
||||
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
|
||||
var future = service.getPrefixes(as);
|
||||
|
|
@ -149,8 +176,12 @@ public class NetworkManager implements Closeable {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes event subscriptions and shuts down the executor.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getSubscriptionsUpdateEvent().remove(subscription);
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,58 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
||||
import ru.kirillius.pf.sdn.core.Util.Wait;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
public class UpdateManager implements Closeable {
|
||||
/**
|
||||
* Periodically refreshes subscription content and network data according to runtime settings.
|
||||
*/
|
||||
public class ResourceUpdateService extends AppService {
|
||||
private final Thread updateThread;
|
||||
|
||||
public UpdateManager(Context context) {
|
||||
this.context = context;
|
||||
/**
|
||||
* Creates the service and prepares the background update worker.
|
||||
*/
|
||||
public ResourceUpdateService(Context context) {
|
||||
super(context);
|
||||
|
||||
updateThread = new Thread(new ThreadWorker());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the update thread; can only be invoked once.
|
||||
*/
|
||||
public void start() {
|
||||
if (updateThread.isAlive()) {
|
||||
throw new IllegalStateException("Started already");
|
||||
}
|
||||
updateThread.start();
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Interrupts the update thread and stops scheduling tasks.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
updateThread.interrupt();
|
||||
}
|
||||
|
||||
private final static String CTX = UpdateManager.class.getSimpleName();
|
||||
private final static String CTX = ResourceUpdateService.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Background runner that performs scheduled resource maintenance tasks.
|
||||
*/
|
||||
private class ThreadWorker implements Runnable {
|
||||
|
||||
/**
|
||||
* Performs periodic subscription and network updates based on configuration intervals.
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
@ -43,7 +65,7 @@ public class UpdateManager implements Closeable {
|
|||
|
||||
if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval() * 60L) == 0) {
|
||||
SystemLogger.message("Updating subscriptions", CTX);
|
||||
var subscriptionManager = context.getSubscriptionManager();
|
||||
var subscriptionManager = context.getServiceManager().getService(SubscriptionService.class);
|
||||
subscriptionManager.triggerUpdate();
|
||||
Wait.until(subscriptionManager::isUpdatingNow);
|
||||
Wait.when(subscriptionManager::isUpdatingNow);
|
||||
|
|
@ -51,7 +73,7 @@ public class UpdateManager implements Closeable {
|
|||
|
||||
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
|
||||
SystemLogger.message("Updating cached AS", CTX);
|
||||
var networkManager = context.getNetworkManager();
|
||||
var networkManager = context.getServiceManager().getService(NetworkingService.class);
|
||||
networkManager.triggerUpdate(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.JSONSerializable;
|
||||
|
||||
/**
|
||||
* Describes a subscription source with its name, implementation type, and origin descriptor.
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JSONSerializable
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
|||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Loads network resource bundles from a repository described by {@link RepositoryConfig}.
|
||||
*/
|
||||
public interface SubscriptionProvider {
|
||||
Map<String, NetworkResourceBundle> getResources(RepositoryConfig config);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package ru.kirillius.pf.sdn.core.Subscription;
|
||||
|
||||
import lombok.Getter;
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -15,22 +15,30 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class SubscriptionManager implements Closeable {
|
||||
/**
|
||||
* Resolves subscription repositories, builds aggregated resource bundles, and publishes update events.
|
||||
*/
|
||||
public class SubscriptionService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final Context context;
|
||||
|
||||
|
||||
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
|
||||
|
||||
public SubscriptionManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
|
||||
|
||||
@Getter
|
||||
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
|
||||
|
||||
public SubscriptionService(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether a subscription update job is currently running.
|
||||
*/
|
||||
public boolean isUpdatingNow() {
|
||||
var future = updateProcess.get();
|
||||
return future != null && !future.isDone() && !future.isCancelled();
|
||||
|
|
@ -40,6 +48,9 @@ public class SubscriptionManager implements Closeable {
|
|||
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Starts a background task that refreshes subscription repositories and aggregates resources.
|
||||
*/
|
||||
public synchronized void triggerUpdate() {
|
||||
if (isUpdatingNow()) {
|
||||
return;
|
||||
|
|
@ -84,9 +95,12 @@ public class SubscriptionManager implements Closeable {
|
|||
}));
|
||||
}
|
||||
|
||||
private final static String CTX = SubscriptionManager.class.getSimpleName();
|
||||
private final static String CTX = SubscriptionService.class.getSimpleName();
|
||||
|
||||
|
||||
/**
|
||||
* Shuts down the executor used for update tasks.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
executor.shutdown();
|
||||
|
|
@ -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.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Provides hashing helpers for password management and compatibility utilities.
|
||||
*/
|
||||
public final class HashUtil {
|
||||
private HashUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MD5 digest for the supplied input string.
|
||||
*/
|
||||
public static String md5(String input) {
|
||||
try {
|
||||
var md = MessageDigest.getInstance("MD5");
|
||||
|
|
@ -29,6 +35,9 @@ public final class HashUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a salted SHA-512 hash, returning the hexadecimal representation.
|
||||
*/
|
||||
public static String hash(String data, String salt) {
|
||||
String generatedPassword = null;
|
||||
MessageDigest md = null;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* Helper methods for IPv4 address manipulation and subnet aggregation logic.
|
||||
*/
|
||||
public class IPv4Util {
|
||||
|
||||
private IPv4Util() {
|
||||
|
|
@ -19,12 +22,18 @@ public class IPv4Util {
|
|||
|
||||
private static final Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
|
||||
/**
|
||||
* Ensures the supplied string is a valid IPv4 address.
|
||||
*/
|
||||
public static void validateAddress(String address) {
|
||||
if (!pattern.matcher(address).matches()) {
|
||||
throw new IllegalArgumentException("Invalid IPv4 address: " + address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the prefix length is within the allowed range 0..32.
|
||||
*/
|
||||
public static void validatePrefix(int prefix) {
|
||||
if (prefix < 0 || prefix > 32) {
|
||||
throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
|
||||
|
|
@ -32,6 +41,9 @@ public class IPv4Util {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a dotted IPv4 address into its numeric representation.
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static long ipAddressToLong(String address) {
|
||||
validateAddress(address);
|
||||
|
|
@ -44,12 +56,18 @@ public class IPv4Util {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bit mask representing the provided prefix length.
|
||||
*/
|
||||
public static long calculateMask(int prefixLength) {
|
||||
validatePrefix(prefixLength);
|
||||
return 0xFFFFFFFFL << (32 - prefixLength);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a numeric IPv4 address into dotted notation.
|
||||
*/
|
||||
public static String longToIpAddress(long ipLong) {
|
||||
if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
|
||||
throw new IllegalArgumentException("Address number should be in range 0 - 4294967295");
|
||||
|
|
@ -57,6 +75,9 @@ public class IPv4Util {
|
|||
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a numeric mask into dotted notation.
|
||||
*/
|
||||
public static String maskToString(long maskLong) {
|
||||
return String.format("%d.%d.%d.%d",
|
||||
(maskLong >> 24) & 0xff,
|
||||
|
|
@ -65,12 +86,24 @@ public class IPv4Util {
|
|||
maskLong & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result contract for subnet summarisation operations.
|
||||
*/
|
||||
public interface SummarisationResult {
|
||||
/**
|
||||
* Returns the resulting set of subnets after summarisation.
|
||||
*/
|
||||
List<IPv4Subnet> getResult();
|
||||
|
||||
/**
|
||||
* Returns the original subnets that were merged during summarisation.
|
||||
*/
|
||||
Set<IPv4Subnet> getMergedSubnets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs subnet merging strategies based on overlap and utilisation heuristics.
|
||||
*/
|
||||
private static class SubnetSummaryUtility implements SummarisationResult {
|
||||
|
||||
@Getter
|
||||
|
|
@ -80,6 +113,9 @@ public class IPv4Util {
|
|||
private final Set<IPv4Subnet> mergedSubnets = new HashSet<>();
|
||||
|
||||
|
||||
/**
|
||||
* Builds the summarisation utility with the supplied subnets and usage threshold.
|
||||
*/
|
||||
public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) {
|
||||
source = subnets;
|
||||
result = new ArrayList<>(subnets);
|
||||
|
|
@ -90,6 +126,9 @@ public class IPv4Util {
|
|||
result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes redundant subnets wholly covered by other subnets.
|
||||
*/
|
||||
private void summaryOverlapped() {
|
||||
if (result.size() < 2) {
|
||||
return;
|
||||
|
|
@ -113,6 +152,9 @@ public class IPv4Util {
|
|||
overlapped.forEach(result::remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to merge adjacent subnets into larger ones while preserving coverage.
|
||||
*/
|
||||
private void mergeNeighbours() {
|
||||
if (result.size() < 2) {
|
||||
return;
|
||||
|
|
@ -155,22 +197,37 @@ public class IPv4Util {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest prefix length currently present in the result set.
|
||||
*/
|
||||
private int findMinPrefixLength() {
|
||||
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest prefix length currently present in the result set.
|
||||
*/
|
||||
private int findMaxPrefixLength() {
|
||||
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest network address found in the result set.
|
||||
*/
|
||||
private long findMinAddress() {
|
||||
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest network address found in the result set.
|
||||
*/
|
||||
private long findMaxAddress() {
|
||||
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces candidate subnets capable of covering the current set at the given prefix length.
|
||||
*/
|
||||
private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) {
|
||||
//создаём подсети-кандидаты, которые покроют наш список
|
||||
var maxAddress = findMaxAddress();
|
||||
|
|
@ -201,6 +258,9 @@ public class IPv4Util {
|
|||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the candidate subnet meets utilisation requirements and performs the merge.
|
||||
*/
|
||||
private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
|
||||
if (result.contains(candidate)) {
|
||||
return false;
|
||||
|
|
@ -234,6 +294,9 @@ public class IPv4Util {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iteratively merges subnets that satisfy the utilisation threshold.
|
||||
*/
|
||||
private void summaryWithUsage(int usePercentage) {
|
||||
if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
|
||||
return;
|
||||
|
|
@ -266,14 +329,23 @@ public class IPv4Util {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summaries the provided subnets using merging heuristics and utilisation thresholds.
|
||||
*/
|
||||
public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
|
||||
return new SubnetSummaryUtility(subnets, usePercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subnet covering the given address at the specified prefix length.
|
||||
*/
|
||||
private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
|
||||
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of addresses represented by a prefix length.
|
||||
*/
|
||||
public static long calculateCountForPrefixLength(long prefixLength) {
|
||||
return 1L << (32L - prefixLength);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@ package ru.kirillius.pf.sdn.core.Util;
|
|||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Provides blocking wait utilities for polling boolean conditions with interruption support.
|
||||
*/
|
||||
public final class Wait {
|
||||
private Wait() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until the supplied condition becomes true or the thread is interrupted.
|
||||
*/
|
||||
public static void until(Supplier<Boolean> condition) throws InterruptedException {
|
||||
if (condition == null) {
|
||||
return;
|
||||
|
|
@ -18,6 +24,9 @@ public final class Wait {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks while the supplied condition remains true or until interrupted.
|
||||
*/
|
||||
public static void when(Supplier<Boolean> condition) throws InterruptedException {
|
||||
if (condition == null) {
|
||||
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>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</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.time.Duration;
|
||||
|
||||
/**
|
||||
* CLI utility that requests managed routes from the SDN API and writes them into an OpenVPN push file.
|
||||
*/
|
||||
public class App {
|
||||
/**
|
||||
* Loads connector configuration from {@code ovpn-connector.json}.
|
||||
*/
|
||||
private static Config loadConfig() {
|
||||
var configFile = new File("ovpn-connector.json");
|
||||
if (!configFile.exists()) {
|
||||
|
|
@ -26,6 +32,9 @@ public class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the received routes to the OpenVPN push file.
|
||||
*/
|
||||
private static void pushRoutes(JSONArray routes, File pushFile) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) {
|
||||
for (var i = 0; i < routes.length(); i++) {
|
||||
|
|
@ -39,6 +48,9 @@ public class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Application entry point.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
var config = loadConfig();
|
||||
|
|
@ -56,6 +68,9 @@ public class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the JSON-RPC request to the SDN API and returns the route list.
|
||||
*/
|
||||
private static JSONArray sendRequest(Config config) throws IOException, InterruptedException {
|
||||
var json = new JSONObject();
|
||||
json.put("jsonrpc", "2.0");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import lombok.Setter;
|
|||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
|
||||
/**
|
||||
* Configuration for the OpenVPN connector utility, including API endpoint and token.
|
||||
*/
|
||||
@JSONSerializable
|
||||
@Getter
|
||||
@Setter
|
||||
|
|
|
|||
62
pom.xml
62
pom.xml
|
|
@ -6,12 +6,15 @@
|
|||
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
|
||||
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>app</module>
|
||||
<module>ovpn-connector</module>
|
||||
<module>launcher</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
|
@ -42,62 +45,6 @@
|
|||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</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>
|
||||
</build>
|
||||
|
||||
|
|
@ -116,7 +63,6 @@
|
|||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
|
|
|||
Loading…
Reference in New Issue