diff --git a/quickfixj-core/src/main/doc/usermanual/usage/configuration.md b/quickfixj-core/src/main/doc/usermanual/usage/configuration.md index fbd3d65844..e5ce397fa6 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/configuration.md +++ b/quickfixj-core/src/main/doc/usermanual/usage/configuration.md @@ -160,6 +160,7 @@ with QuickFIX, followed by an example. | `EndpointIdentificationAlgorithm` | Sets the endpoint identification algorithm. If the algorithm parameter is non-null, the endpoint identification/verification procedures must be handled during SSL/TLS handshaking. See [Endpoint Identification Algorithm Names](https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#jssenames). | | | | `UseSNI` | Enables the SSL engine to use Server Name Indication (SNI). This option is only applicable for initiators. If provided, `SNIHostName` will be used as the server name. Otherwise, `SocketConnectHost` or `SocketConnectHost` will be used. Note: When this option is disabled, the JVM may still implicitly send the SSL `server_name` extension. | `Y`
`N` | `N` | | `SNIHostName` | SNI host name to be used as desired Server Name Indication (SNI) parameter. | | | +| `ReverseDNSEnabled` | Whether to perform a reverse DNS lookup to resolve the peer host name when connected directly to/from an IP address. The resolved host is used as the SSL peer host (e.g. for hostname verification). When disabled, the literal IP address is used as-is and no reverse DNS lookup is performed. Applies to both initiator and acceptor sessions. | `Y`
`N` | `Y` | --- diff --git a/quickfixj-core/src/main/java/quickfix/mina/HostResolutionStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/HostResolutionStrategy.java new file mode 100644 index 0000000000..85dfcc6db7 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/HostResolutionStrategy.java @@ -0,0 +1,16 @@ +package quickfix.mina; + +import java.net.InetSocketAddress; + +@FunctionalInterface +public interface HostResolutionStrategy { + String getHost(InetSocketAddress address); + + HostResolutionStrategy WITH_REVERSE_DNS = InetSocketAddress::getHostName; + + HostResolutionStrategy WITHOUT_REVERSE_DNS = InetSocketAddress::getHostString; + + static HostResolutionStrategy fromReverseDnsEnabled(boolean reverseDnsEnabled) { + return reverseDnsEnabled ? WITH_REVERSE_DNS : WITHOUT_REVERSE_DNS; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java index e5a9afd13b..477d62a3d6 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -41,6 +41,7 @@ import quickfix.SessionSettings; import quickfix.mina.CompositeIoFilterChainBuilder; import quickfix.mina.EventHandlingStrategy; +import quickfix.mina.HostResolutionStrategy; import quickfix.mina.NetworkingOptions; import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; @@ -135,7 +136,9 @@ private void installSSL(AcceptorSocketDescriptor descriptor, log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); - SslFilter sslFilter = new AcceptorSslFilter(sslContext); + HostResolutionStrategy hostResolutionStrategy = HostResolutionStrategy.fromReverseDnsEnabled( + descriptor.isReverseDnsEnabled()); + SslFilter sslFilter = new AcceptorSslFilter(sslContext, hostResolutionStrategy); sslFilter.setNeedClientAuth(sslConfig.isNeedClientAuth()); sslFilter.setEnabledCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); @@ -188,6 +191,8 @@ && getSettings().getBool(sessionID, SSLSupport.SETTING_USE_SSL)) { sslConfig = SSLSupport.getSslConfig(getSettings(), sessionID); } } + boolean reverseDnsEnabled = getSettings().getBoolOrDefault(sessionID, + SSLSupport.SETTING_REVERSE_DNS_ENABLED, true); int acceptPort = (int) settings.getLong(sessionID, Acceptor.SETTING_SOCKET_ACCEPT_PORT); @@ -206,7 +211,7 @@ && getSettings().getBool(sessionID, SSLSupport.SETTING_USE_SSL)) { throw new ConfigError("Conflicting configurations of acceptor socket: " + acceptorAddress); } } else { - descriptor = new AcceptorSocketDescriptor(acceptorAddress, useSSL, sslConfig); + descriptor = new AcceptorSocketDescriptor(acceptorAddress, useSSL, sslConfig, reverseDnsEnabled); socketDescriptorForAddress.put(acceptorAddress, descriptor); } @@ -281,12 +286,15 @@ private static class AcceptorSocketDescriptor { private final SocketAddress address; private final boolean useSSL; private final SSLConfig sslConfig; + private final boolean reverseDnsEnabled; private final Map acceptedSessions = new HashMap<>(); - public AcceptorSocketDescriptor(SocketAddress address, boolean useSSL, SSLConfig sslConfig) { + public AcceptorSocketDescriptor(SocketAddress address, boolean useSSL, SSLConfig sslConfig, + boolean reverseDnsEnabled) { this.address = address; this.useSSL = useSSL; this.sslConfig = sslConfig; + this.reverseDnsEnabled = reverseDnsEnabled; } public void acceptSession(Session session) { @@ -308,6 +316,10 @@ public boolean isUseSSL() { public SSLConfig getSslConfig() { return sslConfig; } + + public boolean isReverseDnsEnabled() { + return reverseDnsEnabled; + } } public Collection getEndpoints() { diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java index 96c7975c7e..649d3fca14 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -38,6 +38,7 @@ import quickfix.SessionSettings; import quickfix.field.converter.BooleanConverter; import quickfix.mina.EventHandlingStrategy; +import quickfix.mina.HostResolutionStrategy; import quickfix.mina.NetworkingOptions; import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; @@ -139,6 +140,10 @@ private void createInitiator(final Session session, final boolean continueInitOn sslConfig = SSLSupport.getSslConfig(getSettings(), sessionID); } + // Defaults to true for backwards compatibility + HostResolutionStrategy hostResolutionStrategy = HostResolutionStrategy.fromReverseDnsEnabled( + getSettings().getBoolOrDefault(sessionID, SSLSupport.SETTING_REVERSE_DNS_ENABLED, true)); + String proxyUser = null; String proxyPassword = null; String proxyHost = null; @@ -177,7 +182,7 @@ && getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_DOMAIN)) { ScheduledExecutorService scheduledExecutorService = (scheduledReconnectExecutor != null ? scheduledReconnectExecutor : getScheduledExecutorService()); try { final IoSessionInitiator ioSessionInitiator = new IoSessionInitiator(session, - socketAddresses, localAddress, connectTimeout, reconnectingIntervals, + socketAddresses, localAddress, hostResolutionStrategy, connectTimeout, reconnectingIntervals, scheduledExecutorService, settings, networkingOptions, getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java index 9c782dc01b..84adf56e18 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -35,6 +35,7 @@ import quickfix.SystemTime; import quickfix.mina.CompositeIoFilterChainBuilder; import quickfix.mina.EventHandlingStrategy; +import quickfix.mina.HostResolutionStrategy; import quickfix.mina.NetworkingOptions; import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; @@ -64,10 +65,10 @@ public class IoSessionInitiator { private Future reconnectFuture; - public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, - SocketAddress localAddress, int connectTimeout, int[] reconnectIntervalInSeconds, - ScheduledExecutorService executor, SessionSettings sessionSettings, NetworkingOptions networkingOptions, - EventHandlingStrategy eventHandlingStrategy, + IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, + SocketAddress localAddress, HostResolutionStrategy hostResolutionStrategy, int connectTimeout, + int[] reconnectIntervalInSeconds, ScheduledExecutorService executor, SessionSettings sessionSettings, + NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig, String proxyType, String proxyVersion, String proxyHost, int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, String proxyWorkstation) throws ConfigError { @@ -83,7 +84,8 @@ public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, reconnectTask = new ConnectTask(sslEnabled, socketAddresses, localAddress, userIoFilterChainBuilder, fixSession, connectTimeoutMillis, reconnectIntervalInMillis, sessionSettings, networkingOptions, eventHandlingStrategy, sslConfig, - proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation, log); + proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, + proxyWorkstation, log, hostResolutionStrategy); } catch (GeneralSecurityException e) { throw new ConfigError(e); } @@ -105,6 +107,7 @@ private static class ConnectTask implements Runnable { private final EventHandlingStrategy eventHandlingStrategy; private final SSLConfig sslConfig; private final Logger log; + private final HostResolutionStrategy hostResolutionStrategy; private IoSession ioSession; private long lastReconnectAttemptTime; @@ -122,13 +125,13 @@ private static class ConnectTask implements Runnable { private final String proxyDomain; private final String proxyWorkstation; - public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, + ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, SocketAddress localAddress, IoFilterChainBuilder userIoFilterChainBuilder, Session fixSession, long connectTimeoutMillis, long[] reconnectIntervalInMillis, SessionSettings sessionSettings, NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, SSLConfig sslConfig, String proxyType, String proxyVersion, String proxyHost, int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, - String proxyWorkstation, Logger log) throws ConfigError, GeneralSecurityException { + String proxyWorkstation, Logger log, HostResolutionStrategy hostResolutionStrategy) throws ConfigError, GeneralSecurityException { this.sslEnabled = sslEnabled; this.socketAddresses = socketAddresses; this.localAddress = localAddress; @@ -141,6 +144,7 @@ public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, this.eventHandlingStrategy = eventHandlingStrategy; this.sslConfig = sslConfig; this.log = log; + this.hostResolutionStrategy = hostResolutionStrategy; this.proxyType = proxyType; this.proxyVersion = proxyVersion; @@ -194,7 +198,7 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { private void installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { final SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); - final SslFilter sslFilter = new InitiatorSslFilter(sslContext, getSniHostName(sslConfig)); + final SslFilter sslFilter = new InitiatorSslFilter(sslContext, getSniHostName(sslConfig), hostResolutionStrategy); sslFilter.setEnabledCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); sslFilter.setEnabledProtocols(sslConfig.getEnabledProtocols() != null ? sslConfig.getEnabledProtocols() @@ -294,7 +298,7 @@ private SocketAddress getNextSocketAddress() { // Recreate socket address to avoid cached address resolution if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetAddr = (InetSocketAddress) socketAddress; - socketAddress = new InetSocketAddress(inetAddr.getHostName(), inetAddr.getPort()); + socketAddress = new InetSocketAddress(hostResolutionStrategy.getHost(inetAddr), inetAddr.getPort()); socketAddresses[nextSocketAddressIndex] = socketAddress; } nextSocketAddressIndex = (nextSocketAddressIndex + 1) % socketAddresses.length; diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/AcceptorSslFilter.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/AcceptorSslFilter.java index a1e4268e96..84ea889d45 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/AcceptorSslFilter.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/AcceptorSslFilter.java @@ -2,13 +2,56 @@ import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.ssl.SslFilter; +import quickfix.mina.HostResolutionStrategy; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import java.net.InetSocketAddress; public final class AcceptorSslFilter extends SslFilter { + private final HostResolutionStrategy hostResolutionStrategy; - public AcceptorSslFilter(SSLContext sslContext) { + public AcceptorSslFilter(SSLContext sslContext, HostResolutionStrategy hostResolutionStrategy) { super(sslContext); + this.hostResolutionStrategy = hostResolutionStrategy; + } + + @Override + protected SSLEngine createEngine(IoSession session, InetSocketAddress addr) { + SSLEngine sslEngine; + + if (addr != null) { + sslEngine = sslContext.createSSLEngine(hostResolutionStrategy.getHost(addr), addr.getPort()); + } else { + sslEngine = sslContext.createSSLEngine(); + } + + if (wantClientAuth) { + sslEngine.setWantClientAuth(true); + } + + if (needClientAuth) { + sslEngine.setNeedClientAuth(true); + } + + if (enabledCipherSuites != null) { + sslEngine.setEnabledCipherSuites(enabledCipherSuites); + } + + if (enabledProtocols != null) { + sslEngine.setEnabledProtocols(enabledProtocols); + } + + if (getEndpointIdentificationAlgorithm() != null) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm()); + sslEngine.setSSLParameters(sslParameters); + } + + sslEngine.setUseClientMode(!session.isServer()); + + return sslEngine; } @Override diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/InitiatorSslFilter.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/InitiatorSslFilter.java index 5faa65248c..7428f3bf1b 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/InitiatorSslFilter.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/InitiatorSslFilter.java @@ -2,6 +2,7 @@ import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.ssl.SslFilter; +import quickfix.mina.HostResolutionStrategy; import javax.net.ssl.SNIHostName; import javax.net.ssl.SSLContext; @@ -10,13 +11,18 @@ import java.net.InetSocketAddress; import java.util.Collections; +/** + * This class is not part of the public API and may be removed or changed in future releases. + */ public final class InitiatorSslFilter extends SslFilter { private final String sniHostName; + private final HostResolutionStrategy hostResolutionStrategy; - public InitiatorSslFilter(SSLContext sslContext, String sniHostName) { + public InitiatorSslFilter(SSLContext sslContext, String sniHostName, HostResolutionStrategy hostResolutionStrategy) { super(sslContext, false); this.sniHostName = sniHostName; + this.hostResolutionStrategy = hostResolutionStrategy; } @Override @@ -24,7 +30,7 @@ protected SSLEngine createEngine(IoSession session, InetSocketAddress addr) { SSLEngine sslEngine; if (addr != null) { - sslEngine = sslContext.createSSLEngine(addr.getHostName(), addr.getPort()); + sslEngine = sslContext.createSSLEngine(hostResolutionStrategy.getHost(addr), addr.getPort()); } else { sslEngine = sslContext.createSSLEngine(); } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java index c8a0e6f5dd..71fe4d5a28 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java @@ -41,6 +41,7 @@ public class SSLSupport { public static final String SETTING_NEED_CLIENT_AUTH = "NeedClientAuth"; public static final String SETTING_ENDPOINT_IDENTIFICATION_ALGORITHM = "EndpointIdentificationAlgorithm"; public static final String SETTING_USE_SNI = "UseSNI"; + public static final String SETTING_REVERSE_DNS_ENABLED = "ReverseDNSEnabled"; public static final String SETTING_SNI_HOST_NAME = "SNIHostName"; public static final String SETTING_ENABLED_PROTOCOLS = "EnabledProtocols"; public static final String SETTING_CIPHER_SUITES = "CipherSuites"; diff --git a/quickfixj-core/src/test/java/quickfix/mina/initiator/HostResolutionStrategyTest.java b/quickfixj-core/src/test/java/quickfix/mina/initiator/HostResolutionStrategyTest.java new file mode 100644 index 0000000000..48db2d4fdd --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/initiator/HostResolutionStrategyTest.java @@ -0,0 +1,395 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix.mina.initiator; + +import org.apache.mina.util.AvailablePortFinder; +import org.burningwave.tools.net.DefaultHostResolver; +import org.burningwave.tools.net.HostResolutionRequestInterceptor; +import org.burningwave.tools.net.MappedHostResolver; +import org.junit.After; +import org.junit.Test; +import quickfix.Acceptor; +import quickfix.ApplicationAdapter; +import quickfix.ConfigError; +import quickfix.DefaultMessageFactory; +import quickfix.FixVersions; +import quickfix.Initiator; +import quickfix.MemoryStoreFactory; +import quickfix.MessageFactory; +import quickfix.MessageStoreFactory; +import quickfix.Session; +import quickfix.SessionFactory; +import quickfix.SessionID; +import quickfix.SessionSettings; +import quickfix.ThreadedSocketAcceptor; +import quickfix.ThreadedSocketInitiator; +import quickfix.mina.HostResolutionStrategy; +import quickfix.mina.ProtocolFactory; +import quickfix.mina.SessionConnector; +import quickfix.mina.ssl.SSLSupport; +import quickfix.test.util.SessionUtil; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * End-to-end test for {@link HostResolutionStrategy} configurations. + * + * @see SSLSupport#SETTING_REVERSE_DNS_ENABLED + * @see SSLSupport#SETTING_REVERSE_DNS_ENABLED + */ +public class HostResolutionStrategyTest { + + private static final String FAKE_HOSTNAME = "fixserver.example.quickfixj"; + private static final String LOOPBACK_IP = "127.0.0.1"; + private static final byte[] LOOPBACK_IP_BYTES = new byte[]{127, 0, 0, 1}; + private static final String QUICKFIXJ_KEY_STORE = "quickfixj.keystore"; + private static final String QUICKFIXJ_KEY_STORE_PASSWORD = "quickfixjpw"; + + private static final SessionID INITIATOR_SESSION_ID = new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA"); + private static final SessionID ACCEPTOR_SESSION_ID = new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"); + + private TestAcceptor acceptor; + private TestInitiator initiator; + private BlockingReverseDnsResolver reverseDnsResolver; + + @After + public void tearDown() throws InterruptedException { + if (reverseDnsResolver != null) { + reverseDnsResolver.releaseReverseDns(); + } + if (initiator != null) { + initiator.stop(); + } + if (acceptor != null) { + acceptor.stop(); + } + Thread.sleep(500); + HostResolutionRequestInterceptor.INSTANCE.uninstall(); + } + + // --- When reverse DNS is available --- + + @Test + public void initiatorShouldLogonWhenConnectingToIpAddressWithReverseDnsEnabled() throws Exception { + assertInitiatorLogsOnWhenReverseDnsAvailable(LOOPBACK_IP, "Y"); + } + + @Test + public void initiatorShouldLogonWhenConnectingToIpAddressWithReverseDnsDisabled() throws Exception { + assertInitiatorLogsOnWhenReverseDnsAvailable(LOOPBACK_IP, "N"); + } + + @Test + public void initiatorShouldLogonWhenConnectingToCustomHostAliasWithReverseDnsEnabled() throws Exception { + assertInitiatorLogsOnWhenReverseDnsAvailable(FAKE_HOSTNAME, "Y"); + } + + @Test + public void initiatorShouldLogonWhenConnectingToCustomHostAliasWithReverseDnsDisabled() throws Exception { + assertInitiatorLogsOnWhenReverseDnsAvailable(FAKE_HOSTNAME, "N"); + } + + // --- When reverse DNS is unavailable --- + + @Test + public void initiatorShouldAttemptToCallReverseDnsDuringLogonWhenReverseDnsIsEnabled() throws Exception { + installBlockingResolver(); + int port = AvailablePortFinder.getNextAvailable(); + + acceptor = new TestAcceptor(createAcceptorSettings(port, "N")); + acceptor.start(); + + initiator = new TestInitiator(createInitiatorSettings(LOOPBACK_IP, port, "Y")); + initiator.start(); + + assertTrue(reverseDnsResolver.awaitReverseDnsLookup(5, TimeUnit.SECONDS)); + initiator.assertNotLoggedOn(INITIATOR_SESSION_ID, 1, TimeUnit.SECONDS); + acceptor.assertNotLoggedOn(ACCEPTOR_SESSION_ID, 1, TimeUnit.SECONDS); + + reverseDnsResolver.releaseReverseDns(); + + initiator.assertLoggedOn(INITIATOR_SESSION_ID); + acceptor.assertLoggedOn(ACCEPTOR_SESSION_ID); + assertEquals(1, reverseDnsResolver.getReverseDnsLookupCount()); + } + + @Test + public void initiatorShouldLogonImmediatelyWhenReverseDnsIsDisabled() throws Exception { + installBlockingResolver(); + int port = AvailablePortFinder.getNextAvailable(); + + acceptor = new TestAcceptor(createAcceptorSettings(port, "N")); + acceptor.start(); + + initiator = new TestInitiator(createInitiatorSettings(LOOPBACK_IP, port, "N")); + initiator.start(); + + initiator.assertLoggedOn(INITIATOR_SESSION_ID); + acceptor.assertLoggedOn(ACCEPTOR_SESSION_ID); + assertFalse(reverseDnsResolver.awaitReverseDnsLookup(250, TimeUnit.MILLISECONDS)); + assertEquals(0, reverseDnsResolver.getReverseDnsLookupCount()); + } + + @Test + public void acceptorShouldAttemptToCallReverseDnsDuringLogonWhenReverseDnsIsEnabled() throws Exception { + installBlockingResolver(); + int port = AvailablePortFinder.getNextAvailable(); + + acceptor = new TestAcceptor(createAcceptorSettings(port, "Y")); + acceptor.start(); + + initiator = new TestInitiator(createInitiatorSettings(LOOPBACK_IP, port, "N")); + initiator.start(); + + assertTrue(reverseDnsResolver.awaitReverseDnsLookup(5, TimeUnit.SECONDS)); + initiator.assertNotLoggedOn(INITIATOR_SESSION_ID, 1, TimeUnit.SECONDS); + acceptor.assertNotLoggedOn(ACCEPTOR_SESSION_ID, 1, TimeUnit.SECONDS); + + reverseDnsResolver.releaseReverseDns(); + + initiator.assertLoggedOn(INITIATOR_SESSION_ID); + acceptor.assertLoggedOn(ACCEPTOR_SESSION_ID); + assertEquals(1, reverseDnsResolver.getReverseDnsLookupCount()); + } + + @Test + public void initiatorAndAcceptorShouldAttemptToCallReverseDnsDuringLogonWhenReverseDnsIsEnabled() + throws Exception { + installBlockingResolver(); + int port = AvailablePortFinder.getNextAvailable(); + + acceptor = new TestAcceptor(createAcceptorSettings(port, "Y")); + acceptor.start(); + + initiator = new TestInitiator(createInitiatorSettings(LOOPBACK_IP, port, "Y")); + initiator.start(); + + assertTrue(reverseDnsResolver.awaitReverseDnsLookups(5, TimeUnit.SECONDS)); + initiator.assertNotLoggedOn(INITIATOR_SESSION_ID, 1, TimeUnit.SECONDS); + acceptor.assertNotLoggedOn(ACCEPTOR_SESSION_ID, 1, TimeUnit.SECONDS); + + reverseDnsResolver.releaseReverseDns(); + + initiator.assertLoggedOn(INITIATOR_SESSION_ID); + acceptor.assertLoggedOn(ACCEPTOR_SESSION_ID); + assertEquals(2, reverseDnsResolver.getReverseDnsLookupCount()); + } + + // --- Helpers --- + + private void assertInitiatorLogsOnWhenReverseDnsAvailable(String connectHost, String reverseDnsEnabled) + throws Exception { + installAvailableResolver(); + int port = AvailablePortFinder.getNextAvailable(); + + acceptor = new TestAcceptor(createAcceptorSettings(port)); + acceptor.start(); + + initiator = new TestInitiator(createInitiatorSettings(connectHost, port, reverseDnsEnabled)); + initiator.start(); + + initiator.assertLoggedOn(INITIATOR_SESSION_ID); + acceptor.assertLoggedOn(ACCEPTOR_SESSION_ID); + } + + private void installAvailableResolver() { + Map hostAliases = new HashMap<>(); + hostAliases.put(FAKE_HOSTNAME, LOOPBACK_IP); + HostResolutionRequestInterceptor.INSTANCE.install(new MappedHostResolver(hostAliases), DefaultHostResolver.INSTANCE); + } + + private void installBlockingResolver() { + Map hostAliases = new HashMap<>(); + hostAliases.put(FAKE_HOSTNAME, LOOPBACK_IP); + reverseDnsResolver = new BlockingReverseDnsResolver(hostAliases); + HostResolutionRequestInterceptor.INSTANCE.install(reverseDnsResolver, DefaultHostResolver.INSTANCE); + } + + static class TestAcceptor { + private final SessionConnector connector; + + TestAcceptor(SessionSettings settings) throws ConfigError { + MessageStoreFactory storeFactory = new MemoryStoreFactory(); + MessageFactory messageFactory = new DefaultMessageFactory(); + connector = new ThreadedSocketAcceptor(new ApplicationAdapter(), storeFactory, settings, messageFactory); + } + + void start() throws Exception { + connector.start(); + } + + void stop() { + connector.stop(true); + } + + void assertLoggedOn(SessionID sessionID) { + SessionUtil.assertLoggedOn(connector, sessionID); + } + + void assertNotLoggedOn(SessionID sessionID, long timeout, TimeUnit unit) { + SessionUtil.assertNotLoggedOn(connector, sessionID, timeout, unit); + } + } + + static class TestInitiator { + private final SessionConnector connector; + + TestInitiator(SessionSettings settings) throws ConfigError { + MessageStoreFactory storeFactory = new MemoryStoreFactory(); + MessageFactory messageFactory = new DefaultMessageFactory(); + connector = new ThreadedSocketInitiator(new ApplicationAdapter(), storeFactory, settings, messageFactory); + } + + void start() throws Exception { + connector.start(); + } + + void stop() { + connector.stop(true); + } + + void assertLoggedOn(SessionID sessionID) { + SessionUtil.assertLoggedOn(connector, sessionID); + } + + void assertNotLoggedOn(SessionID sessionID, long timeout, TimeUnit unit) { + SessionUtil.assertNotLoggedOn(connector, sessionID, timeout, unit); + } + } + + private static SessionSettings createAcceptorSettings(int port) { + return createAcceptorSettings(port, null); + } + + private static SessionSettings createAcceptorSettings(int port, String reverseDnsEnabled) { + HashMap defaults = new HashMap<>(); + defaults.put(Session.SETTING_START_TIME, "00:00:00"); + defaults.put(Session.SETTING_END_TIME, "00:00:00"); + defaults.put(Session.SETTING_HEARTBTINT, "30"); + defaults.put(SessionFactory.SETTING_CONNECTION_TYPE, "acceptor"); + defaults.put(Acceptor.SETTING_SOCKET_ACCEPT_PORT, Integer.toString(port)); + if (reverseDnsEnabled != null) { + defaults.put(SSLSupport.SETTING_REVERSE_DNS_ENABLED, reverseDnsEnabled); + } + defaults.put(SSLSupport.SETTING_USE_SSL, "Y"); + defaults.put(SSLSupport.SETTING_KEY_STORE_NAME, QUICKFIXJ_KEY_STORE); + defaults.put(SSLSupport.SETTING_KEY_STORE_PWD, QUICKFIXJ_KEY_STORE_PASSWORD); + defaults.put(SSLSupport.SETTING_TRUST_STORE_NAME, QUICKFIXJ_KEY_STORE); + defaults.put(SSLSupport.SETTING_TRUST_STORE_PWD, QUICKFIXJ_KEY_STORE_PASSWORD); + defaults.put(SSLSupport.SETTING_NEED_CLIENT_AUTH, "N"); + + SessionSettings settings = new SessionSettings(); + settings.set(defaults); + settings.setString(ACCEPTOR_SESSION_ID, "BeginString", FixVersions.BEGINSTRING_FIX44); + settings.setString(ACCEPTOR_SESSION_ID, "DataDictionary", "FIX44.xml"); + settings.setString(ACCEPTOR_SESSION_ID, "SenderCompID", "ALFA"); + settings.setString(ACCEPTOR_SESSION_ID, "TargetCompID", "ZULU"); + + return settings; + } + + private static SessionSettings createInitiatorSettings(String connectHost, int port, String reverseDnsEnabled) { + HashMap defaults = new HashMap<>(); + defaults.put(Session.SETTING_START_TIME, "00:00:00"); + defaults.put(Session.SETTING_END_TIME, "00:00:00"); + defaults.put(Session.SETTING_HEARTBTINT, "30"); + defaults.put(SessionFactory.SETTING_CONNECTION_TYPE, "initiator"); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_PROTOCOL, ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_HOST, connectHost); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_PORT, Integer.toString(port)); + defaults.put(SSLSupport.SETTING_REVERSE_DNS_ENABLED, reverseDnsEnabled); + defaults.put(SSLSupport.SETTING_USE_SSL, "Y"); + defaults.put(SSLSupport.SETTING_KEY_STORE_NAME, QUICKFIXJ_KEY_STORE); + defaults.put(SSLSupport.SETTING_KEY_STORE_PWD, QUICKFIXJ_KEY_STORE_PASSWORD); + defaults.put(SSLSupport.SETTING_TRUST_STORE_NAME, QUICKFIXJ_KEY_STORE); + defaults.put(SSLSupport.SETTING_TRUST_STORE_PWD, QUICKFIXJ_KEY_STORE_PASSWORD); + + SessionSettings settings = new SessionSettings(); + settings.set(defaults); + settings.setString(INITIATOR_SESSION_ID, "BeginString", FixVersions.BEGINSTRING_FIX44); + settings.setString(INITIATOR_SESSION_ID, "DataDictionary", "FIX44.xml"); + settings.setString(INITIATOR_SESSION_ID, "SenderCompID", "ZULU"); + settings.setString(INITIATOR_SESSION_ID, "TargetCompID", "ALFA"); + + return settings; + } + + static class BlockingReverseDnsResolver extends MappedHostResolver { + private final CountDownLatch reverseDnsLookupStarted; + private final CountDownLatch reverseDnsLookupReleased = new CountDownLatch(1); + private final AtomicInteger reverseDnsLookupCount = new AtomicInteger(); + + BlockingReverseDnsResolver(Map hostAliases) { + this(hostAliases, 1); + } + + BlockingReverseDnsResolver(Map hostAliases, int expectedReverseDnsLookups) { + super(hostAliases); + reverseDnsLookupStarted = new CountDownLatch(expectedReverseDnsLookups); + } + + @Override + public Collection getAllHostNamesForHostAddress(Map requestData) { + if (!Arrays.equals((byte[]) getMethodArguments(requestData)[0], LOOPBACK_IP_BYTES)) { + return super.getAllHostNamesForHostAddress(requestData); + } + + reverseDnsLookupCount.incrementAndGet(); + reverseDnsLookupStarted.countDown(); + + try { + if (!reverseDnsLookupReleased.await(5, TimeUnit.SECONDS)) { + throw new AssertionError("Timed out waiting to release reverse DNS lookup"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting to release reverse DNS lookup", e); + } + + return super.getAllHostNamesForHostAddress(requestData); + } + + boolean awaitReverseDnsLookup(long timeout, TimeUnit unit) throws InterruptedException { + return awaitReverseDnsLookups(timeout, unit); + } + + boolean awaitReverseDnsLookups(long timeout, TimeUnit unit) throws InterruptedException { + return reverseDnsLookupStarted.await(timeout, unit); + } + + int getReverseDnsLookupCount() { + return reverseDnsLookupCount.get(); + } + + void releaseReverseDns() { + reverseDnsLookupReleased.countDown(); + } + } +} diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/AcceptorSslFilterTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/AcceptorSslFilterTest.java new file mode 100644 index 0000000000..c364c1425d --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/AcceptorSslFilterTest.java @@ -0,0 +1,88 @@ +package quickfix.mina.ssl; + +import org.apache.mina.core.session.IoSession; +import org.junit.Before; +import org.junit.Test; +import quickfix.mina.HostResolutionStrategy; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests that {@link AcceptorSslFilter} resolves the SSL engine peer host + * according to the configured {@link HostResolutionStrategy}. + */ +public class AcceptorSslFilterTest { + + private static final String LITERAL_IP = "1.2.3.4"; + private static final byte[] IP_BYTES = {1, 2, 3, 4}; + private static final String HOST_NAME = "example.quickfixj.org"; + private static final int PORT = 5001; + + private SSLContext sslContext; + private IoSession session; + + @Before + public void setUp() throws Exception { + sslContext = buildSslContext(); + session = mock(IoSession.class); + when(session.isServer()).thenReturn(true); + } + + @Test + public void shouldUseHostStringWhenReverseDnsDisabled() throws Exception { + AcceptorSslFilter filter = new AcceptorSslFilter(sslContext, + HostResolutionStrategy.WITHOUT_REVERSE_DNS); + + InetSocketAddress address = new InetSocketAddress(InetAddress.getByAddress(IP_BYTES), PORT); + + SSLEngine engine = filter.createEngine(session, address); + + assertEquals(LITERAL_IP, engine.getPeerHost()); + assertFalse(engine.getUseClientMode()); + } + + @Test + public void shouldUseHostNameWhenReverseDnsEnabled() throws Exception { + AcceptorSslFilter filter = new AcceptorSslFilter(sslContext, + HostResolutionStrategy.WITH_REVERSE_DNS); + + InetSocketAddress address = new InetSocketAddress( + InetAddress.getByAddress(HOST_NAME, IP_BYTES), PORT); + + SSLEngine engine = filter.createEngine(session, address); + + assertEquals(HOST_NAME, engine.getPeerHost()); + assertFalse(engine.getUseClientMode()); + } + + @Test + public void shouldCreateServerEngineWhenAddressIsNull() { + AcceptorSslFilter filter = new AcceptorSslFilter(sslContext, + HostResolutionStrategy.WITHOUT_REVERSE_DNS); + + SSLEngine engine = filter.createEngine(session, null); + + assertNotNull(engine); + assertFalse(engine.getUseClientMode()); + } + + private static SSLContext buildSslContext() throws Exception { + SSLConfig sslConfig = new SSLConfig(); + sslConfig.setKeyStoreName(SSLSupport.QUICKFIXJ_KEY_STORE); + sslConfig.setKeyStorePassword(SSLSupport.QUICKFIXJ_KEY_STORE_PWD.toCharArray()); + sslConfig.setKeyStoreType("JKS"); + sslConfig.setKeyManagerFactoryAlgorithm("SunX509"); + sslConfig.setTrustStoreType("JKS"); + sslConfig.setTrustManagerFactoryAlgorithm("PKIX"); + return SSLContextFactory.getInstance(sslConfig); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/InitiatorSslFilterTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/InitiatorSslFilterTest.java new file mode 100644 index 0000000000..ebc6749b4e --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/InitiatorSslFilterTest.java @@ -0,0 +1,92 @@ +package quickfix.mina.ssl; + +import org.apache.mina.core.session.IoSession; +import org.junit.Before; +import org.junit.Test; +import quickfix.mina.HostResolutionStrategy; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests that {@link InitiatorSslFilter} resolves the SSL engine peer host + * according to the configured {@link HostResolutionStrategy}. + */ +public class InitiatorSslFilterTest { + + private static final String LITERAL_IP = "1.2.3.4"; + private static final byte[] IP_BYTES = {1, 2, 3, 4}; + private static final String HOST_NAME = "example.quickfixj.org"; + private static final int PORT = 5001; + + private SSLContext sslContext; + private IoSession session; + + @Before + public void setUp() throws Exception { + sslContext = buildSslContext(); + session = mock(IoSession.class); + when(session.isServer()).thenReturn(false); + } + + @Test + public void shouldUseHostStringWhenReverseDnsDisabled() throws Exception { + InitiatorSslFilter filter = new InitiatorSslFilter(sslContext, null, + HostResolutionStrategy.WITHOUT_REVERSE_DNS); + + // Address built from raw bytes has no stored host name, so getHostString() + // returns the literal IP without performing a reverse DNS lookup. + InetSocketAddress address = new InetSocketAddress(InetAddress.getByAddress(IP_BYTES), PORT); + + SSLEngine engine = filter.createEngine(session, address); + + assertEquals(LITERAL_IP, engine.getPeerHost()); + assertTrue(engine.getUseClientMode()); + } + + @Test + public void shouldUseHostNameWhenReverseDnsEnabled() throws Exception { + InitiatorSslFilter filter = new InitiatorSslFilter(sslContext, null, + HostResolutionStrategy.WITH_REVERSE_DNS); + + // Address built with an explicit host name resolves to that name via + // getHostName() without performing a DNS lookup. + InetSocketAddress address = new InetSocketAddress( + InetAddress.getByAddress(HOST_NAME, IP_BYTES), PORT); + + SSLEngine engine = filter.createEngine(session, address); + + assertEquals(HOST_NAME, engine.getPeerHost()); + assertTrue(engine.getUseClientMode()); + } + + @Test + public void shouldCreateClientEngineWhenAddressIsNull() { + InitiatorSslFilter filter = new InitiatorSslFilter(sslContext, null, + HostResolutionStrategy.WITHOUT_REVERSE_DNS); + + SSLEngine engine = filter.createEngine(session, null); + + assertNotNull(engine); + assertTrue(engine.getUseClientMode()); + } + + private static SSLContext buildSslContext() throws Exception { + SSLConfig sslConfig = new SSLConfig(); + sslConfig.setKeyStoreName(SSLSupport.QUICKFIXJ_KEY_STORE); + sslConfig.setKeyStorePassword(SSLSupport.QUICKFIXJ_KEY_STORE_PWD.toCharArray()); + sslConfig.setKeyStoreType("JKS"); + sslConfig.setKeyManagerFactoryAlgorithm("SunX509"); + sslConfig.setTrustStoreType("JKS"); + sslConfig.setTrustManagerFactoryAlgorithm("PKIX"); + return SSLContextFactory.getInstance(sslConfig); + } +}