1 /*
2 * Copyright 2014 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.handler.codec.http.websocketx.extensions.compression;
17
18 import io.netty.handler.codec.compression.ZlibCodecFactory;
19 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
20 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
21 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
22 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider;
23 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension;
24 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker;
25
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.Map.Entry;
29
30 import static io.netty.util.internal.ObjectUtil.*;
31
32 /**
33 * <a href="https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a>
34 * handshake implementation.
35 */
36 public final class PerMessageDeflateServerExtensionHandshaker implements WebSocketServerExtensionHandshaker {
37
38 public static final int MIN_WINDOW_SIZE = 8;
39 public static final int MAX_WINDOW_SIZE = 15;
40
41 static final String PERMESSAGE_DEFLATE_EXTENSION = "permessage-deflate";
42 static final String CLIENT_MAX_WINDOW = "client_max_window_bits";
43 static final String SERVER_MAX_WINDOW = "server_max_window_bits";
44 static final String CLIENT_NO_CONTEXT = "client_no_context_takeover";
45 static final String SERVER_NO_CONTEXT = "server_no_context_takeover";
46
47 /**
48 * Default memory level used for deflate compression (zlib MAX_MEM_LEVEL).
49 */
50 public static final int DEFAULT_MEM_LEVEL = 8;
51 public static final int MIN_MEM_LEVEL = 1;
52 public static final int MAX_MEM_LEVEL = 9;
53
54 private final int compressionLevel;
55 private final boolean allowServerWindowSize;
56 private final int preferredClientWindowSize;
57 private final boolean allowServerNoContext;
58 private final boolean preferredClientNoContext;
59 private final int serverWindowSize;
60 private final int memLevel;
61 private final WebSocketExtensionFilterProvider extensionFilterProvider;
62 private final int maxAllocation;
63
64 /**
65 * Constructor with default configuration.
66 *
67 * @deprecated
68 * Use {@link PerMessageDeflateServerExtensionHandshaker#
69 * PerMessageDeflateServerExtensionHandshaker(int)}.
70 */
71 @Deprecated
72 public PerMessageDeflateServerExtensionHandshaker() {
73 this(0);
74 }
75
76 /**
77 * Constructor with default configuration.
78 *
79 * @param maxAllocation
80 * Maximum size of the decompression buffer. Must be >= 0. If zero, maximum size is not limited.
81 */
82 public PerMessageDeflateServerExtensionHandshaker(int maxAllocation) {
83 this(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, false, false,
84 MAX_WINDOW_SIZE, DEFAULT_MEM_LEVEL, maxAllocation);
85 }
86
87 /**
88 * Constructor with custom configuration.
89 *
90 * @param compressionLevel
91 * Compression level between 0 and 9 (default is 6).
92 * @param allowServerWindowSize
93 * allows WebSocket client to customize the server inflater window size
94 * (default is false).
95 * @param preferredClientWindowSize
96 * indicates the preferred client window size to use if client inflater is customizable.
97 * @param allowServerNoContext
98 * allows WebSocket client to activate server_no_context_takeover
99 * (default is false).
100 * @param preferredClientNoContext
101 * indicates if server prefers to activate client_no_context_takeover
102 * if client is compatible with (default is false).
103 * @deprecated
104 * Use {@link PerMessageDeflateServerExtensionHandshaker#PerMessageDeflateServerExtensionHandshaker(
105 * int, boolean, int, boolean, boolean, int)}.
106 */
107 @Deprecated
108 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
109 int preferredClientWindowSize,
110 boolean allowServerNoContext, boolean preferredClientNoContext) {
111 this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
112 preferredClientNoContext, MAX_WINDOW_SIZE, DEFAULT_MEM_LEVEL, 0);
113 }
114
115 /**
116 * Constructor with custom configuration.
117 *
118 * @param compressionLevel
119 * Compression level between 0 and 9 (default is 6).
120 * @param allowServerWindowSize
121 * allows WebSocket client to customize the server inflater window size
122 * (default is false).
123 * @param preferredClientWindowSize
124 * indicates the preferred client window size to use if client inflater is customizable.
125 * @param allowServerNoContext
126 * allows WebSocket client to activate server_no_context_takeover
127 * (default is false).
128 * @param preferredClientNoContext
129 * indicates if server prefers to activate client_no_context_takeover
130 * if client is compatible with (default is false).
131 * @param maxAllocation
132 * Maximum size of the decompression buffer. Must be >= 0. If zero, maximum size is not limited.
133 */
134 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
135 int preferredClientWindowSize,
136 boolean allowServerNoContext, boolean preferredClientNoContext, int maxAllocation) {
137 this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
138 preferredClientNoContext, MAX_WINDOW_SIZE, DEFAULT_MEM_LEVEL, maxAllocation);
139 }
140
141 /**
142 * Constructor with custom configuration including server-side compressor memory limits.
143 *
144 * @param compressionLevel
145 * Compression level between 0 and 9 (default is 6).
146 * @param allowServerWindowSize
147 * allows WebSocket client to customize the server inflater window size
148 * (default is false).
149 * @param preferredClientWindowSize
150 * indicates the preferred client window size to use if client inflater is customizable.
151 * @param allowServerNoContext
152 * allows WebSocket client to activate server_no_context_takeover
153 * (default is false).
154 * @param preferredClientNoContext
155 * indicates if server prefers to activate client_no_context_takeover
156 * if client is compatible with (default is false).
157 * @param serverWindowSize
158 * upper bound (in bits, 8-15) on the server-side deflate window. If the client offers
159 * {@code server_max_window_bits=N}, the negotiated value is {@code min(N, serverWindowSize)}.
160 * Set lower than {@link #MAX_WINDOW_SIZE} to reduce per-connection memory.
161 * @param memLevel
162 * zlib memory level for the server-side deflater (1-9). Lower values reduce per-connection
163 * memory at the cost of compression ratio.
164 * @param maxAllocation
165 * Maximum size of the decompression buffer. Must be >= 0. If zero, maximum size is not limited.
166 */
167 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
168 int preferredClientWindowSize,
169 boolean allowServerNoContext, boolean preferredClientNoContext,
170 int serverWindowSize, int memLevel, int maxAllocation) {
171 this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
172 preferredClientNoContext, serverWindowSize, memLevel,
173 WebSocketExtensionFilterProvider.DEFAULT, maxAllocation);
174 }
175
176 /**
177 * Constructor with custom configuration.
178 *
179 * @param compressionLevel
180 * Compression level between 0 and 9 (default is 6).
181 * @param allowServerWindowSize
182 * allows WebSocket client to customize the server inflater window size
183 * (default is false).
184 * @param preferredClientWindowSize
185 * indicates the preferred client window size to use if client inflater is customizable.
186 * @param allowServerNoContext
187 * allows WebSocket client to activate server_no_context_takeover
188 * (default is false).
189 * @param preferredClientNoContext
190 * indicates if server prefers to activate client_no_context_takeover
191 * if client is compatible with (default is false).
192 * @param extensionFilterProvider
193 * provides server extension filters for per message deflate encoder and decoder.
194 * @deprecated
195 * Use {@link PerMessageDeflateServerExtensionHandshaker#PerMessageDeflateServerExtensionHandshaker(
196 * int, boolean, int, boolean, boolean, WebSocketExtensionFilterProvider, int)}.
197 */
198 @Deprecated
199 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
200 int preferredClientWindowSize,
201 boolean allowServerNoContext, boolean preferredClientNoContext,
202 WebSocketExtensionFilterProvider extensionFilterProvider) {
203 this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
204 preferredClientNoContext, MAX_WINDOW_SIZE, DEFAULT_MEM_LEVEL, extensionFilterProvider, 0);
205 }
206
207 /**
208 * Constructor with custom configuration.
209 *
210 * @param compressionLevel
211 * Compression level between 0 and 9 (default is 6).
212 * @param allowServerWindowSize
213 * allows WebSocket client to customize the server inflater window size
214 * (default is false).
215 * @param preferredClientWindowSize
216 * indicates the preferred client window size to use if client inflater is customizable.
217 * @param allowServerNoContext
218 * allows WebSocket client to activate server_no_context_takeover
219 * (default is false).
220 * @param preferredClientNoContext
221 * indicates if server prefers to activate client_no_context_takeover
222 * if client is compatible with (default is false).
223 * @param extensionFilterProvider
224 * provides server extension filters for per message deflate encoder and decoder.
225 * @param maxAllocation
226 * Maximum size of the decompression buffer. Must be >= 0. If zero, maximum size is not limited.
227 * @deprecated
228 * Use {@link PerMessageDeflateServerExtensionHandshaker#PerMessageDeflateServerExtensionHandshaker(
229 * int, boolean, int, boolean, boolean, int, int, WebSocketExtensionFilterProvider, int)}.
230 */
231 @Deprecated
232 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
233 int preferredClientWindowSize,
234 boolean allowServerNoContext, boolean preferredClientNoContext,
235 WebSocketExtensionFilterProvider extensionFilterProvider,
236 int maxAllocation) {
237 this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
238 preferredClientNoContext, MAX_WINDOW_SIZE, DEFAULT_MEM_LEVEL, extensionFilterProvider, maxAllocation);
239 }
240
241 /**
242 * Constructor with full custom configuration.
243 *
244 * @param compressionLevel
245 * Compression level between 0 and 9 (default is 6).
246 * @param allowServerWindowSize
247 * allows WebSocket client to customize the server inflater window size
248 * (default is false).
249 * @param preferredClientWindowSize
250 * indicates the preferred client window size to use if client inflater is customizable.
251 * @param allowServerNoContext
252 * allows WebSocket client to activate server_no_context_takeover
253 * (default is false).
254 * @param preferredClientNoContext
255 * indicates if server prefers to activate client_no_context_takeover
256 * if client is compatible with (default is false).
257 * @param serverWindowSize
258 * upper bound (in bits, 8-15) on the server-side deflate window. If the client offers
259 * {@code server_max_window_bits=N}, the negotiated value is {@code min(N, serverWindowSize)}.
260 * Per <a href="https://tools.ietf.org/html/rfc7692#section-7.1.2.1">RFC 7692 §7.1.2.1</a>
261 * the server may include {@code server_max_window_bits} with the same or smaller value than the
262 * offer; this handshaker also advertises it unilaterally when {@code serverWindowSize} is less
263 * than {@link #MAX_WINDOW_SIZE}.
264 * @param memLevel
265 * zlib memory level for the server-side deflater (1-9). Lower values reduce per-connection
266 * memory at the cost of compression ratio.
267 * @param extensionFilterProvider
268 * provides server extension filters for per message deflate encoder and decoder.
269 * @param maxAllocation
270 * Maximum size of the decompression buffer. Must be >= 0. If zero, maximum size is not limited.
271 */
272 public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
273 int preferredClientWindowSize,
274 boolean allowServerNoContext, boolean preferredClientNoContext,
275 int serverWindowSize, int memLevel,
276 WebSocketExtensionFilterProvider extensionFilterProvider,
277 int maxAllocation) {
278 if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) {
279 throw new IllegalArgumentException(
280 "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)");
281 }
282 if (compressionLevel < 0 || compressionLevel > 9) {
283 throw new IllegalArgumentException(
284 "compressionLevel: " + compressionLevel + " (expected: 0-9)");
285 }
286 this.compressionLevel = compressionLevel;
287 this.allowServerWindowSize = allowServerWindowSize;
288 this.preferredClientWindowSize = preferredClientWindowSize;
289 this.allowServerNoContext = allowServerNoContext;
290 this.preferredClientNoContext = preferredClientNoContext;
291 this.serverWindowSize = checkInRange(serverWindowSize, MIN_WINDOW_SIZE, MAX_WINDOW_SIZE, "serverWindowSize");
292 this.memLevel = checkInRange(memLevel, MIN_MEM_LEVEL, MAX_MEM_LEVEL, "memLevel");
293 this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
294 this.maxAllocation = checkPositiveOrZero(maxAllocation, "maxAllocation");
295 }
296
297 @Override
298 public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) {
299 if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) {
300 return null;
301 }
302
303 boolean deflateEnabled = true;
304 int clientWindowSize = MAX_WINDOW_SIZE;
305 int negotiatedServerWindowSize = this.serverWindowSize;
306 boolean serverNoContext = false;
307 boolean clientNoContext = false;
308
309 Iterator<Entry<String, String>> parametersIterator =
310 extensionData.parameters().entrySet().iterator();
311 while (deflateEnabled && parametersIterator.hasNext()) {
312 Entry<String, String> parameter = parametersIterator.next();
313
314 if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
315 // RFC 7692: client_max_window_bits may have a value or no value
316 String value = parameter.getValue();
317 if (value != null) {
318 // Let NumberFormatException bubble up if value is invalid
319 clientWindowSize = Integer.parseInt(value);
320 if (clientWindowSize > MAX_WINDOW_SIZE || clientWindowSize < MIN_WINDOW_SIZE) {
321 deflateEnabled = false;
322 }
323 } else {
324 // No value specified, use preferred client window size
325 clientWindowSize = preferredClientWindowSize;
326 }
327 } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
328 // use provided windowSize if it is allowed
329 if (allowServerWindowSize) {
330 int clientOfferedServerWindowSize = Integer.parseInt(parameter.getValue());
331 if (clientOfferedServerWindowSize > MAX_WINDOW_SIZE
332 || clientOfferedServerWindowSize < MIN_WINDOW_SIZE) {
333 deflateEnabled = false;
334 } else {
335 // RFC 7692 §7.1.2.1: server accepts with the same or smaller value than the offer.
336 // Cap at the configured serverWindowSize so the server's memory bound is respected.
337 negotiatedServerWindowSize = Math.min(clientOfferedServerWindowSize, this.serverWindowSize);
338 }
339 } else {
340 deflateEnabled = false;
341 }
342 } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
343 // use preferred clientNoContext because client is compatible with customization
344 clientNoContext = preferredClientNoContext;
345 } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
346 // use server no context if allowed
347 if (allowServerNoContext) {
348 serverNoContext = true;
349 } else {
350 deflateEnabled = false;
351 }
352 } else {
353 // unknown parameter
354 deflateEnabled = false;
355 }
356 }
357
358 if (deflateEnabled) {
359 return new PermessageDeflateExtension(compressionLevel, serverNoContext,
360 negotiatedServerWindowSize, memLevel, clientNoContext, clientWindowSize,
361 extensionFilterProvider, maxAllocation);
362 } else {
363 return null;
364 }
365 }
366
367 private static class PermessageDeflateExtension implements WebSocketServerExtension {
368
369 private final int compressionLevel;
370 private final boolean serverNoContext;
371 private final int serverWindowSize;
372 private final int memLevel;
373 private final boolean clientNoContext;
374 private final int clientWindowSize;
375 private final WebSocketExtensionFilterProvider extensionFilterProvider;
376 private final int maxAllocation;
377
378 PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
379 int serverWindowSize, int memLevel, boolean clientNoContext, int clientWindowSize,
380 WebSocketExtensionFilterProvider extensionFilterProvider, int maxAllocation) {
381 this.compressionLevel = compressionLevel;
382 this.serverNoContext = serverNoContext;
383 this.serverWindowSize = serverWindowSize;
384 this.memLevel = memLevel;
385 this.clientNoContext = clientNoContext;
386 this.clientWindowSize = clientWindowSize;
387 this.extensionFilterProvider = extensionFilterProvider;
388 this.maxAllocation = maxAllocation;
389 }
390
391 @Override
392 public int rsv() {
393 return RSV1;
394 }
395
396 @Override
397 public WebSocketExtensionEncoder newExtensionEncoder() {
398 return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, memLevel, serverNoContext,
399 extensionFilterProvider.encoderFilter());
400 }
401
402 @Override
403 public WebSocketExtensionDecoder newExtensionDecoder() {
404 return new PerMessageDeflateDecoder(clientNoContext, extensionFilterProvider.decoderFilter(),
405 maxAllocation);
406 }
407
408 @Override
409 public WebSocketExtensionData newReponseData() {
410 HashMap<String, String> parameters = new HashMap<String, String>(4);
411 if (serverNoContext) {
412 parameters.put(SERVER_NO_CONTEXT, null);
413 }
414 if (clientNoContext) {
415 parameters.put(CLIENT_NO_CONTEXT, null);
416 }
417 if (serverWindowSize != MAX_WINDOW_SIZE) {
418 parameters.put(SERVER_MAX_WINDOW, Integer.toString(serverWindowSize));
419 }
420 if (clientWindowSize != MAX_WINDOW_SIZE) {
421 parameters.put(CLIENT_MAX_WINDOW, Integer.toString(clientWindowSize));
422 }
423 return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters);
424 }
425 }
426
427 }