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.TypeUtil;
28  import org.eclipse.jetty.util.log.Log;
29  import org.eclipse.jetty.util.log.Logger;
30  import org.eclipse.jetty.websocket.api.BadPayloadException;
31  
32  /**
33   * Deflate Compression Method
34   */
35  public class DeflateCompressionMethod implements CompressionMethod
36  {
37      private static class DeflaterProcess implements CompressionMethod.Process
38      {
39          private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true"));
40  
41          private final Deflater deflater;
42          private int bufferSize = DEFAULT_BUFFER_SIZE;
43  
44          public DeflaterProcess(boolean nowrap)
45          {
46              deflater = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
47              deflater.setStrategy(Deflater.DEFAULT_STRATEGY);
48          }
49  
50          @Override
51          public void begin()
52          {
53              deflater.reset();
54          }
55  
56          @Override
57          public void end()
58          {
59              deflater.reset();
60          }
61  
62          @Override
63          public void input(ByteBuffer input)
64          {
65              if (LOG.isDebugEnabled())
66              {
67                  LOG.debug("input: {}",BufferUtil.toDetailString(input));
68              }
69  
70              // Set the data that is uncompressed to the deflater
71              byte raw[] = BufferUtil.toArray(input);
72              deflater.setInput(raw,0,raw.length);
73              deflater.finish();
74          }
75  
76          @Override
77          public boolean isDone()
78          {
79              return deflater.finished();
80          }
81  
82          @Override
83          public ByteBuffer process()
84          {
85              // prepare the output buffer
86              ByteBuffer buf = ByteBuffer.allocate(bufferSize);
87              BufferUtil.clearToFill(buf);
88  
89              while (!deflater.finished())
90              {
91                  byte out[] = new byte[bufferSize];
92                  int len = deflater.deflate(out,0,out.length,Deflater.SYNC_FLUSH);
93  
94                  if (LOG.isDebugEnabled())
95                  {
96                      LOG.debug("Deflater: finished={}, needsInput={}, len={}",deflater.finished(),deflater.needsInput(),len);
97                  }
98  
99                  buf.put(out,0,len);
100             }
101             BufferUtil.flipToFlush(buf,0);
102 
103             if (BFINAL_HACK)
104             {
105                 /*
106                  * Per the spec, it says that BFINAL 1 or 0 are allowed.
107                  * 
108                  * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1.
109                  * 
110                  * This hack will always set BFINAL 0
111                  */
112                 byte b0 = buf.get(0);
113                 if ((b0 & 1) != 0) // if BFINAL 1
114                 {
115                     buf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0
116                 }
117             }
118             return buf;
119         }
120 
121         public void setBufferSize(int bufferSize)
122         {
123             this.bufferSize = bufferSize;
124         }
125     }
126 
127     private static class InflaterProcess implements CompressionMethod.Process
128     {
129         /** Tail Bytes per Spec */
130         private static final byte[] TAIL = new byte[]
131                 { 0x00, 0x00, (byte)0xFF, (byte)0xFF };
132         private final Inflater inflater;
133         private int bufferSize = DEFAULT_BUFFER_SIZE;
134 
135         public InflaterProcess(boolean nowrap) {
136             inflater = new Inflater(nowrap);
137         }
138 
139         @Override
140         public void begin()
141         {
142             inflater.reset();
143         }
144 
145         @Override
146         public void end()
147         {
148             inflater.reset();
149         }
150 
151         @Override
152         public void input(ByteBuffer input)
153         {
154             if (LOG.isDebugEnabled())
155             {
156                 LOG.debug("inflate: {}",BufferUtil.toDetailString(input));
157                 LOG.debug("Input Data: {}",TypeUtil.toHexString(BufferUtil.toArray(input)));
158             }
159 
160             // Set the data that is compressed (+ TAIL) to the inflater
161             int len = input.remaining() + 4;
162             byte raw[] = new byte[len];
163             int inlen = input.remaining();
164             input.slice().get(raw,0,inlen);
165             System.arraycopy(TAIL,0,raw,inlen,TAIL.length);
166             inflater.setInput(raw,0,raw.length);
167         }
168 
169         @Override
170         public boolean isDone()
171         {
172             return (inflater.getRemaining() <= 0) || inflater.finished();
173         }
174 
175         @Override
176         public ByteBuffer process()
177         {
178             // Establish place for inflated data
179             byte buf[] = new byte[bufferSize];
180             try
181             {
182                 int inflated = inflater.inflate(buf);
183                 if (inflated == 0)
184                 {
185                     return null;
186                 }
187 
188                 ByteBuffer ret = BufferUtil.toBuffer(buf,0,inflated);
189 
190                 if (LOG.isDebugEnabled())
191                 {
192                     LOG.debug("uncompressed={}",BufferUtil.toDetailString(ret));
193                 }
194 
195                 return ret;
196             }
197             catch (DataFormatException e)
198             {
199                 LOG.warn(e);
200                 throw new BadPayloadException(e);
201             }
202         }
203 
204         public void setBufferSize(int bufferSize)
205         {
206             this.bufferSize = bufferSize;
207         }
208     }
209 
210     private static final int DEFAULT_BUFFER_SIZE = 61*1024;
211 
212     private static final Logger LOG = Log.getLogger(DeflateCompressionMethod.class);
213 
214     private int bufferSize = 64 * 1024;
215     private final DeflaterProcess compress;
216     private final InflaterProcess decompress;
217 
218     public DeflateCompressionMethod()
219     {
220         /*
221          * Specs specify that head/tail of deflate are not to be present.
222          * 
223          * So lets not use the wrapped format of bytes.
224          * 
225          * Setting nowrap to true prevents the Deflater from writing the head/tail bytes and the Inflater from expecting the head/tail bytes.
226          */
227         boolean nowrap = true;
228 
229         this.compress = new DeflaterProcess(nowrap);
230         this.decompress = new InflaterProcess(nowrap);
231     }
232 
233     @Override
234     public Process compress()
235     {
236         return compress;
237     }
238 
239     @Override
240     public Process decompress()
241     {
242         return decompress;
243     }
244 
245     public int getBufferSize()
246     {
247         return bufferSize;
248     }
249 
250     public void setBufferSize(int size)
251     {
252         if (size < 64)
253         {
254             throw new IllegalArgumentException("Buffer Size [" + size + "] cannot be less than 64 bytes");
255         }
256         this.bufferSize = size;
257         this.compress.setBufferSize(bufferSize);
258         this.decompress.setBufferSize(bufferSize);
259     }
260 }