View Javadoc
1   /*
2    * Copyright 2019 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;
17  
18  import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
19  import io.netty.util.internal.ObjectUtil;
20  
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.LinkedHashSet;
25  import java.util.Locale;
26  import java.util.Set;
27  
28  import static io.netty.util.internal.ObjectUtil.checkNonEmpty;
29  import static io.netty.util.internal.ObjectUtil.checkNotNull;
30  import static io.netty.util.internal.ObjectUtil.checkPositive;
31  
32  /**
33   * WebSocket server configuration.
34   * <p>
35   * <strong>Security note:</strong> By default no validation of the {@code Origin} header is performed during the
36   * handshake. Browsers do not apply the same-origin policy to WebSocket connections and send ambient credentials
37   * (like cookies) with the handshake request, so applications that authenticate WebSocket connections using such
38   * credentials are exposed to Cross-Site WebSocket Hijacking unless they validate the {@code Origin} header.
39   * Such applications <strong>must</strong> configure {@link Builder#allowedOrigins(String...)} (or perform an
40   * equivalent check in a handler placed before {@link WebSocketServerProtocolHandler}).
41   */
42  public final class WebSocketServerProtocolConfig {
43  
44      static final long DEFAULT_HANDSHAKE_TIMEOUT_MILLIS = 10000L;
45      static final long DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS = 10000L;
46  
47      private final String websocketPath;
48      private final String subprotocols;
49      private final boolean checkStartsWith;
50      private final long handshakeTimeoutMillis;
51      private final long forceCloseTimeoutMillis;
52      private final boolean handleCloseFrames;
53      private final WebSocketCloseStatus sendCloseFrame;
54      private final boolean dropPongFrames;
55      private final WebSocketDecoderConfig decoderConfig;
56      private final Set<String> allowedOrigins;
57  
58      private WebSocketServerProtocolConfig(
59          String websocketPath,
60          String subprotocols,
61          boolean checkStartsWith,
62          long handshakeTimeoutMillis,
63          long forceCloseTimeoutMillis,
64          boolean handleCloseFrames,
65          WebSocketCloseStatus sendCloseFrame,
66          boolean dropPongFrames,
67          WebSocketDecoderConfig decoderConfig,
68          Set<String> allowedOrigins
69      ) {
70          this.websocketPath = websocketPath;
71          this.subprotocols = subprotocols;
72          this.checkStartsWith = checkStartsWith;
73          this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
74          this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
75          this.handleCloseFrames = handleCloseFrames;
76          this.sendCloseFrame = sendCloseFrame;
77          this.dropPongFrames = dropPongFrames;
78          this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
79          this.allowedOrigins = allowedOrigins;
80      }
81  
82      public String websocketPath() {
83          return websocketPath;
84      }
85  
86      public String subprotocols() {
87          return subprotocols;
88      }
89  
90      public boolean checkStartsWith() {
91          return checkStartsWith;
92      }
93  
94      public long handshakeTimeoutMillis() {
95          return handshakeTimeoutMillis;
96      }
97  
98      public long forceCloseTimeoutMillis() {
99          return forceCloseTimeoutMillis;
100     }
101 
102     public boolean handleCloseFrames() {
103         return handleCloseFrames;
104     }
105 
106     public WebSocketCloseStatus sendCloseFrame() {
107         return sendCloseFrame;
108     }
109 
110     public boolean dropPongFrames() {
111         return dropPongFrames;
112     }
113 
114     public WebSocketDecoderConfig decoderConfig() {
115         return decoderConfig;
116     }
117 
118     /**
119      * Returns the (lower-cased) origins that are allowed to open a WebSocket connection, or an empty set if the
120      * {@code Origin} header is not validated.
121      */
122     public Set<String> allowedOrigins() {
123         return allowedOrigins;
124     }
125 
126     @Override
127     public String toString() {
128         return "WebSocketServerProtocolConfig" +
129             " {websocketPath=" + websocketPath +
130             ", subprotocols=" + subprotocols +
131             ", checkStartsWith=" + checkStartsWith +
132             ", handshakeTimeoutMillis=" + handshakeTimeoutMillis +
133             ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis +
134             ", handleCloseFrames=" + handleCloseFrames +
135             ", sendCloseFrame=" + sendCloseFrame +
136             ", dropPongFrames=" + dropPongFrames +
137             ", decoderConfig=" + decoderConfig +
138             ", allowedOrigins=" + allowedOrigins +
139             "}";
140     }
141 
142     public Builder toBuilder() {
143         return new Builder(this);
144     }
145 
146     public static Builder newBuilder() {
147         return new Builder("/", null, false, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS,
148             DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS, true, WebSocketCloseStatus.NORMAL_CLOSURE, true,
149             WebSocketDecoderConfig.DEFAULT, Collections.<String>emptySet());
150     }
151 
152     public static final class Builder {
153         private String websocketPath;
154         private String subprotocols;
155         private boolean checkStartsWith;
156         private long handshakeTimeoutMillis;
157         private long forceCloseTimeoutMillis;
158         private boolean handleCloseFrames;
159         private WebSocketCloseStatus sendCloseFrame;
160         private boolean dropPongFrames;
161         private WebSocketDecoderConfig decoderConfig;
162         private WebSocketDecoderConfig.Builder decoderConfigBuilder;
163         private Set<String> allowedOrigins;
164 
165         private Builder(WebSocketServerProtocolConfig serverConfig) {
166             this(ObjectUtil.checkNotNull(serverConfig, "serverConfig").websocketPath(),
167                  serverConfig.subprotocols(),
168                  serverConfig.checkStartsWith(),
169                  serverConfig.handshakeTimeoutMillis(),
170                  serverConfig.forceCloseTimeoutMillis(),
171                  serverConfig.handleCloseFrames(),
172                  serverConfig.sendCloseFrame(),
173                  serverConfig.dropPongFrames(),
174                  serverConfig.decoderConfig(),
175                  serverConfig.allowedOrigins()
176             );
177         }
178 
179         private Builder(String websocketPath,
180                         String subprotocols,
181                         boolean checkStartsWith,
182                         long handshakeTimeoutMillis,
183                         long forceCloseTimeoutMillis,
184                         boolean handleCloseFrames,
185                         WebSocketCloseStatus sendCloseFrame,
186                         boolean dropPongFrames,
187                         WebSocketDecoderConfig decoderConfig,
188                         Set<String> allowedOrigins) {
189             this.websocketPath = websocketPath;
190             this.subprotocols = subprotocols;
191             this.checkStartsWith = checkStartsWith;
192             this.handshakeTimeoutMillis = handshakeTimeoutMillis;
193             this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
194             this.handleCloseFrames = handleCloseFrames;
195             this.sendCloseFrame = sendCloseFrame;
196             this.dropPongFrames = dropPongFrames;
197             this.decoderConfig = decoderConfig;
198             this.allowedOrigins = allowedOrigins;
199         }
200 
201         /**
202          * URI path component to handle websocket upgrade requests on.
203          */
204         public Builder websocketPath(String websocketPath) {
205             this.websocketPath = websocketPath;
206             return this;
207         }
208 
209         /**
210          * CSV of supported protocols
211          */
212         public Builder subprotocols(String subprotocols) {
213             this.subprotocols = subprotocols;
214             return this;
215         }
216 
217         /**
218          * {@code true} to handle all requests, where URI path component starts from
219          * {@link WebSocketServerProtocolConfig#websocketPath()}, {@code false} for exact match (default).
220          */
221         public Builder checkStartsWith(boolean checkStartsWith) {
222             this.checkStartsWith = checkStartsWith;
223             return this;
224         }
225 
226         /**
227          * Handshake timeout in mills, when handshake timeout, will trigger user
228          * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
229          */
230         public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
231             this.handshakeTimeoutMillis = handshakeTimeoutMillis;
232             return this;
233         }
234 
235         /**
236          * Close the connection if it was not closed by the client after timeout specified
237          */
238         public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
239             this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
240             return this;
241         }
242 
243         /**
244          * {@code true} if close frames should not be forwarded and just close the channel
245          */
246         public Builder handleCloseFrames(boolean handleCloseFrames) {
247             this.handleCloseFrames = handleCloseFrames;
248             return this;
249         }
250 
251         /**
252          * Close frame to send, when close frame was not send manually. Or {@code null} to disable proper close.
253          */
254         public Builder sendCloseFrame(WebSocketCloseStatus sendCloseFrame) {
255             this.sendCloseFrame = sendCloseFrame;
256             return this;
257         }
258 
259         /**
260          * {@code true} if pong frames should not be forwarded
261          */
262         public Builder dropPongFrames(boolean dropPongFrames) {
263             this.dropPongFrames = dropPongFrames;
264             return this;
265         }
266 
267         /**
268          * Frames decoder configuration.
269          */
270         public Builder decoderConfig(WebSocketDecoderConfig decoderConfig) {
271             this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
272             this.decoderConfigBuilder = null;
273             return this;
274         }
275 
276         /**
277          * The origins (for example {@code https://example.com} or {@code http://localhost:8080}) that are allowed
278          * to open a WebSocket connection. Origins are compared case-insensitively against the {@code Origin}
279          * request header (and the {@code Sec-WebSocket-Origin} header used by the older hybi-07 and hybi-08 protocol
280          * versions), which must match exactly (scheme, host and port). Handshake requests carrying an
281          * origin that is not allowed are rejected with {@code 403 Forbidden} and the connection is closed.
282          * <p>
283          * Requests without an {@code Origin} or {@code Sec-WebSocket-Origin} header are not subject to this check,
284          * as they do not originate from a browser (which always sends the header on WebSocket handshakes).
285          * <p>
286          * If not configured (the default) the {@code Origin} header is <strong>not</strong> validated. Applications
287          * that rely on ambient credentials such as cookies <strong>must</strong> configure this to prevent
288          * Cross-Site WebSocket Hijacking.
289          *
290          * @param allowedOrigins the allowed origins, must not be empty.
291          */
292         public Builder allowedOrigins(String... allowedOrigins) {
293             return allowedOrigins(Arrays.asList(checkNonEmpty(allowedOrigins, "allowedOrigins")));
294         }
295 
296         /**
297          * See {@link #allowedOrigins(String...)}.
298          *
299          * @param allowedOrigins the allowed origins, must not be empty.
300          */
301         public Builder allowedOrigins(Collection<String> allowedOrigins) {
302             checkNonEmpty(allowedOrigins, "allowedOrigins");
303             Set<String> origins = new LinkedHashSet<String>(allowedOrigins.size());
304             for (String origin : allowedOrigins) {
305                 origins.add(checkNotNull(origin, "origin").toLowerCase(Locale.ROOT));
306             }
307             this.allowedOrigins = Collections.unmodifiableSet(origins);
308             return this;
309         }
310 
311         private WebSocketDecoderConfig.Builder decoderConfigBuilder() {
312             if (decoderConfigBuilder == null) {
313                 decoderConfigBuilder = decoderConfig.toBuilder();
314             }
315             return decoderConfigBuilder;
316         }
317 
318         public Builder maxFramePayloadLength(int maxFramePayloadLength) {
319             decoderConfigBuilder().maxFramePayloadLength(maxFramePayloadLength);
320             return this;
321         }
322 
323         public Builder expectMaskedFrames(boolean expectMaskedFrames) {
324             decoderConfigBuilder().expectMaskedFrames(expectMaskedFrames);
325             return this;
326         }
327 
328         public Builder allowMaskMismatch(boolean allowMaskMismatch) {
329             decoderConfigBuilder().allowMaskMismatch(allowMaskMismatch);
330             return this;
331         }
332 
333         public Builder allowExtensions(boolean allowExtensions) {
334             decoderConfigBuilder().allowExtensions(allowExtensions);
335             return this;
336         }
337 
338         public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) {
339             decoderConfigBuilder().closeOnProtocolViolation(closeOnProtocolViolation);
340             return this;
341         }
342 
343         public Builder withUTF8Validator(boolean withUTF8Validator) {
344             decoderConfigBuilder().withUTF8Validator(withUTF8Validator);
345             return this;
346         }
347 
348         /**
349          * Build unmodifiable server protocol configuration.
350          */
351         public WebSocketServerProtocolConfig build() {
352             return new WebSocketServerProtocolConfig(
353                 websocketPath,
354                 subprotocols,
355                 checkStartsWith,
356                 handshakeTimeoutMillis,
357                 forceCloseTimeoutMillis,
358                 handleCloseFrames,
359                 sendCloseFrame,
360                 dropPongFrames,
361                 decoderConfigBuilder == null ? decoderConfig : decoderConfigBuilder.build(),
362                 allowedOrigins
363             );
364         }
365     }
366 }