diff options
Diffstat (limited to 'jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java')
-rw-r--r-- | jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java | 228 |
1 files changed, 228 insertions, 0 deletions
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 0000000000..7823c2fb16 --- /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 <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-05.txt">deflate-frame</a> 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. + * <p> + * 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():"<null>", + 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 |