JMXManagedObjectRegistry.java
001 /*
002  *
003  * Licensed to the Apache Software Foundation (ASF) under one
004  * or more contributor license agreements.  See the NOTICE file
005  * distributed with this work for additional information
006  * regarding copyright ownership.  The ASF licenses this file
007  * to you under the Apache License, Version 2.0 (the
008  * "License"); you may not use this file except in compliance
009  * with the License.  You may obtain a copy of the License at
010  *
011  *   http://www.apache.org/licenses/LICENSE-2.0
012  *
013  * Unless required by applicable law or agreed to in writing,
014  * software distributed under the License is distributed on an
015  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016  * KIND, either express or implied.  See the License for the
017  * specific language governing permissions and limitations
018  * under the License.
019  *
020  */
021 package org.apache.qpid.server.management;
022 
023 import org.apache.commons.configuration.ConfigurationException;
024 import org.apache.log4j.Logger;
025 import org.apache.qpid.AMQException;
026 import org.apache.qpid.server.registry.ApplicationRegistry;
027 import org.apache.qpid.server.registry.IApplicationRegistry;
028 import org.apache.qpid.server.security.auth.database.Base64MD5PasswordFilePrincipalDatabase;
029 import org.apache.qpid.server.security.auth.database.PlainPasswordFilePrincipalDatabase;
030 import org.apache.qpid.server.security.auth.database.PrincipalDatabase;
031 import org.apache.qpid.server.security.auth.rmi.RMIPasswordAuthenticator;
032 import org.apache.qpid.server.security.auth.sasl.crammd5.CRAMMD5HashedInitialiser;
033 import org.apache.qpid.server.security.auth.sasl.plain.PlainInitialiser;
034 
035 import javax.management.InstanceNotFoundException;
036 import javax.management.JMException;
037 import javax.management.MBeanRegistrationException;
038 import javax.management.MBeanServer;
039 import javax.management.MBeanServerFactory;
040 import javax.management.ObjectName;
041 import javax.management.remote.JMXConnectorServer;
042 import javax.management.remote.JMXConnectorServerFactory;
043 import javax.management.remote.JMXServiceURL;
044 import javax.management.remote.MBeanServerForwarder;
045 import javax.management.remote.rmi.RMIConnectorServer;
046 import javax.management.remote.rmi.RMIJRMPServerImpl;
047 import javax.management.remote.rmi.RMIServerImpl;
048 import javax.rmi.ssl.SslRMIClientSocketFactory;
049 import javax.rmi.ssl.SslRMIServerSocketFactory;
050 
051 import java.io.File;
052 import java.io.FileNotFoundException;
053 import java.io.IOException;
054 import java.lang.management.ManagementFactory;
055 import java.net.InetAddress;
056 import java.net.ServerSocket;
057 import java.net.Socket;
058 import java.rmi.AlreadyBoundException;
059 import java.rmi.RemoteException;
060 import java.rmi.registry.LocateRegistry;
061 import java.rmi.registry.Registry;
062 import java.rmi.server.RMIClientSocketFactory;
063 import java.rmi.server.RMIServerSocketFactory;
064 import java.rmi.server.UnicastRemoteObject;
065 import java.util.HashMap;
066 import java.util.Map;
067 
068 /**
069  * This class starts up an MBeanserver. If out of the box agent has been enabled then there are no 
070  * security features implemented like user authentication and authorisation.
071  */
072 public class JMXManagedObjectRegistry implements ManagedObjectRegistry
073 {
074     private static final Logger _log = Logger.getLogger(JMXManagedObjectRegistry.class);
075     private static final Logger _startupLog = Logger.getLogger("Qpid.Broker");
076     
077     public static final String MANAGEMENT_PORT_CONFIG_PATH = "management.jmxport";
078     public static final int MANAGEMENT_PORT_DEFAULT = 8999;
079     public static final int PORT_EXPORT_OFFSET = 100;
080 
081     private final MBeanServer _mbeanServer;
082     private Registry _rmiRegistry;
083     
084 
085     public JMXManagedObjectRegistry() throws AMQException
086     {
087         _log.info("Initialising managed object registry using platform MBean server");
088         IApplicationRegistry appRegistry = ApplicationRegistry.getInstance();
089 
090         // Retrieve the config parameters
091         boolean platformServer = appRegistry.getConfiguration().getPlatformMbeanserver();
092 
093         _mbeanServer =
094                 platformServer ? ManagementFactory.getPlatformMBeanServer()
095                 : MBeanServerFactory.createMBeanServer(ManagedObject.DOMAIN);
096     }
097 
098 
099     public void start() throws IOException, ConfigurationException
100     {
101         //check if system properties are set to use the JVM's out-of-the-box JMXAgent
102         if (areOutOfTheBoxJMXOptionsSet())
103         {
104             _log.warn("JMX: Using the out of the box JMX Agent");
105             _startupLog.warn("JMX: Using the out of the box JMX Agent");
106             return;
107         }
108 
109         IApplicationRegistry appRegistry = ApplicationRegistry.getInstance();
110 
111         boolean jmxmpSecurity = appRegistry.getConfiguration().getManagementSecurityEnabled();
112         int port = appRegistry.getConfiguration().getJMXManagementPort();
113 
114         //retrieve the Principal Database assigned to JMX authentication duties
115         String jmxDatabaseName = appRegistry.getConfiguration().getJMXPrincipalDatabase();
116         Map<String, PrincipalDatabase> map = appRegistry.getDatabaseManager().getDatabases();        
117         PrincipalDatabase db = map.get(jmxDatabaseName);
118 
119         final JMXConnectorServer cs;
120         HashMap<String,Object> env = new HashMap<String,Object>();
121 
122         if (jmxmpSecurity)
123         {
124             // For SASL using JMXMP
125             JMXServiceURL jmxURL = new JMXServiceURL("jmxmp", null, port);
126 
127             String saslType = null;
128             if (db instanceof Base64MD5PasswordFilePrincipalDatabase)
129             {
130                 saslType = "SASL/CRAM-MD5";
131                 env.put("jmx.remote.profiles""SASL/CRAM-MD5");
132                 CRAMMD5HashedInitialiser initialiser = new CRAMMD5HashedInitialiser();
133                 initialiser.initialise(db);
134                 env.put("jmx.remote.sasl.callback.handler", initialiser.getCallbackHandler());
135             }
136             else if (db instanceof PlainPasswordFilePrincipalDatabase)
137             {
138                 saslType = "SASL/PLAIN";
139                 PlainInitialiser initialiser = new PlainInitialiser();
140                 initialiser.initialise(db);
141                 env.put("jmx.remote.sasl.callback.handler", initialiser.getCallbackHandler());
142                 env.put("jmx.remote.profiles""SASL/PLAIN");
143             }
144 
145             //workaround NPE generated from env map classloader issue when using Eclipse 3.4 to launch
146             env.put("jmx.remote.profile.provider.class.loader"this.getClass().getClassLoader());
147 
148             _log.warn("Starting JMXMP based JMX ConnectorServer on port '" + port + "' with " + saslType);
149             _startupLog.warn("Starting JMXMP based JMX ConnectorServer on port '" + port + "' with " + saslType);
150             
151             cs = JMXConnectorServerFactory.newJMXConnectorServer(jmxURL, env, _mbeanServer);
152         }
153         else
154         {   
155             //Socket factories for the RMIConnectorServer, either default or SLL depending on configuration
156             RMIClientSocketFactory csf;
157             RMIServerSocketFactory ssf;
158             
159             //check ssl enabled option in config, default to true if option is not set
160             boolean sslEnabled = appRegistry.getConfiguration().getManagementSSLEnabled();
161 
162             if (sslEnabled)
163             {
164                 //set the SSL related system properties used by the SSL RMI socket factories to the values
165                 //given in the configuration file, unless command line settings have already been specified
166                 String keyStorePath;
167                 
168                 if(System.getProperty("javax.net.ssl.keyStore"!= null)
169                 {
170                     keyStorePath = System.getProperty("javax.net.ssl.keyStore");
171                 }
172                 else{
173                     keyStorePath = appRegistry.getConfiguration().getManagementKeyStorePath();
174                 }
175                 
176                 //check the keystore path value is valid
177                 if (keyStorePath == null)
178                 {
179                     throw new ConfigurationException("JMX management SSL keystore path not defined, " +
180                                                  "unable to start SSL protected JMX ConnectorServer");
181                 }
182                 else
183                 {
184                     //ensure the system property is set
185                     System.setProperty("javax.net.ssl.keyStore", keyStorePath);
186                     
187                     //check the file is usable
188                     File ksf = new File(keyStorePath);
189                     
190                     if (!ksf.exists())
191                     {
192                         throw new FileNotFoundException("Cannot find JMX management SSL keystore file " + ksf);
193                     }
194                     if (!ksf.canRead())
195                     {
196                         throw new FileNotFoundException("Cannot read JMX management SSL keystore file: " 
197                                                         + ksf +  ". Check permissions.");
198                     }
199                     
200                     _log.info("JMX ConnectorServer using SSL keystore file " + ksf.getAbsolutePath());
201                     _startupLog.info("JMX ConnectorServer using SSL keystore file " + ksf.getAbsolutePath());
202                 }
203 
204                 //check the key store password is set
205                 if (System.getProperty("javax.net.ssl.keyStorePassword"== null)
206                 {
207                 
208                     if (appRegistry.getConfiguration().getManagementKeyStorePassword() == null)
209                     {
210                         throw new ConfigurationException("JMX management SSL keystore password not defined, " +
211                                                      "unable to start requested SSL protected JMX server");
212                     }
213                     else
214                     {
215                         System.setProperty("javax.net.ssl.keyStorePassword",
216                                 appRegistry.getConfiguration().getManagementKeyStorePassword());
217                     }
218                 }
219 
220                 //create the SSL RMI socket factories
221                 csf = new SslRMIClientSocketFactory();
222                 ssf = new SslRMIServerSocketFactory();
223 
224                 _log.warn("Starting JMX ConnectorServer on port '"+ port + "' (+" 
225                         (port +PORT_EXPORT_OFFSET") with SSL");
226                 _startupLog.warn("Starting JMX ConnectorServer on port '"+ port + "' (+" 
227                         (port +PORT_EXPORT_OFFSET") with SSL");
228             }
229             else
230             {
231                 //Do not specify any specific RMI socket factories, resulting in use of the defaults.
232                 csf = null;
233                 ssf = null;
234                 
235                 _log.warn("Starting JMX ConnectorServer on port '" + port + "' (+" (port +PORT_EXPORT_OFFSET")");
236                 _startupLog.warn("Starting JMX ConnectorServer on port '" + port + "' (+" (port +PORT_EXPORT_OFFSET")");
237             }
238             
239             //add a JMXAuthenticator implementation the env map to authenticate the RMI based JMX connector server
240             RMIPasswordAuthenticator rmipa = new RMIPasswordAuthenticator();
241             rmipa.setPrincipalDatabase(db);
242             env.put(JMXConnectorServer.AUTHENTICATOR, rmipa);
243             
244             /*
245              * Start a RMI registry on the management port, to hold the JMX RMI ConnectorServer stub. 
246              * Using custom socket factory to prevent anyone (including us unfortunately) binding to the registry using RMI.
247              * As a result, only binds made using the object reference will succeed, thus securing it from external change. 
248              */
249             System.setProperty("java.rmi.server.randomIDs""true");
250             _rmiRegistry = LocateRegistry.createRegistry(port, null, new CustomRMIServerSocketFactory());
251             
252             /*
253              * We must now create the RMI ConnectorServer manually, as the JMX Factory methods use RMI calls 
254              * to bind the ConnectorServer to the registry, which will now fail as for security we have
255              * locked it from any RMI based modifications, including our own. Instead, we will manually bind 
256              * the RMIConnectorServer stub to the registry using its object reference, which will still succeed.
257              
258              * The registry is exported on the defined management port 'port'. We will export the RMIConnectorServer
259              * on 'port +1'. Use of these two well-defined ports will ease any navigation through firewall's. 
260              */
261             final RMIServerImpl rmiConnectorServerStub = new RMIJRMPServerImpl(port+PORT_EXPORT_OFFSET, csf, ssf, env)
262             final String hostname = InetAddress.getLocalHost().getHostName();
263             final JMXServiceURL externalUrl = new JMXServiceURL(
264                     "service:jmx:rmi://"+hostname+":"+(port+PORT_EXPORT_OFFSET)+"/jndi/rmi://"+hostname+":"+port+"/jmxrmi");
265 
266             final JMXServiceURL internalUrl = new JMXServiceURL("rmi", hostname, port+PORT_EXPORT_OFFSET);
267             cs = new RMIConnectorServer(internalUrl, env, rmiConnectorServerStub, _mbeanServer)
268             {   
269                 @Override  
270                 public synchronized void start() throws IOException
271                 {   
272                     try
273                     {   
274                         //manually bind the connector server to the registry at key 'jmxrmi', like the out-of-the-box agent                        
275                         _rmiRegistry.bind("jmxrmi", rmiConnectorServerStub);   
276                     }
277                     catch (AlreadyBoundException abe)
278                     {   
279                         //key was already in use. shouldnt happen here as its a new registry, unbindable by normal means.
280                         
281                         //IOExceptions are the only checked type throwable by the method, wrap and rethrow
282                         IOException ioe = new IOException(abe.getMessage());   
283                         ioe.initCause(abe);   
284                         throw ioe;   
285                     }
286                     
287                     //now do the normal tasks
288                     super.start();   
289                 }   
290                 
291                 @Override  
292                 public JMXServiceURL getAddress()
293                 {
294                     //must return our pre-crafted url that includes the full details, inc JNDI details
295                     return externalUrl;
296                 }   
297 
298             };   
299         }
300 
301         //Add the custom invoker as an MBeanServerForwarder, and start the RMIConnectorServer.
302         MBeanServerForwarder mbsf = MBeanInvocationHandlerImpl.newProxyInstance();
303         cs.setMBeanServerForwarder(mbsf);
304         cs.start();
305     }
306 
307     /*
308      * Custom RMIServerSocketFactory class, used to prevent updates to the RMI registry. 
309      * Supplied to the registry at creation, this will prevent RMI-based operations on the
310      * registry such as attempting to bind a new object, thereby securing it from tampering.
311      * This is accomplished by always returning null when attempting to determine the address
312      * of the caller, thus ensuring the registry will refuse the attempt. Calls to bind etc
313      * made using the object reference will not be affected and continue to operate normally.
314      */
315     
316     private class CustomRMIServerSocketFactory implements RMIServerSocketFactory
317     {
318 
319         public ServerSocket createServerSocket(int portthrows IOException
320         {
321             return new NoLocalAddressServerSocket(port);
322         }
323 
324         private class NoLocalAddressServerSocket extends ServerSocket
325         {
326             NoLocalAddressServerSocket(int portthrows IOException
327             {
328                 super(port);
329             }
330 
331             @Override
332             public Socket accept() throws IOException
333             {
334                 Socket s = new NoLocalAddressSocket();
335                 super.implAccept(s);
336                 return s;
337             }
338         }
339 
340         private class NoLocalAddressSocket extends Socket
341         {
342             @Override
343             public InetAddress getInetAddress()
344             {
345                 return null;
346             }
347         }
348     }
349 
350 
351     public void registerObject(ManagedObject managedObjectthrows JMException
352     {
353         _mbeanServer.registerMBean(managedObject, managedObject.getObjectName());
354     }
355 
356     public void unregisterObject(ManagedObject managedObjectthrows JMException
357     {
358         _mbeanServer.unregisterMBean(managedObject.getObjectName());
359     }
360 
361     // checks if the system properties are set which enable the JVM's out-of-the-box JMXAgent.
362     private boolean areOutOfTheBoxJMXOptionsSet()
363     {
364         if (System.getProperty("com.sun.management.jmxremote"!= null)
365         {
366             return true;
367         }
368 
369         if (System.getProperty("com.sun.management.jmxremote.port"!= null)
370         {
371             return true;
372         }
373 
374         return false;
375     }
376 
377     // stops the RMIRegistry, if it was running and bound to a port
378     public void close() throws RemoteException
379     {
380         if (_rmiRegistry != null)
381         {
382             // Stopping the RMI registry
383             UnicastRemoteObject.unexportObject(_rmiRegistry, true);
384         }
385         for (ObjectName name : _mbeanServer.queryNames(null, null))
386         {
387             try
388             {
389                 _mbeanServer.unregisterMBean(name);
390             }
391             catch (JMException e)
392             {
393                 // Really shouldn't happen, but we'll ignore that...
394             }
395         }
396     }
397 
398 }