View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.Queue;
23  import java.util.zip.DataFormatException;
24  import java.util.zip.Deflater;
25  import java.util.zip.Inflater;
26  import java.util.zip.ZipException;
27  
28  import org.eclipse.jetty.util.BufferUtil;
29  import org.eclipse.jetty.util.ConcurrentArrayQueue;
30  import org.eclipse.jetty.util.IteratingCallback;
31  import org.eclipse.jetty.util.log.Log;
32  import org.eclipse.jetty.util.log.Logger;
33  import org.eclipse.jetty.websocket.api.BadPayloadException;
34  import org.eclipse.jetty.websocket.api.BatchMode;
35  import org.eclipse.jetty.websocket.api.WriteCallback;
36  import org.eclipse.jetty.websocket.api.extensions.Frame;
37  import org.eclipse.jetty.websocket.common.OpCode;
38  import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
39  import org.eclipse.jetty.websocket.common.frames.DataFrame;
40  
41  public abstract class CompressExtension extends AbstractExtension
42  {
43      protected static final byte[] TAIL_BYTES = new byte[]{0x00, 0x00, (byte)0xFF, (byte)0xFF};
44      private static final Logger LOG = Log.getLogger(CompressExtension.class);
45  
46      private final Queue<FrameEntry> entries = new ConcurrentArrayQueue<>();
47      private final IteratingCallback flusher = new Flusher();
48      private final Deflater compressor;
49      private final Inflater decompressor;
50  
51      protected CompressExtension()
52      {
53          compressor = new Deflater(Deflater.BEST_COMPRESSION, true);
54          decompressor = new Inflater(true);
55      }
56  
57      public Deflater getDeflater()
58      {
59          return compressor;
60      }
61  
62      public Inflater getInflater()
63      {
64          return decompressor;
65      }
66  
67      /**
68       * Indicates use of RSV1 flag for indicating deflation is in use.
69       */
70      @Override
71      public boolean isRsv1User()
72      {
73          return true;
74      }
75  
76      protected void forwardIncoming(Frame frame, ByteAccumulator accumulator)
77      {
78          DataFrame newFrame = new DataFrame(frame);
79          // Unset RSV1 since it's not compressed anymore.
80          newFrame.setRsv1(false);
81  
82          ByteBuffer buffer = getBufferPool().acquire(accumulator.getLength(), false);
83          try
84          {
85              BufferUtil.flipToFill(buffer);
86              accumulator.transferTo(buffer);
87              newFrame.setPayload(buffer);
88              nextIncomingFrame(newFrame);
89          }
90          finally
91          {
92              getBufferPool().release(buffer);
93          }
94      }
95  
96      protected ByteAccumulator decompress(byte[] input)
97      {
98          // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value.
99          int maxSize = Math.max(getPolicy().getMaxTextMessageSize(), getPolicy().getMaxBinaryMessageBufferSize());
100         ByteAccumulator accumulator = new ByteAccumulator(maxSize);
101 
102         decompressor.setInput(input, 0, input.length);
103         LOG.debug("Decompressing {} bytes", input.length);
104 
105         try
106         {
107             // It is allowed to send DEFLATE blocks with BFINAL=1.
108             // For such blocks, getRemaining() will be > 0 but finished()
109             // will be true, so we need to check for both.
110             // When BFINAL=0, finished() will always be false and we only
111             // check the remaining bytes.
112             while (decompressor.getRemaining() > 0 && !decompressor.finished())
113             {
114                 byte[] output = new byte[Math.min(input.length * 2, 32 * 1024)];
115                 int decompressed = decompressor.inflate(output);
116                 if (decompressed == 0)
117                 {
118                     if (decompressor.needsInput())
119                     {
120                         throw new BadPayloadException("Unable to inflate frame, not enough input on frame");
121                     }
122                     if (decompressor.needsDictionary())
123                     {
124                         throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary");
125                     }
126                 }
127                 else
128                 {
129                     accumulator.addChunk(output, 0, decompressed);
130                 }
131             }
132             LOG.debug("Decompressed {}->{} bytes", input.length, accumulator.getLength());
133             return accumulator;
134         }
135         catch (DataFormatException x)
136         {
137             throw new BadPayloadException(x);
138         }
139     }
140 
141     @Override
142     public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
143     {
144         // We use a queue and an IteratingCallback to handle concurrency.
145         // We must compress and write atomically, otherwise the compression
146         // context on the other end gets confused.
147 
148         if (flusher.isFailed())
149         {
150             notifyCallbackFailure(callback, new ZipException());
151             return;
152         }
153 
154         FrameEntry entry = new FrameEntry(frame, callback, batchMode);
155         LOG.debug("Queuing {}", entry);
156         entries.offer(entry);
157         flusher.iterate();
158     }
159 
160     protected void notifyCallbackSuccess(WriteCallback callback)
161     {
162         try
163         {
164             if (callback != null)
165                 callback.writeSuccess();
166         }
167         catch (Throwable x)
168         {
169             LOG.debug("Exception while notifying success of callback " + callback, x);
170         }
171     }
172 
173     protected void notifyCallbackFailure(WriteCallback callback, Throwable failure)
174     {
175         try
176         {
177             if (callback != null)
178                 callback.writeFailed(failure);
179         }
180         catch (Throwable x)
181         {
182             LOG.debug("Exception while notifying failure of callback " + callback, x);
183         }
184     }
185 
186     @Override
187     public String toString()
188     {
189         return getClass().getSimpleName();
190     }
191 
192     private static class FrameEntry
193     {
194         private final Frame frame;
195         private final WriteCallback callback;
196         private final BatchMode batchMode;
197 
198         private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode)
199         {
200             this.frame = frame;
201             this.callback = callback;
202             this.batchMode = batchMode;
203         }
204 
205         @Override
206         public String toString()
207         {
208             return frame.toString();
209         }
210     }
211 
212     private class Flusher extends IteratingCallback implements WriteCallback
213     {
214         private FrameEntry current;
215         private ByteBuffer payload;
216         private boolean finished = true;
217 
218         @Override
219         protected Action process() throws Exception
220         {
221             if (finished)
222             {
223                 current = entries.poll();
224                 LOG.debug("Processing {}", current);
225                 if (current == null)
226                     return Action.IDLE;
227                 deflate(current);
228             }
229             else
230             {
231                 compress(current, false);
232             }
233             return Action.SCHEDULED;
234         }
235 
236         private void deflate(FrameEntry entry)
237         {
238             Frame frame = entry.frame;
239             BatchMode batchMode = entry.batchMode;
240             if (OpCode.isControlFrame(frame.getOpCode()) || !frame.hasPayload())
241             {
242                 nextOutgoingFrame(frame, this, batchMode);
243                 return;
244             }
245 
246             compress(entry, true);
247         }
248 
249         private void compress(FrameEntry entry, boolean first)
250         {
251             // Get a chunk of the payload to avoid to blow
252             // the heap if the payload is a huge mapped file.
253             Frame frame = entry.frame;
254             ByteBuffer data = frame.getPayload();
255             int remaining = data.remaining();
256             int inputLength = Math.min(remaining, 32 * 1024);
257             LOG.debug("Compressing {}: {} bytes in {} bytes chunk", entry, remaining, inputLength);
258 
259             // Avoid to copy the bytes if the ByteBuffer
260             // is backed by an array.
261             int inputOffset;
262             byte[] input;
263             if (data.hasArray())
264             {
265                 input = data.array();
266                 int position = data.position();
267                 inputOffset = position + data.arrayOffset();
268                 data.position(position + inputLength);
269             }
270             else
271             {
272                 input = new byte[inputLength];
273                 inputOffset = 0;
274                 data.get(input, 0, inputLength);
275             }
276             finished = inputLength == remaining;
277 
278             compressor.setInput(input, inputOffset, inputLength);
279 
280             // Use an additional space in case the content is not compressible.
281             byte[] output = new byte[inputLength + 64];
282             int outputOffset = 0;
283             int outputLength = 0;
284             while (true)
285             {
286                 int space = output.length - outputOffset;
287                 int compressed = compressor.deflate(output, outputOffset, space, Deflater.SYNC_FLUSH);
288                 outputLength += compressed;
289                 if (compressed < space)
290                 {
291                     // Everything was compressed.
292                     break;
293                 }
294                 else
295                 {
296                     // The compressed output is bigger than the uncompressed input.
297                     byte[] newOutput = new byte[output.length * 2];
298                     System.arraycopy(output, 0, newOutput, 0, output.length);
299                     outputOffset += output.length;
300                     output = newOutput;
301                 }
302             }
303 
304             // Skip the last tail bytes bytes generated by SYNC_FLUSH.
305             payload = ByteBuffer.wrap(output, 0, outputLength - TAIL_BYTES.length);
306             LOG.debug("Compressed {}: {}->{} chunk bytes", entry, inputLength, outputLength);
307 
308             boolean continuation = frame.getType().isContinuation() || !first;
309             DataFrame chunk = new DataFrame(frame, continuation);
310             chunk.setRsv1(true);
311             chunk.setPayload(payload);
312             boolean fin = frame.isFin() && finished;
313             chunk.setFin(fin);
314 
315             nextOutgoingFrame(chunk, this, entry.batchMode);
316         }
317 
318         @Override
319         protected void completed()
320         {
321             // This IteratingCallback never completes.
322         }
323 
324         @Override
325         public void writeSuccess()
326         {
327             if (finished)
328                 notifyCallbackSuccess(current.callback);
329             succeeded();
330         }
331 
332         @Override
333         public void writeFailed(Throwable x)
334         {
335             notifyCallbackFailure(current.callback, x);
336             // If something went wrong, very likely the compression context
337             // will be invalid, so we need to fail this IteratingCallback.
338             failed(x);
339             // Now no more frames can be queued, fail those in the queue.
340             FrameEntry entry;
341             while ((entry = entries.poll()) != null)
342                 notifyCallbackFailure(entry.callback, x);
343         }
344     }
345 }