From ac0e30bbb72d21dae6937ed3f6a82117f5e5f8b1 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Tue, 27 Aug 2013 17:33:16 +1000 Subject: [PATCH 1/2] 415745 Include followed by forward using a PrintWriter incurs unnecessary delay --- .../src/main/java/org/eclipse/jetty/server/Response.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 1dab467d790..feb32867453 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -175,6 +175,10 @@ public class Response implements HttpServletResponse public void included() { _include.decrementAndGet(); + if (_outputType == OutputType.WRITER) + { + _writer.reopen(); + } _out.reopen(); } @@ -811,6 +815,8 @@ public class Response implements HttpServletResponse { case WRITER: _writer.close(); + if (!_out.isClosed()) + _out.close(); break; case STREAM: getOutputStream().close(); From 20fbd95bf13d897c1f5d374d4153e994da66f6dc Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 27 Aug 2013 14:20:07 -0700 Subject: [PATCH 2/2] 395444 - Websockets not working with Chrome (deflate problem) + Hopefully final fix to deflate-frame + Splitting out extension named 'deflate-frame' (last spec'd standard) from 'x-webkit-deflate-frame' (standard in use by chrome + safari) --- .../compress/DeflateFrameExtension.java | 228 ++++++++++++++++++ .../compress/FrameCompressionExtension.java | 131 ---------- .../XWebkitDeflateFrameExtension.java | 38 +++ .../websocket/common/frames/DataFrame.java | 35 ++- .../common/io/WriteBytesProvider.java | 20 ++ ...e.jetty.websocket.api.extensions.Extension | 3 +- .../common/IncomingFramesCapture.java | 13 + .../websocket/common/extensions/AllTests.java | 4 +- .../extensions/DummyOutgoingFrames.java | 6 + .../compress/CapturedHexPayloads.java | 55 +++++ ...st.java => DeflateFrameExtensionTest.java} | 99 ++++++-- .../extensions/compress/DeflateTest.java | 82 +++++++ .../test/resources/jetty-logging.properties | 2 +- .../server/browser/BrowserDebugTool.java | 10 +- .../websocket/server/helper/EchoServlet.java | 4 +- 15 files changed, 552 insertions(+), 178 deletions(-) create mode 100644 jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java delete mode 100644 jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java create mode 100644 jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java create mode 100644 jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java rename jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/{FrameCompressionExtensionTest.java => DeflateFrameExtensionTest.java} (77%) create mode 100644 jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java new file mode 100644 index 00000000000..7823c2fb161 --- /dev/null +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java @@ -0,0 +1,228 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.extensions.compress; + +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +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.api.BadPayloadException; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.extensions.AbstractExtension; +import org.eclipse.jetty.websocket.common.frames.DataFrame; + +/** + * Implementation of the deflate-frame extension seen out in the + * wild. + */ +public class DeflateFrameExtension extends AbstractExtension +{ + private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true")); + private static final Logger LOG = Log.getLogger(DeflateFrameExtension.class); + + private static final int OVERHEAD = 64; + /** Tail Bytes per Spec */ + private static final byte[] TAIL = new byte[] + { 0x00, 0x00, (byte)0xFF, (byte)0xFF }; + private int bufferSize = 64 * 1024; + private Deflater compressor; + private Inflater decompressor; + + @Override + public String getName() + { + return "deflate-frame"; + } + + @Override + public synchronized void incomingFrame(Frame frame) + { + if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1()) + { + // Cannot modify incoming control frames or ones with RSV1 set. + nextIncomingFrame(frame); + return; + } + + if (!frame.hasPayload()) + { + // no payload? nothing to do. + nextIncomingFrame(frame); + return; + } + + // Prime the decompressor + ByteBuffer payload = frame.getPayload(); + int inlen = payload.remaining(); + byte compressed[] = new byte[inlen + TAIL.length]; + payload.get(compressed,0,inlen); + System.arraycopy(TAIL,0,compressed,inlen,TAIL.length); + decompressor.setInput(compressed,0,compressed.length); + + // Perform decompression + while (decompressor.getRemaining() > 0 && !decompressor.finished()) + { + DataFrame out = new DataFrame(frame); + out.setRsv1(false); // Unset RSV1 + byte outbuf[] = new byte[Math.min(inlen * 2,bufferSize)]; + try + { + int len = decompressor.inflate(outbuf); + if (len == 0) + { + if (decompressor.needsInput()) + { + throw new BadPayloadException("Unable to inflate frame, not enough input on frame"); + } + if (decompressor.needsDictionary()) + { + throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary"); + } + } + if (len > 0) + { + out.setPayload(ByteBuffer.wrap(outbuf,0,len)); + } + nextIncomingFrame(out); + } + catch (DataFormatException e) + { + LOG.warn(e); + throw new BadPayloadException(e); + } + } + } + + /** + * Indicates use of RSV1 flag for indicating deflation is in use. + *

+ * Also known as the "COMP" framing header bit + */ + @Override + public boolean isRsv1User() + { + return true; + } + + @Override + public synchronized void outgoingFrame(Frame frame, WriteCallback callback) + { + if (OpCode.isControlFrame(frame.getOpCode())) + { + // skip, cannot compress control frames. + nextOutgoingFrame(frame,callback); + return; + } + + if (!frame.hasPayload()) + { + // pass through, nothing to do + nextOutgoingFrame(frame,callback); + return; + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"", + BufferUtil.toDetailString(frame.getPayload())); + } + + // Prime the compressor + byte uncompressed[] = BufferUtil.toArray(frame.getPayload()); + + // Perform the compression + if (!compressor.finished()) + { + compressor.setInput(uncompressed,0,uncompressed.length); + byte compressed[] = new byte[uncompressed.length + OVERHEAD]; + + while (!compressor.needsInput()) + { + int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH); + ByteBuffer outbuf = getBufferPool().acquire(len,true); + BufferUtil.clearToFill(outbuf); + + if (len > 0) + { + outbuf.put(compressed,0,len - 4); + } + + BufferUtil.flipToFlush(outbuf,0); + + if (len > 0 && BFINAL_HACK) + { + /* + * Per the spec, it says that BFINAL 1 or 0 are allowed. + * + * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1. + * + * This hack will always set BFINAL 0 + */ + byte b0 = outbuf.get(0); + if ((b0 & 1) != 0) // if BFINAL 1 + { + outbuf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0 + } + } + + DataFrame out = new DataFrame(frame); + out.setRsv1(true); + out.setPooledBuffer(true); + out.setPayload(outbuf); + + if (!compressor.needsInput()) + { + // this is fragmented + out.setFin(false); + nextOutgoingFrame(out,null); // non final frames have no callback + } + else + { + // pass through the callback + nextOutgoingFrame(out,callback); + } + } + } + } + + @Override + public void setConfig(ExtensionConfig config) + { + super.setConfig(config); + + boolean nowrap = true; + compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap); + compressor.setStrategy(Deflater.DEFAULT_STRATEGY); + + decompressor = new Inflater(nowrap); + } + + @Override + public String toString() + { + return this.getClass().getSimpleName() + "[]"; + } +} \ No newline at end of file diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java deleted file mode 100644 index fefe1195576..00000000000 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java +++ /dev/null @@ -1,131 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2013 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.extensions.compress; - - -import java.nio.ByteBuffer; - -import org.eclipse.jetty.websocket.api.WriteCallback; -import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; -import org.eclipse.jetty.websocket.api.extensions.Frame; -import org.eclipse.jetty.websocket.common.OpCode; -import org.eclipse.jetty.websocket.common.extensions.AbstractExtension; -import org.eclipse.jetty.websocket.common.frames.DataFrame; - -/** - * Implementation of the x-webkit-deflate-frame extension seen out - * in the wild. - */ -public class FrameCompressionExtension extends AbstractExtension -{ - private CompressionMethod method = new DeflateCompressionMethod(); - - @Override - public String getName() - { - return "x-webkit-deflate-frame"; - } - - @Override - public synchronized void incomingFrame(Frame frame) - { - if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1()) - { - // Cannot modify incoming control frames or ones with RSV1 set. - nextIncomingFrame(frame); - return; - } - - ByteBuffer data = frame.getPayload(); - method.decompress().input(data); - while (!method.decompress().isDone()) - { - ByteBuffer uncompressed = method.decompress().process(); - DataFrame out = new DataFrame(frame); - out.setPayload(uncompressed); - if (!method.decompress().isDone()) - { - out.setFin(false); - } - out.setRsv1(false); // Unset RSV1 on decompressed frame - nextIncomingFrame(out); - } - - // reset on every frame. - // method.decompress().end(); - } - - /** - * Indicates use of RSV1 flag for indicating deflation is in use. - *

- * Also known as the "COMP" framing header bit - */ - @Override - public boolean isRsv1User() - { - return true; - } - - @Override - public synchronized void outgoingFrame(Frame frame, WriteCallback callback) - { - if (OpCode.isControlFrame(frame.getOpCode())) - { - // skip, cannot compress control frames. - nextOutgoingFrame(frame,callback); - return; - } - - ByteBuffer data = frame.getPayload(); - - // deflate data - method.compress().input(data); - while (!method.compress().isDone()) - { - ByteBuffer buf = method.compress().process(); - DataFrame out = new DataFrame(frame); - out.setPayload(buf); - out.setRsv1(true); - if (!method.compress().isDone()) - { - out.setFin(false); - nextOutgoingFrame(frame,null); // no callback for start/end frames - } - else - { - nextOutgoingFrame(out,callback); // pass thru callback - } - } - - // reset on every frame. - method.compress().end(); - } - - @Override - public void setConfig(ExtensionConfig config) - { - super.setConfig(config); - } - - @Override - public String toString() - { - return this.getClass().getSimpleName() + "[]"; - } -} \ No newline at end of file diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java new file mode 100644 index 00000000000..94b09d46eaf --- /dev/null +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java @@ -0,0 +1,38 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.extensions.compress; + +/** + * Implementation of the x-webkit-deflate-frame extension seen out + * in the wild. Using the alternate extension identification + */ +public class XWebkitDeflateFrameExtension extends DeflateFrameExtension +{ + @Override + public String getName() + { + return "x-webkit-deflate-frame"; + } + + @Override + public String toString() + { + return this.getClass().getSimpleName() + "[]"; + } +} \ No newline at end of file diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java index 3e21a448c2e..4490f23b425 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.websocket.common.frames; +import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.OpCode; import org.eclipse.jetty.websocket.common.WebSocketFrame; @@ -27,6 +28,8 @@ import org.eclipse.jetty.websocket.common.WebSocketFrame; */ public class DataFrame extends WebSocketFrame { + private boolean isPooledBuffer = false; + protected DataFrame(byte opcode) { super(opcode); @@ -63,14 +66,6 @@ public class DataFrame extends WebSocketFrame /* no extra validation for data frames (yet) here */ } - /** - * Set the data frame to continuation mode - */ - public void setIsContinuation() - { - setOpCode(OpCode.CONTINUATION); - } - @Override public boolean isControlFrame() { @@ -82,4 +77,28 @@ public class DataFrame extends WebSocketFrame { return true; } + + /** + * @return true if payload buffer is from a {@link ByteBufferPool} and can be released when appropriate to do so + */ + public boolean isPooledBuffer() + { + return isPooledBuffer; + } + + /** + * Set the data frame to continuation mode + */ + public void setIsContinuation() + { + setOpCode(OpCode.CONTINUATION); + } + + /** + * Sets a flag indicating that the underlying payload is from a {@link ByteBufferPool} and can be released when appropriate to do so + */ + public void setPooledBuffer(boolean isPooledBuffer) + { + this.isPooledBuffer = isPooledBuffer; + } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteBytesProvider.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteBytesProvider.java index 86da718bcad..bfe683bf5cb 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteBytesProvider.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteBytesProvider.java @@ -35,6 +35,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.Generator; import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.frames.DataFrame; /** * Interface for working with bytes destined for {@link EndPoint#write(Callback, ByteBuffer...)} @@ -101,6 +102,7 @@ public class WriteBytesProvider implements Callback generator.getBufferPool().release(headerBuffer); headerBuffer = null; } + releasePayloadBuffer(frame); } /** @@ -342,6 +344,24 @@ public class WriteBytesProvider implements Callback } } + public void releasePayloadBuffer(Frame frame) + { + if (!frame.hasPayload()) + { + return; + } + + if (frame instanceof DataFrame) + { + DataFrame data = (DataFrame)frame; + if (data.isPooledBuffer()) + { + ByteBuffer payload = frame.getPayload(); + generator.getBufferPool().release(payload); + } + } + } + /** * Set the buffer size used for generating ByteBuffers from the frames. *

diff --git a/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension b/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension index 5442966f294..70af44ee0d6 100644 --- a/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension +++ b/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension @@ -1,4 +1,5 @@ org.eclipse.jetty.websocket.common.extensions.identity.IdentityExtension -org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension +org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension +org.eclipse.jetty.websocket.common.extensions.compress.XWebkitDeflateFrameExtension org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension org.eclipse.jetty.websocket.common.extensions.fragment.FragmentExtension \ No newline at end of file diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java index 32ab04a6b01..46479791c6f 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java @@ -44,6 +44,19 @@ public class IncomingFramesCapture implements IncomingFrames public void assertFrameCount(int expectedCount) { + if (frames.size() != expectedCount) + { + // dump details + System.err.printf("Expected %d frame(s)%n",expectedCount); + System.err.printf("But actually captured %d frame(s)%n",frames.size()); + for (int i = 0; i < frames.size(); i++) + { + Frame frame = frames.get(i); + System.err.printf(" [%d] Frame[%s] - %s%n", i, + OpCode.name(frame.getOpCode()), + BufferUtil.toDetailString(frame.getPayload())); + } + } Assert.assertThat("Captured frame count",frames.size(),is(expectedCount)); } diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/AllTests.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/AllTests.java index b33f3a5657e..e9f19736aa2 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/AllTests.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/AllTests.java @@ -20,14 +20,14 @@ package org.eclipse.jetty.websocket.common.extensions; import org.eclipse.jetty.websocket.common.extensions.compress.DeflateCompressionMethodTest; import org.eclipse.jetty.websocket.common.extensions.compress.MessageCompressionExtensionTest; -import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtensionTest; +import org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtensionTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses( { ExtensionStackTest.class, DeflateCompressionMethodTest.class, MessageCompressionExtensionTest.class, FragmentExtensionTest.class, - IdentityExtensionTest.class, FrameCompressionExtensionTest.class }) + IdentityExtensionTest.class, DeflateFrameExtensionTest.class }) public class AllTests { /* nothing to do here, its all done in the annotations */ diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java index 278017dcc69..fef0de6c1f9 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java @@ -23,6 +23,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; +import org.junit.rules.TestName; /** * Dummy implementation of {@link OutgoingFrames} used for testing @@ -37,6 +38,11 @@ public class DummyOutgoingFrames implements OutgoingFrames this.id = id; } + public DummyOutgoingFrames(TestName testname) + { + this(testname.getMethodName()); + } + @Override public void outgoingFrame(Frame frame, WriteCallback callback) { diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java new file mode 100644 index 00000000000..4abbce4263a --- /dev/null +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java @@ -0,0 +1,55 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.extensions.compress; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; +import org.eclipse.jetty.websocket.common.Hex; +import org.eclipse.jetty.websocket.common.OpCode; + +public class CapturedHexPayloads implements OutgoingFrames +{ + private static final Logger LOG = Log.getLogger(CapturedHexPayloads.class); + private List captured = new ArrayList<>(); + + @Override + public void outgoingFrame(Frame frame, WriteCallback callback) + { + String hexPayload = Hex.asHex(frame.getPayload()); + LOG.debug("outgoingFrame({}: \"{}\", {})", + OpCode.name(frame.getOpCode()), + hexPayload, callback!=null?callback.getClass().getSimpleName():""); + captured.add(hexPayload); + if (callback != null) + { + callback.writeSuccess(); + } + } + + public List getCaptured() + { + return captured; + } +} diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java similarity index 77% rename from jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtensionTest.java rename to jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java index 9e47cd823c5..9d8a2935823 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtensionTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.*; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collections; +import java.util.List; import java.util.zip.Deflater; import java.util.zip.Inflater; @@ -36,6 +37,7 @@ import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.ByteBufferAssert; import org.eclipse.jetty.websocket.common.Generator; +import org.eclipse.jetty.websocket.common.Hex; import org.eclipse.jetty.websocket.common.IncomingFramesCapture; import org.eclipse.jetty.websocket.common.OpCode; import org.eclipse.jetty.websocket.common.OutgoingNetworkBytesCapture; @@ -44,19 +46,24 @@ import org.eclipse.jetty.websocket.common.UnitParser; import org.eclipse.jetty.websocket.common.WebSocketFrame; import org.eclipse.jetty.websocket.common.frames.TextFrame; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; -public class FrameCompressionExtensionTest +public class DeflateFrameExtensionTest { + @Rule + public TestName testname = new TestName(); + private void assertIncoming(byte[] raw, String... expectedTextDatas) { WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); - FrameCompressionExtension ext = new FrameCompressionExtension(); + DeflateFrameExtension ext = new DeflateFrameExtension(); ext.setBufferPool(new MappedByteBufferPool()); ext.setPolicy(policy); - ExtensionConfig config = ExtensionConfig.parse("x-webkit-deflate-frame"); + ExtensionConfig config = ExtensionConfig.parse("deflate-frame"); ext.setConfig(config); // Setup capture of incoming frames @@ -95,11 +102,11 @@ public class FrameCompressionExtensionTest { WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); - FrameCompressionExtension ext = new FrameCompressionExtension(); + DeflateFrameExtension ext = new DeflateFrameExtension(); ext.setBufferPool(new MappedByteBufferPool()); ext.setPolicy(policy); - ExtensionConfig config = ExtensionConfig.parse("x-webkit-deflate-frame"); + ExtensionConfig config = ExtensionConfig.parse("deflate-frame"); ext.setConfig(config); ByteBufferPool bufferPool = new MappedByteBufferPool(); @@ -120,48 +127,92 @@ public class FrameCompressionExtensionTest public void testBlockheadClient_HelloThere() { // Captured from Blockhead Client - "Hello" then "There" via unit test - String hello = "c1 87 00 00 00 00 f2 48 cd c9 c9 07 00".replaceAll("\\s*",""); - String there = "c1 87 00 00 00 00 0a c9 48 2d 4a 05 00".replaceAll("\\s*",""); - byte rawbuf[] = TypeUtil.fromHexString(hello + there); + String hello = "c18700000000f248cdc9c90700"; + String there = "c187000000000ac9482d4a0500"; + byte rawbuf[] = Hex.asByteArray(hello + there); assertIncoming(rawbuf,"Hello","There"); } @Test public void testChrome20_Hello() { - // Captured from Chrome 20.x - "Hello" (sent from browser/client) - byte rawbuf[] = TypeUtil.fromHexString("c187832b5c11716391d84a2c5c"); + // Captured from Chrome 20.x - "Hello" (sent from browser) + byte rawbuf[] = Hex.asByteArray("c187832b5c11716391d84a2c5c"); assertIncoming(rawbuf,"Hello"); } @Test public void testChrome20_HelloThere() { - // Captured from Chrome 20.x - "Hello" then "There" (sent from browser/client) - String hello = "c1 87 7b 19 71 db 89 51 bc 12 b2 1e 71".replaceAll("\\s*",""); - String there = "c1 87 59 ed c8 f4 53 24 80 d9 13 e8 c8".replaceAll("\\s*",""); - byte rawbuf[] = TypeUtil.fromHexString(hello + there); + // Captured from Chrome 20.x - "Hello" then "There" (sent from browser) + String hello = "c1877b1971db8951bc12b21e71"; + String there = "c18759edc8f4532480d913e8c8"; + byte rawbuf[] = Hex.asByteArray(hello + there); assertIncoming(rawbuf,"Hello","There"); } @Test public void testChrome20_Info() { - // Captured from Chrome 20.x - "info:" (sent from browser/client) - byte rawbuf[] = TypeUtil.fromHexString("c187ca4def7f0081a4b47d4fef"); + // Captured from Chrome 20.x - "info:" (sent from browser) + byte rawbuf[] = Hex.asByteArray("c187ca4def7f0081a4b47d4fef"); assertIncoming(rawbuf,"info:"); } @Test public void testChrome20_TimeTime() { - // Captured from Chrome 20.x - "time:" then "time:" once more (sent from browser/client) - String time1 = "c1 87 82 46 74 24 a8 8f b8 69 37 44 74".replaceAll("\\s*",""); - String time2 = "c1 85 3c fd a1 7f 16 fc b0 7f 3c".replaceAll("\\s*",""); - byte rawbuf[] = TypeUtil.fromHexString(time1 + time2); + // Captured from Chrome 20.x - "time:" then "time:" once more (sent from browser) + String time1 = "c18782467424a88fb869374474"; + String time2 = "c1853cfda17f16fcb07f3c"; + byte rawbuf[] = Hex.asByteArray(time1 + time2); assertIncoming(rawbuf,"time:","time:"); } + @Test + public void testPyWebSocket_TimeTimeTime() + { + // Captured from Pywebsocket (r781) - "time:" sent 3 times. + String time1 = "c1876b100104" + "41d9cd49de1201"; + String time2 = "c1852ae3ff01" + "00e2ee012a"; + String time3 = "c18435558caa" + "37468caa"; + byte rawbuf[] = Hex.asByteArray(time1 + time2 + time3); + assertIncoming(rawbuf,"time:","time:","time:"); + } + + @Test + public void testCompress_TimeTimeTime() + { + // What pywebsocket produces for "time:", "time:", "time:" + String expected[] = new String[] + { "2AC9CC4DB50200", "2A01110000", "02130000" }; + + // Lets see what we produce + CapturedHexPayloads capture = new CapturedHexPayloads(); + DeflateFrameExtension ext = new DeflateFrameExtension(); + init(ext); + ext.setNextOutgoingFrames(capture); + + ext.outgoingFrame(new TextFrame().setPayload("time:"),null); + ext.outgoingFrame(new TextFrame().setPayload("time:"),null); + ext.outgoingFrame(new TextFrame().setPayload("time:"),null); + + List actual = capture.getCaptured(); + + for (String entry : actual) + { + System.err.printf("actual: \"%s\"%n",entry); + } + + Assert.assertThat("Compressed Payloads",actual,contains(expected)); + } + + private void init(DeflateFrameExtension ext) + { + ext.setConfig(new ExtensionConfig(ext.getName())); + ext.setBufferPool(new MappedByteBufferPool()); + } + @Test public void testDeflateBasics() throws Exception { @@ -204,7 +255,6 @@ public class FrameCompressionExtensionTest String actual = TypeUtil.toHexString(compressed); String expected = "CaCc4bCbB70200"; // what pywebsocket produces - // String expected = "CbCc4bCbB70200"; // what java produces Assert.assertThat("Compressed data",actual,is(expected)); } @@ -214,12 +264,10 @@ public class FrameCompressionExtensionTest { WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); - FrameCompressionExtension ext = new FrameCompressionExtension(); + DeflateFrameExtension ext = new DeflateFrameExtension(); ext.setBufferPool(new MappedByteBufferPool()); ext.setPolicy(policy); - - ExtensionConfig config = ExtensionConfig.parse("x-webkit-deflate-frame"); - ext.setConfig(config); + ext.setConfig(new ExtensionConfig(ext.getName())); ByteBufferPool bufferPool = new MappedByteBufferPool(); boolean validating = true; @@ -233,7 +281,6 @@ public class FrameCompressionExtensionTest ext.outgoingFrame(new TextFrame().setPayload("There"),null); capture.assertBytes(0,"c107f248cdc9c90700"); - capture.assertBytes(1,"c1070ac9482d4a0500"); } @Test diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java new file mode 100644 index 00000000000..7725c6196fe --- /dev/null +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java @@ -0,0 +1,82 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.extensions.compress; + +import java.nio.ByteBuffer; +import java.util.zip.Deflater; + +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.websocket.common.Hex; +import org.junit.Ignore; +import org.junit.Test; + +public class DeflateTest +{ + private int bufSize = 8 * 1024; + + public String deflate(String inputHex, Deflater deflater, int flushMode) + { + byte uncompressed[] = Hex.asByteArray(inputHex); + deflater.setInput(uncompressed,0,uncompressed.length); + deflater.finish(); + + ByteBuffer out = ByteBuffer.allocate(bufSize); + byte buf[] = new byte[64]; + while (!deflater.finished()) + { + int len = deflater.deflate(buf,0,buf.length,flushMode); + out.put(buf,0,len); + } + + out.flip(); + return Hex.asHex(out); + } + + @Test + @Ignore("just noisy") + public void deflateAllTypes() + { + int levels[] = new int[] + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + boolean nowraps[] = new boolean[] + { true, false }; + int strategies[] = new int[] + { Deflater.DEFAULT_STRATEGY, Deflater.FILTERED, Deflater.HUFFMAN_ONLY }; + int flushmodes[] = new int[] + { Deflater.NO_FLUSH, Deflater.SYNC_FLUSH, Deflater.FULL_FLUSH }; + + String inputHex = Hex.asHex(StringUtil.getUtf8Bytes("time:")); + for (int level : levels) + { + for (boolean nowrap : nowraps) + { + Deflater deflater = new Deflater(level,nowrap); + for (int strategy : strategies) + { + deflater.setStrategy(strategy); + for (int flushmode : flushmodes) + { + String result = deflate(inputHex,deflater,flushmode); + System.out.printf("%d | %b | %d | %d | \"%s\"%n",level,nowrap,strategy,flushmode,result); + } + } + } + } + } +} diff --git a/jetty-websocket/websocket-common/src/test/resources/jetty-logging.properties b/jetty-websocket/websocket-common/src/test/resources/jetty-logging.properties index 73dd42f52f5..683b76a631f 100644 --- a/jetty-websocket/websocket-common/src/test/resources/jetty-logging.properties +++ b/jetty-websocket/websocket-common/src/test/resources/jetty-logging.properties @@ -4,4 +4,4 @@ org.eclipse.jetty.LEVEL=WARN # org.eclipse.jetty.websocket.protocol.Parser.LEVEL=DEBUG # org.eclipse.jetty.websocket.protocol.LEVEL=DEBUG # org.eclipse.jetty.websocket.io.payload.LEVEL=DEBUG -# org.eclipse.jetty.websocket.core.extensions.compress.LEVEL=DEBUG +# org.eclipse.jetty.websocket.common.extensions.LEVEL=DEBUG diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java index 579ad4231c3..ccbd42603e9 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java @@ -23,8 +23,6 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension; -import org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension; import org.eclipse.jetty.websocket.server.WebSocketHandler; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; @@ -84,6 +82,8 @@ public class BrowserDebugTool implements WebSocketCreator String ua = req.getHeader("User-Agent"); String rexts = req.getHeader("Sec-WebSocket-Extensions"); + LOG.debug("User-Agent: {}", ua); + LOG.debug("Sec-WebSocket-Extensions: {}", rexts); BrowserSocket socket = new BrowserSocket(ua,rexts); return socket; } @@ -110,15 +110,11 @@ public class BrowserDebugTool implements WebSocketCreator { LOG.debug("Configuring WebSocketServerFactory ..."); - // Setup some extensions we want to test against - factory.getExtensionFactory().register("x-webkit-deflate-frame",FrameCompressionExtension.class); - factory.getExtensionFactory().register("permessage-compress",MessageDeflateCompressionExtension.class); - // Setup the desired Socket to use for all incoming upgrade requests factory.setCreator(BrowserDebugTool.this); // Set the timeout - factory.getPolicy().setIdleTimeout(2000); + factory.getPolicy().setIdleTimeout(20000); } }; diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java index 877d57b6aa6..cd4133f605c 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java @@ -18,7 +18,7 @@ package org.eclipse.jetty.websocket.server.helper; -import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension; +import org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension; import org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; @@ -33,7 +33,7 @@ public class EchoServlet extends WebSocketServlet public void configure(WebSocketServletFactory factory) { // Setup some extensions we want to test against - factory.getExtensionFactory().register("x-webkit-deflate-frame",FrameCompressionExtension.class); + factory.getExtensionFactory().register("x-webkit-deflate-frame",DeflateFrameExtension.class); factory.getExtensionFactory().register("permessage-compress",MessageDeflateCompressionExtension.class); // Setup the desired Socket to use for all incoming upgrade requests