From 57cf6a5d4327d54f69e605c4918931bb8744a49d Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sun, 8 Dec 2024 01:26:13 +0000 Subject: [PATCH] IP addon finder: add support for sending local mac address (#4477) Signed-off-by: Andrew Fiddian-Green --- .../discovery/addon/ip/IpAddonFinder.java | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 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 1f658db18..822e29e2f 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 @@ -12,7 +12,8 @@ */ package org.openhab.core.config.discovery.addon.ip; -import static org.openhab.core.config.discovery.addon.AddonFinderConstants.*; +import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_NAME_IP; +import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_TYPE_IP; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -21,7 +22,9 @@ import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.NetworkInterface; import java.net.SocketAddress; +import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.StandardProtocolFamily; import java.net.StandardSocketOptions; @@ -34,6 +37,7 @@ import java.text.ParseException; import java.util.Arrays; import java.util.HashSet; import java.util.HexFormat; +import java.util.IllegalFormatException; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -139,6 +143,11 @@ import org.slf4j.LoggerFactory; * timeout to wait for a answers * * + * + * {@code fmtMac} + * format specifier string for mac address + * e.g. '%02X', '%02X:', '%02x-' + * * *

* @@ -154,6 +163,11 @@ import org.slf4j.LoggerFactory; * * * + * + * + * + * + * * * * @@ -201,6 +215,8 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan 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_SRC_MAC = "srcMac"; + private static final String PARAMETER_MAC_FORMAT = "fmtMac"; private static final String PARAMETER_TIMEOUT_MS = "timeoutMs"; private static final String REPLACEMENT_UUID = "uuid"; @@ -346,16 +362,22 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan continue; } } + String macFormat = parameters.getOrDefault(PARAMETER_MAC_FORMAT, "%02X:"); + try { + String.format(macFormat, 123); + } catch (IllegalFormatException e) { + logger.warn("{}: discovery-parameter '{}' invalid format specifier", candidate.getUID(), macFormat); + } // handle known types try { switch (Objects.toString(type)) { case TYPE_IP_BROADCAST: - scanBroadcast(candidate, request, requestPlain, response, timeoutMs, destPort); + scanBroadcast(candidate, request, requestPlain, response, timeoutMs, destPort, macFormat); break; case TYPE_IP_MULTICAST: scanMulticast(candidate, request, requestPlain, response, timeoutMs, listenPort, destIp, - destPort); + destPort, macFormat); break; default: logger.warn("{}: discovery-parameter type \"{}\" is unknown", candidate.getUID(), type); @@ -369,7 +391,7 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan } private void scanBroadcast(AddonInfo candidate, String request, String requestPlain, String response, int timeoutMs, - int destPort) throws ParseException { + int destPort, String macFormat) throws ParseException { if (request.isEmpty() && requestPlain.isEmpty()) { logger.warn("{}: match-property request and requestPlain \"{}\" is unknown", candidate.getUID(), TYPE_IP_BROADCAST); @@ -391,7 +413,7 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan socket.setBroadcast(true); socket.setSoTimeout(timeoutMs); byte[] sendBuffer = requestPlain.isEmpty() ? buildRequestArray(socket.getLocalSocketAddress(), request) - : buildRequestArrayPlain(socket.getLocalSocketAddress(), requestPlain); + : buildRequestArrayPlain(socket.getLocalSocketAddress(), requestPlain, macFormat); DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, InetAddress.getByName(broadcastAddress), destPort); socket.send(sendPacket); @@ -431,7 +453,7 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan } private void scanMulticast(AddonInfo candidate, String request, String requestPlain, String response, int timeoutMs, - int listenPort, @Nullable InetAddress destIp, int destPort) throws ParseException { + int listenPort, @Nullable InetAddress destIp, int destPort, String macFormat) throws ParseException { List ipAddresses = NetUtil.getAllInterfaceAddresses().stream() .filter(a -> a.getAddress() instanceof Inet4Address).map(a -> a.getAddress().getHostAddress()).toList(); @@ -443,7 +465,7 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan Selector selector = Selector.open()) { byte[] requestArray = "".equals(requestPlain) ? buildRequestArray(channel.getLocalAddress(), Objects.toString(request)) - : buildRequestArrayPlain(channel.getLocalAddress(), Objects.toString(requestPlain)); + : buildRequestArrayPlain(channel.getLocalAddress(), Objects.toString(requestPlain), macFormat); if (logger.isTraceEnabled()) { InetSocketAddress sock = (InetSocketAddress) channel.getLocalAddress(); String id = candidate.getUID(); @@ -489,7 +511,7 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan } // build from plaintext string - private byte[] buildRequestArrayPlain(SocketAddress address, String request) + private byte[] buildRequestArrayPlain(SocketAddress address, String request, String macFormat) throws java.io.IOException, ParseException { InetSocketAddress sock = (InetSocketAddress) address; @@ -502,6 +524,9 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan while ((p = req.indexOf("$" + PARAMETER_SRC_PORT)) != -1) { req.replace(p, p + PARAMETER_SRC_PORT.length() + 1, "" + sock.getPort()); } + while ((p = req.indexOf("$" + PARAMETER_SRC_MAC)) != -1) { + req.replace(p, p + PARAMETER_SRC_MAC.length() + 1, macFormat(macFormat, macBytesFrom(sock))); + } while ((p = req.indexOf("$" + REPLACEMENT_UUID)) != -1) { req.replace(p, p + REPLACEMENT_UUID.length() + 1, UUID.randomUUID().toString()); } @@ -531,6 +556,10 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan requestFrame.write((byte) ((dPort >> 8) & 0xff)); requestFrame.write((byte) (dPort & 0xff)); break; + case "$" + PARAMETER_SRC_MAC: + byte[] mac = macBytesFrom(sock); + requestFrame.write(mac); + break; case "$" + REPLACEMENT_UUID: String uuid = UUID.randomUUID().toString(); requestFrame.write(uuid.getBytes()); @@ -567,4 +596,36 @@ public class IpAddonFinder extends BaseAddonFinder implements NetworkAddressChan } return false; } + + /** + * Get mac address bytes associated with the given Internet socket address + * + * @param inetSocketAddress the Internet address + * @return the mac address as an array of bytes + * @throws SocketException if address is not on this PC, or no mac address is associated + */ + private byte[] macBytesFrom(InetSocketAddress inetSocketAddress) throws SocketException { + NetworkInterface networkInterface = NetworkInterface.getByInetAddress(inetSocketAddress.getAddress()); + if (networkInterface == null) { + throw new SocketException("No network interface"); + } + return networkInterface.getHardwareAddress(); + } + + /** + * Use the given format specifier to format an array of mac address bytes + * + * @param format a standard format specifier; optionally ends with a delimiter e.g. "%02x:" or "%02X" + * @param bytes the mac address as an array of bytes + * + * @return e.g. '01:02:03:04:A5:B6:C7:D8', '01-02-03-04:a5:b6:c7:d8', or '01020304A5B6C7D8' + */ + private String macFormat(String format, byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte byt : bytes) { + result.append(String.format(format, byt)); + } + boolean isDelimited = Set.of(':', '-', '.').contains(format.charAt(format.length() - 1)); + return (isDelimited ? result.substring(0, result.length() - 1) : result).toString(); + } }
{@code $srcPort}source port
{@code $srcMac}source mac address
{@code $uuid}String returned by {@code java.util.UUID.randomUUID()}