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    *   http://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.WebSocketServerExtension;
23  import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker;
24  
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.Map.Entry;
28  
29  /**
30   * <a href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a>
31   * handshake implementation.
32   */
33  public final class PerMessageDeflateServerExtensionHandshaker implements WebSocketServerExtensionHandshaker {
34  
35      public static final int MIN_WINDOW_SIZE = 8;
36      public static final int MAX_WINDOW_SIZE = 15;
37  
38      static final String PERMESSAGE_DEFLATE_EXTENSION = "permessage-deflate";
39      static final String CLIENT_MAX_WINDOW = "client_max_window_bits";
40      static final String SERVER_MAX_WINDOW = "server_max_window_bits";
41      static final String CLIENT_NO_CONTEXT = "client_no_context_takeover";
42      static final String SERVER_NO_CONTEXT = "server_no_context_takeover";
43  
44      private final int compressionLevel;
45      private final boolean allowServerWindowSize;
46      private final int preferredClientWindowSize;
47      private final boolean allowServerNoContext;
48      private final boolean preferredClientNoContext;
49  
50      /**
51       * Constructor with default configuration.
52       */
53      public PerMessageDeflateServerExtensionHandshaker() {
54          this(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, false, false);
55      }
56  
57      /**
58       * Constructor with custom configuration.
59       *
60       * @param compressionLevel
61       *            Compression level between 0 and 9 (default is 6).
62       * @param allowServerWindowSize
63       *            allows WebSocket client to customize the server inflater window size
64       *            (default is false).
65       * @param preferredClientWindowSize
66       *            indicates the preferred client window size to use if client inflater is customizable.
67       * @param allowServerNoContext
68       *            allows WebSocket client to activate server_no_context_takeover
69       *            (default is false).
70       * @param preferredClientNoContext
71       *            indicates if server prefers to activate client_no_context_takeover
72       *            if client is compatible with (default is false).
73       */
74      public PerMessageDeflateServerExtensionHandshaker(int compressionLevel,
75              boolean allowServerWindowSize, int preferredClientWindowSize,
76              boolean allowServerNoContext, boolean preferredClientNoContext) {
77          if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) {
78              throw new IllegalArgumentException(
79                      "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)");
80          }
81          if (compressionLevel < 0 || compressionLevel > 9) {
82              throw new IllegalArgumentException(
83                      "compressionLevel: " + compressionLevel + " (expected: 0-9)");
84          }
85          this.compressionLevel = compressionLevel;
86          this.allowServerWindowSize = allowServerWindowSize;
87          this.preferredClientWindowSize = preferredClientWindowSize;
88          this.allowServerNoContext = allowServerNoContext;
89          this.preferredClientNoContext = preferredClientNoContext;
90      }
91  
92      @Override
93      public WebSocketServerExtension handshakeExtension(WebSocketExtensionData extensionData) {
94          if (!PERMESSAGE_DEFLATE_EXTENSION.equals(extensionData.name())) {
95              return null;
96          }
97  
98          boolean deflateEnabled = true;
99          int clientWindowSize = MAX_WINDOW_SIZE;
100         int serverWindowSize = MAX_WINDOW_SIZE;
101         boolean serverNoContext = false;
102         boolean clientNoContext = false;
103 
104         Iterator<Entry<String, String>> parametersIterator =
105                 extensionData.parameters().entrySet().iterator();
106         while (deflateEnabled && parametersIterator.hasNext()) {
107             Entry<String, String> parameter = parametersIterator.next();
108 
109             if (CLIENT_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
110              // use preferred clientWindowSize because client is compatible with customization
111                 clientWindowSize = preferredClientWindowSize;
112             } else if (SERVER_MAX_WINDOW.equalsIgnoreCase(parameter.getKey())) {
113                 // use provided windowSize if it is allowed
114                 if (allowServerWindowSize) {
115                     serverWindowSize = Integer.parseInt(parameter.getValue());
116                     if (serverWindowSize > MAX_WINDOW_SIZE || serverWindowSize < MIN_WINDOW_SIZE) {
117                         deflateEnabled = false;
118                     }
119                 } else {
120                     deflateEnabled = false;
121                 }
122             } else if (CLIENT_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
123                 // use preferred clientNoContext because client is compatible with customization
124                 clientNoContext = preferredClientNoContext;
125             } else if (SERVER_NO_CONTEXT.equalsIgnoreCase(parameter.getKey())) {
126                 // use server no context if allowed
127                 if (allowServerNoContext) {
128                     serverNoContext = true;
129                 } else {
130                     deflateEnabled = false;
131                 }
132             } else {
133                 // unknown parameter
134                 deflateEnabled = false;
135             }
136         }
137 
138         if (deflateEnabled) {
139             return new PermessageDeflateExtension(compressionLevel, serverNoContext,
140                     serverWindowSize, clientNoContext, clientWindowSize);
141         } else {
142             return null;
143         }
144     }
145 
146     private static class PermessageDeflateExtension implements WebSocketServerExtension {
147 
148         private final int compressionLevel;
149         private final boolean serverNoContext;
150         private final int serverWindowSize;
151         private final boolean clientNoContext;
152         private final int clientWindowSize;
153 
154         public PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
155                 int serverWindowSize, boolean clientNoContext, int clientWindowSize) {
156             this.compressionLevel = compressionLevel;
157             this.serverNoContext = serverNoContext;
158             this.serverWindowSize = serverWindowSize;
159             this.clientNoContext = clientNoContext;
160             this.clientWindowSize = clientWindowSize;
161         }
162 
163         @Override
164         public int rsv() {
165             return RSV1;
166         }
167 
168         @Override
169         public WebSocketExtensionEncoder newExtensionEncoder() {
170             return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext);
171         }
172 
173         @Override
174         public WebSocketExtensionDecoder newExtensionDecoder() {
175             return new PerMessageDeflateDecoder(serverNoContext);
176         }
177 
178         @Override
179         public WebSocketExtensionData newReponseData() {
180             HashMap<String, String> parameters = new HashMap<String, String>(4);
181             if (serverNoContext) {
182                 parameters.put(SERVER_NO_CONTEXT, null);
183             }
184             if (clientNoContext) {
185                 parameters.put(CLIENT_NO_CONTEXT, null);
186             }
187             if (serverWindowSize != MAX_WINDOW_SIZE) {
188                 parameters.put(SERVER_MAX_WINDOW, Integer.toString(serverWindowSize));
189             }
190             if (clientWindowSize != MAX_WINDOW_SIZE) {
191                 parameters.put(CLIENT_MAX_WINDOW, Integer.toString(clientWindowSize));
192             }
193             return new WebSocketExtensionData(PERMESSAGE_DEFLATE_EXTENSION, parameters);
194         }
195     }
196 
197 }