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.opends.server.admin.server.ConfigurationChangeListener;
036import org.opends.server.admin.std.server.ConnectionHandlerCfg;
037import org.opends.server.admin.std.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
101
102  /** {@inheritDoc} */
103  @Override
104  public ConfigChangeResult applyConfigurationChange(
105      JMXConnectionHandlerCfg config) {
106    final ConfigChangeResult ccr = new ConfigChangeResult();
107
108    // Determine whether or not the RMI connection needs restarting.
109    boolean rmiConnectorRestart = false;
110    boolean portChanged = false;
111
112    if (currentConfig.getListenPort() != config.getListenPort()) {
113      rmiConnectorRestart = true;
114      portChanged = true;
115    }
116
117    if (currentConfig.getRmiPort() != config.getRmiPort())
118    {
119      rmiConnectorRestart = true;
120    }
121    if (currentConfig.isUseSSL() != config.isUseSSL()) {
122      rmiConnectorRestart = true;
123    }
124
125    if (notEqualsNotNull(config.getSSLCertNickname(), currentConfig.getSSLCertNickname())
126        || notEqualsNotNull(config.getSSLCertNickname(), currentConfig.getSSLCertNickname())) {
127      rmiConnectorRestart = true;
128    }
129
130    // Save the configuration.
131    currentConfig = config;
132
133    // Restart the connector if required.
134    if (rmiConnectorRestart) {
135      if (config.isUseSSL()) {
136        protocol = "JMX+SSL";
137      } else {
138        protocol = "JMX";
139      }
140
141      listeners.clear();
142      listeners.add(HostPort.allAddresses(config.getListenPort()));
143
144      rmiConnector.finalizeConnectionHandler(portChanged);
145      try
146      {
147        rmiConnector.initialize();
148      }
149      catch (RuntimeException e)
150      {
151        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
152        ccr.addMessage(LocalizableMessage.raw(e.getMessage()));
153      }
154    }
155
156    // If the port number has changed then update the JMX port information
157    // stored in the system properties.
158    if (portChanged)
159    {
160      String key = protocol + "_port";
161      String value = String.valueOf(config.getListenPort());
162      System.clearProperty(key);
163      System.setProperty(key, value);
164    }
165
166    return ccr;
167  }
168
169
170  private <T> boolean notEqualsNotNull(T o1, T o2)
171  {
172    return o1 != null && !o1.equals(o2);
173  }
174
175  /** {@inheritDoc} */
176  @Override
177  public void finalizeConnectionHandler(LocalizableMessage finalizeReason) {
178    // Make sure that we don't get notified of any more changes.
179    currentConfig.removeJMXChangeListener(this);
180
181    // We should also close the RMI registry.
182    rmiConnector.finalizeConnectionHandler(true);
183  }
184
185
186  /**
187   * Retrieves the set of active client connections that have been
188   * established through this connection handler.
189   *
190   * @return The set of active client connections that have been
191   *         established through this connection handler.
192   */
193  @Override
194  public Collection<ClientConnection> getClientConnections() {
195    return connectionList;
196  }
197
198
199
200  /**
201   * Retrieves the DN of the configuration entry with which this alert
202   * generator is associated.
203   *
204   * @return The DN of the configuration entry with which this alert
205   *         generator is associated.
206   */
207  @Override
208  public DN getComponentEntryDN() {
209    return currentConfig.dn();
210  }
211
212
213
214  /**
215   * Retrieves the DN of the key manager provider that should be used
216   * for operations associated with this connection handler which need
217   * access to a key manager.
218   *
219   * @return The DN of the key manager provider that should be used
220   *         for operations associated with this connection handler
221   *         which need access to a key manager, or {@code null} if no
222   *         key manager provider has been configured for this
223   *         connection handler.
224   */
225  public DN getKeyManagerProviderDN() {
226    return currentConfig.getKeyManagerProviderDN();
227  }
228
229
230  /**
231   * Get the JMX connection handler's listen address.
232   *
233   * @return Returns the JMX connection handler's listen address.
234   */
235  public InetAddress getListenAddress()
236  {
237    return currentConfig.getListenAddress();
238  }
239
240  /**
241   * Get the JMX connection handler's listen port.
242   *
243   * @return Returns the JMX connection handler's listen port.
244   */
245  public int getListenPort() {
246    return currentConfig.getListenPort();
247  }
248
249  /**
250   * Get the JMX connection handler's rmi port.
251   *
252   * @return Returns the JMX connection handler's rmi port.
253   */
254  public int getRmiPort() {
255    return currentConfig.getRmiPort();
256  }
257
258
259  /**
260   * Get the JMX connection handler's RMI connector.
261   *
262   * @return Returns the JMX connection handler's RMI connector.
263   */
264  public RmiConnector getRMIConnector() {
265    return rmiConnector;
266  }
267
268
269
270  /** {@inheritDoc} */
271  @Override
272  public String getShutdownListenerName() {
273    return connectionHandlerName;
274  }
275
276
277
278  /**
279   * Retrieves the nicknames of the server certificates that should be
280   * used in conjunction with this JMX connection handler.
281   *
282   * @return The nicknames of the server certificates that should be
283   *         used in conjunction with this JMX connection handler.
284   */
285  public SortedSet<String> getSSLServerCertNicknames() {
286    return currentConfig.getSSLCertNickname();
287  }
288
289
290
291  /** {@inheritDoc} */
292  @Override
293  public void initializeConnectionHandler(ServerContext serverContext, JMXConnectionHandlerCfg config)
294         throws ConfigException, InitializationException
295  {
296    // Configuration is ok.
297    currentConfig = config;
298
299    final List<LocalizableMessage> reasons = new LinkedList<>();
300    if (!isPortConfigurationAcceptable(String.valueOf(config.dn()),
301        config.getListenPort(), reasons))
302    {
303      LocalizableMessage message = reasons.get(0);
304      logger.error(message);
305      throw new InitializationException(message);
306    }
307
308    if (config.isUseSSL()) {
309      protocol = "JMX+SSL";
310    } else {
311      protocol = "JMX";
312    }
313
314    listeners.clear();
315    listeners.add(HostPort.allAddresses(config.getListenPort()));
316    connectionHandlerName = "JMX Connection Handler " + config.getListenPort();
317
318    // Create a system property to store the JMX port the server is
319    // listening to. This information can be displayed with jinfo.
320    System.setProperty(
321      protocol + "_port", String.valueOf(config.getListenPort()));
322
323    // Create the associated RMI Connector.
324    rmiConnector = new RmiConnector(DirectoryServer.getJMXMBeanServer(), this);
325
326    // Register this as a change listener.
327    config.addJMXChangeListener(this);
328  }
329
330
331
332  /** {@inheritDoc} */
333  @Override
334  public String getConnectionHandlerName() {
335    return connectionHandlerName;
336  }
337
338
339
340  /** {@inheritDoc} */
341  @Override
342  public String getProtocol() {
343    return protocol;
344  }
345
346
347
348  /** {@inheritDoc} */
349  @Override
350  public Collection<HostPort> getListeners() {
351    return listeners;
352  }
353
354
355  /** {@inheritDoc} */
356  @Override
357  public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
358                                           List<LocalizableMessage> unacceptableReasons)
359  {
360    JMXConnectionHandlerCfg config = (JMXConnectionHandlerCfg) configuration;
361
362    if ((currentConfig == null ||
363        (!currentConfig.isEnabled() && config.isEnabled()) ||
364        currentConfig.getListenPort() != config.getListenPort()) &&
365        !isPortConfigurationAcceptable(String.valueOf(config.dn()),
366          config.getListenPort(), unacceptableReasons))
367    {
368      return false;
369    }
370
371    if (config.getRmiPort() != 0 &&
372        (currentConfig == null ||
373        (!currentConfig.isEnabled() && config.isEnabled()) ||
374        currentConfig.getRmiPort() != config.getRmiPort()) &&
375        !isPortConfigurationAcceptable(String.valueOf(config.dn()),
376          config.getRmiPort(), unacceptableReasons))
377    {
378      return false;
379    }
380
381    return isConfigurationChangeAcceptable(config, unacceptableReasons);
382  }
383
384  /**
385   * Attempt to bind to the port to verify whether the connection
386   * handler will be able to start.
387   * @return true is the port is free to use, false otherwise.
388   */
389  private boolean isPortConfigurationAcceptable(String configDN,
390                      int newPort, List<LocalizableMessage> unacceptableReasons) {
391    try {
392      if (StaticUtils.isAddressInUse(
393          new InetSocketAddress(newPort).getAddress(), newPort, true)) {
394        throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
395      }
396    } catch (Exception e) {
397      LocalizableMessage message = ERR_CONNHANDLER_CANNOT_BIND.get("JMX", configDN,
398              WILDCARD_ADDRESS, newPort, getExceptionMessage(e));
399      unacceptableReasons.add(message);
400      return false;
401    }
402    return true;
403  }
404
405  /** {@inheritDoc} */
406  @Override
407  public boolean isConfigurationChangeAcceptable(
408      JMXConnectionHandlerCfg config,
409      List<LocalizableMessage> unacceptableReasons) {
410    // All validation is performed by the admin framework.
411    return true;
412  }
413
414
415
416  /**
417   * Determines whether or not clients are allowed to connect over JMX
418   * using SSL.
419   *
420   * @return Returns {@code true} if clients are allowed to
421   *         connect over JMX using SSL.
422   */
423  public boolean isUseSSL() {
424    return currentConfig.isUseSSL();
425  }
426
427
428
429  /** {@inheritDoc} */
430  @Override
431  public void processServerShutdown(LocalizableMessage reason) {
432    // We should also close the RMI registry.
433    rmiConnector.finalizeConnectionHandler(true);
434  }
435
436
437
438  /**
439   * Registers a client connection with this JMX connection handler.
440   *
441   * @param connection
442   *          The client connection.
443   */
444  public void registerClientConnection(ClientConnection connection) {
445    connectionList.add(connection);
446  }
447
448
449  /**
450   * Unregisters a client connection from this JMX connection handler.
451   *
452   * @param connection
453   *          The client connection.
454   */
455  public void unregisterClientConnection(ClientConnection connection) {
456    connectionList.remove(connection);
457  }
458
459
460  /** {@inheritDoc} */
461  @Override
462  public void run() {
463    try
464    {
465      rmiConnector.initialize();
466    }
467    catch (RuntimeException ignore)
468    {
469      // Already caught and logged
470    }
471  }
472
473
474
475  /** {@inheritDoc} */
476  @Override
477  public void toString(StringBuilder buffer) {
478    buffer.append(connectionHandlerName);
479  }
480}