View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.websocket.common.extensions.compress;
20  
21  import java.nio.ByteBuffer;
22  import java.util.zip.DataFormatException;
23  import java.util.zip.Deflater;
24  import java.util.zip.Inflater;
25  
26  import org.eclipse.jetty.util.BufferUtil;
27  import org.eclipse.jetty.util.log.Log;
28  import org.eclipse.jetty.util.log.Logger;
29  import org.eclipse.jetty.websocket.api.BadPayloadException;
30  import org.eclipse.jetty.websocket.api.WriteCallback;
31  import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
32  import org.eclipse.jetty.websocket.api.extensions.Frame;
33  import org.eclipse.jetty.websocket.common.OpCode;
34  import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
35  import org.eclipse.jetty.websocket.common.frames.DataFrame;
36  
37  /**
38   * 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
39   * wild.
40   */
41  public class DeflateFrameExtension extends AbstractExtension
42  {
43      private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true"));
44      private static final Logger LOG = Log.getLogger(DeflateFrameExtension.class);
45  
46      private static final int OVERHEAD = 64;
47      /** Tail Bytes per Spec */
48      private static final byte[] TAIL = new byte[]
49      { 0x00, 0x00, (byte)0xFF, (byte)0xFF };
50      private int bufferSize = 64 * 1024;
51      private Deflater compressor;
52      private Inflater decompressor;
53  
54      @Override
55      public String getName()
56      {
57          return "deflate-frame";
58      }
59  
60      @Override
61      public synchronized void incomingFrame(Frame frame)
62      {
63          if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1())
64          {
65              // Cannot modify incoming control frames or ones with RSV1 set.
66              nextIncomingFrame(frame);
67              return;
68          }
69  
70          if (!frame.hasPayload())
71          {
72              // no payload? nothing to do.
73              nextIncomingFrame(frame);
74              return;
75          }
76  
77          // Prime the decompressor
78          ByteBuffer payload = frame.getPayload();
79          int inlen = payload.remaining();
80          byte compressed[] = new byte[inlen + TAIL.length];
81          payload.get(compressed,0,inlen);
82          System.arraycopy(TAIL,0,compressed,inlen,TAIL.length);
83          decompressor.setInput(compressed,0,compressed.length);
84  
85          // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value.
86          int maxSize = Math.max(getPolicy().getMaxTextMessageSize(),getPolicy().getMaxBinaryMessageBufferSize());
87          ByteAccumulator accumulator = new ByteAccumulator(maxSize);
88  
89          DataFrame out = new DataFrame(frame);
90          out.setRsv1(false); // Unset RSV1
91  
92          // Perform decompression
93          while (decompressor.getRemaining() > 0 && !decompressor.finished())
94          {
95              byte outbuf[] = new byte[Math.min(inlen * 2,bufferSize)];
96              try
97              {
98                  int len = decompressor.inflate(outbuf);
99                  if (len == 0)
100                 {
101                     if (decompressor.needsInput())
102                     {
103                         throw new BadPayloadException("Unable to inflate frame, not enough input on frame");
104                     }
105                     if (decompressor.needsDictionary())
106                     {
107                         throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary");
108                     }
109                 }
110                 if (len > 0)
111                 {
112                     accumulator.addBuffer(outbuf,0,len);
113                 }
114             }
115             catch (DataFormatException e)
116             {
117                 LOG.warn(e);
118                 throw new BadPayloadException(e);
119             }
120         }
121 
122         // Forward on the frame
123         out.setPayload(accumulator.getByteBuffer(getBufferPool()));
124         nextIncomingFrame(out);
125     }
126 
127     /**
128      * Indicates use of RSV1 flag for indicating deflation is in use.
129      * <p>
130      * Also known as the "COMP" framing header bit
131      */
132     @Override
133     public boolean isRsv1User()
134     {
135         return true;
136     }
137 
138     @Override
139     public synchronized void outgoingFrame(Frame frame, WriteCallback callback)
140     {
141         if (OpCode.isControlFrame(frame.getOpCode()))
142         {
143             // skip, cannot compress control frames.
144             nextOutgoingFrame(frame,callback);
145             return;
146         }
147 
148         if (!frame.hasPayload())
149         {
150             // pass through, nothing to do
151             nextOutgoingFrame(frame,callback);
152             return;
153         }
154 
155         if (LOG.isDebugEnabled())
156         {
157             LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"<null>",
158                     BufferUtil.toDetailString(frame.getPayload()));
159         }
160 
161         // Prime the compressor
162         byte uncompressed[] = BufferUtil.toArray(frame.getPayload());
163 
164         // Perform the compression
165         if (!compressor.finished())
166         {
167             compressor.setInput(uncompressed,0,uncompressed.length);
168             byte compressed[] = new byte[uncompressed.length + OVERHEAD];
169 
170             while (!compressor.needsInput())
171             {
172                 int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH);
173                 ByteBuffer outbuf = getBufferPool().acquire(len,true);
174                 BufferUtil.clearToFill(outbuf);
175 
176                 if (len > 0)
177                 {
178                     outbuf.put(compressed,0,len - 4);
179                 }
180 
181                 BufferUtil.flipToFlush(outbuf,0);
182 
183                 if (len > 0 && BFINAL_HACK)
184                 {
185                     /*
186                      * Per the spec, it says that BFINAL 1 or 0 are allowed.
187                      * 
188                      * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1.
189                      * 
190                      * This hack will always set BFINAL 0
191                      */
192                     byte b0 = outbuf.get(0);
193                     if ((b0 & 1) != 0) // if BFINAL 1
194                     {
195                         outbuf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0
196                     }
197                 }
198 
199                 DataFrame out = new DataFrame(frame);
200                 out.setRsv1(true);
201                 out.setPooledBuffer(true);
202                 out.setPayload(outbuf);
203 
204                 if (!compressor.needsInput())
205                 {
206                     // this is fragmented
207                     out.setFin(false);
208                     nextOutgoingFrame(out,null); // non final frames have no callback
209                 }
210                 else
211                 {
212                     // pass through the callback
213                     nextOutgoingFrame(out,callback);
214                 }
215             }
216         }
217     }
218 
219     @Override
220     public void setConfig(ExtensionConfig config)
221     {
222         super.setConfig(config);
223 
224         boolean nowrap = true;
225         compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
226         compressor.setStrategy(Deflater.DEFAULT_STRATEGY);
227 
228         decompressor = new Inflater(nowrap);
229     }
230 
231     @Override
232     public String toString()
233     {
234         return this.getClass().getSimpleName() + "[]";
235     }
236 }