org.apache.cassandra.utils.JMXServerUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cassandra.utils.JMXServerUtils.java

Source

/*
 * 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.cassandra.utils;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.rmi.*;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.remote.*;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import javax.security.auth.Subject;

import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.jmx.remote.internal.RMIExporter;
import com.sun.jmx.remote.security.JMXPluggableAuthenticator;
import org.apache.cassandra.auth.jmx.AuthenticationProxy;
import org.apache.cassandra.exceptions.ConfigurationException;
import sun.rmi.registry.RegistryImpl;
import sun.rmi.server.UnicastServerRef2;

public class JMXServerUtils {
    private static final Logger logger = LoggerFactory.getLogger(JMXServerUtils.class);

    private static java.rmi.registry.Registry registry;

    /**
     * Creates a server programmatically. This allows us to set parameters which normally are
     * inaccessable.
     */
    public static JMXConnectorServer createJMXServer(int port, boolean local) throws IOException {
        Map<String, Object> env = new HashMap<>();

        String urlTemplate = "service:jmx:rmi://%1$s/jndi/rmi://%1$s:%2$d/jmxrmi";
        InetAddress serverAddress = null;
        if (local) {
            serverAddress = InetAddress.getLoopbackAddress();
            System.setProperty("java.rmi.server.hostname", serverAddress.getHostAddress());
        }

        // Configure the RMI client & server socket factories, including SSL config.
        env.putAll(configureJmxSocketFactories(serverAddress, local));

        // Configure authn, using a JMXAuthenticator which either wraps a set log LoginModules configured
        // via a JAAS configuration entry, or one which delegates to the standard file based authenticator.
        // Authn is disabled if com.sun.management.jmxremote.authenticate=false
        env.putAll(configureJmxAuthentication());

        // Configure authz - if a custom proxy class is specified an instance will be returned.
        // If not, but a location for the standard access file is set in system properties, the
        // return value is null, and an entry is added to the env map detailing that location
        // If neither method is specified, no access control is applied
        MBeanServerForwarder authzProxy = configureJmxAuthorization(env);

        // Make sure we use our custom exporter so a full GC doesn't get scheduled every
        // sun.rmi.dgc.server.gcInterval millis (default is 3600000ms/1 hour)
        env.put(RMIExporter.EXPORTER_ATTRIBUTE, new Exporter());

        String url = String.format(urlTemplate,
                (serverAddress != null ? serverAddress.getHostAddress() : "0.0.0.0"), port);

        int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
        JMXConnectorServer jmxServer = JMXConnectorServerFactory.newJMXConnectorServer(
                new JMXServiceURL("rmi", null, rmiPort), env, ManagementFactory.getPlatformMBeanServer());

        // If a custom authz proxy was created, attach it to the server now.
        if (authzProxy != null)
            jmxServer.setMBeanServerForwarder(authzProxy);

        jmxServer.start();

        // use a custom Registry to avoid having to interact with it internally using the remoting interface
        configureRMIRegistry(port, env);

        logger.info("Configured JMX server at: {}", url);
        return jmxServer;
    }

    private static void configureRMIRegistry(int port, Map<String, Object> env) throws RemoteException {
        Exporter exporter = (Exporter) env.get(RMIExporter.EXPORTER_ATTRIBUTE);
        // If ssl is enabled, make sure it's also in place for the RMI registry
        // by using the SSL socket factories already created and stashed in env
        if (Boolean.getBoolean("com.sun.management.jmxremote.ssl")) {
            registry = new Registry(port,
                    (RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
                    (RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE),
                    exporter.connectorServer);
        } else {
            registry = new Registry(port, exporter.connectorServer);
        }
    }

    private static Map<String, Object> configureJmxAuthentication() {
        Map<String, Object> env = new HashMap<>();
        if (!Boolean.getBoolean("com.sun.management.jmxremote.authenticate"))
            return env;

        // If authentication is enabled, initialize the appropriate JMXAuthenticator
        // and stash it in the environment settings.
        // A JAAS configuration entry takes precedence. If one is supplied, use
        // Cassandra's own custom JMXAuthenticator implementation which delegates
        // auth to the LoginModules specified by the JAAS configuration entry.
        // If no JAAS entry is found, an instance of the JDK's own
        // JMXPluggableAuthenticator is created. In that case, the admin may have
        // set a location for the JMX password file which must be added to env
        // before creating the authenticator. If no password file has been
        // explicitly set, it's read from the default location
        // $JAVA_HOME/lib/management/jmxremote.password
        String configEntry = System.getProperty("cassandra.jmx.remote.login.config");
        if (configEntry != null) {
            env.put(JMXConnectorServer.AUTHENTICATOR, new AuthenticationProxy(configEntry));
        } else {
            String passwordFile = System.getProperty("com.sun.management.jmxremote.password.file");
            if (passwordFile != null) {
                // stash the password file location where JMXPluggableAuthenticator expects it
                env.put("jmx.remote.x.password.file", passwordFile);
            }

            env.put(JMXConnectorServer.AUTHENTICATOR, new JMXPluggableAuthenticatorWrapper(env));
        }

        return env;
    }

    private static MBeanServerForwarder configureJmxAuthorization(Map<String, Object> env) {
        // If a custom authz proxy is supplied (Cassandra ships with AuthorizationProxy, which
        // delegates to its own role based IAuthorizer), then instantiate and return one which
        // can be set as the JMXConnectorServer's MBeanServerForwarder.
        // If no custom proxy is supplied, check system properties for the location of the
        // standard access file & stash it in env
        String authzProxyClass = System.getProperty("cassandra.jmx.authorizer");
        if (authzProxyClass != null) {
            final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy");
            final Class[] interfaces = { MBeanServerForwarder.class };

            Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler);
            return MBeanServerForwarder.class.cast(proxy);
        } else {
            String accessFile = System.getProperty("com.sun.management.jmxremote.access.file");
            if (accessFile != null) {
                env.put("jmx.remote.x.access.file", accessFile);
            }
            return null;
        }
    }

    private static Map<String, Object> configureJmxSocketFactories(InetAddress serverAddress, boolean localOnly) {
        Map<String, Object> env = new HashMap<>();
        if (Boolean.getBoolean("com.sun.management.jmxremote.ssl")) {
            boolean requireClientAuth = Boolean.getBoolean("com.sun.management.jmxremote.ssl.need.client.auth");
            String[] protocols = null;
            String protocolList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.protocols");
            if (protocolList != null) {
                System.setProperty("javax.rmi.ssl.client.enabledProtocols", protocolList);
                protocols = StringUtils.split(protocolList, ',');
            }

            String[] ciphers = null;
            String cipherList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.cipher.suites");
            if (cipherList != null) {
                System.setProperty("javax.rmi.ssl.client.enabledCipherSuites", cipherList);
                ciphers = StringUtils.split(cipherList, ',');
            }

            SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory();
            SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(ciphers, protocols,
                    requireClientAuth);
            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory);
            env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory);
            env.put("com.sun.jndi.rmi.factory.socket", clientFactory);
            logJmxSslConfig(serverFactory);
        } else if (localOnly) {
            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
                    new RMIServerSocketFactoryImpl(serverAddress));
        }

        return env;
    }

    private static void logJmxSslConfig(SslRMIServerSocketFactory serverFactory) {
        logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }",
                serverFactory.getEnabledProtocols() == null ? "'JVM defaults'"
                        : Arrays.stream(serverFactory.getEnabledProtocols()).collect(
                                Collectors.joining("','", "'", "'")),
                serverFactory.getEnabledCipherSuites() == null ? "'JVM defaults'"
                        : Arrays.stream(serverFactory.getEnabledCipherSuites())
                                .collect(Collectors.joining("','", "'", "'")),
                serverFactory.getNeedClientAuth());
    }

    private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator {
        final Map<?, ?> env;

        private JMXPluggableAuthenticatorWrapper(Map<?, ?> env) {
            this.env = ImmutableMap.copyOf(env);
        }

        public Subject authenticate(Object credentials) {
            JMXPluggableAuthenticator authenticator = new JMXPluggableAuthenticator(env);
            return authenticator.authenticate(credentials);
        }
    }

    /**
     * In the RMI subsystem, the ObjectTable instance holds references to remote
     * objects for distributed garbage collection purposes. When objects are
     * added to the ObjectTable (exported), a flag is passed to * indicate the
     * "permanence" of that object. Exporting as permanent has two effects; the
     * object is not eligible for distributed garbage collection, and its
     * existence will not prevent the JVM from exiting after termination of all
     * non-daemon threads terminate. Neither of these is bad for our case, as we
     * attach the server exactly once (i.e. at startup, not subsequently using
     * the Attach API) and don't disconnect it before shutdown. The primary
     * benefit we gain is that it doesn't trigger the scheduled full GC that
     * is otherwise incurred by programatically configuring the management server.
     *
     * To that end, we use this private implementation of RMIExporter to register
     * our JMXConnectorServer as a permanent object by adding it to the map of
     * environment variables under the key RMIExporter.EXPORTER_ATTRIBUTE
     * (com.sun.jmx.remote.rmi.exporter) prior to calling server.start()
     *
     * See also:
     *  * CASSANDRA-2967 for background
     *  * https://www.jclarity.com/2015/01/27/rmi-system-gc-unplugged/ for more detail
     *  * https://bugs.openjdk.java.net/browse/JDK-6760712 for info on setting the exporter
     *  * sun.management.remote.ConnectorBootstrap to trace how the inbuilt management agent
     *    sets up the JMXConnectorServer
     */
    private static class Exporter implements RMIExporter {
        // the first object to be exported by this instance is *always* the JMXConnectorServer
        // instance created by createJMXServer. Keep a handle to it, as it needs to be supplied
        // to our custom Registry too.
        private Remote connectorServer;

        public Remote exportObject(Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
                throws RemoteException {
            Remote remote = new UnicastServerRef2(port, csf, ssf).exportObject(obj, null, true);
            // Keep a reference to the first object exported, the JMXConnectorServer
            if (connectorServer == null)
                connectorServer = remote;

            return remote;
        }

        public boolean unexportObject(Remote obj, boolean force) throws NoSuchObjectException {
            return UnicastRemoteObject.unexportObject(obj, force);
        }
    }

    /**
     * Using this class avoids the necessity to interact with the registry via its
     * remoting interface. This is necessary because when SSL is enabled for the registry,
     * that remote interaction is treated just the same as one from an external client.
     * That is problematic when binding the JMXConnectorServer to the Registry as it requires
     * the client, which in this case is our own internal code, to connect like any other SSL
     * client, meaning we need a truststore containing our own certificate.
     * This bypasses the binding API completely, which emulates the behaviour of
     * ConnectorBootstrap when the subsystem is initialized by the JVM Agent directly.
     *
     * See CASSANDRA-12109.
     */
    private static class Registry extends RegistryImpl {
        private final static String KEY = "jmxrmi";
        private final Remote connectorServer;

        private Registry(int port, Remote connectorServer) throws RemoteException {
            super(port);
            this.connectorServer = connectorServer;
        }

        private Registry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf, Remote connectorServer)
                throws RemoteException {
            super(port, csf, ssf);
            this.connectorServer = connectorServer;
        }

        public Remote lookup(String name) throws RemoteException, NotBoundException {
            if (name.equals(KEY))
                return connectorServer;

            throw new NotBoundException(
                    String.format("Only the JMX Connector Server named %s " + "is bound in this registry", KEY));
        }

        public void bind(String name, Remote obj) throws RemoteException, AlreadyBoundException {
            throw new UnsupportedOperationException("Unsupported");
        }

        public void unbind(String name) throws RemoteException, NotBoundException {
            throw new UnsupportedOperationException("Unsupported");
        }

        public void rebind(String name, Remote obj) throws RemoteException {
            throw new UnsupportedOperationException("Unsupported");
        }

        public String[] list() throws RemoteException {
            return new String[] { KEY };
        }
    }
}