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   * Per Message Deflate Compression extension for WebSocket.
39   * <p>
40   * Attempts to follow <a href="https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-12">draft-ietf-hybi-permessage-compression-12</a>
41   */
42  public class PerMessageDeflateExtension extends AbstractExtension
43  {
44      private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true"));
45      private static final Logger LOG = Log.getLogger(PerMessageDeflateExtension.class);
46  
47      private static final int OVERHEAD = 64;
48      /** Tail Bytes per Spec */
49      private static final byte[] TAIL = new byte[]
50      { 0x00, 0x00, (byte)0xFF, (byte)0xFF };
51      private int bufferSize = 64 * 1024;
52      private Deflater compressor;
53      private Inflater decompressor;
54  
55      @Override
56      public String getName()
57      {
58          return "permessage-deflate";
59      }
60  
61      @Override
62      public synchronized void incomingFrame(Frame frame)
63      {
64          if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1())
65          {
66              // Cannot modify incoming control frames or ones with RSV1 set.
67              nextIncomingFrame(frame);
68              return;
69          }
70  
71          if (!frame.hasPayload())
72          {
73              // no payload? nothing to do.
74              nextIncomingFrame(frame);
75              return;
76          }
77  
78          // Prime the decompressor
79          ByteBuffer payload = frame.getPayload();
80          int inlen = payload.remaining();
81          byte compressed[] = new byte[inlen + TAIL.length];
82          payload.get(compressed,0,inlen);
83          System.arraycopy(TAIL,0,compressed,inlen,TAIL.length);
84          decompressor.setInput(compressed,0,compressed.length);
85  
86          // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value.
87          int maxSize = Math.max(getPolicy().getMaxTextMessageSize(),getPolicy().getMaxBinaryMessageBufferSize());
88          ByteAccumulator accumulator = new ByteAccumulator(maxSize);
89  
90          DataFrame out = new DataFrame(frame);
91          out.setRsv1(false); // Unset RSV1
92  
93          // Perform decompression
94          while (decompressor.getRemaining() > 0 && !decompressor.finished())
95          {
96              byte outbuf[] = new byte[Math.min(inlen * 2,bufferSize)];
97              try
98              {
99                  int len = decompressor.inflate(outbuf);
100                 if (len == 0)
101                 {
102                     if (decompressor.needsInput())
103                     {
104                         throw new BadPayloadException("Unable to inflate frame, not enough input on frame");
105                     }
106                     if (decompressor.needsDictionary())
107                     {
108                         throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary");
109                     }
110                 }
111                 if (len > 0)
112                 {
113                     accumulator.addBuffer(outbuf,0,len);
114                 }
115             }
116             catch (DataFormatException e)
117             {
118                 LOG.warn(e);
119                 throw new BadPayloadException(e);
120             }
121         }
122 
123         // Forward on the frame
124         out.setPayload(accumulator.getByteBuffer(getBufferPool()));
125         nextIncomingFrame(out);
126     }
127 
128     /**
129      * Indicates use of RSV1 flag for indicating deflation is in use.
130      */
131     @Override
132     public boolean isRsv1User()
133     {
134         return true;
135     }
136 
137     @Override
138     public synchronized void outgoingFrame(Frame frame, WriteCallback callback)
139     {
140         if (OpCode.isControlFrame(frame.getOpCode()))
141         {
142             // skip, cannot compress control frames.
143             nextOutgoingFrame(frame,callback);
144             return;
145         }
146 
147         if (!frame.hasPayload())
148         {
149             // pass through, nothing to do
150             nextOutgoingFrame(frame,callback);
151             return;
152         }
153 
154         if (LOG.isDebugEnabled())
155         {
156             LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"<null>",
157                     BufferUtil.toDetailString(frame.getPayload()));
158         }
159 
160         // Prime the compressor
161         byte uncompressed[] = BufferUtil.toArray(frame.getPayload());
162 
163         // Perform the compression
164         if (!compressor.finished())
165         {
166             compressor.setInput(uncompressed,0,uncompressed.length);
167             byte compressed[] = new byte[uncompressed.length + OVERHEAD];
168 
169             while (!compressor.needsInput())
170             {
171                 int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH);
172                 ByteBuffer outbuf = getBufferPool().acquire(len,true);
173                 BufferUtil.clearToFill(outbuf);
174 
175                 if (len > 0)
176                 {
177                     outbuf.put(compressed,0,len - 4);
178                 }
179 
180                 BufferUtil.flipToFlush(outbuf,0);
181 
182                 if (len > 0 && BFINAL_HACK)
183                 {
184                     /*
185                      * Per the spec, it says that BFINAL 1 or 0 are allowed.
186                      * 
187                      * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1.
188                      * 
189                      * This hack will always set BFINAL 0
190                      */
191                     byte b0 = outbuf.get(0);
192                     if ((b0 & 1) != 0) // if BFINAL 1
193                     {
194                         outbuf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0
195                     }
196                 }
197 
198                 DataFrame out = new DataFrame(frame);
199                 out.setRsv1(true);
200                 out.setPooledBuffer(true);
201                 out.setPayload(outbuf);
202 
203                 if (!compressor.needsInput())
204                 {
205                     // this is fragmented
206                     out.setFin(false);
207                     nextOutgoingFrame(out,null); // non final frames have no callback
208                 }
209                 else
210                 {
211                     // pass through the callback
212                     nextOutgoingFrame(out,callback);
213                 }
214             }
215         }
216     }
217 
218     @Override
219     public void setConfig(final ExtensionConfig config)
220     {
221         ExtensionConfig negotiated = new ExtensionConfig(config.getName());
222 
223         boolean nowrap = true;
224         compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
225         compressor.setStrategy(Deflater.DEFAULT_STRATEGY);
226 
227         decompressor = new Inflater(nowrap);
228 
229         for (String key : config.getParameterKeys())
230         {
231             key = key.trim();
232             String value = config.getParameter(key,null);
233             switch (key)
234             {
235                 case "c2s_max_window_bits":
236                     negotiated.setParameter("s2c_max_window_bits",value);
237                     break;
238                 case "c2s_no_context_takeover":
239                     negotiated.setParameter("s2c_no_context_takeover",value);
240                     break;
241                 case "s2c_max_window_bits":
242                     negotiated.setParameter("c2s_max_window_bits",value);
243                     break;
244                 case "s2c_no_context_takeover":
245                     negotiated.setParameter("c2s_no_context_takeover",value);
246                     break;
247             }
248         }
249 
250         super.setConfig(negotiated);
251     }
252 
253     @Override
254     public String toString()
255     {
256         StringBuilder str = new StringBuilder();
257         str.append(this.getClass().getSimpleName());
258         str.append('[');
259         str.append(']');
260         return str.toString();
261     }
262 }