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-10" >draft-ietf-hybi-thewebsocketprotocol-
38 * 10</a>
39 * </p>
40 */
41 public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
42
43 private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketClientHandshaker08.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 WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
71 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
72 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true,
73 false, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
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 WebSocketClientHandshaker08(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, performMasking,
104 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 WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
134 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
135 boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
136 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
137 allowMaskMismatch, forceCloseTimeoutMillis, false, true);
138 }
139
140 /**
141 * Creates a new instance.
142 *
143 * @param webSocketURL
144 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
145 * sent to this URL.
146 * @param version
147 * Version of web socket specification to use to connect to the server
148 * @param subprotocol
149 * Sub protocol request sent to the server.
150 * @param allowExtensions
151 * Allow extensions to be used in the reserved bits of the web socket frame
152 * @param customHeaders
153 * Map of custom headers to add to the client request
154 * @param maxFramePayloadLength
155 * Maximum length of a frame's payload
156 * @param performMasking
157 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
158 * with the websocket specifications. Client applications that communicate with a non-standard server
159 * which doesn't require masking might set this to false to achieve a higher performance.
160 * @param allowMaskMismatch
161 * When set to true, frames which are not masked properly according to the standard will still be
162 * accepted
163 * @param forceCloseTimeoutMillis
164 * Close the connection if it was not closed by the server after timeout specified.
165 * @param absoluteUpgradeUrl
166 * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
167 * clear HTTP
168 */
169 WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
170 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
171 boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis,
172 boolean absoluteUpgradeUrl) {
173 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
174 allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl, true);
175 }
176
177 /**
178 * Creates a new instance.
179 *
180 * @param webSocketURL
181 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
182 * sent to this URL.
183 * @param version
184 * Version of web socket specification to use to connect to the server
185 * @param subprotocol
186 * Sub protocol request sent to the server.
187 * @param allowExtensions
188 * Allow extensions to be used in the reserved bits of the web socket frame
189 * @param customHeaders
190 * Map of custom headers to add to the client request
191 * @param maxFramePayloadLength
192 * Maximum length of a frame's payload
193 * @param performMasking
194 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
195 * with the websocket specifications. Client applications that communicate with a non-standard server
196 * which doesn't require masking might set this to false to achieve a higher performance.
197 * @param allowMaskMismatch
198 * When set to true, frames which are not masked properly according to the standard will still be
199 * accepted
200 * @param forceCloseTimeoutMillis
201 * Close the connection if it was not closed by the server after timeout specified.
202 * @param absoluteUpgradeUrl
203 * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
204 * clear HTTP
205 * @param generateOriginHeader
206 * Allows to generate a `Sec-WebSocket-Origin` header value for handshake request
207 * according to the given webSocketURL
208 */
209 WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
210 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
211 boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis,
212 boolean absoluteUpgradeUrl, boolean generateOriginHeader) {
213 super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
214 absoluteUpgradeUrl, generateOriginHeader);
215 this.allowExtensions = allowExtensions;
216 this.performMasking = performMasking;
217 this.allowMaskMismatch = allowMaskMismatch;
218 }
219
220 /**
221 * /**
222 * <p>
223 * Sends the opening request to the server:
224 * </p>
225 *
226 * <pre>
227 * GET /chat HTTP/1.1
228 * Host: server.example.com
229 * Upgrade: websocket
230 * Connection: Upgrade
231 * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
232 * Sec-WebSocket-Origin: http://example.com
233 * Sec-WebSocket-Protocol: chat, superchat
234 * Sec-WebSocket-Version: 8
235 * </pre>
236 *
237 */
238 @Override
239 protected FullHttpRequest newHandshakeRequest() {
240 URI wsURL = uri();
241
242 // Get 16 bit nonce and base 64 encode it
243 byte[] nonce = WebSocketUtil.randomBytes(16);
244 String key = WebSocketUtil.base64(nonce);
245
246 String acceptSeed = key + MAGIC_GUID;
247 byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
248 expectedChallengeResponseString = WebSocketUtil.base64(sha1);
249
250 if (logger.isDebugEnabled()) {
251 logger.debug(
252 "WebSocket version 08 client handshake key: {}, expected response: {}",
253 key, expectedChallengeResponseString);
254 }
255
256 // Format request
257 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
258 Unpooled.EMPTY_BUFFER);
259 HttpHeaders headers = request.headers();
260
261 if (customHeaders != null) {
262 headers.add(customHeaders);
263 if (!headers.contains(HttpHeaderNames.HOST)) {
264 // Only add HOST header if customHeaders did not contain it.
265 //
266 // See https://github.com/netty/netty/issues/10101
267 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
268 }
269 } else {
270 headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
271 }
272
273 headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
274 .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
275 .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
276
277 if (generateOriginHeader && !headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) {
278 headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
279 }
280
281 String expectedSubprotocol = expectedSubprotocol();
282 if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
283 headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
284 }
285
286 headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
287 return request;
288 }
289
290 /**
291 * <p>
292 * Process server response:
293 * </p>
294 *
295 * <pre>
296 * HTTP/1.1 101 Switching Protocols
297 * Upgrade: websocket
298 * Connection: Upgrade
299 * Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
300 * Sec-WebSocket-Protocol: chat
301 * </pre>
302 *
303 * @param response
304 * HTTP response returned from the server for the request sent by beginOpeningHandshake00().
305 * @throws WebSocketHandshakeException
306 */
307 @Override
308 protected void verify(FullHttpResponse response) {
309 HttpResponseStatus status = response.status();
310 if (!HttpResponseStatus.SWITCHING_PROTOCOLS.equals(status)) {
311 throw new WebSocketClientHandshakeException("Invalid handshake response getStatus: " + status, response);
312 }
313
314 HttpHeaders headers = response.headers();
315 CharSequence upgrade = headers.get(HttpHeaderNames.UPGRADE);
316 if (!HttpHeaderValues.WEBSOCKET.contentEqualsIgnoreCase(upgrade)) {
317 throw new WebSocketClientHandshakeException("Invalid handshake response upgrade: " + upgrade, response);
318 }
319
320 if (!headers.containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true)) {
321 throw new WebSocketClientHandshakeException("Invalid handshake response connection: "
322 + headers.get(HttpHeaderNames.CONNECTION), response);
323 }
324
325 CharSequence accept = headers.get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT);
326 if (accept == null || !accept.equals(expectedChallengeResponseString)) {
327 throw new WebSocketClientHandshakeException(String.format(
328 "Invalid challenge. Actual: %s. Expected: %s", accept, expectedChallengeResponseString), response);
329 }
330 }
331
332 @Override
333 protected WebSocketFrameDecoder newWebsocketDecoder() {
334 return new WebSocket08FrameDecoder(false, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
335 }
336
337 @Override
338 protected WebSocketFrameEncoder newWebSocketEncoder() {
339 return new WebSocket08FrameEncoder(performMasking);
340 }
341
342 @Override
343 public WebSocketClientHandshaker08 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
344 super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
345 return this;
346 }
347
348 }