From 6cd0845cfa7644563256ee5def01c13f188cc021 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 28 Dec 2016 14:58:16 -0500 Subject: [PATCH] INT-4198: TCP: Add Hook to Customize SSLEngine JIRA: https://jira.spring.io/browse/INT-4198 Enable setting properties like `needClientAuth` on the `SSLEngine` - when not using NIO, this can be set on the server socket with a socket support implementation. Add `nio-connection-support` to namespace. Improved "Advanced Techniques" documentation, using this use case as an example. Fail fast with NIO when SSL handshaking fails. Polishing - PR Comments More Polishing * Final polishing - fix several typos in log messages - clean up `TcpConnectionFactoryFactoryBean` JavaDocs from redundant imports - remove redundant `InitializationBean` functionality from the `DefaultTcpNetSSLSocketFactorySupport` as well Conflicts: spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/SocketSupportTests.java * Resolve Java 8 code style and revert `afterPropertiesSet()` with `@Deprecated` --- .../ip/config/IpAdapterParserUtils.java | 2 + .../TcpConnectionFactoryFactoryBean.java | 59 +++++------- .../ip/config/TcpConnectionFactoryParser.java | 2 + .../DefaultTcpNetSSLSocketFactorySupport.java | 36 ++++--- .../DefaultTcpNioSSLConnectionSupport.java | 39 ++++++-- .../connection/TcpNioConnectionSupport.java | 1 + .../tcp/connection/TcpNioSSLConnection.java | 53 +++++++--- .../TcpNioServerConnectionFactory.java | 6 +- .../ip/config/spring-integration-ip-4.3.xsd | 17 ++++ .../ip/config/ParserUnitTests-context.xml | 4 + .../ip/config/ParserUnitTests.java | 18 ++-- .../ip/tcp/connection/SocketSupportTests.java | 96 +++++++++++++++++-- .../connection/TcpNioConnectionReadTests.java | 3 +- src/reference/asciidoc/ip.adoc | 87 +++++++++++++++-- 14 files changed, 318 insertions(+), 105 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 7728c961da..242c612ed8 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 @@ -118,6 +118,8 @@ public abstract class IpAdapterParserUtils { public static final String SOCKET_SUPPORT = "socket-support"; + public static final String NIO_CONNECTION_SUPPORT = "nio-connection-support"; + public static final String SOCKET_FACTORY_SUPPORT = "socket-factory-support"; public static final String BACKLOG = "backlog"; 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 bc0a24c696..73019f7e64 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 @@ -228,14 +228,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean deserializer) { Assert.notNull(deserializer, "Deserializer may not be null"); @@ -382,7 +367,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean serializer) { Assert.notNull(serializer, "Serializer may not be null"); @@ -391,7 +376,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean + + + + A reference to a TcpNioConnectionSupport strategy implementation. + When 'using-nio' is true, this is used to create connections. + Two default implementations are provided 'DefaultTcpNioConnectionSupport' + and 'DefaultTcpNioSSLConnectionSupport' depending on whether SSL is in + use of not. + + + + + + + + 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 0f389752d9..2544c55078 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 @@ -74,6 +74,7 @@ using-nio="true" ssl-context-support="sslContextSupport" ssl-handshake-timeout="43" + nio-connection-support="nioConnectionSupport" /> @@ -83,6 +84,9 @@ + + > messages = new ArrayList>(); final CountDownLatch latch = new CountDownLatch(1); @@ -326,13 +333,23 @@ Certificate fingerprints: @Test @Ignore public void testNetClientAndServerSSLDifferentContexts() throws Exception { + testNetClientAndServerSSLDifferentContexts(false); + try { + testNetClientAndServerSSLDifferentContexts(true); + fail("expected Exception"); + } + catch (SSLException | SocketException e) { + // NOSONAR + } + } + + private void testNetClientAndServerSSLDifferentContexts(boolean badClient) throws Exception { System.setProperty("javax.net.debug", "all"); // SSL activity in the console TcpNetServerConnectionFactory server = new TcpNetServerConnectionFactory(0); TcpSSLContextSupport serverSslContextSupport = new DefaultTcpSSLContextSupport("server.ks", "server.truststore.ks", "secret", "secret"); DefaultTcpNetSSLSocketFactorySupport serverTcpSocketFactorySupport = new DefaultTcpNetSSLSocketFactorySupport(serverSslContextSupport); - serverTcpSocketFactorySupport.afterPropertiesSet(); server.setTcpSocketFactorySupport(serverTcpSocketFactorySupport); final List> messages = new ArrayList>(); final CountDownLatch latch = new CountDownLatch(1); @@ -345,16 +362,24 @@ Certificate fingerprints: return false; } + }); + server.setTcpSocketSupport(new DefaultTcpSocketSupport() { + + @Override + public void postProcessServerSocket(ServerSocket serverSocket) { + ((SSLServerSocket) serverSocket).setNeedClientAuth(true); + } + }); server.start(); TestingUtilities.waitListening(server, null); TcpNetClientConnectionFactory client = new TcpNetClientConnectionFactory("localhost", server.getPort()); - TcpSSLContextSupport clientSslContextSupport = new DefaultTcpSSLContextSupport("client.ks", + TcpSSLContextSupport clientSslContextSupport = new DefaultTcpSSLContextSupport( + badClient ? "server.ks" : "client.ks", "client.truststore.ks", "secret", "secret"); DefaultTcpNetSSLSocketFactorySupport clientTcpSocketFactorySupport = new DefaultTcpNetSSLSocketFactorySupport(clientSslContextSupport); - clientTcpSocketFactorySupport.afterPropertiesSet(); client.setTcpSocketFactorySupport(clientTcpSocketFactorySupport); client.start(); @@ -377,7 +402,6 @@ Certificate fingerprints: sslContextSupport.setProtocol("SSL"); DefaultTcpNioSSLConnectionSupport tcpNioConnectionSupport = new DefaultTcpNioSSLConnectionSupport(sslContextSupport); - tcpNioConnectionSupport.afterPropertiesSet(); server.setTcpNioConnectionSupport(tcpNioConnectionSupport); final List> messages = new ArrayList>(); final CountDownLatch latch = new CountDownLatch(1); @@ -453,6 +477,63 @@ Certificate fingerprints: server.stop(); } + @Test + public void testNioClientAndServerSSLDifferentContexts() throws Exception { + testNioClientAndServerSSLDifferentContexts(false); + try { + testNioClientAndServerSSLDifferentContexts(true); + fail("expected Exception"); + } + catch (IOException e) { + if (!(e instanceof ClosedChannelException)) { + assertThat(e.getMessage(), containsString("Socket closed during SSL Handshake")); + } + } + } + + private void testNioClientAndServerSSLDifferentContexts(boolean badClient) throws Exception { + System.setProperty("javax.net.debug", "all"); // SSL activity in the console + TcpNioServerConnectionFactory server = new TcpNioServerConnectionFactory(0); + TcpSSLContextSupport serverSslContextSupport = new DefaultTcpSSLContextSupport("server.ks", + "server.truststore.ks", "secret", "secret"); + DefaultTcpNioSSLConnectionSupport tcpNioConnectionSupport = + new DefaultTcpNioSSLConnectionSupport(serverSslContextSupport) { + + @Override + protected void postProcessSSLEngine(SSLEngine sslEngine) { + sslEngine.setNeedClientAuth(true); + } + + }; + server.setTcpNioConnectionSupport(tcpNioConnectionSupport); + final List> messages = new ArrayList>(); + final CountDownLatch latch = new CountDownLatch(1); + server.registerListener(message -> { + messages.add(message); + latch.countDown(); + return false; + }); + server.start(); + TestingUtilities.waitListening(server, null); + + TcpNioClientConnectionFactory client = new TcpNioClientConnectionFactory("localhost", server.getPort()); + TcpSSLContextSupport clientSslContextSupport = new DefaultTcpSSLContextSupport( + badClient ? "server.ks" : "client.ks", + "client.truststore.ks", "secret", "secret"); + DefaultTcpNioSSLConnectionSupport clientTcpNioConnectionSupport = + new DefaultTcpNioSSLConnectionSupport(clientSslContextSupport); + client.setTcpNioConnectionSupport(clientTcpNioConnectionSupport); + client.start(); + + TcpConnection connection = client.getConnection(); + 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 testNioClientAndServerSSLDifferentContextsLargeDataWithReply() throws Exception { System.setProperty("javax.net.debug", "all"); // SSL activity in the console @@ -461,7 +542,6 @@ Certificate fingerprints: "server.truststore.ks", "secret", "secret"); DefaultTcpNioSSLConnectionSupport serverTcpNioConnectionSupport = new DefaultTcpNioSSLConnectionSupport(serverSslContextSupport); - serverTcpNioConnectionSupport.afterPropertiesSet(); server.setTcpNioConnectionSupport(serverTcpNioConnectionSupport); final List> messages = new ArrayList>(); final CountDownLatch latch = new CountDownLatch(2); @@ -476,12 +556,11 @@ Certificate fingerprints: replier.send(message); } catch (Exception e) { - e.printStackTrace(); + throw new RuntimeException(e); } latch.countDown(); return false; } - }); ByteArrayCrLfSerializer deserializer = new ByteArrayCrLfSerializer(); deserializer.setMaxMessageSize(120000); @@ -510,7 +589,6 @@ Certificate fingerprints: "client.truststore.ks", "secret", "secret"); DefaultTcpNioSSLConnectionSupport clientTcpNioConnectionSupport = new DefaultTcpNioSSLConnectionSupport(clientSslContextSupport); - clientTcpNioConnectionSupport.afterPropertiesSet(); client.setTcpNioConnectionSupport(clientTcpNioConnectionSupport); client.registerListener(new TcpListener() { diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java index e8213e7b5b..87a2cb82da 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java @@ -405,7 +405,7 @@ public class TcpNioConnectionReadTests { assertTrue(errorMessageLetch.await(10, TimeUnit.SECONDS)); assertThat(errorMessageRef.get().getMessage(), - containsString("Connection is closed")); + anyOf(containsString("Connection is closed"), containsString("Stream closed after 2 of 3"))); assertTrue(semaphore.tryAcquire(10000, TimeUnit.MILLISECONDS)); assertTrue(removed.size() > 0); @@ -520,6 +520,7 @@ public class TcpNioConnectionReadTests { removed.add(connection); semaphore.release(); } + }); Socket socket = SocketFactory.getDefault().createSocket("localhost", scf.getPort()); socket.getOutputStream().write(shortMessage.getBytes()); diff --git a/src/reference/asciidoc/ip.adoc b/src/reference/asciidoc/ip.adoc index 296dc94fca..3ba8eb59ad 100644 --- a/src/reference/asciidoc/ip.adoc +++ b/src/reference/asciidoc/ip.adoc @@ -900,17 +900,20 @@ After establishing the key stores, the next step is to indicate their locations type="client" host="localhost" port="1234" - ssl-context-support="sslContextSupport" + ssl-context-support="sslContextSupport" /> ---- -The `DefaulTcpSSLContextSupport` class also has an optional 'protocol' property, which can be 'SSL' or 'TLS' (default). +The `DefaulTcpSSLContextSupport` class also has an optional `protocol` property, which can be `SSL` or `TLS` (default). 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 +[[advanced-techniques]] +=== Advanced Techniques + +==== Strategy Interfaces In many cases, the configuration described above is all that is needed to enable secure communication over TCP/IP. However, a number of strategy interfaces are provided to allow customization and modification of socket factories and sockets. @@ -918,18 +921,19 @@ However, a number of strategy interfaces are provided to allow customization and * `TcpSSLContextSupport` * `TcpSocketFactorySupport` * `TcpSocketSupport` +* `TcpNioConnectionSupport` [source,java] ---- public interface TcpSSLContextSupport { - SSLContext getSSLContext() throws Exception; + SSLContext getSSLContext() throws Exception; } ---- Implementations of this interface are responsible for creating an SSLContext. -The sole implementation provided by the framework is the `DefaultTcpSSLContextSupport` described above. +The implementation provided by the framework is the `DefaultTcpSSLContextSupport` described above. If you require different behavior, implement this interface and provide the connection factory with a reference to a bean of your class' implementation. [source,java] @@ -945,8 +949,8 @@ public interface TcpSocketFactorySupport { ---- Implementations of this interface are responsible for obtaining references to `ServerSocketFactory` and `SocketFactory`. -Two implementations are provided; the first is `DefaultTcpNetSocketFactorySupport` for non-SSL sockets (when no 'ssl-context-support' attribute is defined); this simply uses the JDK's default factories. -The second implementation is `DefaultTcpNetSSLSocketFactorySupport`; this is used, by default, when an 'ssl-context-support' attribute is defined; it uses the `SSLContext` created by that bean to create the socket factories. +Two implementations are provided; the first is `DefaultTcpNetSocketFactorySupport` for non-SSL sockets (when no `ssl-context-support` attribute is defined); this simply uses the JDK's default factories. +The second implementation is `DefaultTcpNetSSLSocketFactorySupport`; this is used, by default, when an `ssl-context-support` attribute is defined; it uses the `SSLContext` created by that bean to create the socket factories. NOTE: This interface only applies if `using-nio` is "false"; socket factories are not used by NIO. @@ -958,7 +962,7 @@ public interface TcpSocketSupport { void postProcessSocket(Socket socket); - +} ---- Implementations of this interface can modify sockets after they are created, and after all configured attributes have been applied, but before the sockets are used. @@ -968,6 +972,68 @@ The sole implementation provided by the framework is the `DefaultTcpSocketSuppor To supply your own implementation of `TcpSocketFactorySupport` or `TcpSocketSupport`, provide the connection factory with references to beans of your custom type using the `socket-factory-support` and `socket-support` attributes, respectively. +[source, java] +---- +public interface TcpNioConnectionSupport { + + TcpNioConnection createNewConnection(SocketChannel socketChannel, + boolean server, boolean lookupHost, + ApplicationEventPublisher applicationEventPublisher, + String connectionFactoryName) throws Exception; + +} +---- + +This interface is invoked to create `TcpNioConnection` objects (or subclasses). +Two implementations are provided `DefaultTcpNioSSLConnectionSupport` and `DefaultTcpNioConnectionSupport` which are used depending on whether SSL is in use or not. +A common use case would be to subclass `DefaultTcpNioSSLConnectionSupport` and override `postProcessSSLEngine`; see the example below. + +==== Example: Enabling SSL Client Authentication + +To enable client certificate authentication when using SSL, the technique depends on whether NIO is in use or not. +When NIO is not being used, provide a custom `TcpSocketSupport` implementation to post-process the server socket: + +[source, java] +---- +serverFactory.setTcpSocketSupport(new DefaultTcpSocketSupport() { + + @Override + public void postProcessServerSocket(ServerSocket serverSocket) { + ((SSLServerSocket) serverSocket).setNeedClientAuth(true); + } + +}); +---- + +(When using XML configuration, provide a reference to your bean using the `socket-support` attribute). + +When using NIO, provide a custom `TcpNioSslConnectionSupport` implementation to post-process the `SSLEngine`. + +[source, java] +---- +@Bean +public DefaultTcpNioSSLConnectionSupport tcpNioConnectionSupport() { + return new DefaultTcpNioSSLConnectionSupport(serverSslContextSupport) { + + @Override + protected void postProcessSSLEngine(SSLEngine sslEngine) { + sslEngine.setNeedClientAuth(true); + } + + } +} + +@Bean +public TcpNioServerConnectionFactory server() { + ... + serverFactory.setTcpNioConnectionSupport(tcpNioConnectionSupport()); + ... +} +---- + +(When using XML configuration, since _version 4.3.7_, provide a reference to your bean using the `nio-connection-support` attribute). + + [[ip-endpoint-reference]] === IP Configuration Attributes @@ -1130,6 +1196,11 @@ Defaults to true. | Y | | See <> +| nio-connection-support +| Y +| Y +| +| See <> | read-delay | Y | Y