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      private final int compressionLevel;
48      private final boolean allowServerWindowSize;
49      private final int preferredClientWindowSize;
50      private final boolean allowServerNoContext;
51      private final boolean preferredClientNoContext;
52      private final WebSocketExtensionFilterProvider extensionFilterProvider;
53      private final int maxAllocation;
54  
55      /**
56       * Constructor with default configuration.
57       *
58       * @deprecated
59       *            Use {@link PerMessageDeflateServerExtensionHandshaker#
60       *            PerMessageDeflateServerExtensionHandshaker(int)}.
61       */
62      @Deprecated
63      public PerMessageDeflateServerExtensionHandshaker() {
64          this(0);
65      }
66  
67      /**
68       * Constructor with default configuration.
69       *
70       * @param maxAllocation
71       *            Maximum size of the decompression buffer. Must be &gt;= 0. If zero, maximum size is not limited.
72       */
73      public PerMessageDeflateServerExtensionHandshaker(int maxAllocation) {
74          this(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, false, false, maxAllocation);
75      }
76  
77      /**
78       * Constructor with custom configuration.
79       *
80       * @param compressionLevel
81       *            Compression level between 0 and 9 (default is 6).
82       * @param allowServerWindowSize
83       *            allows WebSocket client to customize the server inflater window size
84       *            (default is false).
85       * @param preferredClientWindowSize
86       *            indicates the preferred client window size to use if client inflater is customizable.
87       * @param allowServerNoContext
88       *            allows WebSocket client to activate server_no_context_takeover
89       *            (default is false).
90       * @param preferredClientNoContext
91       *            indicates if server prefers to activate client_no_context_takeover
92       *            if client is compatible with (default is false).
93       * @deprecated
94       *            Use {@link PerMessageDeflateServerExtensionHandshaker#PerMessageDeflateServerExtensionHandshaker(
95       *            int, boolean, int, boolean, boolean, int)}.
96       */
97      @Deprecated
98      public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
99                                                        int preferredClientWindowSize,
100                                                       boolean allowServerNoContext, boolean preferredClientNoContext) {
101         this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
102                 preferredClientNoContext, 0);
103     }
104 
105     /**
106      * Constructor with custom configuration.
107      *
108      * @param compressionLevel
109      *            Compression level between 0 and 9 (default is 6).
110      * @param allowServerWindowSize
111      *            allows WebSocket client to customize the server inflater window size
112      *            (default is false).
113      * @param preferredClientWindowSize
114      *            indicates the preferred client window size to use if client inflater is customizable.
115      * @param allowServerNoContext
116      *            allows WebSocket client to activate server_no_context_takeover
117      *            (default is false).
118      * @param preferredClientNoContext
119      *            indicates if server prefers to activate client_no_context_takeover
120      *            if client is compatible with (default is false).
121      * @param maxAllocation
122      *            Maximum size of the decompression buffer. Must be &gt;= 0. If zero, maximum size is not limited.
123      */
124     public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
125             int preferredClientWindowSize,
126             boolean allowServerNoContext, boolean preferredClientNoContext, int maxAllocation) {
127         this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
128              preferredClientNoContext, WebSocketExtensionFilterProvider.DEFAULT, maxAllocation);
129     }
130 
131     /**
132      * Constructor with custom configuration.
133      *
134      * @param compressionLevel
135      *            Compression level between 0 and 9 (default is 6).
136      * @param allowServerWindowSize
137      *            allows WebSocket client to customize the server inflater window size
138      *            (default is false).
139      * @param preferredClientWindowSize
140      *            indicates the preferred client window size to use if client inflater is customizable.
141      * @param allowServerNoContext
142      *            allows WebSocket client to activate server_no_context_takeover
143      *            (default is false).
144      * @param preferredClientNoContext
145      *            indicates if server prefers to activate client_no_context_takeover
146      *            if client is compatible with (default is false).
147      * @param extensionFilterProvider
148      *            provides server extension filters for per message deflate encoder and decoder.
149      * @deprecated
150      *            Use {@link PerMessageDeflateServerExtensionHandshaker#PerMessageDeflateServerExtensionHandshaker(
151      *            int, boolean, int, boolean, boolean, WebSocketExtensionFilterProvider, int)}.
152      */
153     @Deprecated
154     public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
155                                                       int preferredClientWindowSize,
156                                                       boolean allowServerNoContext, boolean preferredClientNoContext,
157                                                       WebSocketExtensionFilterProvider extensionFilterProvider) {
158         this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
159                 preferredClientNoContext, extensionFilterProvider, 0);
160     }
161 
162     /**
163      * Constructor with custom configuration.
164      *
165      * @param compressionLevel
166      *            Compression level between 0 and 9 (default is 6).
167      * @param allowServerWindowSize
168      *            allows WebSocket client to customize the server inflater window size
169      *            (default is false).
170      * @param preferredClientWindowSize
171      *            indicates the preferred client window size to use if client inflater is customizable.
172      * @param allowServerNoContext
173      *            allows WebSocket client to activate server_no_context_takeover
174      *            (default is false).
175      * @param preferredClientNoContext
176      *            indicates if server prefers to activate client_no_context_takeover
177      *            if client is compatible with (default is false).
178      * @param extensionFilterProvider
179      *            provides server extension filters for per message deflate encoder and decoder.
180      * @param maxAllocation
181      *            Maximum size of the decompression buffer. Must be &gt;= 0. If zero, maximum size is not limited.
182      */
183     public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
184             int preferredClientWindowSize,
185             boolean allowServerNoContext, boolean preferredClientNoContext,
186             WebSocketExtensionFilterProvider extensionFilterProvider,
187             int maxAllocation) {
188         if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) {
189             throw new IllegalArgumentException(
190                     "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)");
191         }
192         if (compressionLevel < 0 || compressionLevel > 9) {
193             throw new IllegalArgumentException(
194                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
195         }
196         this.compressionLevel = compressionLevel;
197         this.allowServerWindowSize = allowServerWindowSize;
198         this.preferredClientWindowSize = preferredClientWindowSize;
199         this.allowServerNoContext = allowServerNoContext;
200         this.preferredClientNoContext = preferredClientNoContext;
201         this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
202         this.maxAllocation = checkPositiveOrZero(maxAllocation, "maxAllocation");
203     }
204 
205     @Override
206     public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) {
207         if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) {
208             return null;
209         }
210 
211         boolean deflateEnabled = true;
212         int clientWindowSize = MAX_WINDOW_SIZE;
213         int serverWindowSize = MAX_WINDOW_SIZE;
214         boolean serverNoContext = false;
215         boolean clientNoContext = false;
216 
217         Iterator<Entry<String, String>> parametersIterator =
218                 extensionData.parameters().entrySet().iterator();
219         while (deflateEnabled && parametersIterator.hasNext()) {
220             Entry<String, String> parameter = parametersIterator.next();
221 
222             if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
223              // use preferred clientWindowSize because client is compatible with customization
224                 clientWindowSize = preferredClientWindowSize;
225             } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
226                 // use provided windowSize if it is allowed
227                 if (allowServerWindowSize) {
228                     serverWindowSize = Integer.parseInt(parameter.getValue());
229                     if (serverWindowSize > MAX_WINDOW_SIZE || serverWindowSize < MIN_WINDOW_SIZE) {
230                         deflateEnabled = false;
231                     }
232                 } else {
233                     deflateEnabled = false;
234                 }
235             } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
236                 // use preferred clientNoContext because client is compatible with customization
237                 clientNoContext = preferredClientNoContext;
238             } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
239                 // use server no context if allowed
240                 if (allowServerNoContext) {
241                     serverNoContext = true;
242                 } else {
243                     deflateEnabled = false;
244                 }
245             } else {
246                 // unknown parameter
247                 deflateEnabled = false;
248             }
249         }
250 
251         if (deflateEnabled) {
252             return new PermessageDeflateExtension(compressionLevel, serverNoContext,
253                     serverWindowSize, clientNoContext, clientWindowSize, extensionFilterProvider, maxAllocation);
254         } else {
255             return null;
256         }
257     }
258 
259     private static class PermessageDeflateExtension implements WebSocketServerExtension {
260 
261         private final int compressionLevel;
262         private final boolean serverNoContext;
263         private final int serverWindowSize;
264         private final boolean clientNoContext;
265         private final int clientWindowSize;
266         private final WebSocketExtensionFilterProvider extensionFilterProvider;
267         private final int maxAllocation;
268 
269         PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
270                 int serverWindowSize, boolean clientNoContext, int clientWindowSize,
271                 WebSocketExtensionFilterProvider extensionFilterProvider, int maxAllocation) {
272             this.compressionLevel = compressionLevel;
273             this.serverNoContext = serverNoContext;
274             this.serverWindowSize = serverWindowSize;
275             this.clientNoContext = clientNoContext;
276             this.clientWindowSize = clientWindowSize;
277             this.extensionFilterProvider = extensionFilterProvider;
278             this.maxAllocation = maxAllocation;
279         }
280 
281         @Override
282         public int rsv() {
283             return RSV1;
284         }
285 
286         @Override
287         public WebSocketExtensionEncoder newExtensionEncoder() {
288             return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext,
289                                                 extensionFilterProvider.encoderFilter());
290         }
291 
292         @Override
293         public WebSocketExtensionDecoder newExtensionDecoder() {
294             return new PerMessageDeflateDecoder(clientNoContext, extensionFilterProvider.decoderFilter(),
295                                                 maxAllocation);
296         }
297 
298         @Override
299         public WebSocketExtensionData newReponseData() {
300             HashMap<String, String> parameters = new HashMap<String, String>(4);
301             if (serverNoContext) {
302                 parameters.put(SERVER_NO_CONTEXT, null);
303             }
304             if (clientNoContext) {
305                 parameters.put(CLIENT_NO_CONTEXT, null);
306             }
307             if (serverWindowSize != MAX_WINDOW_SIZE) {
308                 parameters.put(SERVER_MAX_WINDOW, Integer.toString(serverWindowSize));
309             }
310             if (clientWindowSize != MAX_WINDOW_SIZE) {
311                 parameters.put(CLIENT_MAX_WINDOW, Integer.toString(clientWindowSize));
312             }
313             return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters);
314         }
315     }
316 
317 }