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