| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.websocket.common; |
| |
| import java.nio.ByteBuffer; |
| import java.util.Arrays; |
| |
| import org.eclipse.jetty.util.BufferUtil; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| import org.eclipse.jetty.websocket.api.StatusCode; |
| import org.eclipse.jetty.websocket.api.WebSocketPolicy; |
| import org.eclipse.jetty.websocket.api.extensions.Frame; |
| import org.eclipse.jetty.websocket.common.frames.BinaryFrame; |
| import org.eclipse.jetty.websocket.common.frames.CloseFrame; |
| import org.eclipse.jetty.websocket.common.frames.PingFrame; |
| import org.eclipse.jetty.websocket.common.frames.TextFrame; |
| import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture; |
| import org.eclipse.jetty.websocket.common.test.UnitGenerator; |
| import org.eclipse.jetty.websocket.common.test.UnitParser; |
| import org.eclipse.jetty.websocket.common.util.Hex; |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import static org.hamcrest.Matchers.is; |
| |
| public class GeneratorTest |
| { |
| private static final Logger LOG = Log.getLogger(GeneratorTest.WindowHelper.class); |
| |
| public static class WindowHelper |
| { |
| final int windowSize; |
| int totalParts; |
| int totalBytes; |
| |
| public WindowHelper(int windowSize) |
| { |
| this.windowSize = windowSize; |
| this.totalParts = 0; |
| this.totalBytes = 0; |
| } |
| |
| public ByteBuffer generateWindowed(Frame... frames) |
| { |
| // Create Buffer to hold all generated frames in a single buffer |
| int completeBufSize = 0; |
| for (Frame f : frames) |
| { |
| completeBufSize += Generator.MAX_HEADER_LENGTH + f.getPayloadLength(); |
| } |
| |
| ByteBuffer completeBuf = ByteBuffer.allocate(completeBufSize); |
| BufferUtil.clearToFill(completeBuf); |
| |
| // Generate from all frames |
| Generator generator = new UnitGenerator(); |
| |
| for (Frame f : frames) |
| { |
| ByteBuffer header = generator.generateHeaderBytes(f); |
| totalBytes += BufferUtil.put(header,completeBuf); |
| |
| if (f.hasPayload()) |
| { |
| ByteBuffer payload=f.getPayload(); |
| totalBytes += payload.remaining(); |
| totalParts++; |
| completeBuf.put(payload.slice()); |
| } |
| } |
| |
| // Return results |
| BufferUtil.flipToFlush(completeBuf,0); |
| return completeBuf; |
| } |
| |
| public void assertTotalParts(int expectedParts) |
| { |
| Assert.assertThat("Generated Parts",totalParts,is(expectedParts)); |
| } |
| |
| public void assertTotalBytes(int expectedBytes) |
| { |
| Assert.assertThat("Generated Bytes",totalBytes,is(expectedBytes)); |
| } |
| } |
| |
| private void assertGeneratedBytes(CharSequence expectedBytes, Frame... frames) |
| { |
| // collect up all frames as single ByteBuffer |
| ByteBuffer allframes = UnitGenerator.generate(frames); |
| // Get hex String form of all frames bytebuffer. |
| String actual = Hex.asHex(allframes); |
| // Validate |
| Assert.assertThat("Buffer",actual,is(expectedBytes.toString())); |
| } |
| |
| private String asMaskedHex(String str, byte[] maskingKey) |
| { |
| byte utf[] = StringUtil.getUtf8Bytes(str); |
| mask(utf,maskingKey); |
| return Hex.asHex(utf); |
| } |
| |
| private void mask(byte[] buf, byte[] maskingKey) |
| { |
| int size = buf.length; |
| for (int i = 0; i < size; i++) |
| { |
| buf[i] ^= maskingKey[i % 4]; |
| } |
| } |
| |
| @Test |
| public void testClose_Empty() |
| { |
| // 0 byte payload (no status code) |
| assertGeneratedBytes("8800",new CloseFrame()); |
| } |
| |
| @Test |
| public void testClose_CodeNoReason() |
| { |
| CloseInfo close = new CloseInfo(StatusCode.NORMAL); |
| // 2 byte payload (2 bytes for status code) |
| assertGeneratedBytes("880203E8",close.asFrame()); |
| } |
| |
| @Test |
| public void testClose_CodeOkReason() |
| { |
| CloseInfo close = new CloseInfo(StatusCode.NORMAL,"OK"); |
| // 4 byte payload (2 bytes for status code, 2 more for "OK") |
| assertGeneratedBytes("880403E84F4B",close.asFrame()); |
| } |
| |
| @Test |
| public void testText_Hello() |
| { |
| WebSocketFrame frame = new TextFrame().setPayload("Hello"); |
| byte utf[] = StringUtil.getUtf8Bytes("Hello"); |
| assertGeneratedBytes("8105" + Hex.asHex(utf),frame); |
| } |
| |
| @Test |
| public void testText_Masked() |
| { |
| WebSocketFrame frame = new TextFrame().setPayload("Hello"); |
| byte maskingKey[] = Hex.asByteArray("11223344"); |
| frame.setMask(maskingKey); |
| |
| // what is expected |
| StringBuilder expected = new StringBuilder(); |
| expected.append("8185").append("11223344"); |
| expected.append(asMaskedHex("Hello",maskingKey)); |
| |
| // validate |
| assertGeneratedBytes(expected,frame); |
| } |
| |
| @Test |
| public void testText_Masked_OffsetSourceByteBuffer() |
| { |
| ByteBuffer payload = ByteBuffer.allocate(100); |
| payload.position(5); |
| payload.put(StringUtil.getUtf8Bytes("Hello")); |
| payload.flip(); |
| payload.position(5); |
| // at this point, we have a ByteBuffer of 100 bytes. |
| // but only a few bytes in the middle are made available for the payload. |
| // we are testing that masking works as intended, even if the provided |
| // payload does not start at position 0. |
| LOG.debug("Payload = {}",BufferUtil.toDetailString(payload)); |
| WebSocketFrame frame = new TextFrame().setPayload(payload); |
| byte maskingKey[] = Hex.asByteArray("11223344"); |
| frame.setMask(maskingKey); |
| |
| // what is expected |
| StringBuilder expected = new StringBuilder(); |
| expected.append("8185").append("11223344"); |
| expected.append(asMaskedHex("Hello",maskingKey)); |
| |
| // validate |
| assertGeneratedBytes(expected,frame); |
| } |
| |
| /** |
| * Prevent regression of masking of many packets. |
| */ |
| @Test |
| public void testManyMasked() |
| { |
| int pingCount = 2; |
| |
| // Prepare frames |
| WebSocketFrame[] frames = new WebSocketFrame[pingCount + 1]; |
| for (int i = 0; i < pingCount; i++) |
| { |
| frames[i] = new PingFrame().setPayload(String.format("ping-%d",i)); |
| } |
| frames[pingCount] = new CloseInfo(StatusCode.NORMAL).asFrame(); |
| |
| // Mask All Frames |
| byte maskingKey[] = Hex.asByteArray("11223344"); |
| for (WebSocketFrame f : frames) |
| { |
| f.setMask(maskingKey); |
| } |
| |
| // Validate result of generation |
| StringBuilder expected = new StringBuilder(); |
| expected.append("8986").append("11223344"); |
| expected.append(asMaskedHex("ping-0",maskingKey)); // ping 0 |
| expected.append("8986").append("11223344"); |
| expected.append(asMaskedHex("ping-1",maskingKey)); // ping 1 |
| expected.append("8882").append("11223344"); |
| byte closure[] = Hex.asByteArray("03E8"); |
| mask(closure,maskingKey); |
| expected.append(Hex.asHex(closure)); // normal closure |
| |
| assertGeneratedBytes(expected,frames); |
| } |
| |
| /** |
| * Test the windowed generate of a frame that has no masking. |
| */ |
| @Test |
| public void testWindowedGenerate() |
| { |
| // A decent sized frame, no masking |
| byte payload[] = new byte[10240]; |
| Arrays.fill(payload,(byte)0x44); |
| |
| WebSocketFrame frame = new BinaryFrame().setPayload(payload); |
| |
| // Generate |
| int windowSize = 1024; |
| WindowHelper helper = new WindowHelper(windowSize); |
| ByteBuffer completeBuffer = helper.generateWindowed(frame); |
| |
| // Validate |
| int expectedHeaderSize = 4; |
| int expectedSize = payload.length + expectedHeaderSize; |
| int expectedParts = 1; |
| |
| helper.assertTotalParts(expectedParts); |
| helper.assertTotalBytes(payload.length + expectedHeaderSize); |
| |
| Assert.assertThat("Generated Buffer",completeBuffer.remaining(),is(expectedSize)); |
| } |
| |
| @Test |
| public void testWindowedGenerateWithMasking() |
| { |
| // A decent sized frame, with masking |
| byte payload[] = new byte[10240]; |
| Arrays.fill(payload,(byte)0x55); |
| |
| byte mask[] = new byte[] |
| { 0x2A, (byte)0xF0, 0x0F, 0x00 }; |
| |
| WebSocketFrame frame = new BinaryFrame().setPayload(payload); |
| frame.setMask(mask); // masking! |
| |
| // Generate |
| int windowSize = 2929; |
| WindowHelper helper = new WindowHelper(windowSize); |
| ByteBuffer completeBuffer = helper.generateWindowed(frame); |
| |
| // Validate |
| int expectedHeaderSize = 8; |
| int expectedSize = payload.length + expectedHeaderSize; |
| int expectedParts = 1; |
| |
| helper.assertTotalParts(expectedParts); |
| helper.assertTotalBytes(payload.length + expectedHeaderSize); |
| |
| Assert.assertThat("Generated Buffer",completeBuffer.remaining(),is(expectedSize)); |
| |
| // Parse complete buffer. |
| WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); |
| Parser parser = new UnitParser(policy); |
| IncomingFramesCapture capture = new IncomingFramesCapture(); |
| parser.setIncomingFramesHandler(capture); |
| |
| parser.parse(completeBuffer); |
| |
| // Assert validity of frame |
| WebSocketFrame actual = capture.getFrames().poll(); |
| Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.BINARY)); |
| Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(payload.length)); |
| |
| // Validate payload contents for proper masking |
| ByteBuffer actualData = actual.getPayload().slice(); |
| Assert.assertThat("Frame.payload.remaining",actualData.remaining(),is(payload.length)); |
| while (actualData.remaining() > 0) |
| { |
| Assert.assertThat("Actual.payload[" + actualData.position() + "]",actualData.get(),is((byte)0x55)); |
| } |
| } |
| } |