Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,15 @@ public void onClosed(Closeable closeable, ICloseType type) throws IOException {
tryReleaseConnectionPermit();

if (!socket.connectPromise.isDone()) {
socket.connectPromise.handleException(new ConnectorIOException(
"Connection is closed before WebSocket is established"));
final Throwable handshakeFailure =
ConnectionManager.getHandshakeFailure(conn);
socket.connectPromise.handleException(handshakeFailure != null
? new ConnectorIOException(
"TLS handshake failed before WebSocket is established: "
+ handshakeFailure.getMessage(),
handshakeFailure)
: new ConnectorIOException(
"Connection is closed before WebSocket is established"));
}
// Immediately try to reconnect
if (System.currentTimeMillis() - lastConnectithenOnException.get() > 30000) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
* ====================
* Portions Copyrighted 2015 ForgeRock AS.
* Portions Copyrighted 2026 3A Systems, LLC
*/

/**
Expand All @@ -32,6 +33,8 @@
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;

import org.forgerock.openicf.framework.remote.ReferenceCountedObject;
import org.forgerock.openicf.framework.remote.rpc.OperationMessageListener;
Expand Down Expand Up @@ -64,6 +67,7 @@
import org.glassfish.grizzly.http.util.MimeHeaders;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder;
import org.glassfish.grizzly.ssl.SSLConnectionContext;
import org.glassfish.grizzly.ssl.SSLContextConfigurator;
import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
import org.glassfish.grizzly.ssl.SSLFilter;
Expand Down Expand Up @@ -308,7 +312,8 @@ public void onTimeout(Connection connection) {
}
final SSLEngineConfigurator configurator =
new SSLEngineConfigurator(context, true, false, false);
final SwitchingSSLFilter filter = new SwitchingSSLFilter(configurator, defaultSecState);
final SwitchingSSLFilter filter =
new SwitchingSSLFilter(configurator, defaultSecState, clientConfig);
fcb.add(filter);

final AsyncHttpClientEventFilter eventFilter =
Expand Down Expand Up @@ -476,6 +481,15 @@ public String getAuthority() {
return connectionInfo.getRemoteURI().getAuthority();
}

/** The host of the remote URI, without the brackets of an IPv6 literal. */
String getHost() {
String host = connectionInfo.getRemoteURI().getHost();
if (host != null && host.startsWith("[") && host.endsWith("]")) {
host = host.substring(1, host.length() - 1);
}
return host;
}

boolean isGracefullyFinishResponseOnClose() {
final HttpResponsePacket response = responsePacket;
return !response.getProcessingState().isKeepAlive() && !response.isChunked()
Expand Down Expand Up @@ -860,23 +874,90 @@ public Object type() {
}
} // END GracefulCloseEvent

/**
* The TLS handshake failure recorded on a connection by
* {@link SwitchingSSLFilter}, so that the reason (an untrusted or
* mismatching server certificate, say) can be reported to whoever waits
* for that connection instead of a bare "connection closed".
*/
static final Attribute<Throwable> HANDSHAKE_FAILURE = Grizzly.DEFAULT_ATTRIBUTE_BUILDER
.createAttribute(SwitchingSSLFilter.class.getName() + ".handshakeFailure");

static Throwable getHandshakeFailure(final Connection<?> connection) {
return HANDSHAKE_FAILURE.get(connection);
}

static final class SwitchingSSLFilter extends SSLFilter {

private final boolean secureByDefault;
private final ConnectionManagerConfig managerConfig;
final Attribute<Boolean> CONNECTION_IS_SECURE = Grizzly.DEFAULT_ATTRIBUTE_BUILDER
.createAttribute(SwitchingSSLFilter.class.getName());

// -------------------------------------------------------- Constructors

SwitchingSSLFilter(final SSLEngineConfigurator clientConfig, final boolean secureByDefault) {
SwitchingSSLFilter(final SSLEngineConfigurator clientConfig, final boolean secureByDefault,
final ConnectionManagerConfig managerConfig) {

super(null, clientConfig);
this.secureByDefault = secureByDefault;
this.managerConfig = managerConfig;

}

// ---------------------------------------------- Methods from SSLFilter

/**
* Creates the engine for the server named in the remote URI (not for
* the proxy the socket may be connected to) and, unless switched off,
* has JSSE check the server certificate against that host during the
* handshake: an {@code SSLEngine} does not do this on its own.
*/
@Override
protected SSLEngine createClientSSLEngine(final SSLConnectionContext sslCtx,
final SSLEngineConfigurator sslEngineConfigurator) {
final RemoteConnectionContext context =
RemoteConnectionContext.get(sslCtx.getConnection());
final String host = context != null ? context.getHost() : null;
final SSLEngine sslEngine =
host != null ? sslEngineConfigurator.createSSLEngine(host, -1) : super
.createClientSSLEngine(sslCtx, sslEngineConfigurator);
if (managerConfig.isHostnameVerification()) {
final SSLParameters parameters = sslEngine.getSSLParameters();
parameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(parameters);
}
return sslEngine;
}

/**
* Grizzly's transport wrapper gives a connection without an
* {@code SSLEngine} a server-mode one on its first read. Through a
* proxy that first read is the plain answer to CONNECT, and the client
* handshake then waits for a ClientHello nobody sends; so reads bypass
* the wrapper until the connection is switched to TLS.
*/
@Override
protected SSLTransportFilterWrapper createOptimizedTransportFilter(
final TransportFilter childFilter) {
return new SSLTransportFilterWrapper(childFilter, this) {
@Override
public NextAction handleRead(final FilterChainContext ctx) throws IOException {
return isSecure(ctx.getConnection()) ? super.handleRead(ctx) : wrappedFilter
.handleRead(ctx);
}
};
}

@Override
protected void notifyHandshakeFailed(final Connection connection, final Throwable t) {
HANDSHAKE_FAILURE.set(connection, t);
final RemoteConnectionContext context = RemoteConnectionContext.get(connection);
logger.warn("TLS handshake with connector server {0} failed: {1}",
context != null ? context.getAuthority() : connection.getPeerAddress(), t);
super.notifyHandshakeFailed(connection, t);
}

@Override
public NextAction handleEvent(FilterChainContext ctx, FilterChainEvent event)
throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* Portions Copyrighted 2026 3A Systems, LLC
*/

package org.forgerock.openicf.framework.client;
Expand All @@ -32,6 +33,17 @@

public class ConnectionManagerConfig {

/**
* System property that turns off TLS hostname verification for every
* client unless a {@link ConnectionManagerConfig} says otherwise. Shared
* with the legacy connector server client and, like there, read on every
* connection, so changing it takes effect without rebuilding the
* framework. Only exactly {@code false} disables verification, so a typo
* or another spelling of the value can not weaken it.
*/
public static final String HOSTNAME_VERIFICATION_PROPERTY =
"org.identityconnectors.framework.remote.hostnameVerification";

// SSL Config

private String trustStoreProvider;
Expand Down Expand Up @@ -72,6 +84,9 @@ public class ConnectionManagerConfig {

protected int maxConnectionLifeTimeInMs;

/** Set by {@link #setHostnameVerification}; {@code null} follows the system property. */
protected Boolean hostnameVerification;

public int getScheduledThreadPoolSize() {
return 5;
}
Expand Down Expand Up @@ -175,6 +190,25 @@ public static Builder newBuilder() {
return new Builder();
}

/**
* Whether the connector server certificate is checked against the host of
* the remote URI during the TLS handshake (RFC 2818 / RFC 6125 "HTTPS"
* endpoint identification). On by default; switching it off leaves the
* connection open to man-in-the-middle attacks by anyone holding a
* certificate the client trusts. Unless set explicitly, follows
* {@link #HOSTNAME_VERIFICATION_PROPERTY} at the time of the call.
*/
public boolean isHostnameVerification() {
final Boolean explicit = hostnameVerification;
return explicit != null
? explicit
: !"false".equals(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY));
}

public void setHostnameVerification(boolean hostnameVerification) {
this.hostnameVerification = hostnameVerification;
}

public static class Builder {

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.forgerock.openicf.framework.client;

import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class ConnectionManagerConfigTest {

private static final String KEY = ConnectionManagerConfig.HOSTNAME_VERIFICATION_PROPERTY;

private String saved;

@BeforeMethod
public void saveProperty() {
saved = System.getProperty(KEY);
System.clearProperty(KEY);
}

@AfterMethod
public void restoreProperty() {
if (saved == null) {
System.clearProperty(KEY);
} else {
System.setProperty(KEY, saved);
}
}

@Test
public void hostnameVerificationDefaultsFromSystemProperty() {
assertTrue(new ConnectionManagerConfig().isHostnameVerification());
System.setProperty(KEY, "false");
assertFalse(new ConnectionManagerConfig().isHostnameVerification());
// exactly "false", as in the legacy client
System.setProperty(KEY, "FALSE");
assertTrue(new ConnectionManagerConfig().isHostnameVerification());
System.setProperty(KEY, "flase");
assertTrue(new ConnectionManagerConfig().isHostnameVerification());
}

/**
* The legacy client reads the property on every connection; an existing
* configuration must follow a later change of it the same way.
*/
@Test
public void hostnameVerificationFollowsLaterPropertyChanges() {
ConnectionManagerConfig config = new ConnectionManagerConfig();
assertTrue(config.isHostnameVerification());
System.setProperty(KEY, "false");
assertFalse(config.isHostnameVerification());
System.clearProperty(KEY);
assertTrue(config.isHostnameVerification());
}

@Test
public void explicitSettingOverridesSystemProperty() {
ConnectionManagerConfig config = new ConnectionManagerConfig();
config.setHostnameVerification(false);
assertFalse(config.isHostnameVerification());

System.setProperty(KEY, "false");
config = new ConnectionManagerConfig();
config.setHostnameVerification(true);
assertTrue(config.isHostnameVerification());
}
}
Loading
Loading