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  
54      /**
55       * Constructor with default configuration.
56       */
57      public PerMessageDeflateServerExtensionHandshaker() {
58          this(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, false, false);
59      }
60  
61      /**
62       * Constructor with custom configuration.
63       *
64       * @param compressionLevel
65       *            Compression level between 0 and 9 (default is 6).
66       * @param allowServerWindowSize
67       *            allows WebSocket client to customize the server inflater window size
68       *            (default is false).
69       * @param preferredClientWindowSize
70       *            indicates the preferred client window size to use if client inflater is customizable.
71       * @param allowServerNoContext
72       *            allows WebSocket client to activate server_no_context_takeover
73       *            (default is false).
74       * @param preferredClientNoContext
75       *            indicates if server prefers to activate client_no_context_takeover
76       *            if client is compatible with (default is false).
77       */
78      public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
79              int preferredClientWindowSize,
80              boolean allowServerNoContext, boolean preferredClientNoContext) {
81          this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
82               preferredClientNoContext, WebSocketExtensionFilterProvider.DEFAULT);
83      }
84  
85      /**
86       * Constructor with custom configuration.
87       *
88       * @param compressionLevel
89       *            Compression level between 0 and 9 (default is 6).
90       * @param allowServerWindowSize
91       *            allows WebSocket client to customize the server inflater window size
92       *            (default is false).
93       * @param preferredClientWindowSize
94       *            indicates the preferred client window size to use if client inflater is customizable.
95       * @param allowServerNoContext
96       *            allows WebSocket client to activate server_no_context_takeover
97       *            (default is false).
98       * @param preferredClientNoContext
99       *            indicates if server prefers to activate client_no_context_takeover
100      *            if client is compatible with (default is false).
101      * @param extensionFilterProvider
102      *            provides server extension filters for per message deflate encoder and decoder.
103      */
104     public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
105             int preferredClientWindowSize,
106             boolean allowServerNoContext, boolean preferredClientNoContext,
107             WebSocketExtensionFilterProvider extensionFilterProvider) {
108         if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) {
109             throw new IllegalArgumentException(
110                     "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)");
111         }
112         if (compressionLevel < 0 || compressionLevel > 9) {
113             throw new IllegalArgumentException(
114                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
115         }
116         this.compressionLevel = compressionLevel;
117         this.allowServerWindowSize = allowServerWindowSize;
118         this.preferredClientWindowSize = preferredClientWindowSize;
119         this.allowServerNoContext = allowServerNoContext;
120         this.preferredClientNoContext = preferredClientNoContext;
121         this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
122     }
123 
124     @Override
125     public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) {
126         if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) {
127             return null;
128         }
129 
130         boolean deflateEnabled = true;
131         int clientWindowSize = MAX_WINDOW_SIZE;
132         int serverWindowSize = MAX_WINDOW_SIZE;
133         boolean serverNoContext = false;
134         boolean clientNoContext = false;
135 
136         Iterator<Entry<String, String>> parametersIterator =
137                 extensionData.parameters().entrySet().iterator();
138         while (deflateEnabled && parametersIterator.hasNext()) {
139             Entry<String, String> parameter = parametersIterator.next();
140 
141             if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
142              // use preferred clientWindowSize because client is compatible with customization
143                 clientWindowSize = preferredClientWindowSize;
144             } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
145                 // use provided windowSize if it is allowed
146                 if (allowServerWindowSize) {
147                     serverWindowSize = Integer.parseInt(parameter.getValue());
148                     if (serverWindowSize > MAX_WINDOW_SIZE || serverWindowSize < MIN_WINDOW_SIZE) {
149                         deflateEnabled = false;
150                     }
151                 } else {
152                     deflateEnabled = false;
153                 }
154             } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
155                 // use preferred clientNoContext because client is compatible with customization
156                 clientNoContext = preferredClientNoContext;
157             } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
158                 // use server no context if allowed
159                 if (allowServerNoContext) {
160                     serverNoContext = true;
161                 } else {
162                     deflateEnabled = false;
163                 }
164             } else {
165                 // unknown parameter
166                 deflateEnabled = false;
167             }
168         }
169 
170         if (deflateEnabled) {
171             return new PermessageDeflateExtension(compressionLevel, serverNoContext,
172                     serverWindowSize, clientNoContext, clientWindowSize, extensionFilterProvider);
173         } else {
174             return null;
175         }
176     }
177 
178     private static class PermessageDeflateExtension implements WebSocketServerExtension {
179 
180         private final int compressionLevel;
181         private final boolean serverNoContext;
182         private final int serverWindowSize;
183         private final boolean clientNoContext;
184         private final int clientWindowSize;
185         private final WebSocketExtensionFilterProvider extensionFilterProvider;
186 
187         PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
188                 int serverWindowSize, boolean clientNoContext, int clientWindowSize,
189                 WebSocketExtensionFilterProvider extensionFilterProvider) {
190             this.compressionLevel = compressionLevel;
191             this.serverNoContext = serverNoContext;
192             this.serverWindowSize = serverWindowSize;
193             this.clientNoContext = clientNoContext;
194             this.clientWindowSize = clientWindowSize;
195             this.extensionFilterProvider = extensionFilterProvider;
196         }
197 
198         @Override
199         public int rsv() {
200             return RSV1;
201         }
202 
203         @Override
204         public WebSocketExtensionEncoder newExtensionEncoder() {
205             return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext,
206                                                 extensionFilterProvider.encoderFilter());
207         }
208 
209         @Override
210         public WebSocketExtensionDecoder newExtensionDecoder() {
211             return new PerMessageDeflateDecoder(clientNoContext, extensionFilterProvider.decoderFilter());
212         }
213 
214         @Override
215         public WebSocketExtensionData newReponseData() {
216             HashMap<String, String> parameters = new HashMap<String, String>(4);
217             if (serverNoContext) {
218                 parameters.put(SERVER_NO_CONTEXT, null);
219             }
220             if (clientNoContext) {
221                 parameters.put(CLIENT_NO_CONTEXT, null);
222             }
223             if (serverWindowSize != MAX_WINDOW_SIZE) {
224                 parameters.put(SERVER_MAX_WINDOW, Integer.toString(serverWindowSize));
225             }
226             if (clientWindowSize != MAX_WINDOW_SIZE) {
227                 parameters.put(CLIENT_MAX_WINDOW, Integer.toString(clientWindowSize));
228             }
229             return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters);
230         }
231     }
232 
233 }