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