View Javadoc
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 &gt;= 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 &gt;= 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 &gt;= 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 &gt;= 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 &gt;= 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 }