//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.jmx;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.IntConsumer;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ShutdownThread;
/**
* <p>LifeCycle wrapper for JMXConnectorServer.</p>
* <p>This class provides the following facilities:</p>
* <ul>
* <li>participates in the {@code Server} lifecycle</li>
* <li>starts the RMI registry if not there already</li>
* <li>allows to bind the RMI registry and the RMI server to the loopback interface</li>
* <li>makes it easy to use TLS for the JMX communication</li>
* </ul>
*/
public class ConnectorServer extends AbstractLifeCycle
{
public static final String RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE = "com.sun.jndi.rmi.factory.socket";
private static final Logger LOG = Log.getLogger(ConnectorServer.class);
private JMXServiceURL _jmxURL;
private final Map<String, Object> _environment;
private final String _objectName;
private final SslContextFactory _sslContextFactory;
private int _registryPort;
private int _rmiPort;
private JMXConnectorServer _connectorServer;
private Registry _registry;
/**
* Constructs a ConnectorServer
*
* @param serviceURL the address of the new ConnectorServer
* @param name object name string to be assigned to ConnectorServer bean
*/
public ConnectorServer(JMXServiceURL serviceURL, String name)
{
this(serviceURL, null, name);
}
/**
* Constructs a ConnectorServer
*
* @param svcUrl the address of the new ConnectorServer
* @param environment a set of attributes to control the new ConnectorServer's behavior.
* This parameter can be null. Keys in this map must
* be Strings. The appropriate type of each associated value depends on
* the attribute. The contents of environment are not changed by this call.
* @param name object name string to be assigned to ConnectorServer bean
*/
public ConnectorServer(JMXServiceURL svcUrl, Map<String, ?> environment, String name)
{
this(svcUrl, environment, name, null);
}
public ConnectorServer(JMXServiceURL svcUrl, Map<String, ?> environment, String name, SslContextFactory sslContextFactory)
{
this._jmxURL = svcUrl;
this._environment = environment == null ? new HashMap<>() : new HashMap<>(environment);
this._objectName = name;
this._sslContextFactory = sslContextFactory;
}
public JMXServiceURL getAddress()
{
return _jmxURL;
}
@Override
public void doStart() throws Exception
{
boolean rmi = "rmi".equals(_jmxURL.getProtocol());
if (rmi)
{
if (!_environment.containsKey(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE))
_environment.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new JMXRMIServerSocketFactory(_jmxURL.getHost(), port -> _rmiPort = port));
if (_sslContextFactory != null)
{
SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
if (!_environment.containsKey(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE))
_environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
if (!_environment.containsKey(RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE))
_environment.put(RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
}
}
String urlPath = _jmxURL.getURLPath();
String jndiRMI = "/jndi/rmi://";
if (urlPath.startsWith(jndiRMI))
{
int startIndex = jndiRMI.length();
int endIndex = urlPath.indexOf('/', startIndex);
HostPort hostPort = new HostPort(urlPath.substring(startIndex, endIndex));
String registryHost = startRegistry(hostPort);
// If the RMI registry was already started, use the existing port.
if (_registryPort == 0)
_registryPort = hostPort.getPort();
urlPath = jndiRMI + registryHost + ":" + _registryPort + urlPath.substring(endIndex);
// Rebuild JMXServiceURL to use it for the creation of the JMXConnectorServer.
_jmxURL = new JMXServiceURL(_jmxURL.getProtocol(), _jmxURL.getHost(), _jmxURL.getPort(), urlPath);
}
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
_connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(_jmxURL, _environment, mbeanServer);
mbeanServer.registerMBean(_connectorServer, new ObjectName(_objectName));
_connectorServer.start();
String rmiHost = normalizeHost(_jmxURL.getHost());
// If _rmiPort is still zero, it's using the same port as the RMI registry.
if (_rmiPort == 0)
_rmiPort = _registryPort;
_jmxURL = new JMXServiceURL(_jmxURL.getProtocol(), rmiHost, _rmiPort, urlPath);
ShutdownThread.register(0, this);
LOG.info("JMX URL: {}", _jmxURL);
}
@Override
public void doStop() throws Exception
{
ShutdownThread.deregister(this);
_connectorServer.stop();
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
mbeanServer.unregisterMBean(new ObjectName(_objectName));
stopRegistry();
}
private String startRegistry(HostPort hostPort) throws Exception
{
String host = hostPort.getHost();
int port = hostPort.getPort(1099);
try
{
// Check if a local registry is already running.
LocateRegistry.getRegistry(host, port).list();
return normalizeHost(host);
}
catch (Throwable ex)
{
LOG.ignore(ex);
}
RMIClientSocketFactory csf = _sslContextFactory == null ? null : new SslRMIClientSocketFactory();
RMIServerSocketFactory ssf = new JMXRMIServerSocketFactory(host, p -> _registryPort = p);
_registry = LocateRegistry.createRegistry(port, csf, ssf);
return normalizeHost(host);
}
private String normalizeHost(String host) throws UnknownHostException
{
return host == null || host.isEmpty() ? InetAddress.getLocalHost().getHostName() : host;
}
private void stopRegistry()
{
if (_registry != null)
{
try
{
UnicastRemoteObject.unexportObject(_registry, true);
}
catch (Exception ex)
{
LOG.ignore(ex);
}
finally
{
_registry = null;
}
}
}
private class JMXRMIServerSocketFactory implements RMIServerSocketFactory
{
private final String _host;
private final IntConsumer _portConsumer;
private JMXRMIServerSocketFactory(String host, IntConsumer portConsumer)
{
this._host = host;
this._portConsumer = portConsumer;
}
@Override
public ServerSocket createServerSocket(int port) throws IOException
{
InetAddress address = _host == null || _host.isEmpty() ? InetAddress.getLocalHost() : InetAddress.getByName(_host);
ServerSocket server = createServerSocket(address, port);
_portConsumer.accept(server.getLocalPort());
return server;
}
private ServerSocket createServerSocket(InetAddress address, int port) throws IOException
{
// A null address binds to the wildcard address.
if (_sslContextFactory == null)
{
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(address, port));
return server;
}
else
{
return _sslContextFactory.newSslServerSocket(address == null ? null : address.getHostName(), port, 0);
}
}
@Override
public int hashCode()
{
return _host != null ? _host.hashCode() : 0;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
JMXRMIServerSocketFactory that = (JMXRMIServerSocketFactory)obj;
return Objects.equals(_host, that._host);
}
}
}