1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
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
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
108
109
110
111
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
145
146
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
252
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
260
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
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
292 break;
293 }
294 else
295 {
296
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
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
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
337
338 failed(x);
339
340 FrameEntry entry;
341 while ((entry = entries.poll()) != null)
342 notifyCallbackFailure(entry.callback, x);
343 }
344 }
345 }