diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java index 79e241e204..7baa0f5a77 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/factory/jmx/ManagementFactory.java @@ -96,6 +96,9 @@ public class ManagementFactory { if (jmxConnector.getConnectorHost() != null) { jmxConnectorConfiguration.setConnectorHost(jmxConnector.getConnectorHost()); } + if (jmxConnector.getRmiRegistryPort() != null) { + jmxConnectorConfiguration.setRmiRegistryPort(jmxConnector.getRmiRegistryPort()); + } if (jmxConnector.getJmxRealm() != null) { jmxConnectorConfiguration.setJmxRealm(jmxConnector.getJmxRealm()); } diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java index bd78481d8a..965de9392d 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JMXConnectorDTO.java @@ -34,6 +34,9 @@ public class JMXConnectorDTO { @XmlAttribute (name = "connector-port", required = true) Integer connectorPort; + @XmlAttribute (name = "rmi-registry-port") + Integer rmiRegistryPort; + @XmlAttribute (name = "jmx-realm") String jmxRealm; @@ -75,6 +78,10 @@ public class JMXConnectorDTO { return connectorPort; } + public Integer getRmiRegistryPort() { + return rmiRegistryPort; + } + public String getJmxRealm() { return jmxRealm; } diff --git a/docs/user-manual/en/management.md b/docs/user-manual/en/management.md index f7c835223d..60ef2a184f 100644 --- a/docs/user-manual/en/management.md +++ b/docs/user-manual/en/management.md @@ -423,7 +423,13 @@ You can also configure the connector using the following: - `connector-port` The port to expose the agent on. - + +- `rmi-registry-port` + + The port that the RMI registry binds to. If not set, the port is + always random. Set to avoid problems with remote JMX connections + tunnelled through firewall. + - `jmx-realm` The jmx realm to use for authentication, defaults to `activemq` to match the diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jmx/JmxConnectionTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jmx/JmxConnectionTest.java new file mode 100644 index 0000000000..9fd2c3a4f6 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jmx/JmxConnectionTest.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.activemq.artemis.tests.integration.jmx; + +import com.sun.jmx.remote.internal.ProxyRef; +import org.apache.activemq.artemis.cli.Artemis; +import org.apache.activemq.artemis.cli.commands.Run; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import sun.rmi.server.UnicastRef; +import sun.rmi.transport.LiveRef; + +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnection; +import javax.management.remote.rmi.RMIConnector; +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.rmi.server.RemoteObject; +import java.rmi.server.RemoteRef; +import java.util.concurrent.TimeUnit; + +/** + * This test checks JMX connection to Artemis with both necessary ports set up so that it's easier to tunnel through + * firewalls. + */ +public class JmxConnectionTest extends ActiveMQTestBase { + + private static final String MANAGEMENT_XML = "/jmx-test-management.xml"; + + // Make sure these values are always the same as in the MANAGEMENT_XML configuration file + private static final String JMX_SERVER_HOSTNAME = "127.0.0.1"; + private static final int JMX_SERVER_PORT = 10099; + private static final int RMI_REGISTRY_PORT = 10098; + + @Override + @Before + public void setUp() throws Exception { + + super.setUp(); + + /* This needs to be disabled because otherwise there would be a lot of complains about Artemis' own running threads + * and I suppose that this kind of leaks is most likely tested somewhere else. + */ + disableCheckThread(); + + // Artemis instance dir + File instanceDir = new File(temporaryFolder.getRoot(), "instance"); + + // Create new Artemis instance + Run.setEmbedded(true); + Artemis.main("create", instanceDir.getAbsolutePath(), "--silent", "--no-fsync", "--no-autotune", + "--no-web", "--no-amqp-acceptor", "--no-mqtt-acceptor", "--no-stomp-acceptor", "--no-hornetq-acceptor"); + + // Configure (this is THE subject of testing) + File managementConfigFile = new File(instanceDir, "etc/management.xml"); + Files.copy(getClass().getResourceAsStream(MANAGEMENT_XML), managementConfigFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + // Point the server to the instance directory + System.setProperty("artemis.instance", instanceDir.getAbsolutePath()); + + // Without this, the RMI server would bind to the default interface IP (the user's local IP mostly) + System.setProperty("java.rmi.server.hostname", JMX_SERVER_HOSTNAME); + + // Enable guest login module (dunno why this isn't automatic) + System.setProperty("java.security.auth.login.config", instanceDir.getAbsolutePath() + "/etc/login.config"); + + // Run Artemis server + Artemis.internalExecute("run"); + } + + @Test + public void testJmxConnection() throws Exception { + + // I don't specify both ports here manually on purpose. See actual RMI registry connection port extraction below. + String urlString = "service:jmx:rmi:///jndi/rmi://" + JMX_SERVER_HOSTNAME + ":" + JMX_SERVER_PORT + "/jmxrmi"; + + JMXServiceURL url = new JMXServiceURL(urlString); + JMXConnector jmxConnector; + + try { + jmxConnector = JMXConnectorFactory.connect(url); + logAndSystemOut("Successfully connected to: " + urlString); + } catch (Exception e) { + logAndSystemOut("JMX connection failed: " + urlString, e); + Assert.fail(); + return; + } + + try { + + /* Now I need to extract the RMI registry port to make sure it's equal to the configured one. It's gonna be + * messy because I have to use reflection to reach the information. + */ + + Assert.assertTrue(jmxConnector instanceof RMIConnector); + + // 1. RMIConnector::connection is expected to be RMIConnectionImpl_Stub + Field connectionField = RMIConnector.class.getDeclaredField("connection"); + connectionField.setAccessible(true); + RMIConnection rmiConnection = (RMIConnection) connectionField.get(jmxConnector); + + // 2. RMIConnectionImpl_Stub extends RemoteStub which extends RemoteObject + Assert.assertTrue(rmiConnection instanceof RemoteObject); + RemoteObject remoteObject = (RemoteObject) rmiConnection; + + // 3. RemoteObject::getRef is hereby expected to return ProxyRef + RemoteRef remoteRef = remoteObject.getRef(); + Assert.assertTrue(remoteRef instanceof ProxyRef); + ProxyRef proxyRef = (ProxyRef) remoteRef; + + // 4. ProxyRef::ref is expected to contain UnicastRef (UnicastRef2 resp.) + Field refField = ProxyRef.class.getDeclaredField("ref"); + refField.setAccessible(true); + remoteRef = (RemoteRef) refField.get(proxyRef); + Assert.assertTrue(remoteRef instanceof UnicastRef); + + // 5. UnicastRef::getLiveRef returns LiveRef + LiveRef liveRef = ((UnicastRef) remoteRef).getLiveRef(); + + // 6. LiveRef::getPort is expected to be the same as the RMI registry port configured via management.xml + /* Accidentally, it can happen that even with the RMI registry port unconfigured the randomly selected port + * will be the same as expected by the test which will make it succeed. But it's highly unlikely. + */ + Assert.assertEquals(RMI_REGISTRY_PORT, liveRef.getPort()); + + } finally { + jmxConnector.close(); + } + } + + @After + @Override + public void tearDown() throws Exception { + Artemis.internalExecute("stop"); + Run.latchRunning.await(5, TimeUnit.SECONDS); + super.tearDown(); + } + +} diff --git a/tests/integration-tests/src/test/resources/jmx-test-management.xml b/tests/integration-tests/src/test/resources/jmx-test-management.xml new file mode 100644 index 0000000000..ac5ba6d703 --- /dev/null +++ b/tests/integration-tests/src/test/resources/jmx-test-management.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file