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