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