1 Commits
Author SHA1 Message Date
SerinaNya 8714a2ff9d feat(web): add /api/node-info endpoint for on-demand node detail queries
- Expose GET /api/node-info?address=<ipv6> 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
2026-08-26 21:27:56 +08:00
2 changed files with 117 additions and 1 deletions
@@ -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<String, String> 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<KLALBNodeInformation> 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<String, String> parseQuery(String rawQuery) {
Map<String, String> 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())) {