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.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
39
40
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
49 private static final byte[] TAIL = new byte[]
50 { 0x00, 0x00, (byte)0xFF, (byte)0xFF };
51 private ExtensionConfig configRequested;
52 private ExtensionConfig configNegotiated;
53 private Deflater compressor;
54 private Inflater decompressor;
55
56 private boolean incomingCompressed = false;
57 private boolean outgoingCompressed = false;
58
59
60
61
62
63 private boolean incomingContextTakeover = true;
64 private boolean outgoingContextTakeover = true;
65
66 @Override
67 public String getName()
68 {
69 return "permessage-deflate";
70 }
71
72 @Override
73 public synchronized void incomingFrame(Frame frame)
74 {
75 switch (frame.getOpCode())
76 {
77 case OpCode.BINARY:
78 case OpCode.TEXT:
79 incomingCompressed = frame.isRsv1();
80 break;
81 case OpCode.CONTINUATION:
82 if (!incomingCompressed)
83 {
84 nextIncomingFrame(frame);
85 }
86 break;
87 default:
88
89 nextIncomingFrame(frame);
90 return;
91 }
92
93 if (!incomingCompressed || !frame.hasPayload())
94 {
95
96 nextIncomingFrame(frame);
97 return;
98 }
99
100
101 ByteBuffer payload = frame.getPayload();
102 int inlen = payload.remaining();
103 byte compressed[] = null;
104
105 if (frame.isFin())
106 {
107 compressed = new byte[inlen + TAIL.length];
108 payload.get(compressed,0,inlen);
109 System.arraycopy(TAIL,0,compressed,inlen,TAIL.length);
110 incomingCompressed = false;
111 }
112 else
113 {
114 compressed = new byte[inlen];
115 payload.get(compressed,0,inlen);
116 }
117
118 decompressor.setInput(compressed,0,compressed.length);
119
120
121 int maxSize = Math.max(getPolicy().getMaxTextMessageSize(),getPolicy().getMaxBinaryMessageBufferSize());
122 ByteAccumulator accumulator = new ByteAccumulator(maxSize);
123
124 DataFrame out = new DataFrame(frame);
125 out.setRsv1(false);
126
127
128 while (decompressor.getRemaining() > 0 && !decompressor.finished())
129 {
130 byte outbuf[] = new byte[inlen];
131 try
132 {
133 int len = decompressor.inflate(outbuf);
134 if (len == 0)
135 {
136 if (decompressor.needsInput())
137 {
138 throw new BadPayloadException("Unable to inflate frame, not enough input on frame");
139 }
140 if (decompressor.needsDictionary())
141 {
142 throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary");
143 }
144 }
145 if (len > 0)
146 {
147 accumulator.addBuffer(outbuf,0,len);
148 }
149 }
150 catch (DataFormatException e)
151 {
152 LOG.warn(e);
153 throw new BadPayloadException(e);
154 }
155 }
156
157
158 out.setPayload(accumulator.getByteBuffer(getBufferPool()));
159 nextIncomingFrame(out);
160 }
161
162
163
164
165 @Override
166 public boolean isRsv1User()
167 {
168 return true;
169 }
170
171 @Override
172 public synchronized void outgoingFrame(Frame frame, WriteCallback callback)
173 {
174 if (OpCode.isControlFrame(frame.getOpCode()))
175 {
176
177 nextOutgoingFrame(frame,callback);
178 return;
179 }
180
181 if (!frame.hasPayload())
182 {
183
184 nextOutgoingFrame(frame,callback);
185 return;
186 }
187
188 if (LOG.isDebugEnabled())
189 {
190 LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"<null>",
191 BufferUtil.toDetailString(frame.getPayload()));
192 }
193
194
195 byte uncompressed[] = BufferUtil.toArray(frame.getPayload());
196
197
198 if (!compressor.finished())
199 {
200 compressor.setInput(uncompressed,0,uncompressed.length);
201 byte compressed[] = new byte[uncompressed.length + OVERHEAD];
202
203 while (!compressor.needsInput())
204 {
205 int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH);
206 ByteBuffer outbuf = getBufferPool().acquire(len,true);
207 BufferUtil.clearToFill(outbuf);
208
209 if (len > 0)
210 {
211 if (len > 4)
212 {
213
214 int idx = len - 4;
215 boolean found = true;
216 for (int n = 0; n < TAIL.length; n++)
217 {
218 if (compressed[idx + n] != TAIL[n])
219 {
220 found = false;
221 break;
222 }
223 }
224 if (found)
225 {
226 len = len - 4;
227 }
228 }
229 outbuf.put(compressed,0,len);
230 }
231
232 BufferUtil.flipToFlush(outbuf,0);
233
234 if (len > 0 && BFINAL_HACK)
235 {
236
237
238
239
240
241
242
243 byte b0 = outbuf.get(0);
244 if ((b0 & 1) != 0)
245 {
246 outbuf.put(0,(b0 ^= 1));
247 }
248 }
249
250 DataFrame out = new DataFrame(frame,outgoingCompressed);
251 out.setRsv1(true);
252 out.setBufferPool(getBufferPool());
253 out.setPayload(outbuf);
254
255 if (!compressor.needsInput())
256 {
257
258 out.setFin(false);
259 nextOutgoingFrame(out,null);
260 }
261 else
262 {
263
264 nextOutgoingFrame(out,callback);
265 }
266
267 outgoingCompressed = !out.isFin();
268 }
269 }
270 }
271
272 @Override
273 protected void nextIncomingFrame(Frame frame)
274 {
275 if (frame.isFin() && !incomingContextTakeover)
276 {
277 LOG.debug("Incoming Context Reset");
278 decompressor.reset();
279 }
280
281 super.nextIncomingFrame(frame);
282 }
283
284 @Override
285 protected void nextOutgoingFrame(Frame frame, WriteCallback callback)
286 {
287 if (frame.isFin() && !outgoingContextTakeover)
288 {
289 LOG.debug("Outgoing Context Reset");
290 compressor.reset();
291 }
292
293 super.nextOutgoingFrame(frame,callback);
294 }
295
296 @Override
297 public void setConfig(final ExtensionConfig config)
298 {
299 configRequested = new ExtensionConfig(config);
300 configNegotiated = new ExtensionConfig(config.getName());
301
302 boolean nowrap = true;
303 compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
304 compressor.setStrategy(Deflater.DEFAULT_STRATEGY);
305
306 decompressor = new Inflater(nowrap);
307
308 for (String key : config.getParameterKeys())
309 {
310 key = key.trim();
311 switch (key)
312 {
313 case "client_max_window_bits":
314 case "server_max_window_bits":
315
316
317 break;
318 case "client_no_context_takeover":
319 configNegotiated.setParameter("client_no_context_takeover");
320 switch (getPolicy().getBehavior())
321 {
322 case CLIENT:
323 incomingContextTakeover = false;
324 break;
325 case SERVER:
326 outgoingContextTakeover = false;
327 break;
328 }
329 break;
330 case "server_no_context_takeover":
331 configNegotiated.setParameter("server_no_context_takeover");
332 switch (getPolicy().getBehavior())
333 {
334 case CLIENT:
335 outgoingContextTakeover = false;
336 break;
337 case SERVER:
338 incomingContextTakeover = false;
339 break;
340 }
341 break;
342 }
343 }
344
345 super.setConfig(configNegotiated);
346 }
347
348 @Override
349 public String toString()
350 {
351 StringBuilder str = new StringBuilder();
352 str.append(this.getClass().getSimpleName());
353 str.append("[requested=").append(configRequested.getParameterizedName());
354 str.append(",negotiated=").append(configNegotiated.getParameterizedName());
355 str.append(']');
356 return str.toString();
357 }
358 }