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 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
67 nextIncomingFrame(frame);
68 return;
69 }
70
71 if (!frame.hasPayload())
72 {
73
74 nextIncomingFrame(frame);
75 return;
76 }
77
78
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
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);
92
93
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
124 out.setPayload(accumulator.getByteBuffer(getBufferPool()));
125 nextIncomingFrame(out);
126 }
127
128
129
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
143 nextOutgoingFrame(frame,callback);
144 return;
145 }
146
147 if (!frame.hasPayload())
148 {
149
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
161 byte uncompressed[] = BufferUtil.toArray(frame.getPayload());
162
163
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
186
187
188
189
190
191 byte b0 = outbuf.get(0);
192 if ((b0 & 1) != 0)
193 {
194 outbuf.put(0,(b0 ^= 1));
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
206 out.setFin(false);
207 nextOutgoingFrame(out,null);
208 }
209 else
210 {
211
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 }