diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java index 648ec687a26..60662741ad8 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java @@ -83,6 +83,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory { SslConnection sslConnection = (SslConnection)connection; sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit()); ContainerLifeCycle connector = (ContainerLifeCycle)context.get(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY); connector.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index 264785be7a1..632265c8172 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -90,6 +90,7 @@ public class SslConnection extends AbstractConnection private final boolean _encryptedDirectBuffers = true; private final boolean _decryptedDirectBuffers = false; private boolean _renegotiationAllowed; + private int _renegotiationLimit = -1; private boolean _closedOutbound; private final Runnable _runCompletWrite = new Runnable() { @@ -170,7 +171,26 @@ public class SslConnection extends AbstractConnection public void setRenegotiationAllowed(boolean renegotiationAllowed) { - this._renegotiationAllowed = renegotiationAllowed; + _renegotiationAllowed = renegotiationAllowed; + } + + /** + * @return The number of renegotions allowed for this connection. When the limit + * is 0 renegotiation will be denied. If the limit is <0 then no limit is applied. + */ + public int getRenegotiationLimit() + { + return _renegotiationLimit; + } + + /** + * @param renegotiationLimit The number of renegotions allowed for this connection. + * When the limit is 0 renegotiation will be denied. If the limit is <0 then no limit is applied. + * Default -1. + */ + public void setRenegotiationLimit(int renegotiationLimit) + { + _renegotiationLimit = renegotiationLimit; } @Override @@ -323,7 +343,7 @@ public class SslConnection extends AbstractConnection synchronized (DecryptedEndPoint.this) { if (LOG.isDebugEnabled()) - LOG.debug("{} write failed", SslConnection.this, x); + LOG.debug("write failed {}", SslConnection.this, x); BufferUtil.clear(_encryptedOutput); releaseEncryptedOutputBuffer(); @@ -567,8 +587,8 @@ public class SslConnection extends AbstractConnection } if (LOG.isDebugEnabled()) { - LOG.debug("{} net={} unwrap {}", SslConnection.this, net_filled, unwrapResult.toString().replace('\n',' ')); - LOG.debug("{} filled {}",SslConnection.this,BufferUtil.toHexSummary(buffer)); + LOG.debug("net={} unwrap {} {}", net_filled, unwrapResult.toString().replace('\n',' '), SslConnection.this); + LOG.debug("filled {} {}",BufferUtil.toHexSummary(buffer), SslConnection.this); } HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); @@ -626,25 +646,13 @@ public class SslConnection extends AbstractConnection case BUFFER_UNDERFLOW: case OK: { - if (unwrapHandshakeStatus == HandshakeStatus.FINISHED && !_handshaken) - { - _handshaken = true; - if (LOG.isDebugEnabled()) - LOG.debug("{} {} handshake succeeded {}/{}", SslConnection.this, - _sslEngine.getUseClientMode() ? "client" : "resumed server", - _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite()); - notifyHandshakeSucceeded(_sslEngine); - } + if (unwrapHandshakeStatus == HandshakeStatus.FINISHED) + handshakeFinished(); - // Check whether renegotiation is allowed - if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed()) - { - if (LOG.isDebugEnabled()) - LOG.debug("{} renegotiation denied", SslConnection.this); - closeInbound(); + // Check whether re-negotiation is allowed + if (!allowRenegotiate(handshakeStatus)) return -1; - } - + // If bytes were produced, don't bother with the handshake status; // pass the decrypted data to the application, which will perform // another call to fill() or flush(). @@ -762,6 +770,51 @@ public class SslConnection extends AbstractConnection } } + private void handshakeFinished() + { + if (_handshaken) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiated {}", SslConnection.this); + if (_renegotiationLimit>0) + _renegotiationLimit--; + } + else + { + _handshaken = true; + if (LOG.isDebugEnabled()) + LOG.debug("{} handshake succeeded {}/{} {}", + _sslEngine.getUseClientMode() ? "client" : "resumed server", + _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite(), + SslConnection.this); + notifyHandshakeSucceeded(_sslEngine); + } + } + + private boolean allowRenegotiate(HandshakeStatus handshakeStatus) + { + if (!_handshaken || handshakeStatus == HandshakeStatus.NOT_HANDSHAKING) + return true; + + if (!isRenegotiationAllowed()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiation denied {}", SslConnection.this); + closeInbound(); + return false; + } + + if (_renegotiationLimit==0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiation limit exceeded {}", SslConnection.this); + closeInbound(); + return false; + } + + return true; + } + private void closeInbound() { try @@ -787,7 +840,7 @@ public class SslConnection extends AbstractConnection if (LOG.isDebugEnabled()) { for (ByteBuffer b : appOuts) - LOG.debug("{} flush {}", SslConnection.this, BufferUtil.toHexSummary(b)); + LOG.debug("flush {} {}", BufferUtil.toHexSummary(b), SslConnection.this); } try @@ -822,7 +875,7 @@ public class SslConnection extends AbstractConnection BufferUtil.flipToFlush(_encryptedOutput, pos); } if (LOG.isDebugEnabled()) - LOG.debug("{} wrap {}", SslConnection.this, wrapResult.toString().replace('\n',' ')); + LOG.debug("wrap {} {}", wrapResult.toString().replace('\n',' '), SslConnection.this); Status wrapResultStatus = wrapResult.getStatus(); @@ -863,29 +916,20 @@ public class SslConnection extends AbstractConnection default: { if (LOG.isDebugEnabled()) - LOG.debug("{} wrap {} {}", SslConnection.this, wrapResultStatus, BufferUtil.toHexSummary(_encryptedOutput)); + LOG.debug("wrap {} {} {}", wrapResultStatus, BufferUtil.toHexSummary(_encryptedOutput), SslConnection.this); - if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED && !_handshaken) - { - _handshaken = true; - if (LOG.isDebugEnabled()) - LOG.debug("{} {} handshake succeeded {}/{}", SslConnection.this, - _sslEngine.getUseClientMode() ? "resumed client" : "server", - _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite()); - notifyHandshakeSucceeded(_sslEngine); - } + if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) + handshakeFinished(); HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); - // Check whether renegotiation is allowed - if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed()) + // Check whether re-negotiation is allowed + if (!allowRenegotiate(handshakeStatus)) { - if (LOG.isDebugEnabled()) - LOG.debug("{} renegotiation denied", SslConnection.this); getEndPoint().shutdownOutput(); return allConsumed; } - + // if we have net bytes, let's try to flush them if (BufferUtil.hasContent(_encryptedOutput)) if (!getEndPoint().flush(_encryptedOutput)) @@ -975,7 +1019,7 @@ public class SslConnection extends AbstractConnection boolean ishut = isInputShutdown(); boolean oshut = isOutputShutdown(); if (LOG.isDebugEnabled()) - LOG.debug("{} shutdownOutput: oshut={}, ishut={}", SslConnection.this, oshut, ishut); + LOG.debug("shutdownOutput: oshut={}, ishut={} {}", oshut, ishut, SslConnection.this); if (oshut) return; diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java index 0ad8cec2fa4..acc9b944b74 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java @@ -77,6 +77,7 @@ public class SelectChannelEndPointSslTest extends SelectChannelEndPointTest engine.setUseClientMode(false); SslConnection sslConnection = new SslConnection(__byteBufferPool, _threadPool, endpoint, engine); sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(__sslCtxFactory.getRenegotiationLimit()); Connection appConnection = super.newConnection(channel,sslConnection.getDecryptedEndPoint()); sslConnection.getDecryptedEndPoint().setConnection(appConnection); return sslConnection; diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java index 98768f441bc..5bcb14db76c 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import org.eclipse.jetty.io.ssl.SslConnection; @@ -80,6 +81,7 @@ public class SslConnectionTest engine.setUseClientMode(false); SslConnection sslConnection = new SslConnection(__byteBufferPool, getExecutor(), endpoint, engine); sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(__sslCtxFactory.getRenegotiationLimit()); Connection appConnection = new TestConnection(sslConnection.getDecryptedEndPoint()); sslConnection.getDecryptedEndPoint().setConnection(appConnection); return sslConnection; @@ -149,6 +151,8 @@ public class SslConnectionTest _threadPool.start(); _scheduler.start(); _manager.start(); + __sslCtxFactory.setRenegotiationAllowed(true); + __sslCtxFactory.setRenegotiationLimit(-1); } @@ -248,7 +252,7 @@ public class SslConnectionTest } } } - protected Socket newClient() throws IOException + protected SSLSocket newClient() throws IOException { SSLSocket socket = __sslCtxFactory.newSslSocket(); socket.connect(_connector.socket().getLocalSocketAddress()); @@ -280,6 +284,112 @@ public class SslConnectionTest client.close(); } + @Test + public void testRenegotiate() throws Exception + { + SSLSocket client = newClient(); + client.setSoTimeout(60000); + + SocketChannel server = _connector.accept(); + server.configureBlocking(false); + _manager.accept(server); + + client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8)); + byte[] buffer = new byte[1024]; + int len=client.getInputStream().read(buffer); + Assert.assertEquals(5, len); + Assert.assertEquals("Hello",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.startHandshake(); + + client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8)); + len=client.getInputStream().read(buffer); + Assert.assertEquals(5, len); + Assert.assertEquals("World",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.close(); + } + + @Test + public void testRenegotiateNotAllowed() throws Exception + { + __sslCtxFactory.setRenegotiationAllowed(false); + + SSLSocket client = newClient(); + client.setSoTimeout(60000); + + SocketChannel server = _connector.accept(); + server.configureBlocking(false); + _manager.accept(server); + + client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8)); + byte[] buffer = new byte[1024]; + int len=client.getInputStream().read(buffer); + Assert.assertEquals(5, len); + Assert.assertEquals("Hello",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.startHandshake(); + + client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8)); + try + { + client.getInputStream().read(buffer); + Assert.fail(); + } + catch(SSLException e) + { + // expected + } + } + + @Test + public void testRenegotiateLimit() throws Exception + { + __sslCtxFactory.setRenegotiationAllowed(true); + __sslCtxFactory.setRenegotiationLimit(2); + + SSLSocket client = newClient(); + client.setSoTimeout(60000); + + SocketChannel server = _connector.accept(); + server.configureBlocking(false); + _manager.accept(server); + + client.getOutputStream().write("Good".getBytes(StandardCharsets.UTF_8)); + byte[] buffer = new byte[1024]; + int len=client.getInputStream().read(buffer); + Assert.assertEquals(4, len); + Assert.assertEquals("Good",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.startHandshake(); + + client.getOutputStream().write("Bye".getBytes(StandardCharsets.UTF_8)); + len=client.getInputStream().read(buffer); + Assert.assertEquals(3, len); + Assert.assertEquals("Bye",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.startHandshake(); + + client.getOutputStream().write("Cruel".getBytes(StandardCharsets.UTF_8)); + len=client.getInputStream().read(buffer); + Assert.assertEquals(5, len); + Assert.assertEquals("Cruel",new String(buffer,0,len,StandardCharsets.UTF_8)); + + client.startHandshake(); + + client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8)); + try + { + client.getInputStream().read(buffer); + Assert.fail(); + } + catch(SSLException e) + { + // expected + } + } + + @Test public void testWriteOnConnect() throws Exception diff --git a/jetty-server/src/main/config/etc/jetty-ssl-context.xml b/jetty-server/src/main/config/etc/jetty-ssl-context.xml index 1b266d1c744..3e84a9e0e0c 100644 --- a/jetty-server/src/main/config/etc/jetty-ssl-context.xml +++ b/jetty-server/src/main/config/etc/jetty-ssl-context.xml @@ -26,4 +26,6 @@ + + diff --git a/jetty-server/src/main/config/modules/ssl.mod b/jetty-server/src/main/config/modules/ssl.mod index 75263c6c994..7638d2594cd 100644 --- a/jetty-server/src/main/config/modules/ssl.mod +++ b/jetty-server/src/main/config/modules/ssl.mod @@ -95,3 +95,7 @@ https://raw.githubusercontent.com/eclipse/jetty.project/master/jetty-server/src/ ## Set the timeout (in seconds) of the SslSession cache timeout # jetty.sslContext.sslSessionTimeout=-1 + +## Allow SSL renegotiation +# jetty.sslContext.renegotiationAllowed=true +# jetty.sslContext.renegotiationLimit=5 diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java index 7a8f0c66a9e..cdb63609022 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java @@ -87,6 +87,7 @@ public class SslConnectionFactory extends AbstractConnectionFactory SslConnection sslConnection = newSslConnection(connector, endPoint, engine); sslConnection.setRenegotiationAllowed(_sslContextFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(_sslContextFactory.getRenegotiationLimit()); configure(sslConnection, connector, endPoint); ConnectionFactory next = connector.getConnectionFactory(_nextProtocol); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 345482bdaf3..c2d7fa3961d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -166,6 +166,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable private String _endpointIdentificationAlgorithm = null; private boolean _trustAll; private boolean _renegotiationAllowed = true; + private int _renegotiationLimit = 5; private Factory _factory; /** @@ -930,6 +931,25 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable _renegotiationAllowed = renegotiationAllowed; } + /** + * @return The number of renegotions allowed for this connection. When the limit + * is 0 renegotiation will be denied. If the limit is <0 then no limit is applied. + */ + public int getRenegotiationLimit() + { + return _renegotiationLimit; + } + + /** + * @param renegotiationLimit The number of renegotions allowed for this connection. + * When the limit is 0 renegotiation will be denied. If the limit is <0 then no limit is applied. + * Default 5. + */ + public void setRenegotiationLimit(int renegotiationLimit) + { + _renegotiationLimit = renegotiationLimit; + } + /** * @return Path to file that contains Certificate Revocation List */ diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java index d6235aa3c0a..33981d9d267 100644 --- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java +++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java @@ -86,6 +86,7 @@ public class WebSocketClientSelectorManager extends SelectorManager SSLEngine engine = newSSLEngine(sslContextFactory,channel); SslConnection sslConnection = new SslConnection(bufferPool,getExecutor(),endPoint,engine); sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit()); EndPoint sslEndPoint = sslConnection.getDecryptedEndPoint(); Connection connection = newUpgradeConnection(channel,sslEndPoint,connectPromise);