Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor extension to generic ip discovery #3943

Merged
merged 9 commits into from
Feb 14, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -54,6 +55,7 @@
import org.openhab.core.config.discovery.addon.AddonFinder;
import org.openhab.core.config.discovery.addon.BaseAddonFinder;
import org.openhab.core.net.NetUtil;
import org.openhab.core.util.StringUtils;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
Expand Down Expand Up @@ -100,9 +102,28 @@
* <td></td>
* </tr>
* <tr>
* <td>{@code listenPort}</td>
* <td>port to use for listening to responses (optional)</td>
* <td>privileged ports ({@code <1024}) not allowed</td>
* </tr>
* <tr>
* <td>{@code request}</td>
* <td>description of request frame as hex bytes separated by spaces (e.g. 0x01 0x02 ...)</td>
* <td>dynamic replacement of variables $srcIp and $srcPort, no others implemented yet
* <td>dynamic replacement of variables $srcIp, $srcPort and $uuid, no others implemented yet
* </tr>
* <tr>
* <td>{@code requestPlain}</td>
* <td>description of request frame as plaintext string</td>
* <td>dynamic replacement of variables $srcIp, $srcPort and $uuid, no others implemented yet;
* there are five XML special characters which need to be escaped:
*
* <pre>{@code
* & - &amp;
* < - &lt;
* > - &gt;
* " - &quot;
* ' - &apos;
* }</pre>
* </tr>
* <tr>
* <td>{@code timeoutMs}</td>
Expand All @@ -111,7 +132,25 @@
* </tr>
* </table>
* <p>
* Packets are sent out on ever available network interface.
* <table border="1">
* <tr>
* <td><b>dynamic replacement</b> (in {@code request*})</td>
* <td><b>value</b></td>
* </tr>
* <tr>
* <td>{@code $srcIp}</td>
* <td>source IP address</td>
* </tr>
* <tr>
* <td>{@code $srcPort}</td>
* <td>source port</td>
* </tr>
* <td>{@code $uuid}</td>
* <td>String returned by {@code java.util.UUID.randomUUID()}</td>
* </tr>
* </table>
* <p>
* Packets are sent out on every available network interface.
* <p>
* There is currently only one match-property defined: {@code response}.
* It allows a regex match, but currently only ".*" is supported.
Expand Down Expand Up @@ -146,10 +185,13 @@ public class IpAddonFinder extends BaseAddonFinder {
private static final String MATCH_PROPERTY_RESPONSE = "response";
private static final String PARAMETER_DEST_IP = "destIp";
private static final String PARAMETER_DEST_PORT = "destPort";
private static final String PARAMETER_LISTEN_PORT = "listenPort";
private static final String PARAMETER_REQUEST = "request";
private static final String PARAMETER_REQUEST_PLAIN = "requestPlain";
private static final String PARAMETER_SRC_IP = "srcIp";
private static final String PARAMETER_SRC_PORT = "srcPort";
private static final String PARAMETER_TIMEOUT_MS = "timeoutMs";
private static final String REPLACEMENT_UUID = "uuid";

private final Logger logger = LoggerFactory.getLogger(IpAddonFinder.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager
Expand Down Expand Up @@ -191,6 +233,7 @@ private void startScan() {
// At the same time we must make sure that a scheduled scan is rescheduled - or (after more than our delay) is
// executed once more.
stopScan();
logger.trace("Scheduling new IP scan");
scanJob = scheduler.schedule(this::scan, 20, TimeUnit.SECONDS);
}

Expand Down Expand Up @@ -227,6 +270,13 @@ private void scan() {
// parse standard set of parameters
String type = Objects.toString(parameters.get("type"), "");
String request = Objects.toString(parameters.get(PARAMETER_REQUEST), "");
String requestPlain = Objects.toString(parameters.get(PARAMETER_REQUEST_PLAIN), "");
// xor
if (!("".equals(request) ^ "".equals(requestPlain))) {
logger.warn("{}: discovery-parameter '{}' or '{}' required", candidate.getUID(), PARAMETER_REQUEST,
PARAMETER_REQUEST_PLAIN);
continue;
}
String response = Objects.toString(matchProperties.get(MATCH_PROPERTY_RESPONSE), "");
int timeoutMs;
try {
Expand All @@ -252,6 +302,22 @@ private void scan() {
PARAMETER_DEST_PORT);
continue;
}
int listenPort = 0; // default, pick a non-privileged port
if (parameters.get(PARAMETER_LISTEN_PORT) != null) {
try {
listenPort = Integer.parseInt(Objects.toString(parameters.get(PARAMETER_LISTEN_PORT)));
} catch (NumberFormatException e) {
logger.warn("{}: discovery-parameter '{}' cannot be parsed", candidate.getUID(),
PARAMETER_LISTEN_PORT);
continue;
}
// do not allow privileged ports
if (listenPort < 1024) {
logger.warn("{}: discovery-parameter '{}' not allowed, privileged port", candidate.getUID(),
PARAMETER_LISTEN_PORT);
continue;
}
}

// handle known types
try {
Expand All @@ -262,25 +328,33 @@ private void scan() {
.map(a -> a.getAddress().getHostAddress()).toList();

for (String localIp : ipAddresses) {
try {
DatagramChannel channel = (DatagramChannel) DatagramChannel
.open(StandardProtocolFamily.INET)
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
.bind(new InetSocketAddress(localIp, 0))
.setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64)
.configureBlocking(false);

byte[] requestArray = buildRequestArray(channel, Objects.toString(request));
try (DatagramChannel channel = (DatagramChannel) DatagramChannel
.open(StandardProtocolFamily.INET)
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
.bind(new InetSocketAddress(localIp, listenPort))
.setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64).configureBlocking(false);
Selector selector = Selector.open()) {
byte[] requestArray = "".equals(requestPlain)
? buildRequestArray(channel, Objects.toString(request))
: buildRequestArrayPlain(channel, Objects.toString(requestPlain));
if (logger.isTraceEnabled()) {
logger.trace("{}: {}", candidate.getUID(),
InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress();
String id = candidate.getUID();
logger.trace("{}: probing {} -> {}:{}", id, localIp,
destIp != null ? destIp.getHostAddress() : "", destPort);
if (!"".equals(requestPlain)) {
logger.trace("{}: \'{}\'", id, new String(requestArray));
}
logger.trace("{}: {}", id,
HexFormat.of().withDelimiter(" ").formatHex(requestArray));
logger.trace("{}: listening on {}:{} for {} ms", id,
sock.getAddress().getHostAddress(), sock.getPort(), timeoutMs);
}

channel.send(ByteBuffer.wrap(requestArray),
new InetSocketAddress(destIp, destPort));

// listen to responses
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.wrap(new byte[50]);
channel.register(selector, SelectionKey.OP_READ);
selector.select(timeoutMs);
Expand All @@ -296,15 +370,15 @@ private void scan() {
suggestions.add(candidate);
logger.debug("Suggested add-on found: {}", candidate.getUID());
} else {
logger.trace("{}: no response", candidate.getUID());
logger.trace("{}: no response received on {}", candidate.getUID(),
localIp);
}
break;
default:
logger.warn("{}: match-property response \"{}\" is unknown",
candidate.getUID(), type);
break; // end loop
}

} catch (IOException e) {
logger.debug("{}: network error", candidate.getUID(), e);
}
Expand All @@ -322,6 +396,30 @@ private void scan() {
logger.trace("IpAddonFinder::scan completed");
}

// build from plaintext string
private byte[] buildRequestArrayPlain(DatagramChannel channel, String request)
throws java.io.IOException, ParseException {
InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress();

// replace first
StringBuilder req = new StringBuilder(request);
int p;
while ((p = req.indexOf("$" + PARAMETER_SRC_IP)) != -1) {
req.replace(p, p + PARAMETER_SRC_IP.length() + 1, sock.getAddress().getHostAddress());
}
while ((p = req.indexOf("$" + PARAMETER_SRC_PORT)) != -1) {
req.replace(p, p + PARAMETER_SRC_PORT.length() + 1, "" + sock.getPort());
}
while ((p = req.indexOf("$" + REPLACEMENT_UUID)) != -1) {
req.replace(p, p + REPLACEMENT_UUID.length() + 1, UUID.randomUUID().toString());
}

@Nullable
String reqUnEscaped = StringUtils.unEscapeXml(req.toString());
return reqUnEscaped != null ? reqUnEscaped.getBytes() : new byte[0];
}

// build from hex string
private byte[] buildRequestArray(DatagramChannel channel, String request)
throws java.io.IOException, ParseException {
InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress();
Expand All @@ -342,6 +440,10 @@ private byte[] buildRequestArray(DatagramChannel channel, String request)
requestFrame.write((byte) ((dPort >> 8) & 0xff));
requestFrame.write((byte) (dPort & 0xff));
break;
case "$" + REPLACEMENT_UUID:
String uuid = UUID.randomUUID().toString();
requestFrame.write(uuid.getBytes());
break;
default:
logger.warn("Unknown token in request frame \"{}\"", token);
throw new ParseException(token, 0);
Expand Down
Loading