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
This commit is contained in:
Gary Russell
2016-12-28 14:58:16 -05:00
committed by Artem Bilan
parent bdab0aa1d3
commit a0f0b6ab64
14 changed files with 297 additions and 108 deletions

View File

@@ -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";

View File

@@ -228,14 +228,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
return new DefaultTcpNetSocketFactorySupport();
}
else {
DefaultTcpNetSSLSocketFactorySupport socketFactorySupport = new DefaultTcpNetSSLSocketFactorySupport(this.sslContextSupport);
try {
socketFactorySupport.afterPropertiesSet();
}
catch (Exception e) {
throw new IllegalStateException("Failed to set up TcpSocketFactorySupport", e);
}
return socketFactorySupport;
return new DefaultTcpNetSSLSocketFactorySupport(this.sslContextSupport);
}
}
@@ -247,16 +240,8 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
return new DefaultTcpNioConnectionSupport();
}
else {
DefaultTcpNioSSLConnectionSupport connectionSupport = new DefaultTcpNioSSLConnectionSupport(this.sslContextSupport);
try {
connectionSupport.afterPropertiesSet();
}
catch (Exception e) {
throw new IllegalStateException("Failed to set up TcpConnectionSupport", e);
}
return connectionSupport;
return new DefaultTcpNioSSLConnectionSupport(this.sslContextSupport);
}
}
/**
@@ -283,8 +268,8 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
}
/**
* @param localAddress The local addres..
* @see org.springframework.integration.ip.tcp.connection.AbstractServerConnectionFactory#setLocalAddress(java.lang.String)
* @param localAddress The local address.
* @see AbstractServerConnectionFactory#setLocalAddress(java.lang.String)
*/
public void setLocalAddress(String localAddress) {
Assert.notNull(localAddress, "LocalAddress may not be null");
@@ -293,7 +278,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soTimeout The timeout.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoTimeout(int)
* @see AbstractConnectionFactory#setSoTimeout(int)
*/
public void setSoTimeout(int soTimeout) {
this.soTimeout = soTimeout;
@@ -301,7 +286,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soReceiveBufferSize The receive buffer size.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoReceiveBufferSize(int)
* @see AbstractConnectionFactory#setSoReceiveBufferSize(int)
*/
public void setSoReceiveBufferSize(int soReceiveBufferSize) {
this.soReceiveBufferSize = soReceiveBufferSize;
@@ -309,7 +294,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soSendBufferSize The send buffer size.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoSendBufferSize(int)
* @see AbstractConnectionFactory#setSoSendBufferSize(int)
*/
public void setSoSendBufferSize(int soSendBufferSize) {
this.soSendBufferSize = soSendBufferSize;
@@ -317,7 +302,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soTcpNoDelay The TCP no delay to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoTcpNoDelay(boolean)
* @see AbstractConnectionFactory#setSoTcpNoDelay(boolean)
*/
public void setSoTcpNoDelay(boolean soTcpNoDelay) {
this.soTcpNoDelay = soTcpNoDelay;
@@ -325,7 +310,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soLinger The SO Linger to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoLinger(int)
* @see AbstractConnectionFactory#setSoLinger(int)
*/
public void setSoLinger(int soLinger) {
this.soLinger = soLinger;
@@ -333,7 +318,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soKeepAlive The SO keepalive to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoKeepAlive(boolean)
* @see AbstractConnectionFactory#setSoKeepAlive(boolean)
*/
public void setSoKeepAlive(boolean soKeepAlive) {
this.soKeepAlive = soKeepAlive;
@@ -341,7 +326,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param soTrafficClass The SO traffic class to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSoTrafficClass(int)
* @see AbstractConnectionFactory#setSoTrafficClass(int)
*/
public void setSoTrafficClass(int soTrafficClass) {
this.soTrafficClass = soTrafficClass;
@@ -356,7 +341,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param usingDirectBuffers the usingDirectBuffers to set.
* @see org.springframework.integration.ip.tcp.connection.TcpNioServerConnectionFactory#setUsingDirectBuffers(boolean)
* @see TcpNioServerConnectionFactory#setUsingDirectBuffers(boolean)
*/
public void setUsingDirectBuffers(boolean usingDirectBuffers) {
this.usingDirectBuffers = usingDirectBuffers;
@@ -364,7 +349,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param taskExecutor The task executor.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setTaskExecutor(java.util.concurrent.Executor)
* @see AbstractConnectionFactory#setTaskExecutor(java.util.concurrent.Executor)
*/
public void setTaskExecutor(Executor taskExecutor) {
Assert.notNull(taskExecutor, "Executor may not be null");
@@ -373,7 +358,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param deserializer The deserializer.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setDeserializer(org.springframework.core.serializer.Deserializer)
* @see AbstractConnectionFactory#setDeserializer(org.springframework.core.serializer.Deserializer)
*/
public void setDeserializer(Deserializer<?> deserializer) {
Assert.notNull(deserializer, "Deserializer may not be null");
@@ -382,7 +367,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param serializer The serializer.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSerializer(org.springframework.core.serializer.Serializer)
* @see AbstractConnectionFactory#setSerializer(org.springframework.core.serializer.Serializer)
*/
public void setSerializer(Serializer<?> serializer) {
Assert.notNull(serializer, "Serializer may not be null");
@@ -391,7 +376,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param mapper The mapper.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setMapper(org.springframework.integration.ip.tcp.connection.TcpMessageMapper)
* @see AbstractConnectionFactory#setMapper(TcpMessageMapper)
*/
public void setMapper(TcpMessageMapper mapper) {
Assert.notNull(mapper, "TcpMessageMapper may not be null");
@@ -401,7 +386,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param singleUse The singleUse to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setSingleUse(boolean)
* @see AbstractConnectionFactory#setSingleUse(boolean)
*/
public void setSingleUse(boolean singleUse) {
this.singleUse = singleUse;
@@ -417,7 +402,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param interceptorFactoryChain The interceptor factory chain.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setInterceptorFactoryChain(org.springframework.integration.ip.tcp.connection.TcpConnectionInterceptorFactoryChain)
* @see AbstractConnectionFactory#setInterceptorFactoryChain(TcpConnectionInterceptorFactoryChain)
*/
public void setInterceptorFactoryChain(
TcpConnectionInterceptorFactoryChain interceptorFactoryChain) {
@@ -427,7 +412,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
* @param lookupHost The lookupHost to set.
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#setLookupHost(boolean)
* @see AbstractConnectionFactory#setLookupHost(boolean)
*/
public void setLookupHost(boolean lookupHost) {
this.lookupHost = lookupHost;
@@ -435,7 +420,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
*
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#start()
* @see AbstractConnectionFactory#start()
*/
@Override
public void start() {
@@ -444,7 +429,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
/**
*
* @see org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory#stop()
* @see AbstractConnectionFactory#stop()
*/
@Override
public void stop() {
@@ -473,7 +458,7 @@ public class TcpConnectionFactoryFactoryBean extends AbstractFactoryBean<Abstrac
}
public void setSslContextSupport(TcpSSLContextSupport sslContextSupport) {
Assert.notNull(sslContextSupport, "TcpSSLConstextSupport may not be null");
Assert.notNull(sslContextSupport, "TcpSSLContextSupport may not be null");
this.sslContextSupport = sslContextSupport;
}

View File

@@ -90,6 +90,8 @@ public class TcpConnectionFactoryParser extends AbstractBeanDefinitionParser {
IpAdapterParserUtils.SOCKET_FACTORY_SUPPORT);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element,
IpAdapterParserUtils.SOCKET_SUPPORT);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element,
IpAdapterParserUtils.NIO_CONNECTION_SUPPORT);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element,
IpAdapterParserUtils.MAPPER);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element,

View File

@@ -16,31 +16,36 @@
package org.springframework.integration.ip.tcp.connection;
import java.io.IOException;
import java.security.GeneralSecurityException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Implementation of TcpSocketFactorySupport
* for SSL sockets {@link javax.net.ssl.SSLServerSocket} and
* {@link javax.net.ssl.SSLSocket}.
* Implementation of {@link TcpSocketFactorySupport}
* for SSL sockets {@link javax.net.ssl.SSLServerSocket} and {@link javax.net.ssl.SSLSocket}.
*
* @author Gary Russell
* @since 2.2
*
*/
public class DefaultTcpNetSSLSocketFactorySupport implements TcpSocketFactorySupport,
InitializingBean {
public class DefaultTcpNetSSLSocketFactorySupport implements TcpSocketFactorySupport {
private final TcpSSLContextSupport sslContextSupport;
private volatile SSLContext sslContext;
private final SSLContext sslContext;
public DefaultTcpNetSSLSocketFactorySupport(TcpSSLContextSupport sslContextSupport) {
Assert.notNull(sslContextSupport, "TcpSSLContextSupport must not be null");
this.sslContextSupport = sslContextSupport;
try {
this.sslContext = sslContextSupport.getSSLContext();
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalArgumentException("Invalid TcpSSLContextSupport - it failed to provide an SSLContext", e);
}
Assert.notNull(this.sslContext, "SSLContext retrieved from context support must not be null");
}
public ServerSocketFactory getServerSocketFactory() {
@@ -51,9 +56,4 @@ public class DefaultTcpNetSSLSocketFactorySupport implements TcpSocketFactorySup
return this.sslContext.getSocketFactory();
}
public void afterPropertiesSet() throws Exception {
this.sslContext = this.sslContextSupport.getSSLContext();
Assert.notNull(this.sslContext, "SSLContext must not be null");
}
}

View File

@@ -16,12 +16,13 @@
package org.springframework.integration.ip.tcp.connection;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.security.GeneralSecurityException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
@@ -32,32 +33,42 @@ import org.springframework.util.Assert;
* @since 2.2
*
*/
public class DefaultTcpNioSSLConnectionSupport implements TcpNioConnectionSupport, InitializingBean {
public class DefaultTcpNioSSLConnectionSupport implements TcpNioConnectionSupport {
private volatile SSLContext sslContext;
private final TcpSSLContextSupport sslContextSupport;
public DefaultTcpNioSSLConnectionSupport(TcpSSLContextSupport sslContextSupport) {
Assert.notNull(sslContextSupport, "TcpSSLContextSupport must not be null");
this.sslContextSupport = sslContextSupport;
try {
this.sslContext = sslContextSupport.getSSLContext();
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalArgumentException("Invalid TcpSSLContextSupport - it failed to provide an SSLContext", e);
}
Assert.notNull(this.sslContext, "SSLContext retrieved from context support must not be null");
}
/**
* Creates a {@link TcpNioSSLConnection}.
*/
@Override
public TcpNioConnection createNewConnection(SocketChannel socketChannel, boolean server, boolean lookupHost,
ApplicationEventPublisher applicationEventPublisher, String connectionFactoryName) throws Exception {
SSLEngine sslEngine = this.sslContext.createSSLEngine();
postProcessSSLEngine(sslEngine);
TcpNioSSLConnection tcpNioSSLConnection = new TcpNioSSLConnection(socketChannel, server, lookupHost,
applicationEventPublisher, connectionFactoryName, sslEngine);
tcpNioSSLConnection.init();
return tcpNioSSLConnection;
}
public void afterPropertiesSet() throws Exception {
this.sslContext = this.sslContextSupport.getSSLContext();
Assert.notNull(this.sslContext, "SSLContext must not be null");
/**
* Subclasses can post-process the ssl engine (set properties).
* @param sslEngine the engine.
* @since 4.3.7
*/
protected void postProcessSSLEngine(SSLEngine sslEngine) {
// NOSONAR (empty)
}
}

View File

@@ -47,4 +47,5 @@ public interface TcpNioConnectionSupport {
boolean server, boolean lookupHost,
ApplicationEventPublisher applicationEventPublisher,
String connectionFactoryName) throws Exception;
}

View File

@@ -27,6 +27,7 @@ import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import org.springframework.context.ApplicationEventPublisher;
@@ -73,6 +74,8 @@ public class TcpNioSSLConnection extends TcpNioConnection {
private boolean needMoreNetworkData;
private SSLHandshakeException sslFatal;
public TcpNioSSLConnection(SocketChannel socketChannel, boolean server, boolean lookupHost,
ApplicationEventPublisher applicationEventPublisher, String connectionFactoryName,
SSLEngine sslEngine) throws Exception {
@@ -106,13 +109,20 @@ public class TcpNioSSLConnection extends TcpNioConnection {
protected void sendToPipe(final ByteBuffer networkBuffer) throws IOException {
Assert.notNull(networkBuffer, "rawBuffer cannot be null");
if (logger.isDebugEnabled()) {
logger.debug("sendToPipe " + this.sslEngine.getHandshakeStatus() + ", remaining:" + networkBuffer.remaining());
logger.debug("sendToPipe " + this.sslEngine.getHandshakeStatus() + ", remaining: " + networkBuffer.remaining());
}
SSLEngineResult result = null;
while (!this.needMoreNetworkData) {
result = decode(networkBuffer);
try {
result = decode(networkBuffer);
}
catch (SSLHandshakeException e) {
this.sslFatal = e;
this.semaphore.release();
throw e;
}
if (logger.isDebugEnabled()) {
logger.debug("result " + resultToString(result) + ", remaining:" + networkBuffer.remaining());
logger.debug("result " + resultToString(result) + ", remaining: " + networkBuffer.remaining());
}
}
this.needMoreNetworkData = false;
@@ -123,7 +133,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
networkBuffer.clear();
}
if (logger.isDebugEnabled()) {
logger.debug("sendToPipe.x " + resultToString(result) + ", remaining:" + networkBuffer.remaining());
logger.debug("sendToPipe.x " + resultToString(result) + ", remaining: " + networkBuffer.remaining());
}
}
@@ -148,7 +158,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
this.decoded.clear();
result = this.sslEngine.unwrap(networkBuffer, this.decoded);
if (logger.isDebugEnabled()) {
logger.debug("After unwrap:" + resultToString(result));
logger.debug("After unwrap: " + resultToString(result));
}
Status status = result.getStatus();
if (status == Status.BUFFER_OVERFLOW) {
@@ -164,7 +174,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
this.encoded.clear();
result = this.sslEngine.wrap(networkBuffer, this.encoded);
if (logger.isDebugEnabled()) {
logger.debug("After wrap:" + resultToString(result));
logger.debug("After wrap: " + resultToString(result));
}
if (result.getStatus() == Status.BUFFER_OVERFLOW) {
this.encoded = this.allocateEncryptionBuffer(this.sslEngine.getSession().getPacketBufferSize());
@@ -197,7 +207,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
private boolean resumeWriterIfNeeded() {
if (this.writerActive) {
if (logger.isTraceEnabled()) {
logger.trace("Waking sender, permits:" + this.semaphore.availablePermits());
logger.trace("Waking sender, permits: " + this.semaphore.availablePermits());
}
this.semaphore.release();
return true;
@@ -285,6 +295,12 @@ public class TcpNioSSLConnection extends TcpNioConnection {
return result.toString().replace('\n', ' ');
}
@Override
public void close() {
super.close();
this.semaphore.release();
}
/**
* Subclass of {@link TcpNioConnection.ChannelOutputStream} to handle encryption
* of outbound data. Wraps an instance of the superclass, which is invoked to
@@ -340,8 +356,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
* Handles SSL handshaking; when network data is needed from the peer, suspends
* until that data is received.
*/
private void doClientSideHandshake(ByteBuffer plainText,
SSLEngineResult result) throws IOException, SSLException {
private void doClientSideHandshake(ByteBuffer plainText, SSLEngineResult result) throws IOException {
TcpNioSSLConnection.this.semaphore.drainPermits();
HandshakeStatus status = TcpNioSSLConnection.this.sslEngine.getHandshakeStatus();
while (status != HandshakeStatus.FINISHED) {
@@ -378,16 +393,20 @@ public class TcpNioSSLConnection extends TcpNioConnection {
private HandshakeStatus waitForHandshakeData(SSLEngineResult result,
HandshakeStatus status) throws IOException {
try {
if (logger.isTraceEnabled()) {
logger.trace("Writer waiting for handshake");
}
logger.trace("Writer waiting for handshake");
if (!TcpNioSSLConnection.this.semaphore.tryAcquire(TcpNioSSLConnection.this.handshakeTimeout,
TimeUnit.SECONDS)) {
throw new MessagingException("SSL Handshaking taking too long");
}
if (logger.isTraceEnabled()) {
logger.trace("Writer resuming handshake");
else {
if (TcpNioSSLConnection.this.sslFatal != null) {
throw TcpNioSSLConnection.this.sslFatal;
}
else if (!isOpen()) {
throw new IOException("Socket closed during SSL Handshake");
}
}
logger.trace("Writer resuming handshake");
status = runTasksIfNeeded(result);
}
catch (InterruptedException e) {
@@ -405,7 +424,9 @@ public class TcpNioSSLConnection extends TcpNioConnection {
TcpNioSSLConnection.this.encoded.clear();
SSLEngineResult result = TcpNioSSLConnection.this.sslEngine.wrap(plainText, TcpNioSSLConnection.this.encoded);
if (logger.isDebugEnabled()) {
logger.debug("After wrap:" + resultToString(result) + " Plaintext buffer @" + plainText.position() + "/" + plainText.limit());
logger.debug("After wrap: "
+ resultToString(result)
+ " Plaintext buffer @" + plainText.position() + "/" + plainText.limit());
}
if (result.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) {
TcpNioSSLConnection.this.encoded = allocateEncryptionBuffer(TcpNioSSLConnection.this.sslEngine.getSession().getPacketBufferSize());
@@ -420,5 +441,7 @@ public class TcpNioSSLConnection extends TcpNioConnection {
void writeEncoded(ByteBuffer encoded) throws IOException {
this.channelOutputStream.doWrite(encoded);
}
}
}

View File

@@ -172,15 +172,13 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
timeout = getReadDelay();
}
if (logger.isTraceEnabled()) {
logger.trace("Delayed reads:" + getDelayedReads().size() + " timeout " + timeout);
logger.trace("Delayed reads: " + getDelayedReads().size() + " timeout " + timeout);
}
selectionCount = selector.select(timeout);
processNioSelections(selectionCount, selector, server, this.channelMap);
}
catch (CancelledKeyException cke) {
if (logger.isDebugEnabled()) {
logger.debug("CancelledKeyException during Selector.select()");
}
logger.debug("CancelledKeyException during Selector.select()");
}
catch (ClosedSelectorException cse) {
if (isActive()) {

View File

@@ -737,6 +737,23 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="nio-connection-support" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.ip.tcp.connection.TcpNioConnectionSupport" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mapper" type="xsd:string">
<xsd:annotation>
<xsd:documentation>

View File

@@ -72,6 +72,7 @@
using-nio="true"
ssl-context-support="sslContextSupport"
ssl-handshake-timeout="43"
nio-connection-support="nioConnectionSupport"
/>
<bean id="sslContextSupport" class="org.springframework.integration.ip.tcp.connection.DefaultTcpSSLContextSupport">
@@ -81,6 +82,9 @@
<constructor-arg value="secret"/>
</bean>
<bean id="nioConnectionSupport"
class="org.springframework.integration.ip.tcp.connection.DefaultTcpNioSSLConnectionSupport" />
<ip:tcp-connection-factory id="secureServer"
type="server"
port="#{tcpIpUtils.findAvailableServerSocket(5250)}"

View File

@@ -36,7 +36,6 @@ import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
@@ -324,29 +323,26 @@ public class ParserUnitTests {
assertEquals(124, tcpIn.getPhase());
TcpMessageMapper cfS1Mapper = TestUtils.getPropertyValue(cfS1, "mapper", TcpMessageMapper.class);
assertSame(mapper, cfS1Mapper);
assertTrue((Boolean) TestUtils.getPropertyValue(cfS1Mapper, "applySequence"));
assertTrue(TestUtils.getPropertyValue(cfS1Mapper, "applySequence", Boolean.class));
Object socketSupport = TestUtils.getPropertyValue(cfS1, "tcpSocketFactorySupport");
assertTrue(socketSupport instanceof DefaultTcpNetSSLSocketFactorySupport);
assertNotNull(TestUtils.getPropertyValue(socketSupport, "sslContext"));
TcpSSLContextSupport contextSupport = TestUtils.getPropertyValue(cfS1, "tcpSocketFactorySupport.sslContextSupport", TcpSSLContextSupport.class);
assertSame(contextSupport, this.contextSupport);
assertTrue(TestUtils.getPropertyValue(contextSupport, "keyStore") instanceof ClassPathResource);
assertTrue(TestUtils.getPropertyValue(contextSupport, "trustStore") instanceof ClassPathResource);
contextSupport = new DefaultTcpSSLContextSupport("http:foo", "file:bar", "", "");
assertTrue(TestUtils.getPropertyValue(contextSupport, "keyStore") instanceof UrlResource);
assertTrue(TestUtils.getPropertyValue(contextSupport, "trustStore") instanceof UrlResource);
TcpSSLContextSupport tcpSSLContextSupport = new DefaultTcpSSLContextSupport("http:foo", "file:bar", "", "");
assertTrue(TestUtils.getPropertyValue(tcpSSLContextSupport, "keyStore") instanceof UrlResource);
assertTrue(TestUtils.getPropertyValue(tcpSSLContextSupport, "trustStore") instanceof UrlResource);
}
@Test
public void testInTcpNioSSLDefaultConfig() {
assertFalse(cfS1Nio.isLookupHost());
assertTrue((Boolean) TestUtils.getPropertyValue(cfS1Nio, "mapper.applySequence"));
assertTrue(TestUtils.getPropertyValue(cfS1Nio, "mapper.applySequence", Boolean.class));
Object connectionSupport = TestUtils.getPropertyValue(cfS1Nio, "tcpNioConnectionSupport");
assertTrue(connectionSupport instanceof DefaultTcpNioSSLConnectionSupport);
assertNotNull(TestUtils.getPropertyValue(connectionSupport, "sslContext"));
assertEquals(43, TestUtils.getPropertyValue(this.cfS1Nio, "sslHandshakeTimeout"));
assertSame(this.ctx.getBean(DefaultTcpNioSSLConnectionSupport.class),
TestUtils.getPropertyValue(this.cfS1Nio, "tcpNioConnectionSupport"));
}
@Test

View File

@@ -16,17 +16,23 @@
package org.springframework.integration.ip.tcp.connection;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -39,6 +45,8 @@ import java.util.concurrent.atomic.AtomicReference;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLServerSocket;
import org.junit.Test;
import org.mockito.Mockito;
@@ -274,7 +282,6 @@ Certificate fingerprints:
"test.truststore.ks", "secret", "secret");
DefaultTcpNetSSLSocketFactorySupport tcpSocketFactorySupport =
new DefaultTcpNetSSLSocketFactorySupport(sslContextSupport);
tcpSocketFactorySupport.afterPropertiesSet();
server.setTcpSocketFactorySupport(tcpSocketFactorySupport);
final List<Message<?>> messages = new ArrayList<Message<?>>();
final CountDownLatch latch = new CountDownLatch(1);
@@ -303,13 +310,23 @@ Certificate fingerprints:
@Test
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<Message<?>> messages = new ArrayList<Message<?>>();
final CountDownLatch latch = new CountDownLatch(1);
@@ -318,15 +335,23 @@ Certificate fingerprints:
latch.countDown();
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();
@@ -349,7 +374,6 @@ Certificate fingerprints:
sslContextSupport.setProtocol("SSL");
DefaultTcpNioSSLConnectionSupport tcpNioConnectionSupport =
new DefaultTcpNioSSLConnectionSupport(sslContextSupport);
tcpNioConnectionSupport.afterPropertiesSet();
server.setTcpNioConnectionSupport(tcpNioConnectionSupport);
final List<Message<?>> messages = new ArrayList<Message<?>>();
final CountDownLatch latch = new CountDownLatch(1);
@@ -391,6 +415,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<Message<?>> messages = new ArrayList<Message<?>>();
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<String>("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
@@ -399,7 +480,6 @@ Certificate fingerprints:
"server.truststore.ks", "secret", "secret");
DefaultTcpNioSSLConnectionSupport serverTcpNioConnectionSupport =
new DefaultTcpNioSSLConnectionSupport(serverSslContextSupport);
serverTcpNioConnectionSupport.afterPropertiesSet();
server.setTcpNioConnectionSupport(serverTcpNioConnectionSupport);
final List<Message<?>> messages = new ArrayList<Message<?>>();
final CountDownLatch latch = new CountDownLatch(2);
@@ -411,7 +491,7 @@ Certificate fingerprints:
replier.send(message);
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
latch.countDown();
return false;
@@ -433,7 +513,6 @@ Certificate fingerprints:
"client.truststore.ks", "secret", "secret");
DefaultTcpNioSSLConnectionSupport clientTcpNioConnectionSupport =
new DefaultTcpNioSSLConnectionSupport(clientSslContextSupport);
clientTcpNioConnectionSupport.afterPropertiesSet();
client.setTcpNioConnectionSupport(clientTcpNioConnectionSupport);
client.registerListener(message -> {
messages.add(message);

View File

@@ -382,7 +382,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);
@@ -473,7 +473,6 @@ public class TcpNioConnectionReadTests {
private void testClosureMidMessageGuts(AbstractByteArraySerializer serializer, String shortMessage)
throws Exception {
final List<Message<?>> responses = new ArrayList<Message<?>>();
final Semaphore semaphore = new Semaphore(0);
final List<TcpConnection> added = new ArrayList<TcpConnection>();
final List<TcpConnection> removed = new ArrayList<TcpConnection>();
@@ -500,6 +499,7 @@ public class TcpNioConnectionReadTests {
removed.add(connection);
semaphore.release();
}
});
Socket socket = SocketFactory.getDefault().createSocket("localhost", scf.getPort());
socket.getOutputStream().write(shortMessage.getBytes());

View File

@@ -886,17 +886,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.
@@ -904,18 +907,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]
@@ -931,8 +935,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.
@@ -944,7 +948,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.
@@ -954,6 +958,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
@@ -1116,6 +1182,11 @@ Defaults to true.
| Y
|
| See <<ssl-tls>>
| nio-connection-support
| Y
| Y
|
| See <<advanced-techniques>>
| read-delay
| Y
| Y