diff --git a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralClient.java b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralClient.java index 02ca87f..28fd763 100644 --- a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralClient.java +++ b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralClient.java @@ -25,6 +25,7 @@ import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities; import com.facebook.openwifi.cloudsdk.models.gw.DeviceConfigureRequest; import com.facebook.openwifi.cloudsdk.models.gw.DeviceListWithStatus; import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus; +import com.facebook.openwifi.cloudsdk.models.gw.ScriptRequest; import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent; import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords; import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults; @@ -562,6 +563,83 @@ public class UCentralClient { } } + /** + * Run a shell script on a device and return the result, or null upon error. + * + * @see #runScript(String, String, int) + */ + public CommandInfo runScript(String serialNumber, String script) { + return runScript(serialNumber, script, 30); + } + + /** + * Run a shell script on a device and return the result, or null upon error. + * + * @see #runScript(String, String, int, String) + */ + public CommandInfo runScript( + String serialNumber, + String script, + int timeoutSec + ) { + return runScript(serialNumber, script, timeoutSec, "shell"); + } + + /** + * Run a script on a device and return the result, or null upon error. + * + * @param serialNumber the device + * @param script the script contents + * @param timeoutSec the timeout in seconds + * @param type the script type (either "shell" or "ucode") + * + * @see UCentralUtils#getScriptOutput(CommandInfo) + */ + public CommandInfo runScript( + String serialNumber, + String script, + int timeoutSec, + String type + ) { + ScriptRequest req = new ScriptRequest(); + req.serialNumber = serialNumber; + req.timeout = timeoutSec; + req.type = type; + req.script = script; + req.scriptId = "1"; // ?? + HttpResponse response = httpPost( + String.format("device/%s/script", serialNumber), + OWGW_SERVICE, + req + ); + if (!response.isSuccess()) { + logger.error("Error: {}", response.getBody()); + return null; + } + try { + return gson.fromJson(response.getBody(), CommandInfo.class); + } catch (JsonSyntaxException e) { + String errMsg = String.format( + "Failed to deserialize to CommandInfo: %s", + response.getBody() + ); + logger.error(errMsg, e); + return null; + } + } + + /** + * Instruct a device (AP) to ping a given destination (IP/hostname), + * returning the raw ping output or null upon error. + */ + public String pingFromDevice(String serialNumber, String host) { + // TODO pass options, parse output + final int PING_COUNT = 5; + String script = String.format("ping -c %d %s", PING_COUNT, host); + CommandInfo info = runScript(serialNumber, script); + return UCentralUtils.getScriptOutput(info); + } + /** Retrieve a list of inventory from owprov. */ public InventoryTagList getProvInventory() { HttpResponse response = httpGet("inventory", OWPROV_SERVICE); diff --git a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralUtils.java b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralUtils.java index 50a0d33..1d73812 100644 --- a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralUtils.java +++ b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/UCentralUtils.java @@ -8,8 +8,10 @@ package com.facebook.openwifi.cloudsdk; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -17,6 +19,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +31,7 @@ import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint; import com.facebook.openwifi.cloudsdk.ies.QbssLoad; import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo; import com.facebook.openwifi.cloudsdk.models.ap.State; +import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -477,4 +482,111 @@ public class UCentralUtils { return null; } } + + /** + * Return a map of Wi-Fi client (STA) MAC addresses to the Client structure + * found for that interface. This does NOT support clients connected on + * multiple interfaces simultaneously. + */ + public static Map getWifiClientInfo( + State state + ) { + Map ret = new HashMap<>(); + + // Aggregate over all interfaces + for (State.Interface iface : state.interfaces) { + if (iface.ssids == null || iface.clients == null) { + continue; + } + + // Convert client array to map (for faster lookups) + Map ifaceMap = new HashMap<>(); + for (State.Interface.Client client : iface.clients) { + ifaceMap.put(client.mac, client); + } + + // Loop over all SSIDs and connected clients + for (State.Interface.SSID ssid : iface.ssids) { + if (ssid.associations == null) { + continue; + } + for ( + State.Interface.SSID.Association association : ssid.associations + ) { + State.Interface.Client client = + ifaceMap.get(association.station); + if (client != null) { + ret.put(association.station, client); + } + } + } + } + + return ret; + } + + /** + * Decompress (inflate) a UTF-8 string using ZLIB. + * + * @param compressed the compressed string + * @param uncompressedSize the uncompressed size (must be known) + */ + private static String inflate(String compressed, int uncompressedSize) + throws DataFormatException { + if (compressed == null) { + throw new NullPointerException("Null compressed string"); + } + if (uncompressedSize < 0) { + throw new IllegalArgumentException("Invalid size"); + } + + byte[] input = compressed.getBytes(StandardCharsets.UTF_8); + byte[] output = new byte[uncompressedSize]; + + Inflater inflater = new Inflater(); + inflater.setInput(input, 0, input.length); + inflater.inflate(output); + inflater.end(); + + return new String(output, StandardCharsets.UTF_8); + } + + /** + * Given the result of the "script" API, return the actual script output + * (decoded/decompressed if needed), or null if the script returned an + * error. + * + * @see UCentralClient#runScript(String, String, int, String) + */ + public static String getScriptOutput(CommandInfo info) { + if (info == null || info.results == null) { + return null; + } + if (!info.results.has("status")) { + return null; + } + JsonObject status = info.results.get("status").getAsJsonObject(); + if (!status.has("error") || status.get("error").getAsInt() != 0) { + return null; + } + if (status.has("result")) { + // Raw result + return status.get("result").getAsString(); + } else if (status.has("result_64") && status.has("result_sz")) { + // Base64+compressed result + // NOTE: untested, not actually implemented on ucentral-client? + try { + String encoded = status.get("result_64").getAsString(); + int uncompressedSize = status.get("result_sz").getAsInt(); + String decoded = new String( + Base64.getDecoder().decode(encoded), + StandardCharsets.UTF_8 + ); + return inflate(decoded, uncompressedSize); + } catch (Exception e) { + logger.error("Failed to decode or inflate script result", e); + } + } + return null; + } } diff --git a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/ap/State.java b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/ap/State.java index d77d272..d8ec24e 100644 --- a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/ap/State.java +++ b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/ap/State.java @@ -54,6 +54,8 @@ public class State { public int ack_signal; public int ack_signal_avg; public JsonObject[] tid_stats; // TODO: see cfg80211_tid_stats + + // TODO ipaddr_v4 - either string or object (ip4leases), but duplicated in "clients" } public Association[] associations; diff --git a/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/gw/ScriptRequest.java b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/gw/ScriptRequest.java new file mode 100644 index 0000000..a1c32f0 --- /dev/null +++ b/lib-cloudsdk/src/main/java/com/facebook/openwifi/cloudsdk/models/gw/ScriptRequest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.openwifi.cloudsdk.models.gw; + +public class ScriptRequest { + public String serialNumber; + public long timeout = 30; // in seconds + public String type; // "shell", "ucode", "uci" + public String script; + public String scriptId; // required but unused? + public long when = 0; +}