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