mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
23 hours ago 9af5bd326500773d89ce30311af5e3cac2a9b3f5
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JMXConnectionHandlerConfiguration.xml
@@ -14,6 +14,7 @@
  Copyright 2007-2009 Sun Microsystems, Inc.
  Portions Copyright 2013-2015 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="jmx-connection-handler"
  plural-name="jmx-connection-handlers"
@@ -66,7 +67,15 @@
    <adm:description>
      If no value is provided, then the
      <adm:user-friendly-name />
      listens on all interfaces.
      listens on all interfaces. The RMI stubs that JMX clients receive
      name the host to connect to: with a specific address, the server sets
      the java.rmi.server.hostname system property to that address, unless
      the property is already set, for instance with
      -Djava.rmi.server.hostname in the Java arguments of the server. The
      property applies to every RMI object that the Java virtual machine
      exports, including those of the platform JMX agent. With several JMX
      connection handlers on different specific addresses, the last one
      started sets it.
    </adm:description>
    <adm:requires-admin-action>
      <adm:server-restart />
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -30,7 +30,6 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -133,8 +132,8 @@
  /** Indicates whether this connection handler is enabled. */
  private boolean enabled;
  /** The set of listeners for this connection handler. */
  private final List<HostPort> listeners = new LinkedList<>();
  /** The addresses and the port the HTTP server listens on, or starts with next. */
  private volatile List<HostPort> listeners = Collections.emptyList();
  /** The HTTP server embedded in OpenDJ. */
  private HttpServer httpServer;
@@ -448,11 +447,7 @@
      friendlyName = config.name();
    }
    int listenPort = config.getListenPort();
    for (InetAddress a : config.getListenAddress())
    {
      listeners.add(new HostPort(a.getHostAddress(), listenPort));
    }
    listeners = toListeners(config);
    handlerName = getHandlerName(config);
@@ -770,7 +765,17 @@
    this.httpServer = createHttpServer();
    this.httpServer.getServerConfiguration().addHttpHandler(newGrizzlyHttpHandler(new RootHttpApplication()));
    logger.trace("Starting HTTP server...");
    this.httpServer.start();
    try
    {
      this.httpServer.start();
    }
    catch (IOException | RuntimeException e)
    {
      // HttpServer.start() stops at the first listener that cannot bind and leaves the listeners started before it
      // bound: release their addresses, since the caller only forgets about the server.
      this.httpServer.shutdownNow();
      throw e;
    }
    logger.trace("HTTP server started");
    logger.info(NOTE_CONNHANDLER_STARTED_LISTENING, handlerName);
  }
@@ -790,13 +795,41 @@
      setHttpStatsProbe(server);
    }
    // Configure the network listener
    final NetworkListener listener = new NetworkListener(
        "OpenDJ-HTTP", NetworkListener.DEFAULT_NETWORK_HOST, initConfig.getListenPort());
    server.addListener(listener);
    // Configure one network listener per listen address, and report them: a configuration change can replace
    // initConfig after the initialization. HttpServer keys its listeners by name, and each listener owns its
    // transport, so neither can be shared.
    listeners = toListeners(initConfig);
    final int numRequestHandlers = getNumRequestHandlers(currentConfig.getNumRequestHandlers(), friendlyName);
    for (InetAddress address : initConfig.getListenAddress())
    {
      final String host = address.getHostAddress();
      final NetworkListener listener = new NetworkListener("OpenDJ-HTTP " + host, host, initConfig.getListenPort());
      server.addListener(listener);
      configureTransport(listener.getTransport(), numRequestHandlers);
    // Configure the network transport
    final TCPNIOTransport transport = listener.getTransport();
      // Configure SSL
      if (sslEngineConfigurator != null)
      {
        listener.setSecure(true);
        listener.setSSLEngineConfig(sslEngineConfigurator);
      }
    }
    return server;
  }
  private static List<HostPort> toListeners(HTTPConnectionHandlerCfg config)
  {
    final List<HostPort> hostPorts = new ArrayList<>();
    for (InetAddress address : config.getListenAddress())
    {
      hostPorts.add(new HostPort(address.getHostAddress(), config.getListenPort()));
    }
    return Collections.unmodifiableList(hostPorts);
  }
  private void configureTransport(TCPNIOTransport transport, int numRequestHandlers)
  {
    transport.setReuseAddress(currentConfig.isAllowTCPReuseAddress());
    transport.setKeepAlive(currentConfig.isUseTCPKeepAlive());
    transport.setTcpNoDelay(currentConfig.isUseTCPNoDelay());
@@ -807,18 +840,8 @@
    transport.setWriteBufferSize(bufferSize);
    transport.setIOStrategy(SameThreadIOStrategy.getInstance());
    final int numRequestHandlers = getNumRequestHandlers(currentConfig.getNumRequestHandlers(), friendlyName);
    transport.setSelectorRunnersCount(numRequestHandlers);
    transport.setServerConnectionBackLog(currentConfig.getAcceptBacklog());
    // Configure SSL
    if (sslEngineConfigurator != null)
    {
      listener.setSecure(true);
      listener.setSSLEngineConfig(sslEngineConfigurator);
    }
    return server;
  }
  private void setHttpStatsProbe(HttpServer server)
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java
@@ -47,6 +47,9 @@
  /** Indicate if we required the client authentication via SSL. */
  private final boolean needClientCertificate;
  /** The address to listen on, which could be INADDR_ANY. */
  private final InetAddress listenAddress;
  /**
   * Constructs a new <code>DirectoryRMIServerSocketFactory</code> with the
   * specified SSL socket configuration.
@@ -59,13 +62,18 @@
   *            connections accepted by server sockets created by this
   *            factory; <code>false</code> to not require client
   *            authentication.
   *
   * @param listenAddress
   *            the address the server sockets created by this factory
   *            listen on
   */
  public DirectoryRMIServerSocketFactory(SSLSocketFactory sslSocketFactory,
      boolean needClientCertificate)
      boolean needClientCertificate, InetAddress listenAddress)
  {
    // Initialize the configuration parameters.
    this.needClientCertificate = needClientCertificate;
    this.sslSocketFactory = sslSocketFactory;
    this.listenAddress = listenAddress;
  }
  /**
@@ -97,7 +105,7 @@
  @Override
  public ServerSocket createServerSocket(int port) throws IOException
  {
    return new ServerSocket(port, 0, InetAddress.getByName("0.0.0.0"))
    return new ServerSocket(port, 0, listenAddress)
    {
      @Override
      public Socket accept() throws IOException
@@ -162,7 +170,8 @@
  private boolean checkParameters(DirectoryRMIServerSocketFactory that)
  {
    return needClientCertificate == that.needClientCertificate
        && sslSocketFactory.equals(that.sslSocketFactory);
        && sslSocketFactory.equals(that.sslSocketFactory)
        && listenAddress.equals(that.listenAddress);
  }
  /**
@@ -177,6 +186,7 @@
  {
    return getClass().hashCode()
        + Boolean.valueOf(needClientCertificate).hashCode()
        + sslSocketFactory.hashCode();
        + sslSocketFactory.hashCode()
        + listenAddress.hashCode();
  }
}
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/JmxConnectionHandler.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.protocols.jmx;
@@ -88,6 +89,14 @@
  private final List<HostPort> listeners = new LinkedList<>();
  /**
   * The address the RMI registry and the RMI connector listen on. A change of
   * listen-address takes effect when the handler restarts, as its
   * configuration says: the registry, the connector and the listeners stay
   * on the same address until then.
   */
  private InetAddress listenAddress;
  /**
   * Creates a new instance of this JMX connection handler. It must be
   * initialized before it may be used.
   */
@@ -136,7 +145,7 @@
      }
      listeners.clear();
      listeners.add(HostPort.allAddresses(config.getListenPort()));
      listeners.add(new HostPort(listenAddress.getHostAddress(), config.getListenPort()));
      rmiConnector.finalizeConnectionHandler(portChanged);
      try
@@ -224,13 +233,14 @@
  /**
   * Get the JMX connection handler's listen address.
   * Get the JMX connection handler's listen address: the one it was
   * initialized with, until it restarts.
   *
   * @return Returns the JMX connection handler's listen address.
   */
  public InetAddress getListenAddress()
  {
    return currentConfig.getListenAddress();
    return listenAddress;
  }
  /**
@@ -301,8 +311,9 @@
      protocol = "JMX";
    }
    listenAddress = config.getListenAddress();
    listeners.clear();
    listeners.add(HostPort.allAddresses(config.getListenPort()));
    listeners.add(new HostPort(listenAddress.getHostAddress(), config.getListenPort()));
    connectionHandlerName = "JMX Connection Handler " + config.getListenPort();
    // Create a system property to store the JMX port the server is
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java
@@ -22,9 +22,11 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIServerSocketFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
@@ -168,6 +170,18 @@
   */
  private String rmiVersion;
  /**
   * The system property that names the host the stubs of the objects exported
   * through RMI advertise.
   */
  private static final String RMI_SERVER_HOSTNAME = "java.rmi.server.hostname";
  /**
   * The value this server gave to {@code java.rmi.server.hostname}, or
   * {@code null} if the server did not set it.
   */
  private static String rmiServerHostnameSetByServer;
  // ===================================================================
  // CONSTRUCTOR
  // ===================================================================
@@ -319,8 +333,10 @@
      // ---------------------
      // init an ssl context
      // ---------------------
      // Both server socket factories listen on the configured listen address:
      // the default RMI factory would listen on every interface.
      SslRMIClientSocketFactory rmiClientSockeyFactory = null;
      DirectoryRMIServerSocketFactory rmiServerSockeyFactory = null;
      RMIServerSocketFactory rmiServerSockeyFactory;
      if (jmxConnectionHandler.isUseSSL())
      {
        if (logger.isTraceEnabled())
@@ -355,7 +371,8 @@
        SSLSocketFactory ssf = ctx.getSocketFactory();
        // set the Server socket factory in the JMX map
        rmiServerSockeyFactory = new DirectoryRMIServerSocketFactory(ssf, false);
        rmiServerSockeyFactory = new DirectoryRMIServerSocketFactory(
            ssf, false, jmxConnectionHandler.getListenAddress());
        env.put(
            "jmx.remote.rmi.server.socket.factory",
            rmiServerSockeyFactory);
@@ -376,6 +393,8 @@
        {
          logger.trace("UNSECURE CONNECTION");
        }
        rmiServerSockeyFactory = new OpendsRmiServerSocketFactory(
            jmxConnectionHandler.getListenAddress());
      }
      // specify the rmi JMX authenticator to be used
@@ -399,6 +418,7 @@
      {
        logger.trace("Create and start the JMX RMI connector");
      }
      advertiseListenAddress(jmxConnectionHandler.getListenAddress());
      OpendsRMIJRMPServerImpl opendsRmiConnectorServer =
          new OpendsRMIJRMPServerImpl(jmxConnectionHandler.getRmiPort(),
              rmiClientSockeyFactory, rmiServerSockeyFactory, env);
@@ -426,6 +446,64 @@
  }
  /**
   * Makes the stub of the RMI connector advertise the listen address.
   * <p>
   * The stub that a client gets from the RMI registry advertises the host that
   * {@code java.rmi.server.hostname} names, or else the address of the local host,
   * whatever address the connector listens on: a connector bound to another
   * address refuses the client. The JDK reads the property again at each export.
   * A value that this server did not set is the operator's, and is kept.
   *
   * @param listenAddress
   *          the address the connector listens on
   */
  private static synchronized void advertiseListenAddress(InetAddress listenAddress)
  {
    final String hostname = System.getProperty(RMI_SERVER_HOSTNAME);
    if (hostname != null && !hostname.equals(rmiServerHostnameSetByServer))
    {
      return;
    }
    if (listenAddress.isAnyLocalAddress())
    {
      // Cleared, the property would leave the JDK advertising the host it last
      // read: the address a connector of this server listened on before, which
      // a remote client cannot reach if it was a loopback one. Advertise the
      // local host instead, as the JDK does by default.
      final InetAddress localHost = getLocalHostOrNull();
      if (rmiServerHostnameSetByServer != null
          && localHost != null && !localHost.isLoopbackAddress())
      {
        rmiServerHostnameSetByServer = localHost.getHostAddress();
        System.setProperty(RMI_SERVER_HOSTNAME, rmiServerHostnameSetByServer);
      }
      else
      {
        System.clearProperty(RMI_SERVER_HOSTNAME);
        rmiServerHostnameSetByServer = null;
      }
    }
    else
    {
      rmiServerHostnameSetByServer = listenAddress.getHostAddress();
      System.setProperty(RMI_SERVER_HOSTNAME, rmiServerHostnameSetByServer);
    }
  }
  private static InetAddress getLocalHostOrNull()
  {
    try
    {
      return InetAddress.getLocalHost();
    }
    catch (UnknownHostException e)
    {
      logger.traceException(e);
      return null;
    }
  }
  static void configureJmxDeserializationProtection(Map<String, Object> env)
  {
    // Tightly constrain the credentials object exchanged during authentication
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
@@ -52,7 +52,10 @@
import java.lang.management.ThreadMXBean;
import java.net.BindException;
import java.net.ConnectException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
@@ -130,6 +133,7 @@
import org.opends.server.util.DynamicConstants;
import org.opends.server.util.LDIFReader;
import org.opends.server.util.TestTimer;
import org.testng.SkipException;
import com.forgerock.opendj.util.OperatingSystem;
@@ -966,6 +970,58 @@
  }
  /**
   * Returns an IPv4 address of the local host other than a loopback one, for the tests of a listen
   * address: a listener bound to the loopback address refuses connections to it, while a listener
   * bound to the wildcard address accepts them.
   *
   * @return an IPv4 address of an interface of the local host which is up and not a loopback one
   * @throws IOException
   *           if the network interfaces cannot be listed
   * @throws SkipException
   *           if the local host has no such address
   */
  public static InetAddress getNonLoopbackAddress() throws IOException
  {
    for (NetworkInterface nif : Collections.list(NetworkInterface.getNetworkInterfaces()))
    {
      if (!nif.isUp() || nif.isLoopback())
      {
        continue;
      }
      for (InetAddress address : Collections.list(nif.getInetAddresses()))
      {
        if (address instanceof Inet4Address && !address.isLinkLocalAddress())
        {
          return address;
        }
      }
    }
    throw new SkipException("the local host has no IPv4 address other than a loopback one");
  }
  /**
   * Tells whether a connection to the given address and port is accepted right now.
   *
   * @param address
   *          the address to connect to
   * @param port
   *          the port to connect to
   * @return {@code true} if the connection is accepted, {@code false} if it fails
   */
  public static boolean isAcceptingConnections(InetAddress address, int port)
  {
    try (Socket socket = new Socket())
    {
      socket.connect(new InetSocketAddress(address, port), 5000);
      return true;
    }
    catch (IOException e)
    {
      return false;
    }
  }
  /**
   * Finds a free server socket port on the local host.
   *
   * @return The free port.
opendj-server-legacy/src/test/java/org/opends/server/protocols/http/HTTPConnectionHandlerTestCase.java
@@ -15,17 +15,38 @@
 */
package org.opends.server.protocols.http;
import static java.util.concurrent.TimeUnit.*;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.server.config.meta.HTTPConnectionHandlerCfgDefn;
import org.forgerock.opendj.server.config.server.HTTPConnectionHandlerCfg;
import org.opends.admin.ads.util.BlindTrustManager;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.DummyAlertHandler;
import org.opends.server.extensions.InitializationUtils;
import org.opends.server.types.Entry;
import org.opends.server.types.HostPort;
import org.opends.server.util.TestTimer;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
@@ -34,6 +55,9 @@
{
  private static final LocalizableMessage STOP_REASON = LocalizableMessage.raw("Don't need a reason.");
  /** An address of TEST-NET-1 (RFC 5737), which is never assigned to an interface of the host. */
  private static final String UNASSIGNED_ADDRESS = "192.0.2.1";
  @BeforeClass
  public void setUp() throws Exception
  {
@@ -53,32 +77,7 @@
  public void testStartWaitsForListenPort() throws Exception
  {
    final int listenPort = TestCaseUtils.findFreePort();
    Entry handlerEntry = TestCaseUtils.makeEntry(
        "dn: cn=HTTP Connection Handler,cn=Connection Handlers,cn=config",
        "objectClass: top",
        "objectClass: ds-cfg-connection-handler",
        "objectClass: ds-cfg-http-connection-handler",
        "cn: HTTP Connection Handler",
        "ds-cfg-java-class: org.opends.server.protocols.http.HTTPConnectionHandler",
        "ds-cfg-enabled: true",
        "ds-cfg-listen-address: 127.0.0.1",
        "ds-cfg-listen-port: " + listenPort,
        "ds-cfg-accept-backlog: 128",
        "ds-cfg-keep-stats: false",
        "ds-cfg-use-tcp-keep-alive: true",
        "ds-cfg-use-tcp-no-delay: true",
        "ds-cfg-allow-tcp-reuse-address: true",
        "ds-cfg-max-request-size: 5 megabytes",
        "ds-cfg-buffer-size: 4096 bytes",
        "ds-cfg-max-blocked-write-time-limit: 2 minutes",
        "ds-cfg-use-ssl: false",
        "ds-cfg-ssl-client-auth-policy: optional",
        "ds-cfg-ssl-cert-nickname: server-cert");
    HTTPConnectionHandlerCfg config =
        InitializationUtils.getConfiguration(HTTPConnectionHandlerCfgDefn.getInstance(), handlerEntry);
    HTTPConnectionHandler handler = new HTTPConnectionHandler();
    handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(), config);
    HTTPConnectionHandler handler = newHandler(listenPort, "127.0.0.1");
    try
    {
      handler.start();
@@ -88,10 +87,222 @@
    }
    finally
    {
      handler.processServerShutdown(STOP_REASON);
      handler.finalizeConnectionHandler(STOP_REASON);
      handler.join(10000);
      assertFalse(handler.isAlive(), "the connection handler thread is still running");
      stop(handler);
    }
  }
  /** A handler restricted to the loopback address must not answer on the other addresses of the host. */
  @Test
  public void listensOnlyOnTheConfiguredListenAddress() throws Exception
  {
    final InetAddress external = getNonLoopbackAddress();
    final int listenPort = TestCaseUtils.findFreePort();
    HTTPConnectionHandler handler = newHandler(listenPort, "127.0.0.1");
    try
    {
      handler.start();
      assertTrue(isAcceptingConnections(loopback(), listenPort), "the configured address is not listened on");
      assertFalse(isAcceptingConnections(external, listenPort),
          "the handler answers on " + external.getHostAddress() + ", which is not one of its listen addresses");
      assertThat(handler.getListeners()).containsExactly(new HostPort("127.0.0.1", listenPort));
    }
    finally
    {
      stop(handler);
    }
  }
  @DataProvider
  public Object[][] useSSL()
  {
    return new Object[][] { { false }, { true } };
  }
  /** Each of several listen addresses gets its own listener, with the handler's SSL settings. */
  @Test(dataProvider = "useSSL")
  public void listensOnEveryConfiguredListenAddress(boolean useSSL) throws Exception
  {
    final InetAddress external = getNonLoopbackAddress();
    final int listenPort = TestCaseUtils.findFreePort();
    HTTPConnectionHandler handler = newHandler(useSSL, listenPort, "127.0.0.1", external.getHostAddress());
    try
    {
      handler.start();
      assertAnswers(loopback(), listenPort, useSSL);
      assertAnswers(external, listenPort, useSSL);
      assertThat(handler.getListeners()).containsOnly(
          new HostPort("127.0.0.1", listenPort), new HostPort(external.getHostAddress(), listenPort));
    }
    finally
    {
      stop(handler);
    }
  }
  /**
   * When one listen address cannot be bound, the handler does not start, and must not keep the
   * addresses it has already bound: nothing would ever release them.
   * <p>
   * The listener of 127.0.0.1 starts before the one of {@value #UNASSIGNED_ADDRESS}: the embedded
   * server starts its listeners in the order of a hash map of their names.
   */
  @Test
  public void releasesTheBoundListenAddressesWhenAnotherCannotBeBound() throws Exception
  {
    final int listenPort = TestCaseUtils.findFreePort();
    HTTPConnectionHandler handler = newHandler(listenPort, "127.0.0.1", UNASSIGNED_ADDRESS);
    try
    {
      handler.start();
      // The handler thread tries twice, then disables the handler: wait for it to give up.
      final InetAddress loopback = loopback();
      new TestTimer.Builder().maxSleep(10, SECONDS).sleepTimes(100, MILLISECONDS).toTimer()
          .repeatUntilSuccess(new TestTimer.CallableVoid()
          {
            @Override
            public void call() throws Exception
            {
              assertFalse(isAcceptingConnections(loopback, listenPort),
                  "127.0.0.1 is still listened on although the handler could not start");
            }
          });
    }
    finally
    {
      stop(handler);
    }
  }
  /**
   * A handler that could not start starts again after a change of its listen address, and then
   * reports the address it listens on, not the one it was initialized with.
   */
  @Test
  public void reportsTheListenAddressItStartsWithAfterAChange() throws Exception
  {
    final int listenPort = TestCaseUtils.findFreePort();
    final int givenUp = DummyAlertHandler.getAlertCount(ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES);
    HTTPConnectionHandler handler = newHandler(listenPort, UNASSIGNED_ADDRESS);
    try
    {
      handler.start();
      // The handler thread tries twice, then disables the handler: a change applied before it gives up is undone.
      final TestTimer timer = new TestTimer.Builder().maxSleep(10, SECONDS).sleepTimes(100, MILLISECONDS).toTimer();
      timer.repeatUntilSuccess(new TestTimer.CallableVoid()
      {
        @Override
        public void call() throws Exception
        {
          assertThat(DummyAlertHandler.getAlertCount(ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES))
              .as("the handler has not given up starting").isGreaterThan(givenUp);
        }
      });
      final HTTPConnectionHandlerCfg changedConfig = newConfig(false, listenPort, "127.0.0.1");
      final InetAddress loopback = loopback();
      timer.repeatUntilSuccess(new TestTimer.CallableVoid()
          {
            @Override
            public void call() throws Exception
            {
              // Applied again until it holds: the handler thread disables the handler just after it sends the alert.
              handler.applyConfigurationChange(changedConfig);
              assertTrue(isAcceptingConnections(loopback, listenPort), "the new listen address is not listened on");
            }
          });
      assertThat(handler.getListeners()).containsExactly(new HostPort("127.0.0.1", listenPort));
    }
    finally
    {
      stop(handler);
    }
  }
  /**
   * Asserts that a listener answers on the address: over TLS, a completed handshake, which a
   * listener without the handler's SSL settings cannot give.
   */
  private static void assertAnswers(InetAddress address, int port, boolean useSSL) throws Exception
  {
    if (!useSSL)
    {
      assertTrue(isAcceptingConnections(address, port), address.getHostAddress() + " is not listened on");
      return;
    }
    final SSLContext client = SSLContext.getInstance("TLS");
    client.init(null, new TrustManager[] { new BlindTrustManager() }, null);
    try (SSLSocket socket = (SSLSocket) client.getSocketFactory().createSocket(address, port))
    {
      socket.setSoTimeout(10000);
      socket.startHandshake();
    }
    catch (IOException e)
    {
      throw new AssertionError(address.getHostAddress() + " does not complete a TLS handshake", e);
    }
  }
  private static HTTPConnectionHandler newHandler(int listenPort, String... listenAddresses) throws Exception
  {
    return newHandler(false, listenPort, listenAddresses);
  }
  private static HTTPConnectionHandler newHandler(boolean useSSL, int listenPort, String... listenAddresses)
      throws Exception
  {
    HTTPConnectionHandler handler = new HTTPConnectionHandler();
    handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(),
        newConfig(useSSL, listenPort, listenAddresses));
    return handler;
  }
  private static HTTPConnectionHandlerCfg newConfig(boolean useSSL, int listenPort, String... listenAddresses)
      throws Exception
  {
    final List<String> ldif = new ArrayList<>();
    Collections.addAll(ldif,
        "dn: cn=HTTP Connection Handler,cn=Connection Handlers,cn=config",
        "objectClass: top",
        "objectClass: ds-cfg-connection-handler",
        "objectClass: ds-cfg-http-connection-handler",
        "cn: HTTP Connection Handler",
        "ds-cfg-java-class: org.opends.server.protocols.http.HTTPConnectionHandler",
        "ds-cfg-enabled: true");
    for (String listenAddress : listenAddresses)
    {
      ldif.add("ds-cfg-listen-address: " + listenAddress);
    }
    Collections.addAll(ldif,
        "ds-cfg-listen-port: " + listenPort,
        "ds-cfg-accept-backlog: 128",
        "ds-cfg-keep-stats: false",
        "ds-cfg-use-tcp-keep-alive: true",
        "ds-cfg-use-tcp-no-delay: true",
        "ds-cfg-allow-tcp-reuse-address: true",
        "ds-cfg-max-request-size: 5 megabytes",
        "ds-cfg-buffer-size: 4096 bytes",
        "ds-cfg-max-blocked-write-time-limit: 2 minutes",
        "ds-cfg-use-ssl: " + useSSL,
        "ds-cfg-key-manager-provider: cn=JKS,cn=Key Manager Providers,cn=config",
        "ds-cfg-ssl-client-auth-policy: optional",
        "ds-cfg-ssl-cert-nickname: server-cert");
    Entry handlerEntry = TestCaseUtils.makeEntry(ldif.toArray(new String[0]));
    return InitializationUtils.getConfiguration(HTTPConnectionHandlerCfgDefn.getInstance(), handlerEntry);
  }
  private static InetAddress loopback() throws UnknownHostException
  {
    return InetAddress.getByName("127.0.0.1");
  }
  private static void stop(HTTPConnectionHandler handler) throws InterruptedException
  {
    handler.processServerShutdown(STOP_REASON);
    handler.finalizeConnectionHandler(STOP_REASON);
    handler.join(10000);
    assertFalse(handler.isAlive(), "the connection handler thread is still running");
  }
}
opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java
New file
@@ -0,0 +1,398 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.protocols.jmx;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.TestCaseUtils.*;
import static org.testng.Assert.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import javax.management.remote.JMXConnector;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.server.config.meta.JMXConnectionHandlerCfgDefn;
import org.forgerock.opendj.server.config.server.JMXConnectionHandlerCfg;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.InitializationUtils;
import org.opends.server.types.Entry;
import org.opends.server.types.HostPort;
import org.testng.SkipException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
 * The JMX connection handler listens on its listen address only: with both its RMI registry, on
 * the listen port, and its RMI connector, on the RMI port. A JMX client reaches it there.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "jmx" }, sequential = true)
public class JmxListenAddressTestCase extends DirectoryServerTestCase
{
  private static final LocalizableMessage STOP_REASON = LocalizableMessage.raw("Don't need a reason.");
  private static final String USER_DN = "cn=JMX Reader,o=test";
  private static final String USER_PASSWORD = "password";
  private static final String RMI_SERVER_HOSTNAME = "java.rmi.server.hostname";
  /** The address the handlers listen on. */
  private InetAddress listenAddress;
  /** An address of the host the handlers do not listen on. */
  private InetAddress otherAddress;
  /** The default SSL context of the JVM before this test replaced it. */
  private SSLContext defaultSSLContext;
  @BeforeClass
  public void setUp() throws Exception
  {
    TestCaseUtils.startServer();
    TestCaseUtils.initializeTestBackend(true);
    TestCaseUtils.addEntries(
        "dn: " + USER_DN,
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "cn: JMX Reader",
        "sn: Reader",
        "userPassword: " + USER_PASSWORD,
        "ds-privilege-name: jmx-read",
        "ds-pwp-password-policy-dn: cn=Clear UserPassword Policy,cn=Password Policies,cn=config");
    // The stub of the RMI connector advertises the local host, unless told otherwise: listen on
    // the address that is not the local host's, or a session would get through even with a stub
    // that does not advertise the listen address. When the local host is exactly 127.0.0.1, the
    // JDK learns its host from the connector's own binding in the registry instead, and no
    // choice of address makes that difference visible.
    final InetAddress loopback = InetAddress.getByName("127.0.0.1");
    final InetAddress external = getNonLoopbackAddress();
    final boolean localHostIsLoopback =
        loopback.getHostAddress().equals(InetAddress.getLocalHost().getHostAddress());
    listenAddress = localHostIsLoopback ? external : loopback;
    otherAddress = localHostIsLoopback ? loopback : external;
    // The stub of an SSL connector carries an SslRMIClientSocketFactory, which takes the default
    // SSL context of the JVM the first time anything uses it, and keeps it: that happens as soon as
    // the connector starts, before any client of this test connects.
    defaultSSLContext = SSLContext.getDefault();
    final SSLContext blindContext = SSLContext.getInstance("TLS");
    blindContext.init(null, new TrustManager[] { new BlindExtendedTrustManager() }, null);
    SSLContext.setDefault(blindContext);
  }
  /**
   * Trusts any server, whatever host name it is reached by: the factory checks the host name of
   * the server when the JDK asks for it, and a trust manager that is not an extended one gets that
   * check added by the JDK. The test certificate names no host.
   */
  private static final class BlindExtendedTrustManager extends X509ExtendedTrustManager
  {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
    {
      // Trusts any client.
    }
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
    {
      // Trusts any client.
    }
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    {
      // Trusts any client.
    }
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
    {
      // Trusts any server.
    }
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
    {
      // Trusts any server.
    }
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
    {
      // Trusts any server.
    }
    @Override
    public X509Certificate[] getAcceptedIssuers()
    {
      return new X509Certificate[0];
    }
  }
  @AfterClass(alwaysRun = true)
  public void restoreDefaultSSLContext()
  {
    if (defaultSSLContext != null)
    {
      SSLContext.setDefault(defaultSSLContext);
    }
  }
  @DataProvider
  public Object[][] useSSL()
  {
    return new Object[][] { { false }, { true } };
  }
  @Test(dataProvider = "useSSL")
  public void listensOnlyOnTheConfiguredListenAddress(boolean useSSL) throws Exception
  {
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final int listenPort = ports[0];
    final int rmiPort = ports[1];
    JmxConnectionHandler handler = newHandler(listenAddress, listenPort, rmiPort, useSSL);
    try
    {
      // Starts the RMI registry and the RMI connector in this thread.
      handler.run();
      assertNotNull(handler.getRMIConnector().jmxRmiConnectorNoClientCertificate, "the RMI connector did not start");
      assertListensOnlyOn(listenAddress, listenPort, rmiPort);
      assertThat(handler.getListeners()).containsExactly(new HostPort(listenAddress.getHostAddress(), listenPort));
      assertOpensASession(listenAddress, listenPort);
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
    }
  }
  /** A change that restarts the RMI connector keeps the listen address that is reported. */
  @Test
  public void reportsTheListenAddressAfterAChangeOfTheRmiPort() throws Exception
  {
    final int[] ports = TestCaseUtils.findFreePorts(3);
    final int listenPort = ports[0];
    JmxConnectionHandler handler = newHandler(listenAddress, listenPort, ports[1], false);
    try
    {
      handler.run();
      applyConfigurationChange(handler, newConfig(listenAddress, listenPort, ports[2], false));
      assertListensOnlyOn(listenAddress, listenPort, ports[2]);
      assertThat(handler.getListeners()).containsExactly(new HostPort(listenAddress.getHostAddress(), listenPort));
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
    }
  }
  /**
   * A change of listen-address takes effect when the handler restarts: until then, the RMI
   * registry, the RMI connector restarted by another change and the listeners stay on the address
   * the handler was initialized with.
   */
  @Test
  public void keepsItsListenAddressUntilItRestarts() throws Exception
  {
    final int[] ports = TestCaseUtils.findFreePorts(3);
    final int listenPort = ports[0];
    JmxConnectionHandler handler = newHandler(listenAddress, listenPort, ports[1], false);
    try
    {
      handler.run();
      applyConfigurationChange(handler, newConfig(otherAddress, listenPort, ports[2], false));
      assertListensOnlyOn(listenAddress, listenPort, ports[2]);
      assertThat(handler.getListeners()).containsExactly(new HostPort(listenAddress.getHostAddress(), listenPort));
      assertOpensASession(listenAddress, listenPort);
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
    }
  }
  /**
   * A handler restarted on the wildcard address no longer sends clients to the loopback address a
   * previous one listened on.
   */
  @Test
  public void stopsAdvertisingTheLoopbackAddressOnTheWildcardAddress() throws Exception
  {
    if (InetAddress.getLocalHost().isLoopbackAddress())
    {
      throw new SkipException("the JDK advertises a loopback address anyway");
    }
    final int[] ports = TestCaseUtils.findFreePorts(4);
    JmxConnectionHandler handler = newHandler(InetAddress.getByName("127.0.0.1"), ports[0], ports[1], false);
    try
    {
      handler.run();
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
    }
    handler = newHandler(InetAddress.getByName("0.0.0.0"), ports[2], ports[3], false);
    try
    {
      handler.run();
      final Registry registry = LocateRegistry.getRegistry("127.0.0.1", ports[2]);
      final String[] names = registry.list();
      assertThat(names).isNotEmpty();
      for (String name : names)
      {
        assertThat(registry.lookup(name).toString()).doesNotContain("[127.0.0.1:").doesNotContain("[0.0.0.0:");
      }
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
    }
  }
  /**
   * The host that the operator gives the stubs to advertise, behind a NAT for instance, is kept.
   * <p>
   * The name is one the server never records itself, which advertises addresses only. It is
   * reachable, since the registry calls the stubs bound in it. Runs last: once cleared, the property
   * leaves the JDK advertising the host it last read.
   */
  @Test(priority = 1)
  public void keepsTheOperatorsRmiServerHostname() throws Exception
  {
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final String operatorsHostname = "localhost";
    System.setProperty(RMI_SERVER_HOSTNAME, operatorsHostname);
    JmxConnectionHandler handler = newHandler(listenAddress, ports[0], ports[1], false);
    try
    {
      handler.run();
      assertNotNull(handler.getRMIConnector().jmxRmiConnectorNoClientCertificate, "the RMI connector did not start");
      assertEquals(System.getProperty(RMI_SERVER_HOSTNAME), operatorsHostname);
    }
    finally
    {
      handler.finalizeConnectionHandler(STOP_REASON);
      System.clearProperty(RMI_SERVER_HOSTNAME);
    }
  }
  private void assertListensOnlyOn(InetAddress address, int listenPort, int rmiPort) throws IOException
  {
    final InetAddress other = address.equals(listenAddress) ? otherAddress : listenAddress;
    assertReachable(other);
    assertTrue(isAcceptingConnections(address, listenPort),
        "the RMI registry does not listen on " + address.getHostAddress());
    assertFalse(isAcceptingConnections(other, listenPort),
        "the RMI registry answers on " + other.getHostAddress() + ", which is not the listen address");
    assertTrue(isAcceptingConnections(address, rmiPort),
        "the RMI connector does not listen on " + address.getHostAddress());
    assertFalse(isAcceptingConnections(other, rmiPort),
        "the RMI connector answers on " + other.getHostAddress() + ", which is not the listen address");
  }
  /** A refusal on an address proves something only if a listener on every address answers there. */
  private static void assertReachable(InetAddress address) throws IOException
  {
    try (ServerSocket control = new ServerSocket(0))
    {
      assertTrue(isAcceptingConnections(address, control.getLocalPort()),
          address.getHostAddress() + " is not reachable from this host, so a refusal on it proves nothing");
    }
  }
  /**
   * Opens a JMX session through the RMI registry, then calls the RMI connector: the call goes to
   * the address that the stub of the connector advertises.
   */
  private static void assertOpensASession(InetAddress address, int listenPort) throws Exception
  {
    final Map<String, Object> env = new HashMap<>();
    env.put(JMXConnector.CREDENTIALS, new String[] { USER_DN, USER_PASSWORD });
    env.put("jmx.remote.x.client.connection.check.period", 0);
    try (OpendsJmxConnector connector = new OpendsJmxConnector(address.getHostAddress(), listenPort, env))
    {
      connector.connect();
      assertThat(connector.getMBeanServerConnection().getMBeanCount()).isPositive();
    }
  }
  private static void applyConfigurationChange(JmxConnectionHandler handler, JMXConnectionHandlerCfg config)
  {
    final ConfigChangeResult result = handler.applyConfigurationChange(config);
    assertEquals(result.getResultCode(), ResultCode.SUCCESS, String.valueOf(result.getMessages()));
  }
  private static JmxConnectionHandler newHandler(InetAddress listenAddress, int listenPort, int rmiPort,
      boolean useSSL) throws Exception
  {
    JmxConnectionHandler handler = new JmxConnectionHandler();
    handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(),
        newConfig(listenAddress, listenPort, rmiPort, useSSL));
    return handler;
  }
  private static JMXConnectionHandlerCfg newConfig(InetAddress listenAddress, int listenPort, int rmiPort,
      boolean useSSL) throws Exception
  {
    Entry handlerEntry = TestCaseUtils.makeEntry(
        "dn: cn=Listen Address JMX Connection Handler,cn=Connection Handlers,cn=config",
        "objectClass: top",
        "objectClass: ds-cfg-connection-handler",
        "objectClass: ds-cfg-jmx-connection-handler",
        "cn: Listen Address JMX Connection Handler",
        "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
        "ds-cfg-enabled: true",
        "ds-cfg-listen-address: " + listenAddress.getHostAddress(),
        "ds-cfg-listen-port: " + listenPort,
        "ds-cfg-rmi-port: " + rmiPort,
        "ds-cfg-use-ssl: " + useSSL,
        "ds-cfg-key-manager-provider: cn=JKS,cn=Key Manager Providers,cn=config",
        "ds-cfg-ssl-cert-nickname: server-cert");
    return InitializationUtils.getConfiguration(JMXConnectionHandlerCfgDefn.getInstance(), handlerEntry);
  }
}