]> git.basschouten.com Git - openhab-addons.git/blob
cf23c1e57bdf0d6101da2685d38a32d8cce90212
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.network.internal.utils;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.net.ConnectException;
19 import java.net.DatagramPacket;
20 import java.net.DatagramSocket;
21 import java.net.Inet4Address;
22 import java.net.InetAddress;
23 import java.net.InetSocketAddress;
24 import java.net.NetworkInterface;
25 import java.net.NoRouteToHostException;
26 import java.net.PortUnreachableException;
27 import java.net.Socket;
28 import java.net.SocketAddress;
29 import java.net.SocketException;
30 import java.net.SocketTimeoutException;
31 import java.net.UnknownHostException;
32 import java.time.Duration;
33 import java.util.ArrayList;
34 import java.util.Enumeration;
35 import java.util.HashSet;
36 import java.util.LinkedHashSet;
37 import java.util.List;
38 import java.util.Optional;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41
42 import org.eclipse.jdt.annotation.NonNullByDefault;
43 import org.eclipse.jdt.annotation.Nullable;
44 import org.openhab.core.io.net.exec.ExecUtil;
45 import org.openhab.core.net.CidrAddress;
46 import org.openhab.core.net.NetUtil;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * Network utility functions for pinging and for determining all interfaces and assigned IP addresses.
52  *
53  * @author David Graeff - Initial contribution
54  */
55 @NonNullByDefault
56 public class NetworkUtils {
57     private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class);
58
59     private LatencyParser latencyParser = new LatencyParser();
60
61     /**
62      * Gets every IPv4 Address on each Interface except the loopback
63      * The Address format is ip/subnet
64      *
65      * @return The collected IPv4 Addresses
66      */
67     public Set<CidrAddress> getInterfaceIPs() {
68         return NetUtil.getAllInterfaceAddresses().stream().filter(a -> a.getAddress() instanceof Inet4Address)
69                 .collect(Collectors.toSet());
70     }
71
72     /**
73      * Gets every IPv4 address on the network defined by its cidr
74      *
75      * @return The collected IPv4 Addresses
76      */
77     private List<String> getIPAddresses(CidrAddress adr) {
78         List<String> result = new ArrayList<>();
79         byte[] octets = adr.getAddress().getAddress();
80         final int addressCount = (1 << (32 - adr.getPrefix())) - 2;
81         final int ipMask = 0xFFFFFFFF << (32 - adr.getPrefix());
82         octets[0] &= ipMask >> 24;
83         octets[1] &= ipMask >> 16;
84         octets[2] &= ipMask >> 8;
85         octets[3] &= ipMask;
86         try {
87             final CidrAddress baseIp = new CidrAddress(InetAddress.getByAddress(octets), (short) adr.getPrefix());
88             for (int i = 1; i <= addressCount; i++) {
89                 int octet = i & ~ipMask;
90                 byte[] segments = baseIp.getAddress().getAddress();
91                 segments[2] += (octet >> 8);
92                 segments[3] += octet;
93                 result.add(InetAddress.getByAddress(segments).getHostAddress());
94             }
95         } catch (UnknownHostException e) {
96             logger.debug("Could not build net ip address.", e);
97         }
98         return result;
99     }
100
101     /**
102      * Get a set of all interface names.
103      *
104      * @return Set of interface names
105      */
106     public Set<String> getInterfaceNames() {
107         Set<String> result = new HashSet<>();
108
109         try {
110             // For each interface ...
111             for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
112                 NetworkInterface networkInterface = en.nextElement();
113                 if (!networkInterface.isLoopback()) {
114                     result.add(networkInterface.getName());
115                 }
116             }
117         } catch (SocketException ignored) {
118             // If we are not allowed to enumerate, we return an empty result set.
119         }
120
121         return result;
122     }
123
124     /**
125      * Determines every IP which can be assigned on all available interfaces
126      *
127      * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
128      * @return Every single IP which can be assigned on the Networks the computer is connected to
129      */
130     public Set<String> getNetworkIPs(int maximumPerInterface) {
131         return getNetworkIPs(getInterfaceIPs(), maximumPerInterface);
132     }
133
134     /**
135      * Takes the interfaceIPs and fetches every IP which can be assigned on their network
136      *
137      * @param interfaceIPs The IPs which are assigned to the Network Interfaces
138      * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
139      * @return Every single IP which can be assigned on the Networks the computer is connected to
140      */
141     private Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) {
142         LinkedHashSet<String> networkIPs = new LinkedHashSet<>();
143
144         short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214
145         if (maximumPerInterface != 0) {
146             // calculate minimum CIDR prefix length from maximumPerInterface
147             // (equals leading unset bits (Integer has 32 bits)
148             minCidrPrefixLength = (short) Integer.numberOfLeadingZeros(maximumPerInterface);
149             if (Integer.bitCount(maximumPerInterface) == 1) {
150                 // if only the highest is set, decrease prefix by 1 to cover all addresses
151                 minCidrPrefixLength--;
152             }
153         }
154         logger.trace("set minCidrPrefixLength to {}, maximumPerInterface is {}", minCidrPrefixLength,
155                 maximumPerInterface);
156
157         for (CidrAddress cidrNotation : interfaceIPs) {
158             if (cidrNotation.getPrefix() < minCidrPrefixLength) {
159                 logger.info(
160                         "CIDR prefix is smaller than /{} on interface with address {}, truncating to /{}, some addresses might be lost",
161                         minCidrPrefixLength, cidrNotation, minCidrPrefixLength);
162                 cidrNotation = new CidrAddress(cidrNotation.getAddress(), minCidrPrefixLength);
163             }
164
165             List<String> addresses = getIPAddresses(cidrNotation);
166             int len = addresses.size();
167             if (maximumPerInterface != 0 && maximumPerInterface < len) {
168                 len = maximumPerInterface;
169             }
170             for (int i = 0; i < len; i++) {
171                 networkIPs.add(addresses.get(i));
172             }
173         }
174
175         return networkIPs;
176     }
177
178     /**
179      * Try to establish a tcp connection to the given port. Returns false if a timeout occurred
180      * or the connection was denied.
181      *
182      * @param host The IP or hostname
183      * @param port The tcp port. Must be not 0.
184      * @param timeout Timeout in ms
185      * @return Ping result information. Optional is empty if ping command was not executed.
186      * @throws IOException
187      */
188     public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException {
189         double execStartTimeInMS = System.currentTimeMillis();
190
191         SocketAddress socketAddress = new InetSocketAddress(host, port);
192         try (Socket socket = new Socket()) {
193             socket.connect(socketAddress, timeout);
194             return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
195         } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
196             return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
197         }
198     }
199
200     /**
201      * Return the working method for the native system ping. If no native ping
202      * works JavaPing is returned.
203      */
204     public IpPingMethodEnum determinePingMethod() {
205         String os = System.getProperty("os.name");
206         IpPingMethodEnum method;
207         if (os == null) {
208             return IpPingMethodEnum.JAVA_PING;
209         } else {
210             os = os.toLowerCase();
211             if (os.indexOf("win") >= 0) {
212                 method = IpPingMethodEnum.WINDOWS_PING;
213             } else if (os.indexOf("mac") >= 0) {
214                 method = IpPingMethodEnum.MAC_OS_PING;
215             } else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0 || os.indexOf("aix") >= 0) {
216                 method = IpPingMethodEnum.IPUTILS_LINUX_PING;
217             } else {
218                 // We cannot estimate the command line for any other operating system and just return false
219                 return IpPingMethodEnum.JAVA_PING;
220             }
221         }
222
223         try {
224             Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000);
225             if (pingResult.isPresent() && pingResult.get().isSuccess()) {
226                 return method;
227             }
228         } catch (IOException ignored) {
229         } catch (InterruptedException e) {
230             Thread.currentThread().interrupt(); // Reset interrupt flag
231         }
232         return IpPingMethodEnum.JAVA_PING;
233     }
234
235     /**
236      * Return true if the external arp ping utility (arping) is available and executable on the given path.
237      */
238     public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) {
239         String result = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(100), arpToolPath, "--help");
240         if (result == null || result.isBlank()) {
241             return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
242         } else if (result.contains("Thomas Habets")) {
243             if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
244                 return ArpPingUtilEnum.THOMAS_HABERT_ARPING;
245             } else {
246                 return ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT;
247             }
248         } else if (result.contains("-w timeout") || result.contains("-w <timeout>")) {
249             return ArpPingUtilEnum.IPUTILS_ARPING;
250         } else if (result.contains("Usage: arp-ping.exe")) {
251             return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS;
252         }
253         return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
254     }
255
256     public enum IpPingMethodEnum {
257         JAVA_PING,
258         WINDOWS_PING,
259         IPUTILS_LINUX_PING,
260         MAC_OS_PING
261     }
262
263     /**
264      * Use the native ping utility of the operating system to detect device presence.
265      *
266      * @param hostname The DNS name, IPv4 or IPv6 address. Must not be null.
267      * @param timeoutInMS Timeout in milliseconds. Be aware that DNS resolution is not part of this timeout.
268      * @return Ping result information. Optional is empty if ping command was not executed.
269      * @throws IOException The ping command could probably not be found
270      */
271     public Optional<PingResult> nativePing(@Nullable IpPingMethodEnum method, String hostname, int timeoutInMS)
272             throws IOException, InterruptedException {
273         double execStartTimeInMS = System.currentTimeMillis();
274
275         Process proc;
276         if (method == null) {
277             return Optional.empty();
278         }
279         // Yes, all supported operating systems have their own ping utility with a different command line
280         switch (method) {
281             case IPUTILS_LINUX_PING:
282                 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
283                         .start();
284                 break;
285             case MAC_OS_PING:
286                 proc = new ProcessBuilder("ping", "-t", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
287                         .start();
288                 break;
289             case WINDOWS_PING:
290                 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS), "-n", "1", hostname).start();
291                 break;
292             case JAVA_PING:
293             default:
294                 // We cannot estimate the command line for any other operating system and just return false
295                 return Optional.empty();
296         }
297
298         // The return code is 0 for a successful ping, 1 if device didn't
299         // respond, and 2 if there is another error like network interface
300         // not ready.
301         // Exception: return code is also 0 in Windows for all requests on the local subnet.
302         // see https://superuser.com/questions/403905/ping-from-windows-7-get-no-reply-but-sets-errorlevel-to-0
303
304         int result = proc.waitFor();
305         if (result != 0) {
306             return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
307         }
308
309         try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
310             String line = r.readLine();
311             if (line == null) {
312                 throw new IOException("Received no output from ping process.");
313             }
314             do {
315                 // Because of the Windows issue, we need to check this. We assume that the ping was successful whenever
316                 // this specific string is contained in the output
317                 if (line.contains("TTL=") || line.contains("ttl=")) {
318                     PingResult pingResult = new PingResult(true, System.currentTimeMillis() - execStartTimeInMS);
319                     latencyParser.parseLatency(line).ifPresent(pingResult::setResponseTimeInMS);
320                     return Optional.of(pingResult);
321                 }
322                 line = r.readLine();
323             } while (line != null);
324
325             return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
326         }
327     }
328
329     public enum ArpPingUtilEnum {
330         DISABLED("Disabled", false),
331         DISABLED_INVALID_IP("Destination is not a valid IPv4 address", false),
332         DISABLED_UNKNOWN_TOOL("Unknown arping tool", false),
333         IPUTILS_ARPING("Iputils Arping", true),
334         THOMAS_HABERT_ARPING("Arping tool by Thomas Habets", true),
335         THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT("Arping tool by Thomas Habets (old version)", true),
336         ELI_FULKERSON_ARP_PING_FOR_WINDOWS("Eli Fulkerson ARPing tool for Windows", true);
337
338         public final String description;
339         public final boolean canProceed;
340
341         ArpPingUtilEnum(String description, boolean canProceed) {
342             this.description = description;
343             this.canProceed = canProceed;
344         }
345     }
346
347     /**
348      * Execute the arping tool to perform an ARP ping (only for IPv4 addresses).
349      * There exist two different arping utils with the same name unfortunatelly.
350      * * iputils arping which is sometimes preinstalled on fedora/ubuntu and the
351      * * https://github.com/ThomasHabets/arping which also works on Windows and MacOS.
352      *
353      * @param arpUtilPath The arping absolute path including filename. Example: "arping" or "/usr/bin/arping" or
354      *            "C:\something\arping.exe" or "arp-ping.exe"
355      * @param interfaceName An interface name, on linux for example "wlp58s0", shown by ifconfig. Must not be null.
356      * @param ipV4address The ipV4 address. Must not be null.
357      * @param timeoutInMS A timeout in milliseconds
358      * @return Ping result information. Optional is empty if ping command was not executed.
359      * @throws IOException The ping command could probably not be found
360      */
361     public Optional<PingResult> nativeARPPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
362             String interfaceName, String ipV4address, int timeoutInMS) throws IOException, InterruptedException {
363         double execStartTimeInMS = System.currentTimeMillis();
364
365         if (arpUtilPath == null || arpingTool == null || !arpingTool.canProceed) {
366             return Optional.empty();
367         }
368         Process proc;
369         if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) {
370             proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start();
371         } else if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING) {
372             proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-C", "1", "-i",
373                     interfaceName, ipV4address).start();
374         } else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
375             proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS), "-x", ipV4address).start();
376         } else {
377             proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", "-I",
378                     interfaceName, ipV4address).start();
379         }
380
381         // The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like
382         // network interface not ready.
383         return Optional.of(new PingResult(proc.waitFor() == 0, System.currentTimeMillis() - execStartTimeInMS));
384     }
385
386     /**
387      * Execute a Java ping.
388      *
389      * @param timeoutInMS A timeout in milliseconds
390      * @param destinationAddress The address to check
391      * @return Ping result information. Optional is empty if ping command was not executed.
392      */
393     public Optional<PingResult> javaPing(int timeoutInMS, InetAddress destinationAddress) {
394         double execStartTimeInMS = System.currentTimeMillis();
395
396         try {
397             if (destinationAddress.isReachable(timeoutInMS)) {
398                 return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
399             } else {
400                 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
401             }
402         } catch (IOException e) {
403             return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
404         }
405     }
406
407     /**
408      * iOS devices are in a deep sleep mode, where they only listen to UDP traffic on port 5353 (Bonjour service
409      * discovery). A packet on port 5353 will wake up the network stack to respond to ARP pings at least.
410      *
411      * @throws IOException
412      */
413     public void wakeUpIOS(InetAddress address) throws IOException {
414         try (DatagramSocket s = new DatagramSocket()) {
415             byte[] buffer = new byte[0];
416             s.send(new DatagramPacket(buffer, buffer.length, address, 5353));
417         } catch (PortUnreachableException ignored) {
418             // We ignore the port unreachable error
419         }
420     }
421 }