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