/* * 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.brooklyn.util.jmx.jmxmp; import java.io.FileInputStream; import java.lang.management.ManagementFactory; import java.net.InetAddress; import java.net.UnknownHostException; import java.rmi.registry.LocateRegistry; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.management.MBeanServer; import javax.management.remote.JMXConnectorServer; import javax.management.remote.JMXConnectorServerFactory; 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.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.apache.brooklyn.util.jmx.jmxmp.JmxmpAgent; /** * This exposes JMX access over JMXMP, suitable for high-security environments, * with support for going through firewalls as well as encrypting and authenticating securely. * <p> * Listens on 11099 unless overridden by system property brooklyn.jmxmp.port. * <p> * Use the usual com.sun.management.jmxremote.ssl to enable both SSL _and_ authentication * (setting brooklyn.jmxmp.ssl.authenticate false if you need to disable authentication for some reason); * unless you disable client-side server authentication you will need to supply brooklyn.jmxmp.ssl.keyStore, * and similarly unless server-side client auth is off you'll need the corresponding trustStore * (both pointing to files on the local file system). * <p> * Service comes up on: service:jmx:jmxmp://${HOSTNAME}:${PORT} * <p> * If {@link #RMI_REGISTRY_PORT_PROPERTY} is also set, this agent will start a normal JMX/RMI server bound to * all interfaces, which is contactable on: service:jmx:rmi:///jndi/rmi://${HOSTNAME}:${RMI_REGISTRY_PORT}/jmxrmi * <p> * NB: To use JConsole with this endpoing, you need the jmxremote_optional JAR, and the * following command (even more complicated if using SSL): * java -classpath $JAVA_HOME/lib/jconsole.jar:$HOME/.m2/repository/javax/management/jmxremote_optional/1.0.1_04/jmxremote_optional-1.0.1_04.jar sun.tools.jconsole.JConsole */ public class JmxmpAgent { /** port to listen on; default to {@link #JMXMP_DEFAULT_PORT} */ public static final String JMXMP_PORT_PROPERTY = "brooklyn.jmxmp.port"; /** hostname to advertise, and if {@value #JMX_SERVER_ADDRESS_WILDCARD_PROPERTY} is false also the hostname/interface to bind to */ public static final String RMI_HOSTNAME_PROPERTY = "java.rmi.server.hostname"; /** whether JMX should bind to all interfaces */ public static final String JMX_SERVER_ADDRESS_WILDCARD_PROPERTY = "jmx.remote.server.address.wildcard"; /** optional port for RMI registry to listen on; if not supplied, RMI is disabled. 1099 is a common choice. * it will *always* use an anonymous high-numbered port as the rmi server it redirects to * (ie it behaves like the default JMX agent, not the custom JmxRmiAgent). */ public static final String RMI_REGISTRY_PORT_PROPERTY = "brooklyn.jmxmp.rmi-port"; /** whether to use SSL (TLS) encryption; requires a keystore to be set */ public static final String USE_SSL_PROPERTY = "com.sun.management.jmxremote.ssl"; /** whether to use SSL (TLS) certificates to authenticate the client; * requires a truststore to be set, and requires {@link #USE_SSL_PROPERTY} true * (different to 'com.sun.management.jmxremote.authenticate' because something else * insists on intercepting that and uses it for passwords); * defaults to true iff {@link #USE_SSL_PROPERTY} is set because * who wouldn't want client authentication if you're encrypting the link */ public static final String AUTHENTICATE_CLIENTS_PROPERTY = "brooklyn.jmxmp.ssl.authenticate"; public static final String JMXMP_KEYSTORE_FILE_PROPERTY = "brooklyn.jmxmp.ssl.keyStore"; public static final String JMXMP_KEYSTORE_PASSWORD_PROPERTY = "brooklyn.jmxmp.ssl.keyStorePassword"; public static final String JMXMP_KEYSTORE_KEY_PASSWORD_PROPERTY = "brooklyn.jmxmp.ssl.keyStore.keyPassword"; public static final String JMXMP_KEYSTORE_TYPE_PROPERTY = "brooklyn.jmxmp.ssl.keyStoreType"; public static final String JMXMP_TRUSTSTORE_FILE_PROPERTY = "brooklyn.jmxmp.ssl.trustStore"; public static final String JMXMP_TRUSTSTORE_PASSWORD_PROPERTY = "brooklyn.jmxmp.ssl.trustStorePassword"; public static final String JMXMP_TRUSTSTORE_TYPE_PROPERTY = "brooklyn.jmxmp.ssl.trustStoreType"; // properties above affect behaviour; those below are simply used in code public static final String TLS_NEED_AUTHENTICATE_CLIENTS_PROPERTY = "jmx.remote.tls.need.client.authentication"; public static final String TLS_WANT_AUTHENTICATE_CLIENTS_PROPERTY = "jmx.remote.tls.want.client.authentication"; public static final String TLS_SOCKET_FACTORY_PROPERTY = "jmx.remote.tls.socket.factory"; public static final String TLS_JMX_REMOTE_PROFILES = "TLS"; public static final int JMXMP_DEFAULT_PORT = 11099; public static void premain(String agentArgs) { doMain(agentArgs); } public static void agentmain(String agentArgs) { doMain(agentArgs); } public static void doMain(final String agentArgs) { // do the work in a daemon thread so that if the main class terminates abnormally, // such that shutdown hooks aren't called, we don't keep the application running // (e.g. if the app is compiled with java7 then run with java6, with a java6 agent here; // that causes the agent to launch, the main to fail, but the process to keep going) Thread t = new Thread() { public void run() { doMainForeground(agentArgs); } }; t.setDaemon(true); t.start(); } public static void doMainForeground(String agentArgs) { final List<JMXConnectorServer> connectors = new JmxmpAgent().startConnectors(System.getProperties()); if (!connectors.isEmpty()) { Runtime.getRuntime().addShutdownHook(new Thread("jmxmp-agent-shutdownHookThread") { @Override public void run() { for (JMXConnectorServer connector: connectors) { try { connector.stop(); } catch (Exception e) { System.err.println("Error closing jmxmp connector "+connector+" in shutdown hook (continuing): "+e); } } }}); } } public List<JMXConnectorServer> startConnectors(Properties properties) { List<JMXConnectorServer> connectors = new ArrayList<JMXConnectorServer>(); addIfNotNull(startJmxmpConnector(properties), connectors); addIfNotNull(startNormalJmxRmiConnectorIfRequested(properties), connectors); return connectors; } private static <T> void addIfNotNull(T item, List<T> list) { if (item!=null) list.add(item); } public JMXConnectorServer startJmxmpConnector(Properties properties) { try { final int port = Integer.parseInt(properties.getProperty(JMXMP_PORT_PROPERTY, ""+JMXMP_DEFAULT_PORT)); String hostname = getLocalhostHostname(properties); JMXServiceURL serviceUrl = new JMXServiceURL("service:jmx:jmxmp://"+hostname+":"+port); Map<String,Object> env = new LinkedHashMap<String, Object>(); propagate(properties, env, JMX_SERVER_ADDRESS_WILDCARD_PROPERTY, null); if (asBoolean(properties, USE_SSL_PROPERTY, false, true)) { setSslEnvFromProperties(env, properties); } else { if (asBoolean(properties, AUTHENTICATE_CLIENTS_PROPERTY, false, true)) { throw new IllegalStateException("Client authentication not supported when not using SSL"); } } MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); JMXConnectorServer connector = JMXConnectorServerFactory.newJMXConnectorServer(serviceUrl, env, platformMBeanServer); connector.start(); System.out.println("JmxmpAgent active at: "+serviceUrl); return connector; } catch (RuntimeException e) { System.err.println("Unable to start JmxmpAgent: "+e); throw e; } catch (Exception e) { System.err.println("Unable to start JmxmpAgent: "+e); throw new RuntimeException(e); } } /** optionally starts a normal JMXRMI connector in addition */ public JMXConnectorServer startNormalJmxRmiConnectorIfRequested(Properties properties) { try { String rmiPortS = properties.getProperty(RMI_REGISTRY_PORT_PROPERTY); if (rmiPortS==null || rmiPortS.length()==0) return null; int rmiPort = Integer.parseInt(rmiPortS); LocateRegistry.createRegistry(rmiPort); MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); String svc = "service:jmx:rmi:///jndi/rmi://localhost:"+rmiPort+"/jmxrmi"; JMXServiceURL url = new JMXServiceURL(svc); RMIConnectorServer rmiServer = new RMIConnectorServer(url, null, mbeanServer); rmiServer.start(); return rmiServer; } catch (Exception e) { System.err.println("Unable to start JmxmpAgent: "+e); throw new RuntimeException(e); } } public static String getLocalhostHostname(Properties properties) throws UnknownHostException { String hostname = properties==null ? null : properties.getProperty(RMI_HOSTNAME_PROPERTY); if (hostname==null || hostname.isEmpty()) { try { hostname = InetAddress.getLocalHost().getHostName(); } catch (Exception e) { System.err.println("Misconfigured hostname when setting JmxmpAgent; reverting to 127.0.0.1: "+e); hostname = "127.0.0.1"; } } return hostname; } /** copies the value of key from the source to the target, if set; * otherwise sets the defaultValueIfNotNull (final arg) if that is not null; * returns whether anything is set */ private static boolean propagate(Properties source, Map<String, Object> target, String key, Object defaultValueIfNotNull) { Object v = source.getProperty(key); if (v==null) v = defaultValueIfNotNull; if (v==null) return false; target.put(key, v); return true; } /** returns boolean interpretation of a string, * defaulting to valueIfUnknownText (last arg) if the value is unset or unrecognised, * throwing exception if that is null and value is unset or unrecognised */ private boolean asBoolean(Properties properties, String key, Boolean valueIfNull, Boolean valueIfUnknownText) { Object v = properties.get(key); if (v==null) { if (valueIfNull==null) throw new IllegalStateException("Property '"+key+"' is required."); return valueIfNull; } String vv = v.toString(); if ("true".equalsIgnoreCase(vv)) return true; if ("false".equalsIgnoreCase(vv)) return false; if (valueIfUnknownText==null) throw new IllegalStateException("Property '"+key+"' has illegal value '"+vv+"'; should be true or false"); return valueIfUnknownText; } public void setSslEnvFromProperties(Map<String, Object> env, Properties properties) throws Exception { env.put("jmx.remote.profiles", TLS_JMX_REMOTE_PROFILES); boolean authenticating = asBoolean(properties, AUTHENTICATE_CLIENTS_PROPERTY, true, null); if (authenticating) { env.put(AUTHENTICATE_CLIENTS_PROPERTY, "true"); // NB: the above seem to be ignored (horrid API!); we need the ones below set propagate(properties, env, TLS_NEED_AUTHENTICATE_CLIENTS_PROPERTY, "true"); // also note, the above seems to be overridden by below internally ! // (setting WANT=false and NEED=true allows access if no trust managers are specified) propagate(properties, env, TLS_WANT_AUTHENTICATE_CLIENTS_PROPERTY, "true"); } if (!propagate(properties, env, TLS_SOCKET_FACTORY_PROPERTY, null)) { String keyStoreFile = properties.getProperty(JMXMP_KEYSTORE_FILE_PROPERTY); String keyStorePass = properties.getProperty(JMXMP_KEYSTORE_PASSWORD_PROPERTY, ""); String keyStoreType = properties.getProperty(JMXMP_KEYSTORE_TYPE_PROPERTY, KeyStore.getDefaultType()); String keyStoreKeyPass = properties.getProperty(JMXMP_KEYSTORE_KEY_PASSWORD_PROPERTY, ""); KeyStore ks = KeyStore.getInstance(keyStoreType); if (keyStoreFile!=null) ks.load(new FileInputStream(keyStoreFile), keyStorePass.toCharArray()); else ks.load(null, null); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, keyStoreKeyPass.toCharArray()); String trustStoreFile = properties.getProperty(JMXMP_TRUSTSTORE_FILE_PROPERTY); String trustStorePass = properties.getProperty(JMXMP_TRUSTSTORE_PASSWORD_PROPERTY, ""); String trustStoreType = properties.getProperty(JMXMP_TRUSTSTORE_TYPE_PROPERTY, KeyStore.getDefaultType()); TrustManager[] tms; if (trustStoreFile!=null) { TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ts = KeyStore.getInstance(trustStoreType); ts.load(new FileInputStream(trustStoreFile), trustStorePass.toCharArray()); tmf.init(ts); // tms = tmf.getTrustManagers(); // line above causes tests to fail! bug in JMXMP TLS impl? tms = new TrustManager[] { newInspectAllTrustManager((X509TrustManager) tmf.getTrustManagers()[0]) }; } else { tms = null; if (authenticating) System.err.println("Authentication required but no truststore supplied to JmxmpAgent. Client connections will likely fail."); } SSLContext ctx = SSLContext.getInstance("TLSv1"); ctx.init(kmf.getKeyManagers(), tms, null); SSLSocketFactory ssf = ctx.getSocketFactory(); env.put(TLS_SOCKET_FACTORY_PROPERTY, ssf); } } public static final TrustManager newInspectAllTrustManager(final X509TrustManager delegate) { return new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { // overriding this method fixes bug where non-accepted issuers have an "accept all" policy, in JMXMP/TLS return new X509Certificate[0]; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { delegate.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { delegate.checkServerTrusted(chain, authType); } }; }; public static void main(String[] args) { premain(""); } }