From e8eecc7d8f55928b17b147937a18338a7936781f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 13 Dec 2016 14:55:53 -0500 Subject: [PATCH] INT-4183: Add SSL Handshake Timeout for TCP JIRA: https://jira.spring.io/browse/INT-4183 Previously, this was hard-coded to 30 seconds. * Fix typos according PR comments Conflicts: spring-integration-ip/src/main/java/org/springframework/integration/ip/config/IpAdapterParserUtils.java spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java * Resolve conflicts for the `SocketSupportTests` where `ApplicationEventPublisher` isn't `@FunctionalInterface` yet in SF-4.3.x --- .../ip/config/IpAdapterParserUtils.java | 7 +- .../TcpConnectionFactoryFactoryBean.java | 14 +++ .../ip/config/TcpConnectionFactoryParser.java | 5 +- .../connection/AbstractConnectionFactory.java | 21 +++++ .../TcpNioClientConnectionFactory.java | 3 + .../tcp/connection/TcpNioSSLConnection.java | 16 +++- .../TcpNioServerConnectionFactory.java | 3 + .../ip/config/spring-integration-ip-4.3.xsd | 8 ++ .../ip/config/ParserUnitTests-context.xml | 12 +++ .../ip/config/ParserUnitTests.java | 5 ++ .../ip/tcp/connection/SocketSupportTests.java | 87 +++++++++++++++++++ src/reference/asciidoc/ip.adoc | 3 + 12 files changed, 181 insertions(+), 3 deletions(-) diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/IpAdapterParserUtils.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/IpAdapterParserUtils.java index 7684773442..7728c961da 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/IpAdapterParserUtils.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/IpAdapterParserUtils.java @@ -124,7 +124,12 @@ public abstract class IpAdapterParserUtils { public static final String MAPPER = "mapper"; - private IpAdapterParserUtils() { } + public static final String READ_DELAY = "read-delay"; + + public static final String SSL_HANDSHAKE_TIMEOUT = "ssl-handshake-timeout"; + + private IpAdapterParserUtils() { + } /** * Adds a constructor-arg to the provided bean definition builder diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/TcpConnectionFactoryFactoryBean.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/TcpConnectionFactoryFactoryBean.java index c5c251eae5..bc0a24c696 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/TcpConnectionFactoryFactoryBean.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/config/TcpConnectionFactoryFactoryBean.java @@ -112,6 +112,8 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean getDelayedReads() { return this.delayedReads; } diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java index cd387c1951..c373c3d05d 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java @@ -88,6 +88,9 @@ public class TcpNioClientConnectionFactory extends socketChannel, false, this.isLookupHost(), this.getApplicationEventPublisher(), this.getComponentName()); connection.setUsingDirectBuffers(this.usingDirectBuffers); connection.setTaskExecutor(this.getTaskExecutor()); + if (getSslHandshakeTimeout() != null && connection instanceof TcpNioSSLConnection) { + ((TcpNioSSLConnection) connection).setHandshakeTimeout(getSslHandshakeTimeout()); + } TcpConnectionSupport wrappedConnection = wrapConnection(connection); initializeConnection(wrappedConnection, socketChannel.socket()); socketChannel.configureBlocking(false); diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioSSLConnection.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioSSLConnection.java index 8b75c59d00..ff63fde733 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioSSLConnection.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioSSLConnection.java @@ -53,6 +53,8 @@ import org.springframework.util.Assert; */ public class TcpNioSSLConnection extends TcpNioConnection { + private static final int DEFAULT_HANDSHAKE_TIMEOUT = 30; + private final SSLEngine sslEngine; private volatile ByteBuffer decoded; @@ -67,6 +69,8 @@ public class TcpNioSSLConnection extends TcpNioConnection { private volatile boolean writerActive; + private volatile int handshakeTimeout = DEFAULT_HANDSHAKE_TIMEOUT; + private boolean needMoreNetworkData; public TcpNioSSLConnection(SocketChannel socketChannel, boolean server, boolean lookupHost, @@ -76,6 +80,15 @@ public class TcpNioSSLConnection extends TcpNioConnection { this.sslEngine = sslEngine; } + /** + * Set the timeout while waiting for handshake data (in seconds). Default 30. + * @param handshakeTimeout the timeout. + * @since 4.3.6 + */ + public void setHandshakeTimeout(int handshakeTimeout) { + this.handshakeTimeout = handshakeTimeout; + } + @Override public SSLSession getSslSession() { return this.sslEngine.getSession(); @@ -368,7 +381,8 @@ public class TcpNioSSLConnection extends TcpNioConnection { if (logger.isTraceEnabled()) { logger.trace("Writer waiting for handshake"); } - if (!TcpNioSSLConnection.this.semaphore.tryAcquire(30, TimeUnit.SECONDS)) { + if (!TcpNioSSLConnection.this.semaphore.tryAcquire(TcpNioSSLConnection.this.handshakeTimeout, + TimeUnit.SECONDS)) { throw new MessagingException("SSL Handshaking taking too long"); } if (logger.isTraceEnabled()) { diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioServerConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioServerConnectionFactory.java index 665dd113c6..1b631989f4 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioServerConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioServerConnectionFactory.java @@ -220,6 +220,9 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto } connection.setTaskExecutor(getTaskExecutor()); connection.setLastRead(now); + if (getSslHandshakeTimeout() != null && connection instanceof TcpNioSSLConnection) { + ((TcpNioSSLConnection) connection).setHandshakeTimeout(getSslHandshakeTimeout()); + } this.channelMap.put(channel, connection); channel.register(selector, SelectionKey.OP_READ, connection); connection.publishConnectionOpenEvent(); diff --git a/spring-integration-ip/src/main/resources/org/springframework/integration/ip/config/spring-integration-ip-4.3.xsd b/spring-integration-ip/src/main/resources/org/springframework/integration/ip/config/spring-integration-ip-4.3.xsd index 033b044364..5e39780775 100644 --- a/spring-integration-ip/src/main/resources/org/springframework/integration/ip/config/spring-integration-ip-4.3.xsd +++ b/spring-integration-ip/src/main/resources/org/springframework/integration/ip/config/spring-integration-ip-4.3.xsd @@ -692,6 +692,14 @@ + + + + The timeout (in seconds) to use while performing handshakes on SSL sockets; + only applies when 'using-nio' is 'true'. Default: 30. + + + diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests-context.xml b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests-context.xml index a03d72240b..0f389752d9 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests-context.xml +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests-context.xml @@ -73,6 +73,7 @@ apply-sequence="true" using-nio="true" ssl-context-support="sslContextSupport" + ssl-handshake-timeout="43" /> @@ -91,6 +92,17 @@ socket-support="socketSupport" socket-factory-support="socketFactorySupport" /> + + diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java index ab46185fb3..e0c8166186 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java @@ -260,6 +260,9 @@ public class ParserUnitTests { @Autowired TcpNetServerConnectionFactory secureServer; + @Autowired + TcpNioServerConnectionFactory secureServerNio; + @Autowired TcpSocketFactorySupport socketFactorySupport; @@ -354,6 +357,7 @@ public class ParserUnitTests { Object connectionSupport = TestUtils.getPropertyValue(cfS1Nio, "tcpNioConnectionSupport"); assertTrue(connectionSupport instanceof DefaultTcpNioSSLConnectionSupport); assertNotNull(TestUtils.getPropertyValue(connectionSupport, "sslContext")); + assertEquals(43, TestUtils.getPropertyValue(this.cfS1Nio, "sslHandshakeTimeout")); } @Test @@ -667,6 +671,7 @@ public class ParserUnitTests { DirectFieldAccessor dfa = new DirectFieldAccessor(secureServer); assertSame(socketFactorySupport, dfa.getPropertyValue("tcpSocketFactorySupport")); assertSame(socketSupport, dfa.getPropertyValue("tcpSocketSupport")); + assertEquals(34, TestUtils.getPropertyValue(this.secureServerNio, "sslHandshakeTimeout")); } @SuppressWarnings({ "unchecked", "deprecation" }) diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java index 019b841737..dbbf8371d3 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; @@ -45,6 +46,8 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.integration.ip.tcp.serializer.ByteArrayCrLfSerializer; import org.springframework.integration.ip.util.TestingUtilities; import org.springframework.integration.test.util.TestUtils; @@ -315,6 +318,9 @@ Certificate fingerprints: assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals("Hello, world!", new String((byte[]) messages.get(0).getPayload())); assertNotNull(messages.get(0).getHeaders().get("cipher")); + + client.stop(); + server.stop(); } @Test @@ -356,12 +362,16 @@ Certificate fingerprints: connection.send(new GenericMessage("Hello, world!")); assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals("Hello, world!", new String((byte[]) messages.get(0).getPayload())); + + client.stop(); + server.stop(); } @Test public void testNioClientAndServerSSL() throws Exception { System.setProperty("javax.net.debug", "all"); // SSL activity in the console TcpNioServerConnectionFactory server = new TcpNioServerConnectionFactory(0); + server.setSslHandshakeTimeout(43); DefaultTcpSSLContextSupport sslContextSupport = new DefaultTcpSSLContextSupport("test.ks", "test.truststore.ks", "secret", "secret"); sslContextSupport.setProtocol("SSL"); @@ -382,10 +392,27 @@ Certificate fingerprints: }); server.setMapper(new SSLMapper()); + final AtomicReference serverConnectionId = new AtomicReference<>(); + server.setApplicationEventPublisher(new ApplicationEventPublisher() { + + @Override + public void publishEvent(Object e) { + + } + + @Override + public void publishEvent(ApplicationEvent e) { + if (e instanceof TcpConnectionOpenEvent) { + serverConnectionId.set(((TcpConnectionEvent) e).getConnectionId()); + } + } + + }); server.start(); TestingUtilities.waitListening(server, null); TcpNioClientConnectionFactory client = new TcpNioClientConnectionFactory("localhost", server.getPort()); + client.setSslHandshakeTimeout(34); client.setTcpNioConnectionSupport(tcpNioConnectionSupport); client.registerListener(new TcpListener() { @@ -394,14 +421,36 @@ Certificate fingerprints: return false; } + }); + client.setApplicationEventPublisher(new ApplicationEventPublisher() { + + @Override + public void publishEvent(Object e) { + + } + + @Override + public void publishEvent(ApplicationEvent event) { + + } + }); client.start(); TcpConnection connection = client.getConnection(); + assertEquals(34, TestUtils.getPropertyValue(connection, "handshakeTimeout")); connection.send(new GenericMessage("Hello, world!")); assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals("Hello, world!", new String((byte[]) messages.get(0).getPayload())); assertNotNull(messages.get(0).getHeaders().get("cipher")); + + Map connections = TestUtils.getPropertyValue(server, "connections", Map.class); + Object serverConnection = connections.get(serverConnectionId.get()); + assertNotNull(serverConnection); + assertEquals(43, TestUtils.getPropertyValue(serverConnection, "handshakeTimeout")); + + client.stop(); + server.stop(); } @Test @@ -437,6 +486,22 @@ Certificate fingerprints: ByteArrayCrLfSerializer deserializer = new ByteArrayCrLfSerializer(); deserializer.setMaxMessageSize(120000); server.setDeserializer(deserializer); + final AtomicReference serverConnectionId = new AtomicReference<>(); + server.setApplicationEventPublisher(new ApplicationEventPublisher() { + + @Override + public void publishEvent(Object e) { + + } + + @Override + public void publishEvent(ApplicationEvent e) { + if (e instanceof TcpConnectionOpenEvent) { + serverConnectionId.set(((TcpConnectionEvent) e).getConnectionId()); + } + } + + }); server.start(); TestingUtilities.waitListening(server, null); @@ -458,9 +523,23 @@ Certificate fingerprints: }); client.setDeserializer(deserializer); + client.setApplicationEventPublisher(new ApplicationEventPublisher() { + + @Override + public void publishEvent(Object e) { + + } + + @Override + public void publishEvent(ApplicationEvent event) { + + } + + }); client.start(); TcpConnection connection = client.getConnection(); + assertEquals(30, TestUtils.getPropertyValue(connection, "handshakeTimeout")); byte[] bytes = new byte[100000]; connection.send(new GenericMessage("Hello, world!" + new String(bytes))); assertTrue(latch.await(60, TimeUnit.SECONDS)); @@ -470,6 +549,14 @@ Certificate fingerprints: payload = (byte[]) messages.get(1).getPayload(); assertEquals(13 + bytes.length, payload.length); assertEquals("Hello, world!", new String(payload).substring(0, 13)); + + Map connections = TestUtils.getPropertyValue(server, "connections", Map.class); + Object serverConnection = connections.get(serverConnectionId.get()); + assertNotNull(serverConnection); + assertEquals(30, TestUtils.getPropertyValue(serverConnection, "handshakeTimeout")); + + client.stop(); + server.stop(); } private static class Replier implements TcpSender { diff --git a/src/reference/asciidoc/ip.adoc b/src/reference/asciidoc/ip.adoc index dc5330815a..296dc94fca 100644 --- a/src/reference/asciidoc/ip.adoc +++ b/src/reference/asciidoc/ip.adoc @@ -907,6 +907,9 @@ The `DefaulTcpSSLContextSupport` class also has an optional 'protocol' property, The keystore file names (first two constructor arguments) use the Spring `Resource` abstraction; by default the files will be located on the classpath, but this can be overridden by using the `file:` prefix, to find the files on the filesystem instead. +Starting with _version 4.3.6_, when using NIO, you can specify an `ssl-handshake-timeout` (seconds) on the connection factory. +This timeout (default 30) is used during SSL handshake when waiting for data; if the timeout is exceeded, the process is aborted and the socket closed. + ==== Advanced Techniques In many cases, the configuration described above is all that is needed to enable secure communication over TCP/IP.