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.netty.handler.codec.http.websocketx;
17
18 import io.netty.buffer.Unpooled;
19 import io.netty.handler.codec.http.DefaultFullHttpRequest;
20 import io.netty.handler.codec.http.FullHttpRequest;
21 import io.netty.handler.codec.http.FullHttpResponse;
22 import io.netty.handler.codec.http.HttpHeaderNames;
23 import io.netty.handler.codec.http.HttpHeaderValues;
24 import io.netty.handler.codec.http.HttpHeaders;
25 import io.netty.handler.codec.http.HttpMethod;
26 import io.netty.handler.codec.http.HttpResponseStatus;
27 import io.netty.handler.codec.http.HttpVersion;
28 import io.netty.util.CharsetUtil;
29 import io.netty.util.internal.logging.InternalLogger;
30 import io.netty.util.internal.logging.InternalLoggerFactory;
31
32 import java.net.URI;
33
34 /**
35 * <p>
36 * Performs client side opening and closing handshakes for web socket specification version <a
37 * href="https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17" >draft-ietf-hybi-thewebsocketprotocol-
38 * 17</a>
39 * </p>
40 */
41 public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
42
43 private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker13.class);
44
45 public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
46
47 private String expectedChallengeResponseString;
48
49 private final boolean allowExtensions;
50 private final boolean performMasking;
51 private final boolean allowMaskMismatch;
52
53 /**
54 * Creates a new instance.
55 *
56 * @param webSocketURL
57 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
58 * sent to this URL.
59 * @param version
60 * Version of web socket specification to use to connect to the server
61 * @param subprotocol
62 * Sub protocol request sent to the server.
63 * @param allowExtensions
64 * Allow extensions to be used in the reserved bits of the web socket frame
65 * @param customHeaders
66 * Map of custom headers to add to the client request
67 * @param maxFramePayloadLength
68 * Maximum length of a frame's payload
69 */
70 public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
71 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
72 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
73 true, false);
74 }
75
76 /**
77 * Creates a new instance.
78 *
79 * @param webSocketURL
80 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
81 * sent to this URL.
82 * @param version
83 * Version of web socket specification to use to connect to the server
84 * @param subprotocol
85 * Sub protocol request sent to the server.
86 * @param allowExtensions
87 * Allow extensions to be used in the reserved bits of the web socket frame
88 * @param customHeaders
89 * Map of custom headers to add to the client request
90 * @param maxFramePayloadLength
91 * Maximum length of a frame's payload
92 * @param performMasking
93 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
94 * with the websocket specifications. Client applications that communicate with a non-standard server
95 * which doesn't require masking might set this to false to achieve a higher performance.
96 * @param allowMaskMismatch
97 * When set to true, frames which are not masked properly according to the standard will still be
98 * accepted.
99 */
100 public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
101 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
102 boolean performMasking, boolean allowMaskMismatch) {
103 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
104 performMasking, allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
105 }
106
107 /**
108 * Creates a new instance.
109 *
110 * @param webSocketURL
111 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
112 * sent to this URL.
113 * @param version
114 * Version of web socket specification to use to connect to the server
115 * @param subprotocol
116 * Sub protocol request sent to the server.
117 * @param allowExtensions
118 * Allow extensions to be used in the reserved bits of the web socket frame
119 * @param customHeaders
120 * Map of custom headers to add to the client request
121 * @param maxFramePayloadLength
122 * Maximum length of a frame's payload
123 * @param performMasking
124 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
125 * with the websocket specifications. Client applications that communicate with a non-standard server
126 * which doesn't require masking might set this to false to achieve a higher performance.
127 * @param allowMaskMismatch
128 * When set to true, frames which are not masked properly according to the standard will still be
129 * accepted
130 * @param forceCloseTimeoutMillis
131 * Close the connection if it was not closed by the server after timeout specified.
132 */
133 public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
134 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
135 boolean performMasking, boolean allowMaskMismatch,
136 long forceCloseTimeoutMillis) {
137 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
138 allowMaskMismatch, forceCloseTimeoutMillis, false);
139 }
140
141 /**
142 * Creates a new instance.
143 *
144 * @param webSocketURL
145 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
146 * sent to this URL.
147 * @param version
148 * Version of web socket specification to use to connect to the server
149 * @param subprotocol
150 * Sub protocol request sent to the server.
151 * @param allowExtensions
152 * Allow extensions to be used in the reserved bits of the web socket frame
153 * @param customHeaders
154 * Map of custom headers to add to the client request
155 * @param maxFramePayloadLength
156 * Maximum length of a frame's payload
157 * @param performMasking
158 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
159 * with the websocket specifications. Client applications that communicate with a non-standard server
160 * which doesn't require masking might set this to false to achieve a higher performance.
161 * @param allowMaskMismatch
162 * When set to true, frames which are not masked properly according to the standard will still be
163 * accepted
164 * @param forceCloseTimeoutMillis
165 * Close the connection if it was not closed by the server after timeout specified.
166 * @param absoluteUpgradeUrl
167 * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
168 * clear HTTP
169 */
170 WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
171 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
172 boolean performMasking, boolean allowMaskMismatch,
173 long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
174 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
175 allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl, true);
176 }
177
178 /**
179 * Creates a new instance.
180 *
181 * @param webSocketURL
182 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
183 * sent to this URL.
184 * @param version
185 * Version of web socket specification to use to connect to the server
186 * @param subprotocol
187 * Sub protocol request sent to the server.
188 * @param allowExtensions
189 * Allow extensions to be used in the reserved bits of the web socket frame
190 * @param customHeaders
191 * Map of custom headers to add to the client request
192 * @param maxFramePayloadLength
193 * Maximum length of a frame's payload
194 * @param performMasking
195 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
196 * with the websocket specifications. Client applications that communicate with a non-standard server
197 * which doesn't require masking might set this to false to achieve a higher performance.
198 * @param allowMaskMismatch
199 * When set to true, frames which are not masked properly according to the standard will still be
200 * accepted
201 * @param forceCloseTimeoutMillis
202 * Close the connection if it was not closed by the server after timeout specified.
203 * @param absoluteUpgradeUrl
204 * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
205 * clear HTTP
206 * @param generateOriginHeader
207 * Allows to generate the `Origin` header value for handshake request
208 * according to the given webSocketURL
209 */
210 WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
211 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
212 boolean performMasking, boolean allowMaskMismatch,
213 long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl,
214 boolean generateOriginHeader) {
215 super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
216 absoluteUpgradeUrl, generateOriginHeader);
217 this.allowExtensions = allowExtensions;
218 this.performMasking = performMasking;
219 this.allowMaskMismatch = allowMaskMismatch;
220 }
221
222 /**
223 * /**
224 * <p>
225 * Sends the opening request to the server:
226 * </p>
227 *
228 * <pre>
229 * GET /chat HTTP/1.1
230 * Host: server.example.com
231 * Upgrade: websocket
232 * Connection: Upgrade
233 * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
234 * Sec-WebSocket-Protocol: chat, superchat
235 * Sec-WebSocket-Version: 13
236 * </pre>
237 *
238 */
239 @Override
240 protected FullHttpRequest newHandshakeRequest() {
241 URI wsURL = uri();
242
243 // Get 16 bit nonce and base 64 encode it
244 byte[] nonce = WebSocketUtil.randomBytes(16);
245 String key = WebSocketUtil.base64(nonce);
246
247 String acceptSeed = key + MAGIC_GUID;
248 byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
249 expectedChallengeResponseString = WebSocketUtil.base64(sha1);
250
251 if (logger.isDebugEnabled()) {
252 logger.debug(
253 "WebSocket version 13 client handshake key: {}, expected response: {}",
254 key, expectedChallengeResponseString);
255 }
256
257 // Format request
258 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
259 Unpooled.EMPTY_BUFFER);
260 HttpHeaders headers = request.headers();
261
262 if (customHeaders != null) {
263 headers.add(customHeaders);
264 if (!headers.contains(HttpHeaderNames.HOST)) {
265 // Only add HOST header if customHeaders did not contain it.
266 //
267 // See https://github.com/netty/netty/issues/10101
268 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
269 }
270 } else {
271 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
272 }
273
274 headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
275 .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
276 .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
277
278 if (generateOriginHeader && !headers.contains(HttpHeaderNames.ORIGIN)) {
279 headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL));
280 }
281
282 String expectedSubprotocol = expectedSubprotocol();
283 if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
284 headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
285 }
286
287 headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
288 return request;
289 }
290
291 /**
292 * <p>
293 * Process server response:
294 * </p>
295 *
296 * <pre>
297 * HTTP/1.1 101 Switching Protocols
298 * Upgrade: websocket
299 * Connection: Upgrade
300 * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
301 * Sec-WebSocket-Protocol: chat
302 * </pre>
303 *
304 * @param response
305 * HTTP response returned from the server for the request sent by beginOpeningHandshake00().
306 * @throws WebSocketHandshakeException if handshake response is invalid.
307 */
308 @Override
309 protected void verify(FullHttpResponse response) {
310 HttpResponseStatus status = response.status();
311 if (!HttpResponseStatus.SWITCHING_PROTOCOLS.equals(status)) {
312 throw new WebSocketClientHandshakeException("Invalid handshake response getStatus: " + status, response);
313 }
314
315 HttpHeaders headers = response.headers();
316 CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
317 if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
318 throw new WebSocketClientHandshakeException("Invalid handshake response upgrade: " + upgrade, response);
319 }
320
321 if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
322 throw new WebSocketClientHandshakeException("Invalid handshake response connection: "
323 + headers.get(HttpHeaderNames.CONNECTION), response);
324 }
325
326 CharSequence accept = headers.get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT);
327 if (accept == null || !accept.equals(expectedChallengeResponseString)) {
328 throw new WebSocketClientHandshakeException(String.format(
329 "Invalid challenge. Actual: %s. Expected: %s", accept, expectedChallengeResponseString), response);
330 }
331 }
332
333 @Override
334 protected WebSocketFrameDecoder newWebsocketDecoder() {
335 return new WebSocket13FrameDecoder(false, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
336 }
337
338 @Override
339 protected WebSocketFrameEncoder newWebSocketEncoder() {
340 return new WebSocket13FrameEncoder(performMasking);
341 }
342
343 @Override
344 public WebSocketClientHandshaker13 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
345 super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
346 return this;
347 }
348
349 public boolean isAllowExtensions() {
350 return allowExtensions;
351 }
352
353 public boolean isPerformMasking() {
354 return performMasking;
355 }
356
357 public boolean isAllowMaskMismatch() {
358 return allowMaskMismatch;
359 }
360 }