View Javadoc
1   /*
2    * Copyright 2012 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.netty5.handler.codec.http.websocketx;
17  
18  import io.netty5.buffer.api.BufferAllocator;
19  import io.netty5.handler.codec.http.DefaultFullHttpRequest;
20  import io.netty5.handler.codec.http.FullHttpRequest;
21  import io.netty5.handler.codec.http.FullHttpResponse;
22  import io.netty5.handler.codec.http.HttpHeaderNames;
23  import io.netty5.handler.codec.http.HttpHeaderValues;
24  import io.netty5.handler.codec.http.HttpHeaders;
25  import io.netty5.handler.codec.http.HttpMethod;
26  import io.netty5.handler.codec.http.HttpResponseStatus;
27  import io.netty5.handler.codec.http.HttpVersion;
28  import io.netty5.util.internal.StringUtil;
29  
30  import java.net.URI;
31  
32  /**
33   * <p>
34   * Performs client side opening and closing handshakes for web socket specification version
35   * <a href="https://datatracker.ietf.org/doc/html/rfc6455">websocketprotocol-v13</a>
36   * </p>
37   */
38  public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
39  
40      private final boolean allowExtensions;
41      private final boolean performMasking;
42      private final boolean allowMaskMismatch;
43  
44      private volatile String sentNonce;
45  
46      /**
47       * Creates a new instance.
48       *
49       * @param webSocketURL
50       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
51       *            sent to this URL.
52       * @param subprotocol
53       *            Sub protocol request sent to the server.
54       * @param allowExtensions
55       *            Allow extensions to be used in the reserved bits of the web socket frame
56       * @param customHeaders
57       *            Map of custom headers to add to the client request
58       * @param maxFramePayloadLength
59       *            Maximum length of a frame's payload
60       */
61      public WebSocketClientHandshaker13(URI webSocketURL, String subprotocol,
62                                         boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
63          this(webSocketURL, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
64                  true, false);
65      }
66  
67      /**
68       * Creates a new instance.
69       *
70       * @param webSocketURL
71       *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
72       *            sent to this URL.
73       * @param subprotocol
74       *            Sub protocol request sent to the server.
75       * @param allowExtensions
76       *            Allow extensions to be used in the reserved bits of the web socket frame
77       * @param customHeaders
78       *            Map of custom headers to add to the client request
79       * @param maxFramePayloadLength
80       *            Maximum length of a frame's payload
81       * @param performMasking
82       *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
83       *            with the websocket specifications. Client applications that communicate with a non-standard server
84       *            which doesn't require masking might set this to false to achieve a higher performance.
85       * @param allowMaskMismatch
86       *            When set to true, frames which are not masked properly according to the standard will still be
87       *            accepted.
88       */
89      public WebSocketClientHandshaker13(URI webSocketURL, String subprotocol,
90              boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
91              boolean performMasking, boolean allowMaskMismatch) {
92          this(webSocketURL, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
93                  performMasking, allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
94      }
95  
96      /**
97       * Creates a new instance.
98       *
99       * @param webSocketURL
100      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
101      *            sent to this URL.
102      * @param subprotocol
103      *            Sub protocol request sent to the server.
104      * @param allowExtensions
105      *            Allow extensions to be used in the reserved bits of the web socket frame
106      * @param customHeaders
107      *            Map of custom headers to add to the client request
108      * @param maxFramePayloadLength
109      *            Maximum length of a frame's payload
110      * @param performMasking
111      *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
112      *            with the websocket specifications. Client applications that communicate with a non-standard server
113      *            which doesn't require masking might set this to false to achieve a higher performance.
114      * @param allowMaskMismatch
115      *            When set to true, frames which are not masked properly according to the standard will still be
116      *            accepted
117      * @param forceCloseTimeoutMillis
118      *            Close the connection if it was not closed by the server after timeout specified.
119      */
120     public WebSocketClientHandshaker13(URI webSocketURL, String subprotocol,
121                                        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
122                                        boolean performMasking, boolean allowMaskMismatch,
123                                        long forceCloseTimeoutMillis) {
124         this(webSocketURL, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
125                 allowMaskMismatch, forceCloseTimeoutMillis, false);
126     }
127 
128     /**
129      * Creates a new instance.
130      *
131      * @param webSocketURL
132      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
133      *            sent to this URL.
134      * @param subprotocol
135      *            Sub protocol request sent to the server.
136      * @param allowExtensions
137      *            Allow extensions to be used in the reserved bits of the web socket frame
138      * @param customHeaders
139      *            Map of custom headers to add to the client request
140      * @param maxFramePayloadLength
141      *            Maximum length of a frame's payload
142      * @param performMasking
143      *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
144      *            with the websocket specifications. Client applications that communicate with a non-standard server
145      *            which doesn't require masking might set this to false to achieve a higher performance.
146      * @param allowMaskMismatch
147      *            When set to true, frames which are not masked properly according to the standard will still be
148      *            accepted
149      * @param forceCloseTimeoutMillis
150      *            Close the connection if it was not closed by the server after timeout specified.
151      * @param  absoluteUpgradeUrl
152      *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
153      *            clear HTTP
154      */
155     WebSocketClientHandshaker13(URI webSocketURL, String subprotocol,
156                                 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
157                                 boolean performMasking, boolean allowMaskMismatch,
158                                 long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
159         super(webSocketURL, WebSocketVersion.V13, subprotocol, customHeaders, maxFramePayloadLength,
160               forceCloseTimeoutMillis, absoluteUpgradeUrl);
161         this.allowExtensions = allowExtensions;
162         this.performMasking = performMasking;
163         this.allowMaskMismatch = allowMaskMismatch;
164     }
165 
166     /**
167      * /**
168      * <p>
169      * Sends the opening request to the server:
170      * </p>
171      *
172      * <pre>
173      * GET /chat HTTP/1.1
174      * Host: server.example.com
175      * Upgrade: websocket
176      * Connection: Upgrade
177      * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
178      * Sec-WebSocket-Protocol: chat, superchat
179      * Sec-WebSocket-Version: 13
180      * </pre>
181      *
182      */
183     @Override
184     protected FullHttpRequest newHandshakeRequest(BufferAllocator allocator) {
185         URI wsURL = uri();
186         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
187                 allocator.allocate(0));
188         HttpHeaders headers = request.headers();
189 
190         if (customHeaders != null) {
191             headers.add(customHeaders);
192             if (!headers.contains(HttpHeaderNames.HOST)) {
193                 // Only add HOST header if customHeaders did not contain it.
194                 // See https://github.com/netty/netty/issues/10101.
195                 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
196             }
197         } else {
198             headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
199         }
200 
201         String nonce = createNonce();
202         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
203                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
204                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, nonce);
205 
206         sentNonce = nonce;
207         String expectedSubprotocol = expectedSubprotocol();
208         if (!StringUtil.isNullOrEmpty(expectedSubprotocol)) {
209             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
210         }
211 
212         headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
213         return request;
214     }
215 
216     /**
217      * <p>
218      * Process server response:
219      * </p>
220      *
221      * <pre>
222      * HTTP/1.1 101 Switching Protocols
223      * Upgrade: websocket
224      * Connection: Upgrade
225      * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
226      * Sec-WebSocket-Protocol: chat
227      * </pre>
228      *
229      * @param response
230      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
231      * @throws WebSocketHandshakeException if handshake response is invalid.
232      */
233     @Override
234     protected void verify(FullHttpResponse response) {
235         HttpResponseStatus status = response.status();
236         if (!HttpResponseStatus.SWITCHING_PROTOCOLS.equals(status)) {
237             throw new WebSocketClientHandshakeException("Invalid handshake response status: " + status, response);
238         }
239 
240         HttpHeaders headers = response.headers();
241         CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
242         if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
243             throw new WebSocketClientHandshakeException("Invalid handshake response upgrade: " + upgrade, response);
244         }
245 
246         if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
247             throw new WebSocketClientHandshakeException("Invalid handshake response connection: "
248                     + headers.get(HttpHeaderNames.CONNECTION), response);
249         }
250 
251         String accept = headers.get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT);
252         if (accept == null) {
253             throw new WebSocketClientHandshakeException("Invalid handshake response sec-websocket-accept: null",
254                                                         response);
255         }
256 
257         String expectedAccept = WebSocketUtil.calculateV13Accept(sentNonce);
258         if (!expectedAccept.equals(accept.trim())) {
259             throw new WebSocketClientHandshakeException("Invalid handshake response sec-websocket-accept: " + accept +
260                                                         ", expected: " + expectedAccept, response);
261         }
262     }
263 
264     @Override
265     protected WebSocketFrameDecoder newWebsocketDecoder() {
266         return new WebSocket13FrameDecoder(false, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
267     }
268 
269     @Override
270     protected WebSocketFrameEncoder newWebSocketEncoder() {
271         return new WebSocket13FrameEncoder(performMasking);
272     }
273 
274     @Override
275     public WebSocketClientHandshaker13 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
276         super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
277         return this;
278     }
279 
280     /**
281      * Creates a nonce consisting of a randomly selected 16-byte value
282      * that has been base64-encoded.
283      */
284     private static String createNonce() {
285         var nonce = WebSocketUtil.randomBytes(16);
286         return WebSocketUtil.base64(nonce);
287     }
288 }