diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java index cd65a196b41..1f472423580 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java +++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java @@ -105,7 +105,7 @@ public class TestOSGiUtil return CoreOptions.when(Boolean.getBoolean("pax.exam.debug.remote")) .useOptions(CoreOptions.vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005")); } - + public static List coreJettyDependencies() { List res = new ArrayList<>(); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java index a71d271ceb3..dc97224481a 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java @@ -31,6 +31,7 @@ import org.eclipse.jetty.websocket.core.BadPayloadException; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.core.ProtocolException; /** * Per Message Deflate Compression extension for WebSocket. @@ -73,9 +74,16 @@ public class PerMessageDeflateExtension extends CompressExtension return; } + if (frame.getOpCode() == OpCode.CONTINUATION && frame.isRsv1()) + { + // Per RFC7692 we MUST Fail the websocket connection + throw new ProtocolException("Invalid RSV1 set on permessage-deflate CONTINUATION frame"); + } + //TODO fix this to use long instead of int if (getWebSocketChannel().getMaxFrameSize() > Integer.MAX_VALUE) throw new IllegalArgumentException("maxFrameSize too large for ByteAccumulator"); + ByteAccumulator accumulator = new ByteAccumulator((int)getWebSocketChannel().getMaxFrameSize()); try diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java index c2d19eab1ad..8e10baa967c 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java @@ -18,6 +18,12 @@ package org.eclipse.jetty.websocket.core.extensions; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.jetty.toolchain.test.ByteBufferAssert; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -27,18 +33,14 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.IncomingFramesCapture; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.OutgoingFramesCapture; +import org.eclipse.jetty.websocket.core.ProtocolException; import org.eclipse.jetty.websocket.core.internal.compress.CompressExtension; import org.eclipse.jetty.websocket.core.internal.compress.PerMessageDeflateExtension; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Client side behavioral tests for permessage-deflate extension. @@ -219,6 +221,56 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest tester.assertHasFrames("Hello"); } + /** + * Decode fragmented message (3 parts: TEXT, CONTINUATION, CONTINUATION) + */ + @Test + public void testParseFragmentedMessage_Good() + { + ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + tester.parseIncomingHex(// 1 message, 3 frame + "410C", // HEADER TEXT / fin=false / rsv1=true + "F248CDC9C95700000000FFFF", + "000B", // HEADER CONTINUATION / fin=false / rsv1=false + "0ACF2FCA4901000000FFFF", + "8003", // HEADER CONTINUATION / fin=true / rsv1=false + "520400" + ); + + Frame txtFrame = new Frame(OpCode.TEXT).setPayload("Hello ").setFin(false); + Frame con1Frame = new Frame(OpCode.CONTINUATION).setPayload("World").setFin(false); + Frame con2Frame = new Frame(OpCode.CONTINUATION).setPayload("!").setFin(true); + + tester.assertHasFrames(txtFrame, con1Frame, con2Frame); + } + + /** + * Decode fragmented message (3 parts: TEXT, CONTINUATION, CONTINUATION) + * + * Continuation frames have RSV1 set, which MUST result in Failure + * + */ + @Test + public void testParseFragmentedMessage_BadRsv1() + { + ExtensionTool.Tester tester = clientExtensions.newTester("permessage-deflate"); + + tester.assertNegotiated("permessage-deflate"); + + assertThrows(ProtocolException.class, () -> + tester.parseIncomingHex(// 1 message, 3 frame + "410C", // Header TEXT / fin=false / rsv1=true + "F248CDC9C95700000000FFFF", // Payload + "400B", // Header CONTINUATION / fin=false / rsv1=true + "0ACF2FCA4901000000FFFF", // Payload + "C003", // Header CONTINUATION / fin=true / rsv1=true + "520400" // Payload + )); + } + /** * Incoming PING (Control Frame) should pass through extension unmodified */ @@ -255,6 +307,43 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest } /** + * Incoming Text Message fragmented into 3 pieces. + */ + @Test + public void testIncomingFragmented() + { + PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + ExtensionConfig config = ExtensionConfig.parse("permessage-deflate"); + ext.init(config, bufferPool); + + // Setup capture of incoming frames + IncomingFramesCapture capture = new IncomingFramesCapture(); + + // Wire up stack + ext.setNextIncomingFrames(capture); + + String payload = "Are you there?"; + Frame ping = new Frame(OpCode.PING).setPayload(payload); + ext.onFrame(ping, Callback.NOOP); + + capture.assertFrameCount(1); + capture.assertHasOpCount(OpCode.PING, 1); + Frame actual = capture.frames.poll(); + + assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.PING)); + assertThat("Frame.fin", actual.isFin(), is(true)); + assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + assertThat("Frame.rsv3", actual.isRsv3(), is(false)); + + ByteBuffer expected = BufferUtil.toBuffer(payload, StandardCharsets.UTF_8); + assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); + } + + /** + + /** * Verify that incoming uncompressed frames are properly passed through */ @Test
+ * Continuation frames have RSV1 set, which MUST result in Failure + *