001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.protocols.jmx;
018
019import static org.opends.messages.ProtocolMessages.*;
020import static org.opends.server.types.HostPort.*;
021import static org.opends.server.util.StaticUtils.*;
022
023import java.io.IOException;
024import java.net.InetAddress;
025import java.net.InetSocketAddress;
026import java.util.Collection;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.SortedSet;
030import java.util.concurrent.CopyOnWriteArrayList;
031
032import org.forgerock.i18n.LocalizableMessage;
033import org.forgerock.i18n.slf4j.LocalizedLogger;
034import org.forgerock.opendj.config.server.ConfigException;
035import org.forgerock.opendj.config.server.ConfigurationChangeListener;
036import org.forgerock.opendj.server.config.server.ConnectionHandlerCfg;
037import org.forgerock.opendj.server.config.server.JMXConnectionHandlerCfg;
038import org.opends.server.api.ClientConnection;
039import org.opends.server.api.ConnectionHandler;
040import org.opends.server.api.ServerShutdownListener;
041import org.opends.server.core.DirectoryServer;
042import org.opends.server.core.ServerContext;
043import org.forgerock.opendj.config.server.ConfigChangeResult;
044import org.forgerock.opendj.ldap.DN;
045import org.opends.server.types.HostPort;
046import org.opends.server.types.InitializationException;
047import org.opends.server.util.StaticUtils;
048
049/**
050 * This class defines a connection handler that will be used for
051 * communicating with administrative clients over JMX. The connection
052 * handler is responsible for accepting new connections, reading
053 * requests from the clients and parsing them as operations. A single
054 * request handler should be used.
055 */
056public final class JmxConnectionHandler extends
057    ConnectionHandler<JMXConnectionHandlerCfg> implements
058    ServerShutdownListener,
059    ConfigurationChangeListener<JMXConnectionHandlerCfg> {
060
061  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
062
063
064  /**
065   * Key that may be placed into a JMX connection environment map to
066   * provide a custom {@code javax.net.ssl.TrustManager} array
067   * for a connection.
068   */
069  public static final String TRUST_MANAGER_ARRAY_KEY =
070    "org.opends.server.protocol.jmx.ssl.trust.manager.array";
071
072  /** The list of active client connection. */
073  private final List<ClientConnection> connectionList;
074
075  /** The current configuration state. */
076  private JMXConnectionHandlerCfg currentConfig;
077
078  /** The JMX RMI Connector associated with the Connection handler. */
079  private RmiConnector rmiConnector;
080
081  /** The unique name for this connection handler. */
082  private String connectionHandlerName;
083
084  /** The protocol used to communicate with clients. */
085  private String protocol;
086
087  /** The set of listeners for this connection handler. */
088  private final List<HostPort> listeners = new LinkedList<>();
089
090  /**
091   * Creates a new instance of this JMX connection handler. It must be
092   * initialized before it may be used.
093   */
094  public JmxConnectionHandler() {
095    super("JMX Connection Handler Thread");
096
097    this.connectionList = new CopyOnWriteArrayList<>();
098  }
099
100  @Override
101  public ConfigChangeResult applyConfigurationChange(
102      JMXConnectionHandlerCfg config) {
103    final ConfigChangeResult ccr = new ConfigChangeResult();
104
105    // Determine whether the RMI connection needs restarting.
106    boolean rmiConnectorRestart = false;
107    boolean portChanged = false;
108
109    if (currentConfig.getListenPort() != config.getListenPort()) {
110      rmiConnectorRestart = true;
111      portChanged = true;
112    }
113
114    if (currentConfig.getRmiPort() != config.getRmiPort())
115    {
116      rmiConnectorRestart = true;
117    }
118    if (currentConfig.isUseSSL() != config.isUseSSL()) {
119      rmiConnectorRestart = true;
120    }
121
122    if (notEqualsNotNull(config.getSSLCertNickname(), currentConfig.getSSLCertNickname())
123        || notEqualsNotNull(config.getSSLCertNickname(), currentConfig.getSSLCertNickname())) {
124      rmiConnectorRestart = true;
125    }
126
127    // Save the configuration.
128    currentConfig = config;
129
130    // Restart the connector if required.
131    if (rmiConnectorRestart) {
132      if (config.isUseSSL()) {
133        protocol = "JMX+SSL";
134      } else {
135        protocol = "JMX";
136      }
137
138      listeners.clear();
139      listeners.add(HostPort.allAddresses(config.getListenPort()));
140
141      rmiConnector.finalizeConnectionHandler(portChanged);
142      try
143      {
144        rmiConnector.initialize();
145      }
146      catch (RuntimeException e)
147      {
148        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
149        ccr.addMessage(LocalizableMessage.raw(e.getMessage()));
150      }
151    }
152
153    // If the port number has changed then update the JMX port information
154    // stored in the system properties.
155    if (portChanged)
156    {
157      String key = protocol + "_port";
158      String value = String.valueOf(config.getListenPort());
159      System.clearProperty(key);
160      System.setProperty(key, value);
161    }
162
163    return ccr;
164  }
165
166
167  private <T> boolean notEqualsNotNull(T o1, T o2)
168  {
169    return o1 != null && !o1.equals(o2);
170  }
171
172  @Override
173  public void finalizeConnectionHandler(LocalizableMessage finalizeReason) {
174    // Make sure that we don't get notified of any more changes.
175    currentConfig.removeJMXChangeListener(this);
176
177    // We should also close the RMI registry.
178    rmiConnector.finalizeConnectionHandler(true);
179  }
180
181
182  /**
183   * Retrieves the set of active client connections that have been
184   * established through this connection handler.
185   *
186   * @return The set of active client connections that have been
187   *         established through this connection handler.
188   */
189  @Override
190  public Collection<ClientConnection> getClientConnections() {
191    return connectionList;
192  }
193
194
195
196  /**
197   * Retrieves the DN of the configuration entry with which this alert
198   * generator is associated.
199   *
200   * @return The DN of the configuration entry with which this alert
201   *         generator is associated.
202   */
203  @Override
204  public DN getComponentEntryDN() {
205    return currentConfig.dn();
206  }
207
208
209
210  /**
211   * Retrieves the DN of the key manager provider that should be used
212   * for operations associated with this connection handler which need
213   * access to a key manager.
214   *
215   * @return The DN of the key manager provider that should be used
216   *         for operations associated with this connection handler
217   *         which need access to a key manager, or {@code null} if no
218   *         key manager provider has been configured for this
219   *         connection handler.
220   */
221  public DN getKeyManagerProviderDN() {
222    return currentConfig.getKeyManagerProviderDN();
223  }
224
225
226  /**
227   * Get the JMX connection handler's listen address.
228   *
229   * @return Returns the JMX connection handler's listen address.
230   */
231  public InetAddress getListenAddress()
232  {
233    return currentConfig.getListenAddress();
234  }
235
236  /**
237   * Get the JMX connection handler's listen port.
238   *
239   * @return Returns the JMX connection handler's listen port.
240   */
241  public int getListenPort() {
242    return currentConfig.getListenPort();
243  }
244
245  /**
246   * Get the JMX connection handler's rmi port.
247   *
248   * @return Returns the JMX connection handler's rmi port.
249   */
250  public int getRmiPort() {
251    return currentConfig.getRmiPort();
252  }
253
254
255  /**
256   * Get the JMX connection handler's RMI connector.
257   *
258   * @return Returns the JMX connection handler's RMI connector.
259   */
260  public RmiConnector getRMIConnector() {
261    return rmiConnector;
262  }
263
264  @Override
265  public String getShutdownListenerName() {
266    return connectionHandlerName;
267  }
268
269
270
271  /**
272   * Retrieves the nicknames of the server certificates that should be
273   * used in conjunction with this JMX connection handler.
274   *
275   * @return The nicknames of the server certificates that should be
276   *         used in conjunction with this JMX connection handler.
277   */
278  public SortedSet<String> getSSLServerCertNicknames() {
279    return currentConfig.getSSLCertNickname();
280  }
281
282  @Override
283  public void initializeConnectionHandler(ServerContext serverContext, JMXConnectionHandlerCfg config)
284         throws ConfigException, InitializationException
285  {
286    // Configuration is ok.
287    currentConfig = config;
288
289    final List<LocalizableMessage> reasons = new LinkedList<>();
290    if (!isPortConfigurationAcceptable(String.valueOf(config.dn()),
291        config.getListenPort(), reasons))
292    {
293      LocalizableMessage message = reasons.get(0);
294      logger.error(message);
295      throw new InitializationException(message);
296    }
297
298    if (config.isUseSSL()) {
299      protocol = "JMX+SSL";
300    } else {
301      protocol = "JMX";
302    }
303
304    listeners.clear();
305    listeners.add(HostPort.allAddresses(config.getListenPort()));
306    connectionHandlerName = "JMX Connection Handler " + config.getListenPort();
307
308    // Create a system property to store the JMX port the server is
309    // listening to. This information can be displayed with jinfo.
310    System.setProperty(
311      protocol + "_port", String.valueOf(config.getListenPort()));
312
313    // Create the associated RMI Connector.
314    rmiConnector = new RmiConnector(DirectoryServer.getJMXMBeanServer(), this);
315
316    // Register this as a change listener.
317    config.addJMXChangeListener(this);
318  }
319
320  @Override
321  public String getConnectionHandlerName() {
322    return connectionHandlerName;
323  }
324
325  @Override
326  public String getProtocol() {
327    return protocol;
328  }
329
330  @Override
331  public Collection<HostPort> getListeners() {
332    return listeners;
333  }
334
335  @Override
336  public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
337                                           List<LocalizableMessage> unacceptableReasons)
338  {
339    JMXConnectionHandlerCfg config = (JMXConnectionHandlerCfg) configuration;
340
341    if ((currentConfig == null ||
342        (!currentConfig.isEnabled() && config.isEnabled()) ||
343        currentConfig.getListenPort() != config.getListenPort()) &&
344        !isPortConfigurationAcceptable(String.valueOf(config.dn()),
345          config.getListenPort(), unacceptableReasons))
346    {
347      return false;
348    }
349
350    if (config.getRmiPort() != 0 &&
351        (currentConfig == null ||
352        (!currentConfig.isEnabled() && config.isEnabled()) ||
353        currentConfig.getRmiPort() != config.getRmiPort()) &&
354        !isPortConfigurationAcceptable(String.valueOf(config.dn()),
355          config.getRmiPort(), unacceptableReasons))
356    {
357      return false;
358    }
359
360    return isConfigurationChangeAcceptable(config, unacceptableReasons);
361  }
362
363  /**
364   * Attempt to bind to the port to verify whether the connection
365   * handler will be able to start.
366   * @return true is the port is free to use, false otherwise.
367   */
368  private boolean isPortConfigurationAcceptable(String configDN,
369                      int newPort, List<LocalizableMessage> unacceptableReasons) {
370    try {
371      if (StaticUtils.isAddressInUse(
372          new InetSocketAddress(newPort).getAddress(), newPort, true)) {
373        throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
374      }
375      return true;
376    } catch (Exception e) {
377      LocalizableMessage message = ERR_CONNHANDLER_CANNOT_BIND.get("JMX", configDN,
378              WILDCARD_ADDRESS, newPort, getExceptionMessage(e));
379      unacceptableReasons.add(message);
380      return false;
381    }
382  }
383
384  @Override
385  public boolean isConfigurationChangeAcceptable(
386      JMXConnectionHandlerCfg config,
387      List<LocalizableMessage> unacceptableReasons) {
388    // All validation is performed by the admin framework.
389    return true;
390  }
391
392
393
394  /**
395   * Determines whether clients are allowed to connect over JMX using SSL.
396   *
397   * @return Returns {@code true} if clients are allowed to
398   *         connect over JMX using SSL.
399   */
400  public boolean isUseSSL() {
401    return currentConfig.isUseSSL();
402  }
403
404  @Override
405  public void processServerShutdown(LocalizableMessage reason) {
406    // We should also close the RMI registry.
407    rmiConnector.finalizeConnectionHandler(true);
408  }
409
410
411
412  /**
413   * Registers a client connection with this JMX connection handler.
414   *
415   * @param connection
416   *          The client connection.
417   */
418  public void registerClientConnection(ClientConnection connection) {
419    connectionList.add(connection);
420  }
421
422
423  /**
424   * Unregisters a client connection from this JMX connection handler.
425   *
426   * @param connection
427   *          The client connection.
428   */
429  public void unregisterClientConnection(ClientConnection connection) {
430    connectionList.remove(connection);
431  }
432
433  @Override
434  public void run() {
435    try
436    {
437      rmiConnector.initialize();
438    }
439    catch (RuntimeException ignore)
440    {
441      // Already caught and logged
442    }
443  }
444
445  @Override
446  public void toString(StringBuilder buffer) {
447    buffer.append(connectionHandlerName);
448  }
449}