diff --git a/jetty-websocket/pom.xml b/jetty-websocket/pom.xml index 0501bf303f6..4dd8817c856 100644 --- a/jetty-websocket/pom.xml +++ b/jetty-websocket/pom.xml @@ -14,7 +14,7 @@ websocket-core - + websocket-client websocket-server diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/frames/FrameBuilder.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/frames/FrameBuilder.java new file mode 100644 index 00000000000..01a7c81d7de --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/frames/FrameBuilder.java @@ -0,0 +1,234 @@ +package org.eclipse.jetty.websocket.frames; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.PolicyViolationException; +import org.eclipse.jetty.websocket.protocol.OpCode; + + +public class FrameBuilder +{ + public static FrameBuilder binaryFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.BINARY)); + } + + public static FrameBuilder closeFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.CLOSE)); + } + + public static FrameBuilder continuationFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.CONTINUATION)); + } + + public static FrameBuilder pingFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.PING)); + } + + public static FrameBuilder pongFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.PONG)); + } + + public static FrameBuilder textFrame() + { + return new FrameBuilder(new BaseFrame(OpCode.TEXT)); + } + + private BaseFrame frame; + + public FrameBuilder(BaseFrame frame) + { + this.frame = frame; + this.frame.setFin(true); // default + } + + public byte[] asByteArray() + { + return BufferUtil.toArray(asByteBuffer()); + } + + public ByteBuffer asByteBuffer() + { + ByteBuffer buffer = ByteBuffer.allocate(frame.getPayloadLength() + 32 ); + byte b; + + // Setup fin thru opcode + b = 0x00; + if (frame.isFin()) + { + b |= 0x80; // 1000_0000 + } + if (frame.isRsv1()) + { + b |= 0x40; // 0100_0000 + // TODO: extensions can negotiate this (somehow) + throw new PolicyViolationException("RSV1 not allowed to be set"); + } + if (frame.isRsv2()) + { + b |= 0x20; // 0010_0000 + // TODO: extensions can negotiate this (somehow) + throw new PolicyViolationException("RSV2 not allowed to be set"); + } + if (frame.isRsv3()) + { + b |= 0x10; + // TODO: extensions can negotiate this (somehow) + throw new PolicyViolationException("RSV3 not allowed to be set"); + } + + byte opcode = frame.getOpCode().getCode(); + + if (frame.isContinuation()) + { + // Continuations are not the same OPCODE + opcode = OpCode.CONTINUATION.getCode(); + } + + b |= opcode & 0x0F; + + buffer.put(b); + + // is masked + b = 0x00; + b |= (frame.isMasked()?0x80:0x00); + + // payload lengths + int payloadLength = frame.getPayloadLength(); + + /* + * if length is over 65535 then its a 7 + 64 bit length + */ + if (payloadLength > 0xFF_FF) + { + // we have a 64 bit length + b |= 0x7F; + buffer.put(b); // indicate 8 byte length + buffer.put((byte)0); // + buffer.put((byte)0); // anything over an + buffer.put((byte)0); // int is just + buffer.put((byte)0); // intsane! + buffer.put((byte)((payloadLength >> 24) & 0xFF)); + buffer.put((byte)((payloadLength >> 16) & 0xFF)); + buffer.put((byte)((payloadLength >> 8) & 0xFF)); + buffer.put((byte)(payloadLength & 0xFF)); + } + /* + * if payload is ge 126 we have a 7 + 16 bit length + */ + else if (payloadLength >= 0x7E) + { + b |= 0x7E; + buffer.put(b); // indicate 2 byte length + buffer.put((byte)(payloadLength >> 8)); + buffer.put((byte)(payloadLength & 0xFF)); + } + /* + * we have a 7 bit length + */ + else + { + b |= (payloadLength & 0x7F); + buffer.put(b); + } + + // masking key + if (frame.isMasked()) + { + // TODO: figure out maskgen + buffer.put(frame.getMask()); + } + + // now the payload itself + + // call back into masking check/method on this class? + + // remember the position + int positionPrePayload = buffer.position(); + + // generate payload + if (frame.getPayloadLength() > 0) + { + BufferUtil.put(frame.getPayload(),buffer); + } + + int positionPostPayload = buffer.position(); + + // mask it if needed + if (frame.isMasked()) + { + // move back to remembered position. + int size = positionPostPayload - positionPrePayload; + byte[] mask = frame.getMask(); + int pos; + for (int i = 0; i < size; i++) + { + pos = positionPrePayload + i; + // Mask each byte by its absolute position in the bytebuffer + buffer.put(pos,(byte)(buffer.get(pos) ^ mask[i % 4])); + } + } + + BufferUtil.flipToFlush(buffer,0); + + return buffer; + } + + public BaseFrame asFrame() + { + return frame; + } + + public FrameBuilder isFin( boolean fin ) + { + frame.setFin(fin); + + return this; + } + + public FrameBuilder isRsv1(boolean rsv1) + { + frame.setRsv1(rsv1); + + return this; + } + + public FrameBuilder isRsv2(boolean rsv2) + { + frame.setRsv2(rsv2); + + return this; + } + + public FrameBuilder isRsv3(boolean rsv3) + { + frame.setRsv3(rsv3); + + return this; + } + + public FrameBuilder withMask(byte[] mask) + { + frame.setMasked(true); + frame.setMask(mask); + + return this; + } + + public FrameBuilder withPayload(byte[] bytes) + { + frame.setPayload(bytes); + return this; + } + + public FrameBuilder withPayload(ByteBuffer payload) + { + frame.setPayload(payload); + return this; + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/frames/FrameBuilderTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/frames/FrameBuilderTest.java new file mode 100644 index 00000000000..f283ca18535 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/frames/FrameBuilderTest.java @@ -0,0 +1,58 @@ +package org.eclipse.jetty.websocket.frames; + +import org.junit.Assert; +import org.junit.Test; + +public class FrameBuilderTest +{ + public void testSimpleAsFrame() + { + PingFrame frame = (PingFrame)FrameBuilder.pingFrame().asFrame(); + + Assert.assertTrue(frame instanceof PingFrame); + } + + @Test + public void testSimpleInvalidCloseFrameBuilder() + { + byte[] actual = FrameBuilder.closeFrame().isFin(false).asByteArray(); + + byte[] expected = new byte[] + { (byte)0x08, (byte)0x00 }; + + Assert.assertArrayEquals(expected,actual); + } + + @Test + public void testSimpleInvalidPingFrameBuilder() + { + byte[] actual = FrameBuilder.pingFrame().isFin(false).asByteArray(); + + byte[] expected = new byte[] + { (byte)0x09, (byte)0x00 }; + + Assert.assertArrayEquals(expected,actual); + } + + @Test + public void testSimpleValidCloseFrame() + { + byte[] actual = FrameBuilder.closeFrame().asByteArray(); + + byte[] expected = new byte[] + { (byte)0x88, (byte)0x00 }; + + Assert.assertArrayEquals(expected,actual); + } + + @Test + public void testSimpleValidPingFrame() + { + byte[] actual = FrameBuilder.pingFrame().asByteArray(); + + byte[] expected = new byte[] + { (byte)0x89, (byte)0x00 }; + + Assert.assertArrayEquals(expected,actual); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455Test.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/generator/WebSocketGeneratorRFC6455Test.java similarity index 99% rename from jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455Test.java rename to jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/generator/WebSocketGeneratorRFC6455Test.java index 40d64dc9fb0..106b66264bb 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455Test.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/generator/WebSocketGeneratorRFC6455Test.java @@ -13,7 +13,7 @@ * * You may elect to redistribute this code under either of these licenses. *******************************************************************************/ -package org.eclipse.jetty.websocket; +package org.eclipse.jetty.websocket.generator; /** */ diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase5.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase5.java index b61175165f6..2b096f7c739 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase5.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase5.java @@ -13,13 +13,16 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.ByteBufferAssert; +import org.eclipse.jetty.websocket.protocol.OpCode; import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.eclipse.jetty.websocket.frames.BaseFrame; import org.eclipse.jetty.websocket.frames.CloseFrame; +import org.eclipse.jetty.websocket.frames.FrameBuilder; import org.eclipse.jetty.websocket.frames.PingFrame; +import org.eclipse.jetty.websocket.frames.PongFrame; import org.eclipse.jetty.websocket.frames.TextFrame; import org.eclipse.jetty.websocket.generator.FrameGenerator; -import org.eclipse.jetty.websocket.protocol.OpCode; import org.eclipse.jetty.websocket.server.SimpleServletServer; import org.eclipse.jetty.websocket.server.WebSocketServerFactory; import org.eclipse.jetty.websocket.server.WebSocketServlet; @@ -63,14 +66,14 @@ public class TestABCase5 } // echo the message back. - try - { - getConnection().write(message); - } - catch (IOException e) - { - e.printStackTrace(System.err); - } + //try + // { + // getConnection().write(message); + // } + //catch (IOException e) + // { + // e.printStackTrace(System.err); + // } } } @@ -128,10 +131,46 @@ public class TestABCase5 client.writeRaw(buf2); - // Read frame (hopefully text frame) + // Read frame Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); - CloseFrame closeFrame = (CloseFrame)frames.remove(); - Assert.assertThat("CloseFrame.status code",closeFrame.getStatusCode(),is(1002)); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be close frame", frame instanceof CloseFrame); + + Assert.assertThat("CloseFrame.status code",((CloseFrame)frame).getStatusCode(),is(1002)); + } + finally + { + client.close(); + } + } + + @Test + public void testCase5_1PingIn2PacketsWithBuilder() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + String fragment1 = "fragment1"; + ByteBuffer frame1 = FrameBuilder.pingFrame().isFin(false).withPayload(fragment1.getBytes()).asByteBuffer(); + + client.writeRaw(frame1); + + String fragment2 = "fragment2"; + ByteBuffer frame2 = FrameBuilder.pingFrame().withPayload(fragment2.getBytes()).asByteBuffer(); + client.writeRaw(frame2); + + // Read frame + Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be close frame", frame instanceof CloseFrame); + + Assert.assertThat("CloseFrame.status code",((CloseFrame)frame).getStatusCode(),is(1002)); } finally { @@ -178,10 +217,13 @@ public class TestABCase5 client.writeRaw(buf2); - // Read frame (hopefully text frame) + // Read frame Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); - CloseFrame closeFrame = (CloseFrame)frames.remove(); - Assert.assertThat("CloseFrame.status code",closeFrame.getStatusCode(),is(1002)); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be close frame", frame instanceof CloseFrame); + + Assert.assertThat("CloseFrame.status code",((CloseFrame)frame).getStatusCode(),is(1002)); } finally { @@ -190,9 +232,45 @@ public class TestABCase5 } + @Test + public void testCase5_2PongIn2PacketsWithBuilder() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + String fragment1 = "fragment1"; + + ByteBuffer frame1 = FrameBuilder.pongFrame().isFin(false).withPayload(fragment1.getBytes()).asByteBuffer(); + + client.writeRaw(frame1); + + String fragment2 = "fragment2"; + + ByteBuffer frame2 = FrameBuilder.continuationFrame().isFin(false).withPayload(fragment2.getBytes()).asByteBuffer(); + + client.writeRaw(frame2); + + // Read frame + Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be close frame", frame instanceof CloseFrame); + + Assert.assertThat("CloseFrame.status code",((CloseFrame)frame).getStatusCode(),is(1002)); + } + finally + { + client.close(); + } + } + @Test - @Ignore ("not re-assembling the strings yet on server side echo") + @Ignore ("not supported in implementation yet, requires server side message aggregation") public void testCase5_3TextIn2Packets() throws Exception { BlockheadClient client = new BlockheadClient(server.getServerUri()); @@ -231,10 +309,204 @@ public class TestABCase5 client.writeRaw(buf2); - // Read frame (hopefully text frame) + // Read frame Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); - TextFrame textFrame = (TextFrame)frames.remove(); - Assert.assertThat("TextFrame.payload",textFrame.getPayloadUTF8(),is(fragment1 + fragment2)); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be text frame", frame instanceof TextFrame); + + Assert.assertThat("TextFrame.payload",((TextFrame)frame).getPayloadUTF8(),is(fragment1 + fragment2)); + } + finally + { + client.close(); + } + } + + @Test + @Ignore ("not supported in implementation yet, requires server side message aggregation") + public void testCase5_6TextPingRemainingText() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + // Send a text packet + + ByteBuffer buf = ByteBuffer.allocate(FrameGenerator.OVERHEAD + 2); + BufferUtil.clearToFill(buf); + + String fragment1 = "fragment1"; + + buf.put((byte)(0x00 | OpCode.TEXT.getCode())); + + byte b = 0x00; // no masking + b |= fragment1.length() & 0x7F; + buf.put(b); + buf.put(fragment1.getBytes()); + BufferUtil.flipToFlush(buf,0); + + client.writeRaw(buf); + + // Send a ping with payload + + ByteBuffer pingBuf = ByteBuffer.allocate(FrameGenerator.OVERHEAD + 2); + BufferUtil.clearToFill(pingBuf); + + String pingPayload = "ping payload"; + + pingBuf.put((byte)(0x00 | OpCode.PING.getCode())); + + b = 0x00; // no masking + b |= pingPayload.length() & 0x7F; + pingBuf.put(b); + pingBuf.put(pingPayload.getBytes()); + BufferUtil.flipToFlush(pingBuf,0); + + client.writeRaw(buf); + + // Send remaining text as continuation + + ByteBuffer buf2 = ByteBuffer.allocate(FrameGenerator.OVERHEAD + 2); + BufferUtil.clearToFill(buf2); + + String fragment2 = "fragment2"; + + buf2.put((byte)(0x80 | OpCode.CONTINUATION.getCode())); + b = 0x00; // no masking + b |= fragment2.length() & 0x7F; + buf2.put(b); + buf2.put(fragment2.getBytes()); + BufferUtil.flipToFlush(buf2,0); + + client.writeRaw(buf2); + + // Should be 2 frames, pong frame followed by combined echo'd text frame + Queue frames = client.readFrames(2,TimeUnit.MILLISECONDS,500); + BaseFrame frame = frames.remove(); + + Assert.assertTrue("first frame should be pong frame", frame instanceof PongFrame ); + + ByteBuffer payload1 = ByteBuffer.allocate(pingPayload.length()); + payload1.flip(); + + ByteBufferAssert.assertEquals("payloads should be equal" , payload1, ((PongFrame)frame).getPayload() ); + + frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("second frame should be text frame", frame instanceof TextFrame ); + + + Assert.assertThat("TextFrame.payload",((TextFrame)frame).getPayloadUTF8(),is(fragment1 + fragment2)); + } + finally + { + client.close(); + } + } + + + @Test + @Ignore ("not supported in implementation yet, requires server side message aggregation") + public void testCase5_6TextPingRemainingTextWithBuilder() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + // Send a text packet + String textPayload1 = "fragment1"; + + ByteBuffer frame1 = FrameBuilder.textFrame().isFin(false).withPayload(textPayload1.getBytes()).asByteBuffer(); + BufferUtil.flipToFlush(frame1,0); + client.writeRaw(frame1); + + // Send a ping with payload + + String pingPayload = "ping payload"; + ByteBuffer frame2 = FrameBuilder.pingFrame().withPayload(pingPayload.getBytes()).asByteBuffer(); + BufferUtil.flipToFlush(frame2,0); + + client.writeRaw(frame2); + + // Send remaining text as continuation + String textPayload2 = "fragment2"; + + ByteBuffer frame3 = FrameBuilder.continuationFrame().withPayload(textPayload2.getBytes()).asByteBuffer(); + BufferUtil.flipToFlush(frame3,0); + + client.writeRaw(frame3); + + // Should be 2 frames, pong frame followed by combined echo'd text frame + Queue frames = client.readFrames(2,TimeUnit.MILLISECONDS,500); + BaseFrame frame = frames.remove(); + + Assert.assertTrue("first frame should be pong frame", frame instanceof PongFrame ); + + ByteBuffer payload1 = ByteBuffer.allocate(pingPayload.length()); + payload1.flip(); + + ByteBufferAssert.assertEquals("payloads should be equal" , payload1, ((PongFrame)frame).getPayload() ); + + frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("second frame should be text frame", frame instanceof TextFrame ); + + + Assert.assertThat("TextFrame.payload",((TextFrame)frame).getPayloadUTF8(),is(textPayload1 + textPayload2)); + } + finally + { + client.close(); + } + } + + @Test + @Ignore ("AB tests have chop concepts currently unsupported by test...I think, also the string being returns is not Bad Continuation") + public void testCase5_9BadContinuation() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + // Send a text packet + + ByteBuffer buf = ByteBuffer.allocate(FrameGenerator.OVERHEAD + 2); + BufferUtil.clearToFill(buf); + + String fragment1 = "fragment"; + + // continutation w / FIN + + buf.put((byte)(0x80 | OpCode.CONTINUATION.getCode())); + + byte b = 0x00; // no masking + b |= fragment1.length() & 0x7F; + buf.put(b); + buf.put(fragment1.getBytes()); + BufferUtil.flipToFlush(buf,0); + + client.writeRaw(buf); + + // Read frame + Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); + BaseFrame frame = (BaseFrame)frames.remove(); + + Assert.assertTrue("frame should be close frame", frame instanceof CloseFrame); + + Assert.assertThat("CloseFrame.status code",((CloseFrame)frame).getStatusCode(),is(1002)); + + Assert.assertThat("CloseFrame.reason", ((CloseFrame)frame).getReason(),is("Bad Continuation") ); // TODO put close reasons into public strings in impl someplace + } finally { diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_9.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_9.java index 8d1282c8608..4e867b827a4 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_9.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/TestABCase7_9.java @@ -16,6 +16,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.eclipse.jetty.websocket.frames.BaseFrame; import org.eclipse.jetty.websocket.frames.CloseFrame; +import org.eclipse.jetty.websocket.frames.FrameBuilder; import org.eclipse.jetty.websocket.generator.FrameGenerator; import org.eclipse.jetty.websocket.protocol.OpCode; import org.eclipse.jetty.websocket.server.SimpleServletServer; @@ -168,4 +169,46 @@ public class TestABCase7_9 } } + /** + * Test the requirement of issuing + */ + @Test + public void testCase7_9_XInvalidCloseStatusCodesWithBuilder() throws Exception + { + BlockheadClient client = new BlockheadClient(server.getServerUri()); + try + { + client.connect(); + client.sendStandardRequest(); + client.expectUpgradeResponse(); + + ByteBuffer frame = FrameBuilder.closeFrame().withMask(new byte[] + { 0x44, 0x44, 0x44, 0x44 }).asByteBuffer(); + + ByteBuffer buf = ByteBuffer.allocate(FrameGenerator.OVERHEAD + 2); + BufferUtil.clearToFill(buf); + + // Create Close Frame manually, as we are testing the server's behavior of a bad client. + buf.put((byte)(0x80 | OpCode.CLOSE.getCode())); + buf.put((byte)(0x80 | 2)); + byte mask[] = new byte[] + { 0x44, 0x44, 0x44, 0x44 }; + buf.put(mask); + int position = buf.position(); + buf.putChar((char)this.invalidStatusCode); + remask(buf,position,mask); + BufferUtil.flipToFlush(buf,0); + client.writeRaw(buf); + + // Read frame (hopefully text frame) + Queue frames = client.readFrames(1,TimeUnit.MILLISECONDS,500); + CloseFrame closeFrame = (CloseFrame)frames.remove(); + Assert.assertThat("CloseFrame.status code",closeFrame.getStatusCode(),is(1002)); + } + finally + { + client.close(); + } + } + }