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 
  24 import java.io.IOException;
  25 import java.net.InetAddress;
  26 import java.net.InetSocketAddress;
  27 import java.net.MalformedURLException;
  28 import java.net.Socket;
  29 import java.net.SocketAddress;
  30 import java.net.UnknownHostException;
  31 import java.util.HashMap;
  32 import java.util.Map;
  33 import java.util.concurrent.CountDownLatch;
  34 import java.util.concurrent.TimeUnit;
  35 
  36 import javax.management.remote.JMXConnector;
  37 import javax.management.remote.JMXConnectorFactory;
  38 import javax.management.remote.JMXServiceURL;
  39 import javax.management.remote.rmi.RMIConnectorServer;
  40 import javax.net.ssl.SSLSocket;
  41 import javax.net.ssl.SSLSocketFactory;
  42 import 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  */
  75 public 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 InetAddress 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(InetAddress 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.getHostAddress() + " on port " + jmxPort);
 167             JMXServiceURL url;
 168             try {
 169                 url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://"
 170                         + addr.getHostAddress() + ":" + 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.getHostAddress() + "/" + 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 InetAddress bindAddress;
 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.bindAddress = bindAddress;
 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                     bindAddress, 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 }