/* * 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.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") @Test(groups = { "precommit", "http" }, sequential = true) public class HTTPConnectionHandlerTestCase extends DirectoryServerTestCase { 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 { // This test suite depends on having the schema available, so we'll start the server. TestCaseUtils.startServer(); } /** * The start method must not return before the handler thread has attempted to start the embedded * HTTP server, otherwise a client connecting right after the handler has been enabled through * dsconfig can be refused. * * @throws Exception * if the handler cannot be instantiated or started. */ @Test public void testStartWaitsForListenPort() throws Exception { final int listenPort = TestCaseUtils.findFreePort(); HTTPConnectionHandler handler = newHandler(listenPort, "127.0.0.1"); try { handler.start(); // No retry loop here on purpose: once start() has returned, the port must already be open. TestCaseUtils.assertPortIsAcceptingConnections(listenPort); } finally { 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. *
* 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