Fixes #3787 - Jetty client throws EOFException instead of SSLHandshakeException on certificate errors.
Now exceptions thrown by fill() or flush() are stored in a field. Further fill() operations will rethrow the original exception rather than returning -1. Returning -1 to application was causing them to close() with a generic failure that was triggering the EOFException reported in this issue. Now applications see the original exception and can close() with the proper cause. Re-enabled HostnameVerificationTest that was reproducing this issue reliably but was @Disabled a while back and never re-enabled. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
d12df30df7
commit
2c75e876a3
|
@ -22,7 +22,6 @@ import java.io.IOException;
|
|||
import java.security.cert.CertificateException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
@ -36,7 +35,6 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
|||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -47,7 +45,6 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||
* This test class runs tests to make sure that hostname verification (http://www.ietf.org/rfc/rfc2818.txt
|
||||
* section 3.1) is configurable in SslContextFactory and works as expected.
|
||||
*/
|
||||
@Disabled
|
||||
public class HostnameVerificationTest
|
||||
{
|
||||
private SslContextFactory clientSslContextFactory = new SslContextFactory.Client();
|
||||
|
@ -70,7 +67,7 @@ public class HostnameVerificationTest
|
|||
server.setHandler(new DefaultHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
response.getWriter().write("foobar");
|
||||
|
@ -94,30 +91,33 @@ public class HostnameVerificationTest
|
|||
{
|
||||
client.stop();
|
||||
server.stop();
|
||||
server.join();
|
||||
}
|
||||
|
||||
/**
|
||||
* This test is supposed to verify that hostname verification works as described in:
|
||||
* http://www.ietf.org/rfc/rfc2818.txt section 3.1. It uses a certificate with a common name different to localhost
|
||||
* and sends a request to localhost. This should fail with an SSLHandshakeException.
|
||||
*
|
||||
* @throws Exception on test failure
|
||||
*/
|
||||
@Test
|
||||
public void simpleGetWithHostnameVerificationEnabledTest() throws Exception
|
||||
public void simpleGetWithHostnameVerificationEnabledTest()
|
||||
{
|
||||
clientSslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
|
||||
String uri = "https://localhost:" + connector.getLocalPort() + "/";
|
||||
|
||||
ExecutionException x = assertThrows(ExecutionException.class, () ->
|
||||
{
|
||||
client.GET(uri);
|
||||
});
|
||||
ExecutionException x = assertThrows(ExecutionException.class, () -> client.GET(uri));
|
||||
|
||||
Throwable cause = x.getCause();
|
||||
assertThat(cause, Matchers.instanceOf(SSLHandshakeException.class));
|
||||
Throwable root = cause.getCause().getCause();
|
||||
assertThat(root, Matchers.instanceOf(CertificateException.class));
|
||||
|
||||
// Search for the CertificateException.
|
||||
Throwable certificateException = cause.getCause();
|
||||
while (certificateException != null)
|
||||
{
|
||||
if (certificateException instanceof CertificateException)
|
||||
break;
|
||||
certificateException = certificateException.getCause();
|
||||
}
|
||||
assertThat(certificateException, Matchers.instanceOf(CertificateException.class));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -146,7 +146,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return getDecryptedEndPoint().getFillInterest().getCallbackInvocationType();
|
||||
return _decryptedEndPoint.getFillInterest().getCallbackInvocationType();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -364,6 +364,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
public class DecryptedEndPoint extends AbstractEndPoint
|
||||
{
|
||||
private final Callback _incompleteWriteCallback = new IncompleteWriteCallback();
|
||||
private Throwable _failure;
|
||||
|
||||
public DecryptedEndPoint()
|
||||
{
|
||||
|
@ -529,12 +530,12 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
case NEED_WRAP:
|
||||
if (_flushState == FlushState.IDLE && flush(BufferUtil.EMPTY_BUFFER))
|
||||
{
|
||||
rethrow(_failure);
|
||||
if (_sslEngine.isInboundDone())
|
||||
// TODO this is probably a JVM bug, work around it by -1
|
||||
return -1;
|
||||
return filled = -1;
|
||||
continue;
|
||||
}
|
||||
// handle in needsFillInterest
|
||||
// Handle in needsFillInterest().
|
||||
return filled = 0;
|
||||
|
||||
default:
|
||||
|
@ -598,6 +599,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
switch (unwrap)
|
||||
{
|
||||
case CLOSED:
|
||||
rethrow(_failure);
|
||||
return filled = -1;
|
||||
|
||||
case BUFFER_UNDERFLOW:
|
||||
|
@ -607,6 +609,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
if (netFilled < 0 && _sslEngine.getUseClientMode())
|
||||
{
|
||||
closeInbound();
|
||||
if (_flushState == FlushState.WAIT_FOR_FILL)
|
||||
throw new SSLHandshakeException("Abruptly closed by peer");
|
||||
return filled = -1;
|
||||
}
|
||||
return filled = netFilled;
|
||||
|
@ -639,15 +643,14 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
handshakeFailed(x);
|
||||
|
||||
Throwable failure = handleException(x, "fill");
|
||||
handshakeFailed(failure);
|
||||
if (_flushState == FlushState.WAIT_FOR_FILL)
|
||||
{
|
||||
_flushState = FlushState.IDLE;
|
||||
getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().onFail(x));
|
||||
getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().onFail(failure));
|
||||
}
|
||||
|
||||
throw x;
|
||||
throw failure;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -676,10 +679,9 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(SslConnection.this.toString(), x);
|
||||
close(x);
|
||||
throw x;
|
||||
rethrow(x);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -694,15 +696,18 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
synchronized (_decryptedEndPoint)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug(">needFillInterest uf={} {}", _underflown, SslConnection.this);
|
||||
LOG.debug("ei={} di={}", BufferUtil.toDetailString(_encryptedInput), BufferUtil.toDetailString(_decryptedInput));
|
||||
}
|
||||
LOG.debug(">needFillInterest s={}/{} uf={} ei={} di={} {}",
|
||||
_flushState,
|
||||
_fillState,
|
||||
_underflown,
|
||||
BufferUtil.toDetailString(_encryptedInput),
|
||||
BufferUtil.toDetailString(_decryptedInput),
|
||||
SslConnection.this);
|
||||
|
||||
if (_fillState != FillState.IDLE)
|
||||
return;
|
||||
|
||||
// Fillable if we have decrypted Input OR encrypted input that has not yet been underflown.
|
||||
// Fillable if we have decrypted input OR enough encrypted input.
|
||||
fillable = BufferUtil.hasContent(_decryptedInput) || (BufferUtil.hasContent(_encryptedInput) && !_underflown);
|
||||
|
||||
HandshakeStatus status = _sslEngine.getHandshakeStatus();
|
||||
|
@ -966,8 +971,9 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
handshakeFailed(x);
|
||||
throw x;
|
||||
Throwable failure = handleException(x, "flush");
|
||||
handshakeFailed(failure);
|
||||
throw failure;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -979,10 +985,9 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(SslConnection.this.toString(), x);
|
||||
close(x);
|
||||
throw x;
|
||||
rethrow(x);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1092,7 +1097,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
boolean ishut = endp.isInputShutdown();
|
||||
boolean oshut = endp.isOutputShutdown();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("shutdownOutput: {} oshut={}, ishut={} {}", SslConnection.this, oshut, ishut);
|
||||
LOG.debug("shutdownOutput: {} oshut={}, ishut={}", SslConnection.this, oshut, ishut);
|
||||
|
||||
closeOutbound();
|
||||
|
||||
|
@ -1110,15 +1115,11 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
{
|
||||
if (!flush(BufferUtil.EMPTY_BUFFER) && !close)
|
||||
{
|
||||
Thread.yield();
|
||||
// if we still can't flush, but we are not closing the endpoint,
|
||||
// If we still can't flush, but we are not closing the endpoint,
|
||||
// let's just flush the encrypted output in the background.
|
||||
// and continue as if we are closed. The assumption here is that
|
||||
// the encrypted buffer will contain the entire close handshake
|
||||
// and that a call to flush(EMPTY_BUFFER) is not needed.
|
||||
endp.write(Callback.from(() ->
|
||||
{
|
||||
}, t -> endp.close()), _encryptedOutput);
|
||||
ByteBuffer write = _encryptedOutput;
|
||||
if (BufferUtil.hasContent(write))
|
||||
endp.write(Callback.from(Callback.NOOP::succeeded, t -> endp.close()), write);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1282,6 +1283,39 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
|
|||
return TLS_1_3.equals(protocol);
|
||||
}
|
||||
|
||||
private Throwable handleException(Throwable x, String context)
|
||||
{
|
||||
synchronized (_decryptedEndPoint)
|
||||
{
|
||||
if (_failure == null)
|
||||
{
|
||||
_failure = x;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(this + " stored " + context + " exception", x);
|
||||
}
|
||||
else if (x != _failure)
|
||||
{
|
||||
_failure.addSuppressed(x);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(this + " suppressed " + context + " exception", x);
|
||||
}
|
||||
return _failure;
|
||||
}
|
||||
}
|
||||
|
||||
private void rethrow(Throwable x) throws IOException
|
||||
{
|
||||
if (x == null)
|
||||
return;
|
||||
if (x instanceof RuntimeException)
|
||||
throw (RuntimeException)x;
|
||||
if (x instanceof Error)
|
||||
throw (Error)x;
|
||||
if (x instanceof IOException)
|
||||
throw (IOException)x;
|
||||
throw new IOException(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue