001/*
002 * $Id: NetworkTester.java,v 1.11 2017/08/07 07:04:02 oboehm Exp $
003 *
004 * Copyright (c) 2017 by Oliver Boehm
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 *
018 * (c)reated 26.07.2017 by oboehm (ob@oasd.de)
019 */
020
021package patterntesting.runtime.junit;
022
023import org.apache.logging.log4j.LogManager;
024import org.apache.logging.log4j.Logger;
025
026import java.io.IOException;
027import java.net.*;
028import java.util.List;
029import java.util.Observable;
030import java.util.Observer;
031import java.util.concurrent.*;
032
033import static org.junit.jupiter.api.Assertions.assertFalse;
034import static org.junit.jupiter.api.Assertions.assertTrue;
035
036/**
037 * With the NetworkTester you can assert if a host is online or offline.
038 *
039 * @author oboehm
040 * @version $Revision: 1.11 $
041 * @since 1.8 (26.07.2017)
042 */
043public final class NetworkTester {
044
045    private static final Logger LOG = LogManager.getLogger(NetworkTester.class);
046    private static final int CONNECTION_TIMEOUT_IN_MINUTES = 2;
047
048    /** Utility class - no need to instantiate it. */
049    private NetworkTester() {
050    }
051
052    /**
053     * Asserts, that the given host is online.
054     *
055     * @param host the hostname or IP address
056     */
057    public static void assertOnline(String host) {
058        assertOnline(host, CONNECTION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
059    }
060
061    /**
062     * Checks if the given host is online.
063     *
064     * @param host the hostname or IP address
065     * @return true if host is online
066     * @since 2.0
067     */
068    public static boolean isOnline(String host) {
069        return isOnline(host, CONNECTION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
070    }
071
072    /**
073     * Asserts, that the given host is online. The given time is the maximal
074     * time we try to connect to the given host.
075     *
076     * @param host hostname or ip address
077     * @param time how long should we try to connect to host?
078     * @param unit the time unit
079     */
080    public static void assertOnline(String host, int time, TimeUnit unit) {
081        assertTrue(isOnline(host, time, unit), host + " is offline");
082    }
083
084    /**
085     * Checks if the given host is online. The given time is the maximal
086     * time we try to connect to the given host.
087     *
088     * @param host hostname or ip address
089     * @param time how long should we try to connect to host?
090     * @param unit the time unit
091     * @return true if host is online
092     * @since 2.0
093     */
094    public static boolean isOnline(String host, int time, TimeUnit unit) {
095        PortScanner scanner = scanPortsOf(host, time, unit);
096        return scanner.openPortDetected();
097    }
098
099    private static PortScanner scanPortsOf(String host, int timeout, TimeUnit unit) {
100        PortScanner scanner = new PortScanner(host);
101        scanner.scanPorts(timeout, unit);
102        return scanner;
103    }
104
105    /**
106     * Asserts, that the given host is online at the given port.
107     *
108     * @param host the hostname or IP address
109     * @param port the port between 0 and 0xFFFF
110     */
111    public static void assertOnline(String host, int port) {
112        assertTrue(isOnline(host, port), host + ":" + port + " is offline");
113    }
114
115    /**
116     * Asserts, that the socket address is online.
117     *
118     * @param host the hostname or IP address
119     */
120    public static void assertOnline(InetSocketAddress host) {
121        assertOnline(host.getHostName(), host.getPort());
122    }
123
124    /**
125     * Asserts, that the given host is online.
126     *
127     * @param host the IP address
128     */
129    public static void assertOnline(InetAddress host) {
130        assertOnline(host.getHostName());
131    }
132
133    /**
134     * Asserts, that the given host is online. The given time is the maximal
135     * time we try to connect to the given host.
136     *
137     * @param host hostname or ip address
138     * @param time how long should we try to connect to host?
139     * @param unit the time unit
140     */
141    public static void assertOnline(InetAddress host, int time, TimeUnit unit) {
142        assertOnline(host.getHostName(), time, unit);
143    }
144
145
146    /**
147     * Asserts, that the given host is online at the given port.
148     *
149     * @param host the IP address
150     * @param port the port between 0 and 0xFFFF
151     */
152    public static void assertOnline(InetAddress host, int port) {
153        assertOnline(host.getHostName(), port);
154    }
155
156
157    /**
158     * Checks if the given host is online.
159     *
160     * @param host the hostname or IP address
161     * @param port the port between 0 and 0xFFFF
162     * @return true if host is online
163     * @since 2.0.2
164     */
165    public static boolean isOnline(String host, int port) {
166        try (Socket socket = new Socket(host, port)) {
167            LOG.debug("Socket {} for {}:{} is created.", socket, host, port);
168            return socket.isConnected();
169        } catch (IOException ioe) {
170            LOG.debug("{}:{} seems to be offline ({}).", host, port, ioe.getMessage());
171            LOG.trace("Details:", ioe);
172            return false;
173        }
174    }
175
176    /**
177     * Asserts, that the given host is offline. This is the opposite of
178     * {@link #assertOnline(String)}.
179     *
180     * @param host the hostname or IP address
181     */
182    public static void assertOffline(String host) {
183        assertOffline(host, CONNECTION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
184    }
185
186    /**
187     * Asserts, that the given host is offline. The given time is the maximal
188     * time we try to connect to the given host. Normally it takes about at
189     * least 8 minutes to realize that a host is offline. So if you want to
190     * wait a shorter time use this method.
191     *
192     * @param host hostname or ip address
193     * @param time how long should we try to connect to host?
194     * @param unit the time unit
195     */
196    public static void assertOffline(String host, int time, TimeUnit unit) {
197        PortScanner scanner = scanPortsOf(host, time, unit);
198        assertFalse(scanner.openPortDetected(), host + " is online");
199    }
200
201    /**
202     * Asserts, that the port of the given host is offline.
203     *
204     * @param host the hostname or IP address
205     * @param port the port between 0 and 0xFFFF
206     */
207    public static void assertOffline(String host, int port) {
208        assertFalse(isOnline(host, port), host + ":" + port + " is online");
209    }
210
211    /**
212     * Asserts, that the socket address is offline.
213     *
214     * @param host the hostname or IP address
215     */
216    public static void assertOffline(InetSocketAddress host) {
217        assertOffline(host.getHostName(), host.getPort());
218    }
219
220    /**
221     * Asserts, that the given host is offline. This is the opposite of
222     * {@link #assertOnline(InetAddress)}.
223     *
224     * @param host the hostname or IP address
225     */
226    public static void assertOffline(InetAddress host) {
227        assertOffline(host.getHostName());
228    }
229
230    /**
231     * Asserts, that the given host is offline. The given time is the maximal
232     * time we try to connect to the given host. Normally it takes about at
233     * least 8 minutes to realize that a host is offline. So if you want to
234     * wait a shorter time use this method.
235     *
236     * @param host hostname or ip address
237     * @param time how long should we try to connect to host?
238     * @param unit the time unit
239     */
240    public static void assertOffline(InetAddress host, int time, TimeUnit unit) {
241        assertOffline(host.getHostName(), time, unit);
242    }
243
244    /**
245     * Asserts, that the port of the given host is offline.
246     *
247     * @param host the hostname or IP address
248     * @param port the port between 0 and 0xFFFF
249     */
250    public static void assertOffline(InetAddress host, int port) {
251        assertOffline(host.getHostName(), port);
252    }
253
254    /**
255     * Asserts, that the given URI exists and is reachable.
256     *
257     * @param uri a valid URI
258     */
259    public static void assertExists(URI uri) {
260        assertTrue(exists(uri));
261    }
262
263    /**
264     * Asserts, that the given URL exists and is reachable.
265     *
266     * @param url a valid URL
267     */
268    public static void assertExists(URL url) {
269        assertTrue(exists(url));
270    }
271
272    /**
273     * Checks, if the given URI exists and is reachable.
274     *
275     * @param uri a valid URI
276     * @return true if uri exists
277     * @since 2.0.2
278     */
279    public static boolean exists(URI uri) {
280        try {
281            return exists(uri.toURL());
282        } catch (MalformedURLException ex) {
283            throw new IllegalArgumentException("invalid URI: " + uri, ex);
284        }
285    }
286
287    /**
288     * Checks, if the given URL exists and is reachable.
289     *
290     * @param url a valid URL
291     * @return true if url exists
292     * @since 2.0.2
293     */
294    public static boolean exists(URL url) {
295        try {
296            URLConnection connection = url.openConnection();
297            LOG.trace("Got connection {} to {}.", connection,  url);
298            connection.connect();
299            LOG.debug("{} is online.", url);
300            return true;
301        } catch (IOException ioe) {
302            LOG.debug("Cannot open {} ({}).", url, ioe.getMessage());
303            LOG.trace("Details:", ioe);
304            return false;
305        }
306    }
307
308
309
310    private static class PortScanner implements Observer {
311
312        private final List<Future<Boolean>> startedThreads = new CopyOnWriteArrayList<>();
313        private final String host;
314        private long endTime;
315        long openPort = 0;
316
317        public PortScanner(String host) {
318            this.host = host;
319        }
320
321        /**
322         * First implementation uses Thread to start each PortKnocker. But this
323         * results in a OutOfMemoryException on a Mac. Now we uses an
324         * {@link ExecutorService} together with a thread pool of 512 threads
325         * (1000 threads didn't work).
326         *
327         * @param timeout timeout
328         * @param unit    time unit
329         */
330        public void scanPorts(int timeout, TimeUnit unit) {
331            endTime = System.currentTimeMillis() + unit.toMillis(timeout);
332            ExecutorService es = Executors.newFixedThreadPool(512);
333            for (int port = 1; (port <= 0xFFFF) && (openPort == 0) && (System.currentTimeMillis() < endTime); port++) {
334                PortKnocker knocker = new PortKnocker(new InetSocketAddress(host, port), this);
335                Future<Boolean> t = es.submit(knocker);
336                startedThreads.add(t);
337            }
338        }
339
340        public boolean openPortDetected() {
341            try {
342                for (Future<Boolean> t : startedThreads) {
343                    if (System.currentTimeMillis() >= endTime) {
344                        break;
345                    }
346                    Boolean online = t.get(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
347                    if (Boolean.TRUE.equals(online)) {
348                        return true;
349                    }
350                }
351            } catch (InterruptedException | ExecutionException | TimeoutException | CancellationException ex) {
352                LOG.debug("Wait was cancelled:", ex);
353                Thread.currentThread().interrupt();
354            }
355            return openPort > 0;
356        }
357
358        @Override
359        public void update(Observable o, Object arg) {
360            boolean online = (Boolean) arg;
361            if (online) {
362                PortKnocker knocker = (PortKnocker) o;
363                openPort = knocker.getAddress().getPort();
364                LOG.debug("Open port {} for host '{}' found.",  openPort, host);
365                stopThreads();
366            }
367        }
368
369        private void stopThreads() {
370            for (Future<?> t : startedThreads) {
371                if (!t.isDone() && !t.isCancelled()) {
372                    t.cancel(true);
373                }
374            }
375            startedThreads.clear();
376        }
377
378    }
379
380
381
382    private static class PortKnocker extends Observable implements Callable<Boolean> {
383
384        private final InetSocketAddress address;
385        private final Observer observer;
386
387        public PortKnocker(InetSocketAddress address, Observer observer) {
388            this.address = address;
389            this.observer = observer;
390        }
391
392        public InetSocketAddress getAddress() {
393            return address;
394        }
395
396        @Override
397        public Boolean call() {
398            boolean online = isOnline(address.getHostName(), address.getPort());
399            observer.update(this, online);
400            return online;
401        }
402
403    }
404
405
406
407}