1/*
2 * Copyright (c) 2015, Red Hat Inc
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import java.io.IOException;
25import java.net.InetAddress;
26import java.net.InetSocketAddress;
27import java.net.MalformedURLException;
28import java.net.Socket;
29import java.net.SocketAddress;
30import java.net.UnknownHostException;
31import java.util.HashMap;
32import java.util.Map;
33import java.util.concurrent.CountDownLatch;
34import java.util.concurrent.TimeUnit;
35
36import javax.management.remote.JMXConnector;
37import javax.management.remote.JMXConnectorFactory;
38import javax.management.remote.JMXServiceURL;
39import javax.management.remote.rmi.RMIConnectorServer;
40import javax.net.ssl.SSLSocket;
41import javax.net.ssl.SSLSocketFactory;
42import javax.rmi.ssl.SslRMIClientSocketFactory;
43
44/**
45 * Tests client connections to the JDK's built-in JMX agent server on the given
46 * ports/interface combinations.
47 *
48 * @see JMXInterfaceBindingTest
49 *
50 * @author Severin Gehwolf <sgehwolf@redhat.com>
51 *
52 * Usage:
53 *
54 * SSL:
55 *        java -Dcom.sun.management.jmxremote.ssl.need.client.auth=true \
56 *             -Dcom.sun.management.jmxremote.host=127.0.0.1 \
57 *             -Dcom.sun.management.jmxremote.port=9111 \
58 *             -Dcom.sun.management.jmxremote.rmi.port=9112 \
59 *             -Dcom.sun.management.jmxremote.authenticate=false \
60 *             -Dcom.sun.management.jmxremote.ssl=true \
61 *             -Dcom.sun.management.jmxremote.registry.ssl=true
62 *             -Djavax.net.ssl.keyStore=... \
63 *             -Djavax.net.ssl.keyStorePassword=... \
64 *             JMXAgentInterfaceBinding 127.0.0.1 9111 9112 true
65 *
66 * Non-SSL:
67 *        java -Dcom.sun.management.jmxremote.host=127.0.0.1 \
68 *             -Dcom.sun.management.jmxremote.port=9111 \
69 *             -Dcom.sun.management.jmxremote.rmi.port=9112 \
70 *             -Dcom.sun.management.jmxremote.authenticate=false \
71 *             -Dcom.sun.management.jmxremote.ssl=false \
72 *             JMXAgentInterfaceBinding 127.0.0.1 9111 9112 false
73 *
74 */
75public class JMXAgentInterfaceBinding {
76
77    private final MainThread mainThread;
78
79    public JMXAgentInterfaceBinding(InetAddress bindAddress,
80                                   int jmxPort,
81                                   int rmiPort,
82                                   boolean useSSL) {
83        this.mainThread = new MainThread(bindAddress, jmxPort, rmiPort, useSSL);
84    }
85
86    public void startEndpoint() {
87        mainThread.start();
88        try {
89            mainThread.join();
90        } catch (InterruptedException e) {
91            throw new RuntimeException("Test failed", e);
92        }
93        if (mainThread.isFailed()) {
94            mainThread.rethrowException();
95        }
96    }
97
98    public static void main(String[] args) {
99        if (args.length != 4) {
100            throw new RuntimeException(
101                    "Test failed. usage: java JMXInterfaceBindingTest <BIND_ADDRESS> <JMX_PORT> <RMI_PORT> {true|false}");
102        }
103        int jmxPort = parsePortFromString(args[1]);
104        int rmiPort = parsePortFromString(args[2]);
105        boolean useSSL = Boolean.parseBoolean(args[3]);
106        String strBindAddr = args[0];
107        System.out.println(
108                "DEBUG: Running test for triplet (hostname,jmxPort,rmiPort) = ("
109                        + strBindAddr + "," + jmxPort + "," + rmiPort + "), useSSL = " + useSSL);
110        InetAddress bindAddress;
111        try {
112            bindAddress = InetAddress.getByName(args[0]);
113        } catch (UnknownHostException e) {
114            throw new RuntimeException("Test failed. Unknown ip: " + args[0]);
115        }
116        JMXAgentInterfaceBinding test = new JMXAgentInterfaceBinding(bindAddress,
117                jmxPort, rmiPort, useSSL);
118        test.startEndpoint(); // Expect for main test to terminate process
119    }
120
121    private static int parsePortFromString(String port) {
122        try {
123            return Integer.parseInt(port);
124        } catch (NumberFormatException e) {
125            throw new RuntimeException(
126                    "Invalid port specified. Not an integer! Value was: "
127                            + port);
128        }
129    }
130
131    private static class JMXConnectorThread extends Thread {
132
133        private final String addr;
134        private final int jmxPort;
135        private final int rmiPort;
136        private final boolean useSSL;
137        private final CountDownLatch latch;
138        private boolean failed;
139        private boolean jmxConnectWorked;
140        private boolean rmiConnectWorked;
141
142        private JMXConnectorThread(String addr,
143                                   int jmxPort,
144                                   int rmiPort,
145                                   boolean useSSL,
146                                   CountDownLatch latch) {
147            this.addr = addr;
148            this.jmxPort = jmxPort;
149            this.rmiPort = rmiPort;
150            this.latch = latch;
151            this.useSSL = useSSL;
152        }
153
154        @Override
155        public void run() {
156            try {
157                connect();
158            } catch (IOException e) {
159                failed = true;
160            }
161        }
162
163        private void connect() throws IOException {
164            System.out.println(
165                    "JMXConnectorThread: Attempting JMX connection on: "
166                            + addr + " on port " + jmxPort);
167            JMXServiceURL url;
168            try {
169                url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://"
170                        + addr + ":" + jmxPort + "/jmxrmi");
171            } catch (MalformedURLException e) {
172                throw new RuntimeException("Test failed.", e);
173            }
174            Map<String, Object> env = new HashMap<>();
175            if (useSSL) {
176                SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
177                env.put("com.sun.jndi.rmi.factory.socket", csf);
178                env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
179            }
180            // connect and immediately close
181            JMXConnector c = JMXConnectorFactory.connect(url, env);
182            c.close();
183            System.out.println("JMXConnectorThread: connection to JMX worked");
184            jmxConnectWorked = true;
185            checkRmiSocket();
186            latch.countDown(); // signal we are done.
187        }
188
189        private void checkRmiSocket() throws IOException {
190            Socket rmiConnection;
191            if (useSSL) {
192                rmiConnection = SSLSocketFactory.getDefault().createSocket();
193            } else {
194                rmiConnection = new Socket();
195            }
196            SocketAddress target = new InetSocketAddress(addr, rmiPort);
197            rmiConnection.connect(target);
198            if (useSSL) {
199                ((SSLSocket)rmiConnection).startHandshake();
200            }
201            System.out.println(
202                    "JMXConnectorThread: connection to rmi socket worked host/port = "
203                            + addr + "/" + rmiPort);
204            rmiConnectWorked = true;
205            // Closing the channel without sending any data will cause an
206            // java.io.EOFException on the server endpoint. We don't care about this
207            // though, since we only want to test if we can connect.
208            rmiConnection.close();
209        }
210
211        public boolean isFailed() {
212            return failed;
213        }
214
215        public boolean jmxConnectionWorked() {
216            return jmxConnectWorked;
217        }
218
219        public boolean rmiConnectionWorked() {
220            return rmiConnectWorked;
221        }
222    }
223
224    private static class MainThread extends Thread {
225
226        private static final int WAIT_FOR_JMX_AGENT_TIMEOUT_MS = 500;
227        private final String addr;
228        private final int jmxPort;
229        private final int rmiPort;
230        private final boolean useSSL;
231        private boolean terminated = false;
232        private boolean jmxAgentStarted = false;
233        private Exception excptn;
234
235        private MainThread(InetAddress bindAddress, int jmxPort, int rmiPort, boolean useSSL) {
236            this.addr = wrapAddress(bindAddress.getHostAddress());
237            this.jmxPort = jmxPort;
238            this.rmiPort = rmiPort;
239            this.useSSL = useSSL;
240        }
241
242        @Override
243        public void run() {
244            try {
245                waitUntilReadyForConnections();
246                // Do nothing, but wait for termination.
247                try {
248                    while (!terminated) {
249                        Thread.sleep(100);
250                    }
251                } catch (InterruptedException e) { // ignore
252                }
253                System.out.println("MainThread: Thread stopped.");
254            } catch (Exception e) {
255                this.excptn = e;
256            }
257        }
258
259        private void waitUntilReadyForConnections() {
260            CountDownLatch latch = new CountDownLatch(1);
261            JMXConnectorThread connectionTester = new JMXConnectorThread(
262                    addr, jmxPort, rmiPort, useSSL, latch);
263            connectionTester.start();
264            boolean expired = false;
265            try {
266                expired = !latch.await(WAIT_FOR_JMX_AGENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
267                System.out.println(
268                        "MainThread: Finished waiting for JMX agent to become available: expired == "
269                                + expired);
270                jmxAgentStarted = !expired;
271            } catch (InterruptedException e) {
272                throw new RuntimeException("Test failed", e);
273            }
274            if (!jmxAgentStarted) {
275                throw new RuntimeException(
276                        "Test failed. JMX server agents not becoming available.");
277            }
278            if (connectionTester.isFailed()
279                    || !connectionTester.jmxConnectionWorked()
280                    || !connectionTester.rmiConnectionWorked()) {
281                throw new RuntimeException(
282                        "Test failed. JMX agent does not seem ready. See log output for details.");
283            }
284            // The main test expects this exact message being printed
285            System.out.println("MainThread: Ready for connections");
286        }
287
288        private boolean isFailed() {
289            return excptn != null;
290        }
291
292        private void rethrowException() throws RuntimeException {
293            throw new RuntimeException(excptn);
294        }
295    }
296
297    /**
298     * Will wrap IPv6 address in '[]'
299     */
300    static String wrapAddress(String address) {
301        if (address.contains(":")) {
302            return "[" + address + "]";
303        }
304        return address;
305    }
306}
307