diff --git a/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java b/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java index e4d1c2f005f7e7830dae732d9e5b7ef3a18153d7..71a6ab3dd98feb3b2ee3377b88c5ea68b380a9d0 100644 --- a/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java +++ b/src/share/classes/sun/management/jmxremote/ConnectorBootstrap.java @@ -30,9 +30,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.lang.management.ManagementFactory; import java.net.InetAddress; import java.net.MalformedURLException; +import java.net.Socket; +import java.net.ServerSocket; import java.net.UnknownHostException; import java.rmi.NoSuchObjectException; import java.rmi.Remote; @@ -40,6 +43,7 @@ import java.rmi.RemoteException; import java.rmi.registry.Registry; import java.rmi.server.RMIClientSocketFactory; import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.RMISocketFactory; import java.rmi.server.RemoteObject; import java.rmi.server.UnicastRemoteObject; import java.security.KeyStore; @@ -60,6 +64,8 @@ import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnectorServer; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory; @@ -107,6 +113,8 @@ public final class ConnectorBootstrap { public static final String PORT = "com.sun.management.jmxremote.port"; + public static final String HOST = + "com.sun.management.jmxremote.host"; public static final String RMI_PORT = "com.sun.management.jmxremote.rmi.port"; public static final String CONFIG_FILE_NAME = @@ -424,10 +432,14 @@ public final class ConnectorBootstrap { checkAccessFile(accessFileName); } + final String bindAddress = + props.getProperty(PropertyNames.HOST); + if (log.debugOn()) { log.debug("startRemoteConnectorServer", Agent.getText("jmxremote.ConnectorBootstrap.starting") + "\n\t" + PropertyNames.PORT + "=" + port + + (bindAddress == null ? "" : "\n\t" + PropertyNames.HOST + "=" + bindAddress) + "\n\t" + PropertyNames.RMI_PORT + "=" + rmiPort + "\n\t" + PropertyNames.USE_SSL + "=" + useSsl + "\n\t" + PropertyNames.USE_REGISTRY_SSL + "=" + useRegistrySsl + @@ -458,7 +470,7 @@ public final class ConnectorBootstrap { sslConfigFileName, enabledCipherSuitesList, enabledProtocolsList, sslNeedClientAuth, useAuthentication, loginConfigName, - passwordFileName, accessFileName); + passwordFileName, accessFileName, bindAddress); cs = data.jmxConnectorServer; url = data.jmxRemoteURL; log.config("startRemoteConnectorServer", @@ -628,12 +640,13 @@ public final class ConnectorBootstrap { String sslConfigFileName, String[] enabledCipherSuites, String[] enabledProtocols, - boolean sslNeedClientAuth) { + boolean sslNeedClientAuth, + String bindAddress) { if (sslConfigFileName == null) { - return new SslRMIServerSocketFactory( + return new HostAwareSslSocketFactory( enabledCipherSuites, enabledProtocols, - sslNeedClientAuth); + sslNeedClientAuth, bindAddress); } else { checkRestrictedFile(sslConfigFileName); try { @@ -687,11 +700,11 @@ public final class ConnectorBootstrap { SSLContext ctx = SSLContext.getInstance("SSL"); ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return new SslRMIServerSocketFactory( + return new HostAwareSslSocketFactory( ctx, enabledCipherSuites, enabledProtocols, - sslNeedClientAuth); + sslNeedClientAuth, bindAddress); } catch (Exception e) { throw new AgentConfigurationError(AGENT_EXCEPTION, e, e.toString()); } @@ -711,7 +724,8 @@ public final class ConnectorBootstrap { boolean useAuthentication, String loginConfigName, String passwordFileName, - String accessFileName) + String accessFileName, + String bindAddress) throws IOException, MalformedURLException { /* Make sure we use non-guessable RMI object IDs. Otherwise @@ -719,7 +733,7 @@ public final class ConnectorBootstrap { * IDs. */ System.setProperty("java.rmi.server.randomIDs", "true"); - JMXServiceURL url = new JMXServiceURL("rmi", null, rmiPort); + JMXServiceURL url = new JMXServiceURL("rmi", bindAddress, rmiPort); Map env = new HashMap<>(); @@ -727,6 +741,8 @@ public final class ConnectorBootstrap { env.put(RMIExporter.EXPORTER_ATTRIBUTE, exporter); + boolean useSocketFactory = bindAddress != null && !useSsl; + if (useAuthentication) { if (loginConfigName != null) { env.put("jmx.remote.x.login.config", loginConfigName); @@ -751,7 +767,7 @@ public final class ConnectorBootstrap { csf = new SslRMIClientSocketFactory(); ssf = createSslRMIServerSocketFactory( sslConfigFileName, enabledCipherSuites, - enabledProtocols, sslNeedClientAuth); + enabledProtocols, sslNeedClientAuth, bindAddress); } if (useSsl) { @@ -761,6 +777,12 @@ public final class ConnectorBootstrap { ssf); } + if (useSocketFactory) { + ssf = new HostAwareSocketFactory(bindAddress); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, + ssf); + } + JMXConnectorServer connServer = null; try { connServer = @@ -780,6 +802,10 @@ public final class ConnectorBootstrap { registry = new SingleEntryRegistry(port, csf, ssf, "jmxrmi", exporter.firstExported); + } else if (useSocketFactory) { + registry = + new SingleEntryRegistry(port, csf, ssf, + "jmxrmi", exporter.firstExported); } else { registry = new SingleEntryRegistry(port, @@ -813,4 +839,172 @@ public final class ConnectorBootstrap { private static final ClassLogger log = new ClassLogger(ConnectorBootstrap.class.getPackage().getName(), "ConnectorBootstrap"); + + private static class HostAwareSocketFactory implements RMIServerSocketFactory { + + private final String bindAddress; + + private HostAwareSocketFactory(String bindAddress) { + this.bindAddress = bindAddress; + } + + @Override + public ServerSocket createServerSocket(int port) throws IOException { + if (bindAddress == null) { + return new ServerSocket(port); + } else { + try { + InetAddress addr = InetAddress.getByName(bindAddress); + return new ServerSocket(port, 0, addr); + } catch (UnknownHostException e) { + return new ServerSocket(port); + } + } + } + } + + private static class HostAwareSslSocketFactory extends SslRMIServerSocketFactory { + + private final String bindAddress; + private final String[] enabledCipherSuites; + private final String[] enabledProtocols; + private final boolean needClientAuth; + private final SSLContext context; + + private HostAwareSslSocketFactory(String[] enabledCipherSuites, + String[] enabledProtocols, + boolean sslNeedClientAuth, + String bindAddress) throws IllegalArgumentException { + this(null, enabledCipherSuites, enabledProtocols, sslNeedClientAuth, bindAddress); + } + + private HostAwareSslSocketFactory(SSLContext ctx, + String[] enabledCipherSuites, + String[] enabledProtocols, + boolean sslNeedClientAuth, + String bindAddress) throws IllegalArgumentException { + this.context = ctx; + this.bindAddress = bindAddress; + this.enabledProtocols = enabledProtocols; + this.enabledCipherSuites = enabledCipherSuites; + this.needClientAuth = sslNeedClientAuth; + checkValues(ctx, enabledCipherSuites, enabledProtocols); + } + + @Override + public ServerSocket createServerSocket(int port) throws IOException { + if (bindAddress != null) { + try { + InetAddress addr = InetAddress.getByName(bindAddress); + return new SslServerSocket(port, 0, addr, context, + enabledCipherSuites, enabledProtocols, needClientAuth); + } catch (UnknownHostException e) { + return new SslServerSocket(port, context, + enabledCipherSuites, enabledProtocols, needClientAuth); + } + } else { + return new SslServerSocket(port, context, + enabledCipherSuites, enabledProtocols, needClientAuth); + } + } + + private static void checkValues(SSLContext context, + String[] enabledCipherSuites, + String[] enabledProtocols) throws IllegalArgumentException { + // Force the initialization of the default at construction time, + // rather than delaying it to the first time createServerSocket() + // is called. + // + final SSLSocketFactory sslSocketFactory = + context == null ? + (SSLSocketFactory)SSLSocketFactory.getDefault() : context.getSocketFactory(); + SSLSocket sslSocket = null; + if (enabledCipherSuites != null || enabledProtocols != null) { + try { + sslSocket = (SSLSocket) sslSocketFactory.createSocket(); + } catch (Exception e) { + final String msg = "Unable to check if the cipher suites " + + "and protocols to enable are supported"; + throw (IllegalArgumentException) + new IllegalArgumentException(msg).initCause(e); + } + } + + // Check if all the cipher suites and protocol versions to enable + // are supported by the underlying SSL/TLS implementation and if + // true create lists from arrays. + // + if (enabledCipherSuites != null) { + sslSocket.setEnabledCipherSuites(enabledCipherSuites); + } + if (enabledProtocols != null) { + sslSocket.setEnabledProtocols(enabledProtocols); + } + } + } + + private static class SslServerSocket extends ServerSocket { + + private static SSLSocketFactory defaultSSLSocketFactory; + private final String[] enabledCipherSuites; + private final String[] enabledProtocols; + private final boolean needClientAuth; + private final SSLContext context; + + private SslServerSocket(int port, + SSLContext ctx, + String[] enabledCipherSuites, + String[] enabledProtocols, + boolean needClientAuth) throws IOException { + super(port); + this.enabledProtocols = enabledProtocols; + this.enabledCipherSuites = enabledCipherSuites; + this.needClientAuth = needClientAuth; + this.context = ctx; + } + + private SslServerSocket(int port, + int backlog, + InetAddress bindAddr, + SSLContext ctx, + String[] enabledCipherSuites, + String[] enabledProtocols, + boolean needClientAuth) throws IOException { + super(port, backlog, bindAddr); + this.enabledProtocols = enabledProtocols; + this.enabledCipherSuites = enabledCipherSuites; + this.needClientAuth = needClientAuth; + this.context = ctx; + } + + @Override + public Socket accept() throws IOException { + final SSLSocketFactory sslSocketFactory = + context == null ? + getDefaultSSLSocketFactory() : context.getSocketFactory(); + Socket socket = super.accept(); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket( + socket, socket.getInetAddress().getHostName(), + socket.getPort(), true); + sslSocket.setUseClientMode(false); + if (enabledCipherSuites != null) { + sslSocket.setEnabledCipherSuites(enabledCipherSuites); + } + if (enabledProtocols != null) { + sslSocket.setEnabledProtocols(enabledProtocols); + } + sslSocket.setNeedClientAuth(needClientAuth); + return sslSocket; + } + + private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() { + if (defaultSSLSocketFactory == null) { + defaultSSLSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); + return defaultSSLSocketFactory; + } else { + return defaultSSLSocketFactory; + } + } + + } } diff --git a/src/share/lib/management/management.properties b/src/share/lib/management/management.properties index db08b780ebd4498aef6329f4bf785dab85af8fad..70efa2eefbdcd407d02b5502b29e375890bf79d1 100644 --- a/src/share/lib/management/management.properties +++ b/src/share/lib/management/management.properties @@ -316,3 +316,16 @@ # For a non-default password file location use the following line # com.sun.management.jmxremote.access.file=filepath +# + +# ################ Management agent listen interface ######################### +# +# com.sun.management.jmxremote.host= +# Specifies the local interface on which the JMX RMI agent will bind. +# This is useful when running on machines which have several +# interfaces defined. It makes it possible to listen to a specific +# subnet accessible through that interface. +# +# The format of the value for that property is any string accepted +# by java.net.InetAddress.getByName(String). +# diff --git a/test/sun/management/jmxremote/bootstrap/JMXAgentInterfaceBinding.java b/test/sun/management/jmxremote/bootstrap/JMXAgentInterfaceBinding.java new file mode 100644 index 0000000000000000000000000000000000000000..8b46a4c3169b7a87ddca970a93ff3e41e8a5f974 --- /dev/null +++ b/test/sun/management/jmxremote/bootstrap/JMXAgentInterfaceBinding.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2015, Red Hat Inc + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +/** + * Tests client connections to the JDK's built-in JMX agent server on the given + * ports/interface combinations. + * + * @see JMXInterfaceBindingTest + * + * @author Severin Gehwolf + * + * Usage: + * + * SSL: + * java -Dcom.sun.management.jmxremote.ssl.need.client.auth=true \ + * -Dcom.sun.management.jmxremote.host=127.0.0.1 \ + * -Dcom.sun.management.jmxremote.port=9111 \ + * -Dcom.sun.management.jmxremote.rmi.port=9112 \ + * -Dcom.sun.management.jmxremote.authenticate=false \ + * -Dcom.sun.management.jmxremote.ssl=true \ + * -Dcom.sun.management.jmxremote.registry.ssl=true + * -Djavax.net.ssl.keyStore=... \ + * -Djavax.net.ssl.keyStorePassword=... \ + * JMXAgentInterfaceBinding 127.0.0.1 9111 9112 true + * + * Non-SSL: + * java -Dcom.sun.management.jmxremote.host=127.0.0.1 \ + * -Dcom.sun.management.jmxremote.port=9111 \ + * -Dcom.sun.management.jmxremote.rmi.port=9112 \ + * -Dcom.sun.management.jmxremote.authenticate=false \ + * -Dcom.sun.management.jmxremote.ssl=false \ + * JMXAgentInterfaceBinding 127.0.0.1 9111 9112 false + * + */ +public class JMXAgentInterfaceBinding { + + private final MainThread mainThread; + + public JMXAgentInterfaceBinding(InetAddress bindAddress, + int jmxPort, + int rmiPort, + boolean useSSL) { + this.mainThread = new MainThread(bindAddress, jmxPort, rmiPort, useSSL); + } + + public void startEndpoint() { + mainThread.start(); + try { + mainThread.join(); + } catch (InterruptedException e) { + throw new RuntimeException("Test failed", e); + } + if (mainThread.isFailed()) { + mainThread.rethrowException(); + } + } + + public static void main(String[] args) { + if (args.length != 4) { + throw new RuntimeException( + "Test failed. usage: java JMXInterfaceBindingTest {true|false}"); + } + int jmxPort = parsePortFromString(args[1]); + int rmiPort = parsePortFromString(args[2]); + boolean useSSL = Boolean.parseBoolean(args[3]); + String strBindAddr = args[0]; + System.out.println( + "DEBUG: Running test for triplet (hostname,jmxPort,rmiPort) = (" + + strBindAddr + "," + jmxPort + "," + rmiPort + "), useSSL = " + useSSL); + InetAddress bindAddress; + try { + bindAddress = InetAddress.getByName(args[0]); + } catch (UnknownHostException e) { + throw new RuntimeException("Test failed. Unknown ip: " + args[0]); + } + JMXAgentInterfaceBinding test = new JMXAgentInterfaceBinding(bindAddress, + jmxPort, rmiPort, useSSL); + test.startEndpoint(); // Expect for main test to terminate process + } + + private static int parsePortFromString(String port) { + try { + return Integer.parseInt(port); + } catch (NumberFormatException e) { + throw new RuntimeException( + "Invalid port specified. Not an integer! Value was: " + + port); + } + } + + private static class JMXConnectorThread extends Thread { + + private final InetAddress addr; + private final int jmxPort; + private final int rmiPort; + private final boolean useSSL; + private final CountDownLatch latch; + private boolean failed; + private boolean jmxConnectWorked; + private boolean rmiConnectWorked; + + private JMXConnectorThread(InetAddress addr, + int jmxPort, + int rmiPort, + boolean useSSL, + CountDownLatch latch) { + this.addr = addr; + this.jmxPort = jmxPort; + this.rmiPort = rmiPort; + this.latch = latch; + this.useSSL = useSSL; + } + + @Override + public void run() { + try { + connect(); + } catch (IOException e) { + failed = true; + } + } + + private void connect() throws IOException { + System.out.println( + "JMXConnectorThread: Attempting JMX connection on: " + + addr.getHostAddress() + " on port " + jmxPort); + JMXServiceURL url; + try { + url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + + addr.getHostAddress() + ":" + jmxPort + "/jmxrmi"); + } catch (MalformedURLException e) { + throw new RuntimeException("Test failed.", e); + } + Map env = new HashMap<>(); + if (useSSL) { + SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); + env.put("com.sun.jndi.rmi.factory.socket", csf); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf); + } + // connect and immediately close + JMXConnector c = JMXConnectorFactory.connect(url, env); + c.close(); + System.out.println("JMXConnectorThread: connection to JMX worked"); + jmxConnectWorked = true; + checkRmiSocket(); + latch.countDown(); // signal we are done. + } + + private void checkRmiSocket() throws IOException { + Socket rmiConnection; + if (useSSL) { + rmiConnection = SSLSocketFactory.getDefault().createSocket(); + } else { + rmiConnection = new Socket(); + } + SocketAddress target = new InetSocketAddress(addr, rmiPort); + rmiConnection.connect(target); + if (useSSL) { + ((SSLSocket)rmiConnection).startHandshake(); + } + System.out.println( + "JMXConnectorThread: connection to rmi socket worked host/port = " + + addr.getHostAddress() + "/" + rmiPort); + rmiConnectWorked = true; + // Closing the channel without sending any data will cause an + // java.io.EOFException on the server endpoint. We don't care about this + // though, since we only want to test if we can connect. + rmiConnection.close(); + } + + public boolean isFailed() { + return failed; + } + + public boolean jmxConnectionWorked() { + return jmxConnectWorked; + } + + public boolean rmiConnectionWorked() { + return rmiConnectWorked; + } + } + + private static class MainThread extends Thread { + + private static final int WAIT_FOR_JMX_AGENT_TIMEOUT_MS = 500; + private final InetAddress bindAddress; + private final int jmxPort; + private final int rmiPort; + private final boolean useSSL; + private boolean terminated = false; + private boolean jmxAgentStarted = false; + private Exception excptn; + + private MainThread(InetAddress bindAddress, int jmxPort, int rmiPort, boolean useSSL) { + this.bindAddress = bindAddress; + this.jmxPort = jmxPort; + this.rmiPort = rmiPort; + this.useSSL = useSSL; + } + + @Override + public void run() { + try { + waitUntilReadyForConnections(); + // Do nothing, but wait for termination. + try { + while (!terminated) { + Thread.sleep(100); + } + } catch (InterruptedException e) { // ignore + } + System.out.println("MainThread: Thread stopped."); + } catch (Exception e) { + this.excptn = e; + } + } + + private void waitUntilReadyForConnections() { + CountDownLatch latch = new CountDownLatch(1); + JMXConnectorThread connectionTester = new JMXConnectorThread( + bindAddress, jmxPort, rmiPort, useSSL, latch); + connectionTester.start(); + boolean expired = false; + try { + expired = !latch.await(WAIT_FOR_JMX_AGENT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + System.out.println( + "MainThread: Finished waiting for JMX agent to become available: expired == " + + expired); + jmxAgentStarted = !expired; + } catch (InterruptedException e) { + throw new RuntimeException("Test failed", e); + } + if (!jmxAgentStarted) { + throw new RuntimeException( + "Test failed. JMX server agents not becoming available."); + } + if (connectionTester.isFailed() + || !connectionTester.jmxConnectionWorked() + || !connectionTester.rmiConnectionWorked()) { + throw new RuntimeException( + "Test failed. JMX agent does not seem ready. See log output for details."); + } + // The main test expects this exact message being printed + System.out.println("MainThread: Ready for connections"); + } + + private boolean isFailed() { + return excptn != null; + } + + private void rethrowException() throws RuntimeException { + throw new RuntimeException(excptn); + } + } + +} diff --git a/test/sun/management/jmxremote/bootstrap/JMXInterfaceBindingTest.java b/test/sun/management/jmxremote/bootstrap/JMXInterfaceBindingTest.java new file mode 100644 index 0000000000000000000000000000000000000000..86dbeb8408e608cc54f45a78c7acffbf05e1d221 --- /dev/null +++ b/test/sun/management/jmxremote/bootstrap/JMXInterfaceBindingTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2015, Red Hat Inc + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import jdk.testlibrary.ProcessTools; + +/** + * NOTE: + * This test requires at least a setup similar to the following in + * /etc/hosts file (or the windows equivalent). I.e. it expects it to + * be multi-homed and not both being the loop-back interface. + * For example: + * ----->8-------- /etc/hosts ----------->8--- + * 127.0.0.1 localhost + * 192.168.0.1 localhost + * ----->8-------- /etc/hosts ----------->8--- + * + * @test + * @bug 6425769 + * @summary Test JMX agent host address binding. Same ports but different + * interfaces to bind to (using plain sockets and SSL sockets). + * + * @modules java.management/sun.management + * java.management/sun.management.jmxremote + * @library /lib/testlibrary + * @build jdk.testlibrary.* JMXAgentInterfaceBinding + * @run main/timeout=5 JMXInterfaceBindingTest + */ +public class JMXInterfaceBindingTest { + + public static final int COMMUNICATION_ERROR_EXIT_VAL = 1; + public static final int STOP_PROCESS_EXIT_VAL = 137; + public static final int JMX_PORT = 9111; + public static final int RMI_PORT = 9112; + public static final String READY_MSG = "MainThread: Ready for connections"; + public static final String TEST_CLASS = JMXAgentInterfaceBinding.class.getSimpleName(); + public static final String KEYSTORE_LOC = System.getProperty("test.src", ".") + + File.separator + + "ssl" + + File.separator + + "keystore"; + public static final String TRUSTSTORE_LOC = System.getProperty("test.src", ".") + + File.separator + + "ssl" + + File.separator + + "truststore"; + public static final String TEST_CLASSPATH = System.getProperty("test.classes", "."); + + public void run(InetAddress[] addrs) { + System.out.println("DEBUG: Running tests with plain sockets."); + runTests(addrs, false); + System.out.println("DEBUG: Running tests with SSL sockets."); + runTests(addrs, true); + } + + private void runTests(InetAddress[] addrs, boolean useSSL) { + TestProcessThread[] jvms = new TestProcessThread[addrs.length]; + for (int i = 0; i < addrs.length; i++) { + System.out.println(); + String msg = String.format("DEBUG: Launching java tester for triplet (HOSTNAME,JMX_PORT,RMI_PORT) == (%s,%d,%d)", + addrs[i].getHostAddress(), + JMX_PORT, + RMI_PORT); + System.out.println(msg); + jvms[i] = runJMXBindingTest(addrs[i], useSSL); + jvms[i].start(); + System.out.println("DEBUG: Started " + (i + 1) + " Process(es)."); + } + int failedProcesses = 0; + for (TestProcessThread pt: jvms) { + try { + pt.stopProcess(); + pt.join(); + } catch (InterruptedException e) { + System.err.println("Failed to stop process: " + pt.getName()); + throw new RuntimeException("Test failed", e); + } + int exitValue = pt.getExitValue(); + // If there is a communication error (the case we care about) + // we get a exit code of 1 + if (exitValue == COMMUNICATION_ERROR_EXIT_VAL) { + // Failure case since the java processes should still be + // running. + System.err.println("Test FAILURE on " + pt.getName()); + failedProcesses++; + } else if (exitValue == STOP_PROCESS_EXIT_VAL) { + System.out.println("DEBUG: OK. Spawned java process terminated with expected exit code of " + STOP_PROCESS_EXIT_VAL); + } else { + System.err.println("Test FAILURE on " + pt.getName() + " reason: Unexpected exit code => " + exitValue); + failedProcesses++; + } + } + if (failedProcesses > 0) { + throw new RuntimeException("Test FAILED. " + failedProcesses + " out of " + addrs.length + " process(es) failed to start the JMX agent."); + } + } + + private TestProcessThread runJMXBindingTest(InetAddress a, boolean useSSL) { + List args = new ArrayList<>(); + args.add("-classpath"); + args.add(TEST_CLASSPATH); + args.add("-Dcom.sun.management.jmxremote.host=" + a.getHostAddress()); + args.add("-Dcom.sun.management.jmxremote.port=" + JMX_PORT); + args.add("-Dcom.sun.management.jmxremote.rmi.port=" + RMI_PORT); + args.add("-Dcom.sun.management.jmxremote.authenticate=false"); + args.add("-Dcom.sun.management.jmxremote.ssl=" + Boolean.toString(useSSL)); + if (useSSL) { + args.add("-Dcom.sun.management.jmxremote.registry.ssl=true"); + args.add("-Djavax.net.ssl.keyStore=" + KEYSTORE_LOC); + args.add("-Djavax.net.ssl.trustStore=" + TRUSTSTORE_LOC); + args.add("-Djavax.net.ssl.keyStorePassword=password"); + args.add("-Djavax.net.ssl.trustStorePassword=trustword"); + } + args.add(TEST_CLASS); + args.add(a.getHostAddress()); + args.add(Integer.toString(JMX_PORT)); + args.add(Integer.toString(RMI_PORT)); + args.add(Boolean.toString(useSSL)); + try { + ProcessBuilder builder = ProcessTools.createJavaProcessBuilder(args.toArray(new String[] {})); + System.out.println(ProcessTools.getCommandLine(builder)); + TestProcessThread jvm = new TestProcessThread("JMX-Tester-" + a.getHostAddress(), JMXInterfaceBindingTest::isJMXAgentResponseAvailable, builder); + return jvm; + } catch (Exception e) { + throw new RuntimeException("Test failed", e); + } + + } + + private static boolean isJMXAgentResponseAvailable(String line) { + if (line.equals(READY_MSG)) { + System.out.println("DEBUG: Found expected READY_MSG."); + return true; + } else if (line.startsWith("Error:")) { + // Allow for a JVM process that exits with + // "Error: JMX connector server communication error: ..." + // to continue as well since we handle that case elsewhere. + // This has the effect that the test does not timeout and + // fails with an exception in the test. + System.err.println("PROBLEM: JMX agent of target JVM did not start as it should."); + return true; + } else { + return false; + } + } + + public static void main(String[] args) { + InetAddress[] addrs = getAddressesForLocalHost(); + if (addrs.length < 2) { + System.out.println("Ignoring manual test since no more than one IPs are configured for 'localhost'"); + System.exit(0); + } + JMXInterfaceBindingTest test = new JMXInterfaceBindingTest(); + test.run(addrs); + System.out.println("All tests PASSED."); + } + + private static InetAddress[] getAddressesForLocalHost() { + InetAddress[] addrs; + try { + addrs = InetAddress.getAllByName("localhost"); + } catch (UnknownHostException e) { + throw new RuntimeException("Test failed", e); + } + return addrs; + } + + private static class TestProcessThread extends Thread { + + private final Predicate predicate; + private final ProcessBuilder pb; + private final CountDownLatch latch; + private Process process; + + public TestProcessThread(String threadName, Predicate predicate, ProcessBuilder pb) { + super(threadName); + this.predicate = predicate; + this.pb = pb; + this.latch = new CountDownLatch(1); + } + + @Override + public void run() { + try { + process = ProcessTools.startProcess(getName(), pb, predicate, 10, TimeUnit.SECONDS); + latch.countDown(); + process.waitFor(); + } catch (Exception e) { + throw new RuntimeException("Test failed", e); + } + } + + public void stopProcess() { + try { + latch.await(); + } catch (InterruptedException e1) { + throw new RuntimeException("Test failed", e1); + } + if (process != null) { + process.destroyForcibly(); + try { + process.waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException("Test failed", e); + } + } + } + + public int getExitValue() { + return process.exitValue(); + } + } +}