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 }