From cc803f47a7350195ade68b5255087d55a5be398a Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:27:56 +0800 Subject: [PATCH] feat(web): add /api/node-info endpoint for on-demand node detail queries - Expose GET /api/node-info?address= endpoint in KLALBWebServer - Return local deviceName and deviceDescription immediately for self node - Perform asynchronous full node-info query (NODE_INFO_FULL_REQ) via KLALBRoutingProtocolAPIClient for remote nodes with 3s timeout fallback --- dashboard | 2 +- .../network/klalb/web/KLALBWebServer.java | 116 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/dashboard b/dashboard index f38d640..ad94d2c 160000 --- a/dashboard +++ b/dashboard @@ -1 +1 @@ -Subproject commit f38d640ac43f67cb1063e6a8af9f026637a74616 +Subproject commit ad94d2c2747b851546ed1a07d8df505bfda87b15 diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index e473d75..19d7f71 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -19,8 +19,10 @@ 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.KLALBNodeInformation; import org.kne.cloud.network.srv6.KLALBRoutingProtocol; import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; import org.kne.cloud.network.srv6.NeighborInfo; import org.kne.cloud.network.srv6.RouterInfo; import org.kne.cloud.network.srv6.SRv6Router; @@ -68,6 +70,7 @@ public class KLALBWebServer { server.createContext("/api/links/reconnect", this::handleReconnectAll); server.createContext("/api/routes", this::handleRoutes); server.createContext("/api/nodes", this::handleNodes); + server.createContext("/api/node-info", this::handleNodeInfo); server.createContext("/api/interfaces", this::handleInterfaces); server.createContext("/api/config", this::handleConfig); @@ -453,6 +456,119 @@ public class KLALBWebServer { sendJsonResponse(exchange, 200, routesArray); } + private KLALBRoutingProtocolAPIClient nodeInfoClient; + + private synchronized KLALBRoutingProtocolAPIClient getNodeInfoClient(KLALBController kc) { + if (nodeInfoClient == null) { + nodeInfoClient = new KLALBRoutingProtocolAPIClient(kc.getIpv6Router().getKlalbRouteProtol()); + } + return nodeInfoClient; + } + + private void handleNodeInfo(HttpExchange exchange) throws IOException { + if (handleCorsPreflight(exchange)) return; + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + Map params = parseQuery(exchange.getRequestURI().getRawQuery()); + String addressStr = params.get("address"); + if (addressStr == null || addressStr.trim().isEmpty()) { + sendError(exchange, 400, "address parameter is required"); + return; + } + + KLALBController kc = proxySystem.getKlalbController(); + if (kc == null || kc.getIpv6Router() == null || kc.getIpv6Router().getKlalbRouteProtol() == null) { + sendError(exchange, 503, "Controller not ready"); + return; + } + + IPv6Address target; + try { + target = new IPv6Address((Inet6Address) InetAddress.getByName(addressStr.trim())); + } catch (Exception e) { + sendError(exchange, 400, "Invalid IPv6 address: " + addressStr); + return; + } + + SRv6Router router = kc.getIpv6Router(); + boolean isSelf = router.getLocator().getAddress().equals(target); + + JsonObject resp = new JsonObject(); + resp.addProperty("address", target.toString()); + + if (isSelf) { + // 本机:描述直接取本地控制器配置 + KLALBControllerConfigItem cfg = proxySystem.getControllerConfig(); + resp.addProperty("isSelf", true); + String dname = router.getDeviceName() != null ? router.getDeviceName() + : (cfg != null && cfg.getDeviceName() != null ? cfg.getDeviceName() : ""); + resp.addProperty("deviceName", dname); + resp.addProperty("deviceDescription", + cfg != null && cfg.getDeviceDescription() != null ? cfg.getDeviceDescription() : ""); + JsonArray lines = new JsonArray(); + for (MultiProtocolSocketAddress mpsa : kc.getExternalEndpoints()) { + lines.add(new JsonPrimitive(mpsa.toString())); + } + resp.add("openLines", lines); + sendJsonResponse(exchange, 200, resp); + return; + } + + // 远端节点:经 SRv6 虚拟网络发送完整节点信息查询(异步回调,限时等待) + resp.addProperty("isSelf", false); + CompletableFuture future = new CompletableFuture<>(); + try { + getNodeInfoClient(kc).requestNodeInfoFull( + new InetSocketAddress(target.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), + info -> future.complete(info)); + + KLALBNodeInformation info; + try { + info = future.get(3, TimeUnit.SECONDS); + } catch (TimeoutException te) { + info = null; + } + + String dname = info != null && info.getDeviceName() != null && !info.getDeviceName().isEmpty() + ? info.getDeviceName() + : router.getKlalbRouteProtol().getDeviceName(target); + resp.addProperty("deviceName", dname != null ? dname : ""); + resp.addProperty("deviceDescription", + info != null && info.getDeviceDescription() != null ? info.getDeviceDescription() : ""); + resp.addProperty("reachable", info != null); + JsonArray lines = new JsonArray(); + if (info != null && info.getOpenLines() != null) { + for (MultiProtocolSocketAddress mpsa : info.getOpenLines()) { + lines.add(new JsonPrimitive(mpsa.toString())); + } + } + resp.add("openLines", lines); + } catch (Exception e) { + resp.addProperty("deviceName", ""); + resp.addProperty("deviceDescription", ""); + resp.addProperty("reachable", false); + resp.add("openLines", new JsonArray()); + } + sendJsonResponse(exchange, 200, resp); + } + + private Map parseQuery(String rawQuery) { + Map params = new HashMap<>(); + if (rawQuery == null || rawQuery.isEmpty()) return params; + for (String pair : rawQuery.split("&")) { + int idx = pair.indexOf('='); + if (idx <= 0) continue; + try { + params.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8), + URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8)); + } catch (Exception ignored) {} + } + return params; + } + private void handleNodes(HttpExchange exchange) throws IOException { if (handleCorsPreflight(exchange)) return; if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {