From 6c7017bc754e76a9437bf5242ec9ffa023c91cd1 Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:01:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(web):=20integrate=20embedded?= =?UTF-8?q?=20web=20server,=20REST/SSE=20APIs=20and=20modernize=20configur?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add embedded KLALBWebServer with REST API, SSE streaming (200ms) and SPA hosting - Introduce enableTUN configuration flag with fallback and non-admin execution support - Refactor LineTable and ConnectLineTable to openConnections and autoConnections - Rename denyLineTableQuery/Broadcast to denyConnectionQuery/Broadcast with backwards compatibility - Support runtime config hot-reloading and automatic persistence to klalb-config.json - Update default Web API port to 4665 and update dashboard submodule reference --- .gitignore | 9 + AGENTS.md | 18 +- dashboard | 2 +- klalb-config.json | 26 +- skills-lock.json | 11 + .../cloud/network/klalb/KLALBController.java | 15 +- .../klalb/KLALBControllerConfigItem.java | 118 ++- .../kne/cloud/network/klalb/KLALBMain.java | 44 + .../cloud/network/klalb/KLALBProxySystem.java | 95 +- .../network/klalb/web/KLALBWebServer.java | 876 ++++++++++++++++++ .../network/srv6/KLALBRoutingProtocol.java | 4 + .../srv6/KLALBRoutingProtocolAPIServer.java | 2 +- 12 files changed, 1148 insertions(+), 72 deletions(-) create mode 100644 skills-lock.json create mode 100644 src/org/kne/cloud/network/klalb/web/KLALBWebServer.java diff --git a/.gitignore b/.gitignore index 52b1fb8..20854e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ /klalbs4.json /klalbs.json /klalbs2.json + +# Dashboard / Frontend +dashboard/node_modules/ +dashboard/dist/ +dashboard/.pnpm-store/ +.pnpm-debug.log* +dashboard/.env.local +dashboard/.env.*.local + diff --git a/AGENTS.md b/AGENTS.md index 62671b6..ba90dc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ IDE metadata targets JDK 26 (`jdk-26.0.1`); the tree also compiles cleanly on JD Runtime gotchas (all verified): - On JDK 25, `KNEOptimize.jar`'s `FastLib` reflects into `jdk.internal.misc.Unsafe`; without the two JVM flags above it throws `InaccessibleObjectException` at startup (app still runs). - Creating the SRv6 TUN adapter (`WintunCreateAdapter`) requires an elevated shell; without admin rights it logs "创建虚拟网卡失败" and continues with only the `inLoopBack` interface — links/bridges still work. -- To disable TUN creation completely (e.g. for non-admin UI/routing testing), set `"TUNName": null` in `klalb-config.json` or leave the TUN Name empty in GUI settings. If omitted, it defaults to `"KLALB_SRv6"`. +- To disable TUN creation completely (e.g. for non-admin UI/routing testing), set `"enableTUN": false` in `klalb-config.json` or toggle off "启用 TUN 虚拟网卡" in GUI/Web settings. - Routing broadcast (`RouterInfo`) transmits `deviceName` across the network, which topology and node overview panels display. `deviceDescription` and `ExtraRoutes` remain local controller configs. ## Verification @@ -35,7 +35,10 @@ No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `ma ## Architecture -- Entrypoint `org.kne.cloud.network.klalb.KLALBMain`: load config → build `KLALBProxySystem` → open Swing GUI (`KLALBStateGUI3`) unless `"nogui": true` → interactive console (`help`, `links-state`, `route`, `kperf`, ...). +- Entrypoint `org.kne.cloud.network.klalb.KLALBMain`: load config → build `KLALBProxySystem` → open Swing GUI (`KLALBStateGUI3`) unless `"nogui": true` → start `KLALBWebServer` (if `"webUI": true` or web server enabled) → interactive console (`help`, `links-state`, `route`, `kperf`, ...). +- `org.kne.cloud.network.klalb.web.KLALBWebServer` — built-in HTTP/SSE server (JDK `HttpServer`): + - API endpoints: `/api/status`, `/api/events` (SSE stream, 200ms intervals), `/api/links`, `/api/links/action`, `/api/links/reconnect`, `/api/routes`, `/api/nodes` (topology graph), `/api/interfaces`, `/api/config`. + - Static file hosting / SPA fallback: serves `dashboard/dist/` assets if built. - `org.kne.cloud.network` — generic socket framework: `VirtualSocket*` hierarchy, `SocketBridge` port-forwarding proxies, `ProtocolDetector` (multi-protocol mux on one port), `MultiProtocolSocketAddress` = URI-style addresses (`tcp://`, `udp://`, `kltp://`, `ntp://`) dispatched through the `SocketType` registry. - `...network.klalb` — app core: `KLALBController` (the virtual SRv6 network), `KLALBRemoteLink` (WAN lines), `*Packet` wire-format classes, virtual socket implementations. - `...network.congestion` — pluggable congestion control (BBR, Vegas2, DCTCP...), chosen via `"congestionAlgorithm"` in config. @@ -44,6 +47,17 @@ No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `ma - `...network.frpc` — frp client integration. - `...klalb.ui` — all Swing UI code. +## Frontend (Dashboard) + +Located in `dashboard/`: +- **Stack**: Vite + React 19 + TypeScript + Tailwind CSS v4 + `@base-ui/react` (style: `base-nova`, icons: `lucide-react`). +- **Package Manager**: `pnpm` (run all commands from `dashboard/` directory). +- **Component installation**: **Must** use CLI via `pnpm dlx shadcn@latest add ` (e.g. `pnpm dlx shadcn@latest add alert card badge`). Never create or fake shadcn components manually. +- **Commands**: + - `pnpm dev` — Start Vite dev server (proxies to backend or connects to API on localhost). + - `pnpm build` — Typecheck and build SPA to `dashboard/dist` (which Java `KLALBWebServer` serves directly). + - `pnpm lint` / `pnpm typecheck` — Verification. + ## Config `klalb-config.json` is an array of items discriminated by their `"Type"` field. Adding a new item type requires a `KLALBConfigItem` subclass **plus** new cases in both `KLALBConfigItem.getDefaultJsonDeserializer()` and `getDefaultJsonSerializer()`; unknown types are preserved as `UnknownKLALBConfigItem`. Any Gson instance handling config must register these adapters via `registerToGsonBuilder` (see `KLALBProxySystem`). diff --git a/dashboard b/dashboard index ba65c43..4531e53 160000 --- a/dashboard +++ b/dashboard @@ -1 +1 @@ -Subproject commit ba65c43183784044809f17245c2654b4a873a5dc +Subproject commit 4531e5366bb652e6db116a3848b1d25da1c796e4 diff --git a/klalb-config.json b/klalb-config.json index 9be7de6..c1b8c4e 100644 --- a/klalb-config.json +++ b/klalb-config.json @@ -10,16 +10,19 @@ "TCPListen": "tcp://0.0.0.0:4565", "UDPListen": "udp://0.0.0.0:4572", "VirtualSocketName": "kltp", - "LineTable": [ + "openConnections": [ "tcp://kne03.yoyo250.fun:4565", - "tcp://kne04.yoyo250.fun:4565" - ], - "ConnectLineTable": [ + "tcp://kne04.yoyo250.fun:4565", "tcp://07f4acdef99b.ofalias.net:4565", "tcp://kne01.yoyo250.fun:4565", "tcp://kne02.yoyo250.fun:4565" ], - "ntpServerTable": [ + "autoConnections": [ + "tcp://07f4acdef99b.ofalias.net:4565", + "tcp://kne01.yoyo250.fun:4565", + "tcp://kne02.yoyo250.fun:4565" + ], + "ntpServers": [ "ntp://ntp1.aliyun.com", "ntp://ntp2.aliyun.com", "ntp://ntp3.aliyun.com", @@ -38,8 +41,8 @@ "ntp://us.ntp.org.cn" ], "ExtraRoutes": [], - "denyLineTableQuery": false, - "denyLineTableBroadcast": false, + "denyConnectionQuery": false, + "denyConnectionBroadcast": false, "congestionAlgorithm": "BBR", "burstLimit": 2.0, "delayUpperBound": 1.2, @@ -47,10 +50,13 @@ "nagleDelayTime": 1000000, "linkNagleDelayTime": 0, "linkConnectionsCount": 1, - "TUNName": null, + "enableTUN": false, + "TUNName": "KLALB_SRv6", "performanceStrategy": "multiscatter", - "DeviceName": "SerinaNya PC", - "DeviceDescription": "desc", + "DeviceName": "Device Name", + "DeviceDescription": "The Description of Device", + "webUI": true, + "webPort": 4665, "NetworkInterfaceExcepts": [], "Type": "KLALBController" }, diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..ddb729d --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "shadcn": { + "source": "shadcn/ui", + "sourceType": "github", + "skillPath": "skills/shadcn/SKILL.md", + "computedHash": "c1a68ee06a668aced9ab2b5fbdea5f989864123794eb2e056b339a072dbb7f10" + } + } +} diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index 3eb1857..1413d65 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -210,7 +210,7 @@ public class KLALBController { for (Iterator iterator = ipmd.iterator(); iterator.hasNext(); ) { IPMulticastDiscovery ipMulticastDiscovery = (IPMulticastDiscovery) iterator.next(); if (ipMulticastDiscovery.isClosed() || (!ipMulticastDiscovery.getInterface().isUp()) - || (configItem != null && configItem.isDenyLineTableBroadcast())) { + || (configItem != null && configItem.isDenyConnectionBroadcast())) { iterator.remove(); try { ipMulticastDiscovery.close(); @@ -222,7 +222,7 @@ public class KLALBController { } } - if (configItem != null && configItem.isDenyLineTableBroadcast()) { + if (configItem != null && configItem.isDenyConnectionBroadcast()) { } else { List interfaceList = networkInterfaceManager.getAllAvaliableNetworkInterface(); @@ -647,8 +647,9 @@ public class KLALBController { rawPortBinder= new PortBinder(this.getSelf().getAddress()); System.out.println(" Loaded: SRv6 Stack"); - String name=(configItem!=null)?configItem.getTUNName():CONST.KLALB_S_RV6; - boolean enableTUN = enableVirtualAdapter && (name != null) && !name.trim().isEmpty() && !name.trim().equalsIgnoreCase("null"); + String name = (configItem != null && configItem.getTUNName() != null) ? configItem.getTUNName() : CONST.KLALB_S_RV6; + boolean isEnabled = configItem == null || configItem.isEnableTUN(); + boolean enableTUN = enableVirtualAdapter && isEnabled && (name != null) && !name.trim().isEmpty() && !name.trim().equalsIgnoreCase("null"); if (enableTUN) { Thread t=new Thread(()->{ try { @@ -800,18 +801,18 @@ public class KLALBController { getIpv6Router().setASN(vasn); } - List linele = configItem.getLineTable(); + List linele = configItem.getOpenConnections(); if (linele != null) { getSelflineTable().addAll(linele); } - List linetoc = configItem.getConnectLineTable(); + List linetoc = configItem.getAutoConnections(); if (linetoc != null) { linetoc.forEach((aline) -> { addRemoteLines(aline); }); } - List ntps = configItem.getNtpServerTable(); + List ntps = configItem.getNtpServers(); if (ntps != null) { getNTPTable().addAll(ntps); } diff --git a/src/org/kne/cloud/network/klalb/KLALBControllerConfigItem.java b/src/org/kne/cloud/network/klalb/KLALBControllerConfigItem.java index 76d6bfa..0b51c1a 100644 --- a/src/org/kne/cloud/network/klalb/KLALBControllerConfigItem.java +++ b/src/org/kne/cloud/network/klalb/KLALBControllerConfigItem.java @@ -16,12 +16,12 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { private MultiProtocolSocketAddress TCPListen=new MultiProtocolSocketAddress("0.0.0.0",4565); private MultiProtocolSocketAddress UDPListen=new MultiProtocolSocketAddress("udp","0.0.0.0",4565); private String VirtualSocketName; - private ListLineTable=new ArrayList<>(); - private ListConnectLineTable=new ArrayList<>(); - private ListntpServerTable=new ArrayList<>(); - private ListExtraRoutes=new ArrayList<>(); - private boolean denyLineTableQuery=false; - private boolean denyLineTableBroadcast=false; + private List openConnections = new ArrayList<>(); + private List autoConnections = new ArrayList<>(); + private List ntpServers = new ArrayList<>(); + private List ExtraRoutes = new ArrayList<>(); + private boolean denyConnectionQuery = false; + private boolean denyConnectionBroadcast = false; private String congestionAlgorithm="BBR"; private double burstLimit=1.50; private double delayUpperBound=1.20; @@ -29,10 +29,37 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { private long nagleDelayTime=1000000L; private long linkNagleDelayTime=1000000L; private int linkConnectionsCount=1; + private boolean enableTUN = true; private String TUNName=CONST.KLALB_S_RV6; private String performanceStrategy="multifill"; private String DeviceName; private String DeviceDescription; + private boolean webUI = false; + private int webPort = 4665; + + public boolean isEnableTUN() { + return enableTUN; + } + + public void setEnableTUN(boolean enableTUN) { + this.enableTUN = enableTUN; + } + + public boolean isWebUI() { + return webUI; + } + + public void setWebUI(boolean webUI) { + this.webUI = webUI; + } + + public int getWebPort() { + return webPort; + } + + public void setWebPort(int webPort) { + this.webPort = webPort; + } public String getPerformanceStrategy() { return performanceStrategy; @@ -177,39 +204,52 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { - public List getLineTable() { - return LineTable; + public List getOpenConnections() { + return openConnections; } + public void setOpenConnections(List openConnections) { + this.openConnections = openConnections; + } + public List getLineTable() { + return openConnections; + } public void setLineTable(List lineTable) { - LineTable = lineTable; + this.openConnections = lineTable; } + public List getAutoConnections() { + return autoConnections; + } + public void setAutoConnections(List autoConnections) { + this.autoConnections = autoConnections; + } public List getConnectLineTable() { - return ConnectLineTable; + return autoConnections; } - - public void setConnectLineTable(List connectLineTable) { - ConnectLineTable = connectLineTable; + this.autoConnections = connectLineTable; } - + public List getNtpServers() { + return ntpServers; + } + + public void setNtpServers(List ntpServers) { + this.ntpServers = ntpServers; + } public List getNtpServerTable() { - return ntpServerTable; + return ntpServers; } - - - public void setNtpServerTable(List ntpServerTable) { - this.ntpServerTable = ntpServerTable; + this.ntpServers = ntpServerTable; } public List getExtraRoutes() { @@ -280,23 +320,36 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { - public boolean isDenyLineTableQuery() { - return denyLineTableQuery; + public boolean isDenyConnectionQuery() { + return denyConnectionQuery; } + public void setDenyConnectionQuery(boolean denyConnectionQuery) { + this.denyConnectionQuery = denyConnectionQuery; + } + + public boolean isDenyLineTableQuery() { + return denyConnectionQuery; + } public void setDenyLineTableQuery(boolean denyLineTableQuery) { - this.denyLineTableQuery = denyLineTableQuery; + this.denyConnectionQuery = denyLineTableQuery; } + public boolean isDenyConnectionBroadcast() { + return denyConnectionBroadcast; + } + + public void setDenyConnectionBroadcast(boolean denyConnectionBroadcast) { + this.denyConnectionBroadcast = denyConnectionBroadcast; + } public boolean isDenyLineTableBroadcast() { - return denyLineTableBroadcast; + return denyConnectionBroadcast; } - public void setDenyLineTableBroadcast(boolean denyLineTableBroadcast) { - this.denyLineTableBroadcast = denyLineTableBroadcast; + this.denyConnectionBroadcast = denyLineTableBroadcast; } @@ -352,12 +405,12 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { ", TCPListen=" + TCPListen + ", UDPListen=" + UDPListen + ", VirtualSocketName='" + VirtualSocketName + '\'' + - ", LineTable=" + LineTable + - ", ConnectLineTable=" + ConnectLineTable + - ", ntpServerTable=" + ntpServerTable + + ", openConnections=" + openConnections + + ", autoConnections=" + autoConnections + + ", ntpServers=" + ntpServers + ", ExtraRoutes=" + ExtraRoutes + - ", denyLineTableQuery=" + denyLineTableQuery + - ", denyLineTableBroadcast=" + denyLineTableBroadcast + + ", denyConnectionQuery=" + denyConnectionQuery + + ", denyConnectionBroadcast=" + denyConnectionBroadcast + ", congestionAlgorithm='" + congestionAlgorithm + '\'' + ", burstLimit=" + burstLimit + ", delayUpperBound=" + delayUpperBound + @@ -365,6 +418,7 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { ", nagleDelayTime=" + nagleDelayTime + ", linkNagleDelayTime=" + linkNagleDelayTime + ", linkConnectionsCount=" + linkConnectionsCount + + ", enableTUN=" + enableTUN + ", TUNName='" + TUNName + '\'' + ", performanceStrategy='" + performanceStrategy + '\'' + ", DeviceName='" + DeviceName + '\'' + @@ -378,11 +432,11 @@ public class KLALBControllerConfigItem extends KLALBConfigItem { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; KLALBControllerConfigItem that = (KLALBControllerConfigItem) o; - return nogui == that.nogui && denyLineTableQuery == that.denyLineTableQuery && denyLineTableBroadcast == that.denyLineTableBroadcast && Double.compare(burstLimit, that.burstLimit) == 0 && Double.compare(delayUpperBound, that.delayUpperBound) == 0 && Double.compare(delayLowerBound, that.delayLowerBound) == 0 && nagleDelayTime == that.nagleDelayTime && linkNagleDelayTime == that.linkNagleDelayTime && linkConnectionsCount == that.linkConnectionsCount && Objects.equals(language, that.language) && Objects.equals(VirtualAddress, that.VirtualAddress) && Objects.equals(VirtualASN, that.VirtualASN) && Objects.equals(DNS, that.DNS) && Objects.equals(TCPListen, that.TCPListen) && Objects.equals(UDPListen, that.UDPListen) && Objects.equals(VirtualSocketName, that.VirtualSocketName) && Objects.equals(LineTable, that.LineTable) && Objects.equals(ConnectLineTable, that.ConnectLineTable) && Objects.equals(ntpServerTable, that.ntpServerTable) && Objects.equals(ExtraRoutes, that.ExtraRoutes) && Objects.equals(congestionAlgorithm, that.congestionAlgorithm) && Objects.equals(TUNName, that.TUNName) && Objects.equals(performanceStrategy, that.performanceStrategy) && Objects.equals(DeviceName, that.DeviceName) && Objects.equals(DeviceDescription, that.DeviceDescription) && Objects.equals(NetworkInterfaceExcepts, that.NetworkInterfaceExcepts); + return nogui == that.nogui && enableTUN == that.enableTUN && denyConnectionQuery == that.denyConnectionQuery && denyConnectionBroadcast == that.denyConnectionBroadcast && Double.compare(burstLimit, that.burstLimit) == 0 && Double.compare(delayUpperBound, that.delayUpperBound) == 0 && Double.compare(delayLowerBound, that.delayLowerBound) == 0 && nagleDelayTime == that.nagleDelayTime && linkNagleDelayTime == that.linkNagleDelayTime && linkConnectionsCount == that.linkConnectionsCount && Objects.equals(language, that.language) && Objects.equals(VirtualAddress, that.VirtualAddress) && Objects.equals(VirtualASN, that.VirtualASN) && Objects.equals(DNS, that.DNS) && Objects.equals(TCPListen, that.TCPListen) && Objects.equals(UDPListen, that.UDPListen) && Objects.equals(VirtualSocketName, that.VirtualSocketName) && Objects.equals(openConnections, that.openConnections) && Objects.equals(autoConnections, that.autoConnections) && Objects.equals(ntpServers, that.ntpServers) && Objects.equals(ExtraRoutes, that.ExtraRoutes) && Objects.equals(congestionAlgorithm, that.congestionAlgorithm) && Objects.equals(TUNName, that.TUNName) && Objects.equals(performanceStrategy, that.performanceStrategy) && Objects.equals(DeviceName, that.DeviceName) && Objects.equals(DeviceDescription, that.DeviceDescription) && Objects.equals(NetworkInterfaceExcepts, that.NetworkInterfaceExcepts); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), language, nogui, VirtualAddress, VirtualASN, DNS, TCPListen, UDPListen, VirtualSocketName, LineTable, ConnectLineTable, ntpServerTable, ExtraRoutes, denyLineTableQuery, denyLineTableBroadcast, congestionAlgorithm, burstLimit, delayUpperBound, delayLowerBound, nagleDelayTime, linkNagleDelayTime, linkConnectionsCount, TUNName, performanceStrategy, DeviceName, DeviceDescription, NetworkInterfaceExcepts); + return Objects.hash(super.hashCode(), language, nogui, VirtualAddress, VirtualASN, DNS, TCPListen, UDPListen, VirtualSocketName, openConnections, autoConnections, ntpServers, ExtraRoutes, denyConnectionQuery, denyConnectionBroadcast, congestionAlgorithm, burstLimit, delayUpperBound, delayLowerBound, nagleDelayTime, linkNagleDelayTime, linkConnectionsCount, enableTUN, TUNName, performanceStrategy, DeviceName, DeviceDescription, NetworkInterfaceExcepts); } } diff --git a/src/org/kne/cloud/network/klalb/KLALBMain.java b/src/org/kne/cloud/network/klalb/KLALBMain.java index a3e9f66..72380ea 100644 --- a/src/org/kne/cloud/network/klalb/KLALBMain.java +++ b/src/org/kne/cloud/network/klalb/KLALBMain.java @@ -52,6 +52,13 @@ public class KLALBMain { }catch(Throwable e) { e.printStackTrace(); } + try { + if(kpcje.getControllerConfig() != null && kpcje.getControllerConfig().isWebUI()) { + kpcje.enableWebServer(); + } + } catch(Throwable e) { + System.err.println("Failed to start web server: " + e.getMessage()); + } dtb.putTime("UI"); //dtb.print(); /*MultipurposeSocketAddress mpa=new MultipurposeSocketAddress("127.9.9.9", 49573); @@ -84,6 +91,7 @@ public class KLALBMain { case "help": System.out.println(" help / ?: see help"); System.out.println(" monitor: show monitor GUI"); + System.out.println(" web [start|stop|status ]: manage web dashboard"); System.out.println(" links-state: query link states"); System.out.println(" links-add : add link"); System.out.println(" links-remove : remove link"); @@ -102,6 +110,42 @@ public class KLALBMain { e.printStackTrace(); } break; + case "web": + if(sc.length >= 2) { + String action = sc[1].toLowerCase(); + if("start".equals(action)) { + int p = 4665; + if(sc.length >= 3) { + try { p = Integer.parseInt(sc[2]); } catch (NumberFormatException ignored) {} + } else if(kpcje.getControllerConfig() != null && kpcje.getControllerConfig().getWebPort() > 0) { + p = kpcje.getControllerConfig().getWebPort(); + } + try { + kpcje.enableWebServer(p); + System.out.println("Web dashboard started on http://localhost:" + p); + } catch(Exception e) { + System.out.println("Failed to start web server: " + e.getMessage()); + } + } else if("stop".equals(action)) { + kpcje.disableWebServer(); + System.out.println("Web dashboard stopped."); + } else if("status".equals(action)) { + if(kpcje.isWebServerEnabled()) { + System.out.println("Web dashboard is running on port " + kpcje.getWebServer().getPort()); + } else { + System.out.println("Web dashboard is stopped."); + } + } else { + System.out.println("Usage: web [start|stop|status ]"); + } + } else { + if(kpcje.isWebServerEnabled()) { + System.out.println("Web dashboard is running on port " + kpcje.getWebServer().getPort()); + } else { + System.out.println("Web dashboard is not running. Use 'web start [port]' to start."); + } + } + break; case "links-state": System.out.println("links state:"); //System.out.println("状态\t上传流量\t下载流量\t上传速度\t下载速度\t上传延迟\t下载延迟\t上传抖动\t下载抖动"); diff --git a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java index d1200ef..97c264a 100644 --- a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java +++ b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java @@ -5,25 +5,35 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; -import java.net.InetSocketAddress; +import java.lang.reflect.Type; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.util.HashSet; import org.kne.cloud.network.*; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; import org.kne.cloud.network.klalb.ui.Language; import org.kne.cloud.network.klalb.ui.UIEnv; +import org.kne.cloud.network.klalb.web.KLALBWebServer; import java.util.Set; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; import com.google.gson.JsonParser; public class KLALBProxySystem { private Set proxys=new HashSet<>(); private KLALBController klalbController; private KLALBRemoteManagement krm; + private KLALBWebServer webServer; private KLALBConfig config; private Gson gson; private File jsonFile; @@ -31,8 +41,28 @@ public class KLALBProxySystem { GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); MultiProtocolSocketAddress.registerToGsonBuilder(gb); KLALBConfigItem.registerToGsonBuilder(gb); + gb.registerTypeAdapter(InetAddress.class, new JsonSerializer() { + @Override + public JsonElement serialize(InetAddress src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getHostAddress()); + } + }); + gb.registerTypeAdapter(InetAddress.class, new JsonDeserializer() { + @Override + public InetAddress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return InetAddress.getByName(json.getAsString()); + } catch (Exception e) { + throw new JsonParseException(e); + } + } + }); gson=gb.create(); } + public Gson getGson() { + return gson; + } + public Set getProxys() { return proxys; } @@ -81,6 +111,39 @@ public class KLALBProxySystem { krm=null; } } + + public void enableWebServer() throws IOException { + int port = 4665; + KLALBControllerConfigItem cci = getControllerConfig(); + if (cci != null && cci.getWebPort() > 0) { + port = cci.getWebPort(); + } + enableWebServer(port); + } + + public void enableWebServer(int port) throws IOException { + if (webServer == null) { + webServer = new KLALBWebServer(this, port); + webServer.start(); + } else { + throw new IllegalStateException("Web server already enabled!"); + } + } + + public boolean isWebServerEnabled() { + return webServer != null && webServer.isRunning(); + } + + public KLALBWebServer getWebServer() { + return webServer; + } + + public void disableWebServer() { + if (webServer != null) { + webServer.stop(); + webServer = null; + } + } public void loadConfigJson(File jsonFile) throws IOException { this.jsonFile=jsonFile; @@ -195,30 +258,24 @@ public class KLALBProxySystem { } + public void saveConfigToFile() { + if (jsonFile != null && config != null) { + String json = gson.toJson(config); + try (FileWriter fw = new FileWriter(jsonFile)) { + fw.write(json); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + private KLALBStateGUI3 kgui; public KLALBStateGUI3 getKLALBGUI() { if(kgui==null) { kgui=new KLALBStateGUI3(klalbController); kgui.loadConfig(config); kgui.setSaveComsumer((cfg)->{ - String json=gson.toJson(cfg); - if(jsonFile!=null) { - FileWriter fw = null; - try { - fw=new FileWriter(jsonFile); - fw.write(json); - }catch(IOException e) { - e.printStackTrace(); - }finally { - if(fw!=null) - try { - fw.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - + saveConfigToFile(); }); } return kgui; diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java new file mode 100644 index 0000000..c6cba48 --- /dev/null +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -0,0 +1,876 @@ +package org.kne.cloud.network.klalb.web; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.sun.net.httpserver.*; +import com.google.gson.*; + +import org.kne.cloud.clock.HighAccuracyClock; +import org.kne.cloud.network.MultiProtocolSocketAddress; +import org.kne.cloud.network.ipv6.IPv6Address; +import org.kne.cloud.network.ipv6.IPv6AddressGroup; +import org.kne.cloud.network.ipv6.IPv6NetworkLink; +import org.kne.cloud.network.ipv6.RouteItem; +import org.kne.cloud.network.klalb.*; +import org.kne.cloud.network.monitor.LinkStatus; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; +import org.kne.cloud.network.srv6.NeighborInfo; +import org.kne.cloud.network.srv6.RouterInfo; +import org.kne.cloud.network.srv6.SRv6Router; + +/** + * KLALB Web Server providing REST API, SSE live event stream, + * and static dashboard hosting. + */ +public class KLALBWebServer { + private final KLALBProxySystem proxySystem; + private final int port; + private HttpServer server; + private final Gson gson; + private final ScheduledExecutorService sseExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "KLALB-Web-SSE"); + t.setDaemon(true); + return t; + }); + private final Set sseClients = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final AtomicBoolean running = new AtomicBoolean(false); + + public KLALBWebServer(KLALBProxySystem proxySystem, int port) { + this.proxySystem = proxySystem; + this.port = port; + this.gson = proxySystem.getGson(); + } + + public synchronized void start() throws IOException { + if (running.get()) return; + + server = HttpServer.create(new InetSocketAddress(port), 0); + server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + + // API Contexts + server.createContext("/api/status", this::handleStatus); + server.createContext("/api/events", this::handleEvents); + server.createContext("/api/links", this::handleLinks); + server.createContext("/api/links/action", this::handleLinkAction); + server.createContext("/api/links/reconnect", this::handleReconnectAll); + server.createContext("/api/routes", this::handleRoutes); + server.createContext("/api/nodes", this::handleNodes); + server.createContext("/api/interfaces", this::handleInterfaces); + server.createContext("/api/config", this::handleConfig); + + // Static Files / SPA Fallback Handler + server.createContext("/", this::handleStatic); + + server.start(); + running.set(true); + startSseBroadcaster(); + + System.out.println("KLALB Web Dashboard started at http://localhost:" + port); + } + + public synchronized void stop() { + if (!running.get()) return; + running.set(false); + sseExecutor.shutdownNow(); + for (HttpExchange client : sseClients) { + try { + client.close(); + } catch (Exception ignored) {} + } + sseClients.clear(); + if (server != null) { + server.stop(1); + server = null; + } + System.out.println("KLALB Web Dashboard stopped."); + } + + public boolean isRunning() { + return running.get(); + } + + public int getPort() { + return port; + } + + private void setCorsHeaders(HttpExchange exchange) { + Headers headers = exchange.getResponseHeaders(); + headers.set("Access-Control-Allow-Origin", "*"); + headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept"); + } + + private boolean handleCorsPreflight(HttpExchange exchange) throws IOException { + setCorsHeaders(exchange); + if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + exchange.close(); + return true; + } + return false; + } + + private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException { + setCorsHeaders(exchange); + byte[] bytes = gson.toJson(data).getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException { + JsonObject error = new JsonObject(); + error.addProperty("error", message); + sendJsonResponse(exchange, statusCode, error); + } + + private String readRequestBody(HttpExchange exchange) throws IOException { + try (InputStream is = exchange.getRequestBody()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + // ------------------------------------------------------------- + // API Handlers + // ------------------------------------------------------------- + + private JsonObject getStatusJson() { + JsonObject root = new JsonObject(); + KLALBController kc = proxySystem.getKlalbController(); + if (kc == null) { + root.addProperty("status", "not_ready"); + return root; + } + + root.addProperty("version", CONST.klalbver); + root.addProperty("title", CONST.klalb); + + IPv6AddressGroup self = kc.getSelf(); + root.addProperty("address", self != null ? self.getAddress().toString() : ""); + + SRv6Router router = kc.getIpv6Router(); + if (router != null && router.getKlalbRouteProtol() != null) { + root.addProperty("onlineDevices", router.getKlalbRouteProtol().getDevicesFound()); + root.addProperty("deviceName", router.getDeviceName() != null ? router.getDeviceName() : ""); + } else { + root.addProperty("onlineDevices", 0); + root.addProperty("deviceName", ""); + } + + KLALBControllerConfigItem configItem = proxySystem.getControllerConfig(); + if (configItem != null) { + root.addProperty("deviceDescription", configItem.getDeviceDescription() != null ? configItem.getDeviceDescription() : ""); + root.addProperty("enableTUN", configItem.isEnableTUN()); + root.addProperty("tunName", configItem.isEnableTUN() && configItem.getTUNName() != null ? configItem.getTUNName() : ""); + root.addProperty("congestionAlgorithm", configItem.getCongestionAlgorithm() != null ? configItem.getCongestionAlgorithm() : ""); + root.addProperty("performanceStrategy", configItem.getPerformanceStrategy() != null ? configItem.getPerformanceStrategy() : ""); + } + + // Clock + HighAccuracyClock hac = kc.getClock(); + if (hac != null) { + long delta = hac.getFrequency() - 1000000000; + double ppm = delta / 1000.0; + root.addProperty("timeStr", hac.toString2()); + root.addProperty("timePpm", ppm); + } + + // Metrics + JsonObject metrics = new JsonObject(); + if (kc.getLinkMonitor() != null) { + metrics.addProperty("upSpeed", kc.getLinkMonitor().getOutSpeed()); + metrics.addProperty("downSpeed", kc.getLinkMonitor().getInSpeed()); + metrics.addProperty("upSpeedMax", kc.getLinkMonitor().getOutSpeedMax()); + metrics.addProperty("downSpeedMax", kc.getLinkMonitor().getInSpeedMax()); + metrics.addProperty("upPPS", kc.getLinkMonitor().getOutPPS()); + metrics.addProperty("downPPS", kc.getLinkMonitor().getInPPS()); + metrics.addProperty("upPPSMax", kc.getLinkMonitor().getOutPPSMax()); + metrics.addProperty("downPPSMax", kc.getLinkMonitor().getInPPSMax()); + metrics.addProperty("upTraffic", kc.getLinkMonitor().getOutTraffic()); + metrics.addProperty("downTraffic", kc.getLinkMonitor().getInTraffic()); + } + + if (kc.getDatatMonitor() != null) { + metrics.addProperty("dataUpSpeed", kc.getDatatMonitor().getOutSpeed()); + metrics.addProperty("dataDownSpeed", kc.getDatatMonitor().getInSpeed()); + metrics.addProperty("dataUpPPS", kc.getDatatMonitor().getOutPPS()); + metrics.addProperty("dataDownPPS", kc.getDatatMonitor().getInPPS()); + metrics.addProperty("dataUpTraffic", kc.getDatatMonitor().getOutTraffic()); + metrics.addProperty("dataDownTraffic", kc.getDatatMonitor().getInTraffic()); + } + + if (router != null) { + metrics.addProperty("backplaneDelay", router.getBackplaneTime()); + metrics.addProperty("backplanePPS", router.getBackplanePPS()); + metrics.addProperty("backplanePPSMax", router.getBackplanePPSMax()); + metrics.addProperty("backplaneECNRate", router.getECNRate()); + metrics.addProperty("backplaneLossRate", router.getLossRate()); + } + root.add("metrics", metrics); + + return root; + } + + private void handleStatus(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + sendJsonResponse(exchange, 200, getStatusJson()); + } + + private JsonArray getLinksJson() { + JsonArray array = new JsonArray(); + KLALBController kc = proxySystem.getKlalbController(); + if (kc == null) return array; + + List linesList = kc.getLines(); + synchronized (linesList) { + for (IPv6NetworkLink link : linesList) { + if (!(link instanceof KLALBRemoteLink)) continue; + KLALBRemoteLink remoteLink = (KLALBRemoteLink) link; + + JsonObject obj = new JsonObject(); + obj.addProperty("name", remoteLink.getName()); + obj.addProperty("state", LinkStatus.stateToString(remoteLink.getState())); + obj.addProperty("stateCode", remoteLink.getState()); + + MultiProtocolSocketAddress mpsa = remoteLink.getSocketAddress(); + obj.addProperty("socketAddress", mpsa != null ? mpsa.toString() : ""); + + IPv6AddressGroup vaddr = remoteLink.getRemoteVaddr(); + obj.addProperty("vaddr", vaddr != null ? vaddr.getAddress().toString() : ""); + + if (remoteLink.getMonitor() != null) { + obj.addProperty("upSpeed", remoteLink.getMonitor().getOutSpeed()); + obj.addProperty("downSpeed", remoteLink.getMonitor().getInSpeed()); + obj.addProperty("upPPS", remoteLink.getMonitor().getOutPPS()); + obj.addProperty("downPPS", remoteLink.getMonitor().getInPPS()); + obj.addProperty("upTraffic", remoteLink.getMonitor().getOutTraffic()); + obj.addProperty("downTraffic", remoteLink.getMonitor().getInTraffic()); + obj.addProperty("upDelay", remoteLink.getMonitor().getOutDelay()); + obj.addProperty("downDelay", remoteLink.getMonitor().getInDelay()); + obj.addProperty("upDelayMin", remoteLink.getMonitor().getOutDelayMin()); + obj.addProperty("downDelayMin", remoteLink.getMonitor().getInDelayMin()); + obj.addProperty("upJitter", remoteLink.getMonitor().getOutJitter()); + obj.addProperty("downJitter", remoteLink.getMonitor().getInJitter()); + } + array.add(obj); + } + } + return array; + } + + private void handleLinks(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + String method = exchange.getRequestMethod(); + KLALBController kc = proxySystem.getKlalbController(); + + if ("GET".equalsIgnoreCase(method)) { + sendJsonResponse(exchange, 200, getLinksJson()); + } else if ("POST".equalsIgnoreCase(method)) { + String body = readRequestBody(exchange); + try { + JsonObject req = new JsonParser().parse(body).getAsJsonObject(); + String address = req.get("address").getAsString().trim(); + if (address.isEmpty()) { + sendError(exchange, 400, "Address is required"); + return; + } + MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address); + List added = kc.addRemoteLines(mpsa); + JsonObject resp = new JsonObject(); + resp.addProperty("success", !added.isEmpty()); + resp.addProperty("count", added.size()); + sendJsonResponse(exchange, 200, resp); + } catch (Exception e) { + sendError(exchange, 400, "Failed to add link: " + e.getMessage()); + } + } else if ("DELETE".equalsIgnoreCase(method)) { + String query = exchange.getRequestURI().getQuery(); + String address = null; + if (query != null && query.startsWith("address=")) { + address = URLDecoder.decode(query.substring(8), StandardCharsets.UTF_8); + } else { + String body = readRequestBody(exchange); + if (!body.isEmpty()) { + try { + JsonObject req = new JsonParser().parse(body).getAsJsonObject(); + if (req.has("address")) { + address = req.get("address").getAsString(); + } + } catch (Exception ignored) {} + } + } + + if (address == null || address.trim().isEmpty()) { + sendError(exchange, 400, "Address parameter required"); + return; + } + + try { + MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address.trim()); + List removed = kc.removeRemoteLines(mpsa); + JsonObject resp = new JsonObject(); + resp.addProperty("success", !removed.isEmpty()); + resp.addProperty("count", removed.size()); + sendJsonResponse(exchange, 200, resp); + } catch (Exception e) { + sendError(exchange, 400, "Failed to remove link: " + e.getMessage()); + } + } else { + sendError(exchange, 405, "Method not allowed"); + } + } + + private void handleLinkAction(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String body = readRequestBody(exchange); + try { + JsonObject req = new JsonParser().parse(body).getAsJsonObject(); + String action = req.get("action").getAsString(); + String address = req.has("address") ? req.get("address").getAsString() : null; + + KLALBController kc = proxySystem.getKlalbController(); + KLALBRemoteLink target = null; + if (address != null) { + for (IPv6NetworkLink link : kc.getLines()) { + if (link instanceof KLALBRemoteLink rl) { + if (rl.getName().equalsIgnoreCase(address) || + (rl.getSocketAddress() != null && rl.getSocketAddress().toString().equalsIgnoreCase(address))) { + target = rl; + break; + } + } + } + } + + JsonObject resp = new JsonObject(); + if (target != null) { + switch (action) { + case "reconnect" -> { + target.reconnectImmediately(); + resp.addProperty("success", true); + } + case "disconnect" -> { + target.dislink(); + resp.addProperty("success", true); + } + case "remove" -> { + MultiProtocolSocketAddress mtar = target.getSocketAddress(); + if (mtar != null) { + kc.removeRemoteLines(mtar); + } else { + target.close(); + } + resp.addProperty("success", true); + } + default -> { + sendError(exchange, 400, "Unknown action: " + action); + return; + } + } + } else { + sendError(exchange, 404, "Target link not found: " + address); + return; + } + sendJsonResponse(exchange, 200, resp); + } catch (Exception e) { + sendError(exchange, 400, "Failed to execute link action: " + e.getMessage()); + } + } + + private void handleReconnectAll(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + KLALBController kc = proxySystem.getKlalbController(); + if (kc != null) { + kc.reconnectImmediately(); + JsonObject resp = new JsonObject(); + resp.addProperty("success", true); + sendJsonResponse(exchange, 200, resp); + } else { + sendError(exchange, 500, "Controller not ready"); + } + } + + private void handleRoutes(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + KLALBController kc = proxySystem.getKlalbController(); + JsonArray routesArray = new JsonArray(); + if (kc != null && kc.getIpv6Router() != null) { + List list = new ArrayList<>(kc.getIpv6Router().getCurrentRouteTabel()); + Collections.sort(list); + for (RouteItem item : list) { + JsonObject obj = new JsonObject(); + obj.addProperty("destination", item.getDestination() != null ? item.getDestination().toString() : ""); + obj.addProperty("protocol", item.getProto()); + obj.addProperty("preference", item.getPre()); + obj.addProperty("cost", item.getCost()); + obj.addProperty("flag", item.getFlag()); + obj.addProperty("nexthop", item.getNexthop() != null ? item.getNexthop().toString() : ""); + obj.addProperty("interface", item.getDestlink() != null ? item.getDestlink().getName() : ""); + routesArray.add(obj); + } + } + sendJsonResponse(exchange, 200, routesArray); + } + + private void handleNodes(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + KLALBController kc = proxySystem.getKlalbController(); + JsonObject result = new JsonObject(); + JsonArray nodesArray = new JsonArray(); + JsonArray edgesArray = new JsonArray(); + + if (kc != null && kc.getIpv6Router() != null && kc.getIpv6Router().getKlalbRouteProtol() != null) { + KLALBRoutingProtocol rproto = kc.getIpv6Router().getKlalbRouteProtol(); + Map addrs = rproto.getAddresses(); + IPv6Address selfAddr = kc.getIpv6Router().getLocator().getAddress(); + + if (addrs != null) { + for (IPv6Address addr : addrs.keySet()) { + JsonObject nodeObj = new JsonObject(); + nodeObj.addProperty("id", addr.toString()); + nodeObj.addProperty("address", addr.toString()); + nodeObj.addProperty("compressedAddress", addr.toCompressedString()); + nodeObj.addProperty("isSelf", addr.equals(selfAddr)); + String dname = rproto.getDeviceName(addr); + nodeObj.addProperty("deviceName", dname != null ? dname : ""); + nodesArray.add(nodeObj); + } + } + + Map netmap = rproto.getNetmap(); + if (netmap != null) { + Set seenEdges = new HashSet<>(); + for (Map.Entry entry : netmap.entrySet()) { + IPv6Address from = entry.getKey(); + RouterInfo info = entry.getValue(); + if (info != null && info.getNeighborAddresses() != null) { + for (NeighborInfo nb : info.getNeighborAddresses()) { + if (nb != null && nb.getLocator() != null) { + String to = nb.getLocator().getAddress().toString(); + String edgeKey = from.toString().compareTo(to) < 0 + ? from.toString() + "->" + to + : to + "->" + from.toString(); + if (!seenEdges.contains(edgeKey)) { + seenEdges.add(edgeKey); + JsonObject edgeObj = new JsonObject(); + edgeObj.addProperty("source", from.toString()); + edgeObj.addProperty("target", to); + edgeObj.addProperty("delay", nb.getUploadDelay()); + edgeObj.addProperty("cost", nb.getUploadDelayMin()); + edgesArray.add(edgeObj); + } + } + } + } + } + } + } + + result.add("nodes", nodesArray); + result.add("edges", edgesArray); + sendJsonResponse(exchange, 200, result); + } + + private void handleInterfaces(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + KLALBController kc = proxySystem.getKlalbController(); + JsonArray ifacesArray = new JsonArray(); + if (kc != null && kc.getNetworkInterfaceManager() != null) { + List ifaces = kc.getNetworkInterfaceManager().getAllAvaliableNetworkInterface(); + for (NetworkInterface nif : ifaces) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", nif.getName()); + obj.addProperty("displayName", nif.getDisplayName()); + JsonArray ips = new JsonArray(); + List addrs = kc.getNetworkInterfaceManager().getNetworkInterfaceAddress(nif); + for (InetAddress a : addrs) { + ips.add(new JsonPrimitive(a.getHostAddress())); + } + obj.add("addresses", ips); + ifacesArray.add(obj); + } + } + sendJsonResponse(exchange, 200, ifacesArray); + } + + private void handleConfig(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + String method = exchange.getRequestMethod(); + + if ("GET".equalsIgnoreCase(method)) { + KLALBControllerConfigItem config = proxySystem.getControllerConfig(); + if (config != null) { + sendJsonResponse(exchange, 200, config); + } else { + sendError(exchange, 404, "Configuration not found"); + } + } else if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) { + String body = readRequestBody(exchange); + try { + JsonObject json = new JsonParser().parse(body).getAsJsonObject(); + KLALBControllerConfigItem current = proxySystem.getControllerConfig(); + if (current != null) { + if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { + current.setDeviceName(json.get("deviceName").getAsString()); + } else if (json.has("DeviceName") && !json.get("DeviceName").isJsonNull()) { + current.setDeviceName(json.get("DeviceName").getAsString()); + } + + if (json.has("deviceDescription") && !json.get("deviceDescription").isJsonNull()) { + current.setDeviceDescription(json.get("deviceDescription").getAsString()); + } else if (json.has("DeviceDescription") && !json.get("DeviceDescription").isJsonNull()) { + current.setDeviceDescription(json.get("DeviceDescription").getAsString()); + } + + if (json.has("language") && !json.get("language").isJsonNull()) { + current.setLanguage(json.get("language").getAsString()); + } + + if (json.has("virtualAddress") && !json.get("virtualAddress").isJsonNull()) { + current.setVirtualAddress(json.get("virtualAddress").getAsString()); + } else if (json.has("VirtualAddress") && !json.get("VirtualAddress").isJsonNull()) { + current.setVirtualAddress(json.get("VirtualAddress").getAsString()); + } + + if (json.has("virtualASN") && !json.get("virtualASN").isJsonNull()) { + current.setVirtualASN(json.get("virtualASN").getAsLong()); + } else if (json.has("VirtualASN") && !json.get("VirtualASN").isJsonNull()) { + current.setVirtualASN(json.get("VirtualASN").getAsLong()); + } + + if (json.has("virtualSocketName") && !json.get("virtualSocketName").isJsonNull()) { + current.setVirtualSocketName(json.get("virtualSocketName").getAsString()); + } else if (json.has("VirtualSocketName") && !json.get("VirtualSocketName").isJsonNull()) { + current.setVirtualSocketName(json.get("VirtualSocketName").getAsString()); + } + + if (json.has("enableTUN")) { + current.setEnableTUN(json.get("enableTUN").getAsBoolean()); + } else if (json.has("EnableTUN")) { + current.setEnableTUN(json.get("EnableTUN").getAsBoolean()); + } + + if (json.has("tunName") && !json.get("tunName").isJsonNull()) { + current.setTUNName(json.get("tunName").getAsString()); + } else if (json.has("TUNName") && !json.get("TUNName").isJsonNull()) { + current.setTUNName(json.get("TUNName").getAsString()); + } + + if (json.has("webUI")) { + current.setWebUI(json.get("webUI").getAsBoolean()); + } + + if (json.has("webPort")) { + current.setWebPort(json.get("webPort").getAsInt()); + } + + if (json.has("nogui")) { + current.setNogui(json.get("nogui").getAsBoolean()); + } + + if (json.has("tcpListen") && !json.get("tcpListen").isJsonNull()) { + current.setTCPListen(new MultiProtocolSocketAddress(json.get("tcpListen").getAsString())); + } else if (json.has("TCPListen") && !json.get("TCPListen").isJsonNull()) { + current.setTCPListen(new MultiProtocolSocketAddress(json.get("TCPListen").getAsString())); + } + + if (json.has("udpListen") && !json.get("udpListen").isJsonNull()) { + current.setUDPListen(new MultiProtocolSocketAddress(json.get("udpListen").getAsString())); + } else if (json.has("UDPListen") && !json.get("UDPListen").isJsonNull()) { + current.setUDPListen(new MultiProtocolSocketAddress(json.get("UDPListen").getAsString())); + } + + if (json.has("openConnections") || json.has("OpenConnections") || json.has("lineTable") || json.has("LineTable")) { + JsonArray arr = json.has("openConnections") ? json.getAsJsonArray("openConnections") + : json.has("OpenConnections") ? json.getAsJsonArray("OpenConnections") + : json.has("lineTable") ? json.getAsJsonArray("lineTable") : json.getAsJsonArray("LineTable"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + list.add(new MultiProtocolSocketAddress(el.getAsString())); + } + } + current.setOpenConnections(list); + } + + if (json.has("autoConnections") || json.has("AutoConnections") || json.has("connectLineTable") || json.has("ConnectLineTable")) { + JsonArray arr = json.has("autoConnections") ? json.getAsJsonArray("autoConnections") + : json.has("AutoConnections") ? json.getAsJsonArray("AutoConnections") + : json.has("connectLineTable") ? json.getAsJsonArray("connectLineTable") : json.getAsJsonArray("ConnectLineTable"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + list.add(new MultiProtocolSocketAddress(el.getAsString())); + } + } + current.setAutoConnections(list); + } + + if (json.has("ntpServers") || json.has("NtpServers") || json.has("ntpServerTable")) { + JsonArray arr = json.has("ntpServers") ? json.getAsJsonArray("ntpServers") + : json.has("NtpServers") ? json.getAsJsonArray("NtpServers") + : json.getAsJsonArray("ntpServerTable"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + list.add(new MultiProtocolSocketAddress(el.getAsString())); + } + } + current.setNtpServers(list); + } + + if (json.has("dns") || json.has("DNS")) { + JsonArray arr = json.has("dns") ? json.getAsJsonArray("dns") : json.getAsJsonArray("DNS"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + try { + list.add(InetAddress.getByName(el.getAsString())); + } catch (Exception ignored) {} + } + } + current.setDNS(list); + } + + if (json.has("extraRoutes") || json.has("ExtraRoutes")) { + JsonArray arr = json.has("extraRoutes") ? json.getAsJsonArray("extraRoutes") : json.getAsJsonArray("ExtraRoutes"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + list.add(el.getAsString()); + } + } + current.setExtraRoutes(list); + } + + if (json.has("networkInterfaceExcepts") || json.has("NetworkInterfaceExcepts")) { + JsonArray arr = json.has("networkInterfaceExcepts") ? json.getAsJsonArray("networkInterfaceExcepts") : json.getAsJsonArray("NetworkInterfaceExcepts"); + List list = new ArrayList<>(); + for (JsonElement el : arr) { + if (el.isJsonPrimitive()) { + list.add(el.getAsString()); + } + } + current.setNetworkInterfaceExcepts(list); + } + + if (json.has("congestionAlgorithm") && !json.get("congestionAlgorithm").isJsonNull()) { + current.setCongestionAlgorithm(json.get("congestionAlgorithm").getAsString()); + } + + if (json.has("performanceStrategy") && !json.get("performanceStrategy").isJsonNull()) { + current.setPerformanceStrategy(json.get("performanceStrategy").getAsString()); + } + + if (json.has("linkConnectionsCount")) { + current.setLinkConnectionsCount(json.get("linkConnectionsCount").getAsInt()); + } + + if (json.has("burstLimit")) { + current.setBurstLimit(json.get("burstLimit").getAsDouble()); + } + + if (json.has("delayUpperBound")) { + current.setDelayUpperBound(json.get("delayUpperBound").getAsDouble()); + } + + if (json.has("delayLowerBound")) { + current.setDelayLowerBound(json.get("delayLowerBound").getAsDouble()); + } + + if (json.has("nagleDelayTime")) { + current.setNagleDelayTime(json.get("nagleDelayTime").getAsLong()); + } + + if (json.has("linkNagleDelayTime")) { + current.setLinkNagleDelayTime(json.get("linkNagleDelayTime").getAsLong()); + } + + if (json.has("denyConnectionQuery")) { + current.setDenyConnectionQuery(json.get("denyConnectionQuery").getAsBoolean()); + } else if (json.has("denyLineTableQuery")) { + current.setDenyConnectionQuery(json.get("denyLineTableQuery").getAsBoolean()); + } + + if (json.has("denyConnectionBroadcast")) { + current.setDenyConnectionBroadcast(json.get("denyConnectionBroadcast").getAsBoolean()); + } else if (json.has("denyLineTableBroadcast")) { + current.setDenyConnectionBroadcast(json.get("denyLineTableBroadcast").getAsBoolean()); + } + + // Trigger GUI save consumer or save directly + if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) { + proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig()); + } else { + proxySystem.saveConfigToFile(); + } + + JsonObject resp = new JsonObject(); + resp.addProperty("success", true); + resp.addProperty("message", "Configuration updated successfully"); + sendJsonResponse(exchange, 200, resp); + } else { + sendError(exchange, 500, "Current configuration is null"); + } + } catch (Exception e) { + sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); + } + } else { + sendError(exchange, 405, "Method not allowed"); + } + } + + // ------------------------------------------------------------- + // SSE Handler & Broadcaster + // ------------------------------------------------------------- + + private void handleEvents(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + Headers headers = exchange.getResponseHeaders(); + headers.set("Content-Type", "text/event-stream; charset=utf-8"); + headers.set("Cache-Control", "no-cache, no-transform"); + headers.set("Connection", "keep-alive"); + headers.set("Access-Control-Allow-Origin", "*"); + + exchange.sendResponseHeaders(200, 0); + sseClients.add(exchange); + } + + private void startSseBroadcaster() { + sseExecutor.scheduleAtFixedRate(() -> { + if (sseClients.isEmpty()) return; + + JsonObject eventData = new JsonObject(); + eventData.add("status", getStatusJson()); + eventData.add("links", getLinksJson()); + String eventStr = "data: " + eventData.toString() + "\n\n"; + byte[] bytes = eventStr.getBytes(StandardCharsets.UTF_8); + + Iterator it = sseClients.iterator(); + while (it.hasNext()) { + HttpExchange ex = it.next(); + try { + OutputStream os = ex.getResponseBody(); + os.write(bytes); + os.flush(); + } catch (Exception e) { + try { + ex.close(); + } catch (Exception ignored) {} + it.remove(); + } + } + }, 200, 200, TimeUnit.MILLISECONDS); + } + + // ------------------------------------------------------------- + // Static Resource / SPA Handler + // ------------------------------------------------------------- + + private void handleStatic(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + + String path = exchange.getRequestURI().getPath(); + if (path.startsWith("/api/")) { + sendError(exchange, 404, "API endpoint not found"); + return; + } + + // Look for dashboard/dist + File distDir = new File("dashboard/dist"); + File targetFile = null; + + if (distDir.exists() && distDir.isDirectory()) { + String relPath = path.equals("/") ? "index.html" : path.substring(1); + targetFile = new File(distDir, relPath); + if (!targetFile.exists() || targetFile.isDirectory()) { + targetFile = new File(distDir, "index.html"); + } + } + + if (targetFile != null && targetFile.exists() && targetFile.isFile()) { + String mime = getMimeType(targetFile.getName()); + byte[] content = Files.readAllBytes(targetFile.toPath()); + setCorsHeaders(exchange); + exchange.getResponseHeaders().set("Content-Type", mime); + exchange.sendResponseHeaders(200, content.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } else { + // Fallback welcome message if frontend not built yet + String html = "KLALB Web Dashboard" + + "" + + "

KLALB Web Dashboard API Active

" + + "

The backend API is ready. Start the frontend dev server or build the dashboard:
cd dashboard && pnpm build

" + + "

View /api/status

"; + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + setCorsHeaders(exchange); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + } + + private String getMimeType(String filename) { + String lower = filename.toLowerCase(); + if (lower.endsWith(".html")) return "text/html; charset=utf-8"; + if (lower.endsWith(".js") || lower.endsWith(".mjs")) return "application/javascript; charset=utf-8"; + if (lower.endsWith(".css")) return "text/css; charset=utf-8"; + if (lower.endsWith(".json")) return "application/json; charset=utf-8"; + if (lower.endsWith(".svg")) return "image/svg+xml"; + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".ico")) return "image/x-icon"; + if (lower.endsWith(".woff2")) return "font/woff2"; + if (lower.endsWith(".woff")) return "font/woff"; + if (lower.endsWith(".ttf")) return "font/ttf"; + return "application/octet-stream"; + } +} diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java index 372a6cd..1a9980f 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java @@ -43,6 +43,10 @@ public class KLALBRoutingProtocol extends Thread{ private RouterInfo selfRouterInfo; private Map netmap=new ConcurrentHashMap<>(); + + public Map getNetmap() { + return netmap; + } private volatile Mapaddresses; diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java index 46f6014..89f1d8a 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java @@ -22,7 +22,7 @@ public class KLALBRoutingProtocolAPIServer { InetSocketAddress addrs=(InetSocketAddress) addr; switch(dataobj.getType()){ case KLALBRoutingProtocolJsonData.OPEN_LINES_REQ: - if(controller.getConfigItem()==null||(!controller.getConfigItem().isDenyLineTableQuery())) { + if(controller.getConfigItem()==null||(!controller.getConfigItem().isDenyConnectionQuery())) { KLALBRoutingProtocolJsonData json=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.OPEN_LINES_RESP,dataobj.getUuid(),controller.getSelflineTable()); routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(json),addr); }