From 5d5eb58a0339c1950eb281fbf82bd2c70db4e548 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Wed, 20 Dec 2023 14:54:45 +0100 Subject: [PATCH 1/9] Minor extension to generic ip discovery Signed-off-by: Holger Friedrich --- .../core/config/discovery/addon/ip/IpAddonFinder.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 6cbeca5456e..92fe6820229 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -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; @@ -150,6 +151,7 @@ public class IpAddonFinder extends BaseAddonFinder { 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 @@ -342,6 +344,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); From bde718a44ccb699a531da52c05ddb435845a5507 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 24 Dec 2023 17:00:28 +0100 Subject: [PATCH 2/9] new parameters requestPlain and listenPort Signed-off-by: Holger Friedrich --- .../discovery/addon/ip/IpAddonFinder.java | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 92fe6820229..8b7fda3e83c 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -55,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; @@ -147,7 +148,9 @@ 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"; @@ -229,6 +232,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 (!(request.isEmpty() ^ requestPlain.isEmpty())) { + 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 { @@ -254,6 +264,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; + } + // not 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 { @@ -268,15 +294,13 @@ private void scan() { DatagramChannel channel = (DatagramChannel) DatagramChannel .open(StandardProtocolFamily.INET) .setOption(StandardSocketOptions.SO_REUSEADDR, true) - .bind(new InetSocketAddress(localIp, 0)) + .bind(new InetSocketAddress(localIp, listenPort)) .setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64) .configureBlocking(false); byte[] requestArray = buildRequestArray(channel, Objects.toString(request)); - if (logger.isTraceEnabled()) { - logger.trace("{}: {}", candidate.getUID(), - HexFormat.of().withDelimiter(" ").formatHex(requestArray)); - } + logger.trace("{}: {}", candidate.getUID(), + HexFormat.of().withDelimiter(" ").formatHex(requestArray)); channel.send(ByteBuffer.wrap(requestArray), new InetSocketAddress(destIp, destPort)); @@ -324,6 +348,28 @@ 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 + StringBuffer req = new StringBuffer(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()); + } + + return StringUtils.unEscapeXml(req.toString()).getBytes(); + } + + // build from hex string private byte[] buildRequestArray(DatagramChannel channel, String request) throws java.io.IOException, ParseException { InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); From 5c1ecc6fdcde2df81bb610f010601556afccb979 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Mon, 25 Dec 2023 09:31:33 +0100 Subject: [PATCH 3/9] logging Signed-off-by: Holger Friedrich --- .../discovery/addon/ip/IpAddonFinder.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 8b7fda3e83c..0613fad6c15 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -196,6 +196,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); } @@ -234,7 +235,7 @@ private void scan() { String request = Objects.toString(parameters.get(PARAMETER_REQUEST), ""); String requestPlain = Objects.toString(parameters.get(PARAMETER_REQUEST_PLAIN), ""); // xor - if (!(request.isEmpty() ^ requestPlain.isEmpty())) { + if (!("".equals(request) ^ "".equals(requestPlain))) { logger.warn("{}: discovery-parameter '{}' or '{}' required", candidate.getUID(), PARAMETER_REQUEST, PARAMETER_REQUEST_PLAIN); continue; @@ -298,9 +299,19 @@ private void scan() { .setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64) .configureBlocking(false); - byte[] requestArray = buildRequestArray(channel, Objects.toString(request)); - logger.trace("{}: {}", candidate.getUID(), - HexFormat.of().withDelimiter(" ").formatHex(requestArray)); + byte[] requestArray = "".equals(requestPlain) + ? buildRequestArray(channel, Objects.toString(request)) + : buildRequestArrayPlain(channel, Objects.toString(requestPlain)); + if (logger.isTraceEnabled()) { + InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); + String id = candidate.getUID(); + logger.trace("{}: probing {} -> {}:{}", id, localIp, + destIp != null ? destIp.getHostAddress() : "", destPort); + 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)); @@ -322,7 +333,8 @@ 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: @@ -366,7 +378,8 @@ private byte[] buildRequestArrayPlain(DatagramChannel channel, String request) req.replace(p, p + REPLACEMENT_UUID.length() + 1, UUID.randomUUID().toString()); } - return StringUtils.unEscapeXml(req.toString()).getBytes(); + String reqEscaped = Objects.toString(StringUtils.unEscapeXml(req.toString()), ""); + return reqEscaped.getBytes(); } // build from hex string From e353ece39d6c72ba700b4e6d565401e605b938fb Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Mon, 25 Dec 2023 22:32:54 +0100 Subject: [PATCH 4/9] extend logging Signed-off-by: Holger Friedrich --- .../core/config/discovery/addon/ip/IpAddonFinder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 0613fad6c15..4fe62966a96 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -378,8 +378,9 @@ private byte[] buildRequestArrayPlain(DatagramChannel channel, String request) req.replace(p, p + REPLACEMENT_UUID.length() + 1, UUID.randomUUID().toString()); } - String reqEscaped = Objects.toString(StringUtils.unEscapeXml(req.toString()), ""); - return reqEscaped.getBytes(); + String reqUnEscaped = Objects.toString(StringUtils.unEscapeXml(req.toString()), ""); + logger.trace("plaintext: \'{}\'", reqUnEscaped); + return reqUnEscaped.getBytes(); } // build from hex string From 3e6a70d3cbfb2f154bfbe7d586d8311e2d61a3e7 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Mon, 25 Dec 2023 22:46:48 +0100 Subject: [PATCH 5/9] SAT Signed-off-by: Holger Friedrich --- .../core/config/discovery/addon/ip/IpAddonFinder.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 4fe62966a96..0036316b5bd 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -307,6 +307,9 @@ private void scan() { 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, @@ -378,9 +381,9 @@ private byte[] buildRequestArrayPlain(DatagramChannel channel, String request) req.replace(p, p + REPLACEMENT_UUID.length() + 1, UUID.randomUUID().toString()); } - String reqUnEscaped = Objects.toString(StringUtils.unEscapeXml(req.toString()), ""); - logger.trace("plaintext: \'{}\'", reqUnEscaped); - return reqUnEscaped.getBytes(); + @Nullable + String reqUnEscaped = StringUtils.unEscapeXml(req.toString()); + return reqUnEscaped != null ? reqUnEscaped.getBytes() : new byte[0]; } // build from hex string From f640faaca5b92b719ca73eaa4532180c5d79e0e3 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Wed, 27 Dec 2023 16:56:50 +0100 Subject: [PATCH 6/9] review comment Signed-off-by: Holger Friedrich --- .../openhab/core/config/discovery/addon/ip/IpAddonFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 0036316b5bd..44042280054 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -274,7 +274,7 @@ private void scan() { PARAMETER_LISTEN_PORT); continue; } - // not not allow privileged ports + // do not allow privileged ports if (listenPort < 1024) { logger.warn("{}: discovery-parameter '{}' not allowed, privileged port", candidate.getUID(), PARAMETER_LISTEN_PORT); From c458c65859748786d7cf6b1068e9ff5e85b65aab Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Wed, 27 Dec 2023 21:21:13 +0100 Subject: [PATCH 7/9] javadoc Signed-off-by: Holger Friedrich --- .../discovery/addon/ip/IpAddonFinder.java | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 44042280054..2f30575d050 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -102,9 +102,28 @@ * * * + * {@code listenPort} + * port to use for listening to responses (optional) + * privileged ports ({@code <1024}) not allowed + * + * * {@code request} * description of request frame as hex bytes separated by spaces (e.g. 0x01 0x02 ...) - * dynamic replacement of variables $srcIp and $srcPort, no others implemented yet + * dynamic replacement of variables $srcIp, $srcPort and $uuid, no others implemented yet + * + * + * {@code requestPlain} + * description of request frame as plaintext string + * dynamic replacement of variables $srcIp, $srcPort and $uuid, no others implemented yet; + * there are five XML special characters which need to be escaped: + * + *
{@code
+ * & - &
+ * < - <
+ * > - >
+ * " - "
+ * ' - '
+ * }
* * * {@code timeoutMs} @@ -113,7 +132,25 @@ * * *

- * Packets are sent out on ever available network interface. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
dynamic replacement (in {@code request*})value
{@code $srcIp}source IP address
{@code $srcPort}source port
{@code $uuid}String returned by {@code java.util.UUID.randomUUID()}
+ *

+ * Packets are sent out on every available network interface. *

* There is currently only one match-property defined: {@code response}. * It allows a regex match, but currently only ".*" is supported. From 6b3862351e4c6cb6cfc14209bc61a24b221e85c0 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Thu, 28 Dec 2023 01:34:33 +0100 Subject: [PATCH 8/9] try with resources for DatagramChannel Signed-off-by: Holger Friedrich --- .../config/discovery/addon/ip/IpAddonFinder.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 2f30575d050..8dec1e885ea 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -328,14 +328,12 @@ 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, listenPort)) - .setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64) - .configureBlocking(false); - + 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)); @@ -357,7 +355,6 @@ private void scan() { 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); @@ -382,7 +379,6 @@ private void scan() { candidate.getUID(), type); break; // end loop } - } catch (IOException e) { logger.debug("{}: network error", candidate.getUID(), e); } From 573ce3032560f8d0cdb4851744f813aa38e0c13e Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Tue, 13 Feb 2024 21:50:19 +0100 Subject: [PATCH 9/9] StringBuilder Signed-off-by: Holger Friedrich --- .../openhab/core/config/discovery/addon/ip/IpAddonFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java index 8dec1e885ea..33f4c5d8be5 100644 --- a/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java +++ b/bundles/org.openhab.core.config.discovery.addon.ip/src/main/java/org/openhab/core/config/discovery/addon/ip/IpAddonFinder.java @@ -402,7 +402,7 @@ private byte[] buildRequestArrayPlain(DatagramChannel channel, String request) InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); // replace first - StringBuffer req = new StringBuffer(request); + 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());