1 /*
2 * Copyright 2013 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.channel.ChannelHandlerContext;
19 import io.netty.channel.ChannelInboundHandler;
20 import io.netty.channel.ChannelPipeline;
21 import io.netty.handler.codec.http.HttpHeaders;
22
23 import java.net.URI;
24 import java.util.List;
25
26 import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_ALLOW_MASK_MISMATCH;
27 import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_DROP_PONG_FRAMES;
28 import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_HANDLE_CLOSE_FRAMES;
29 import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_PERFORM_MASKING;
30 import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_WITH_UTF8_VALIDATOR;
31 import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS;
32 import static io.netty.util.internal.ObjectUtil.*;
33
34 /**
35 * This handler does all the heavy lifting for you to run a websocket client.
36 *
37 * It takes care of websocket handshaking as well as processing of Ping, Pong frames. Text and Binary
38 * data frames are passed to the next handler in the pipeline (implemented by you) for processing.
39 * Also the close frame is passed to the next handler as you may want inspect it before close the connection if
40 * the {@code handleCloseFrames} is {@code false}, default is {@code true}.
41 *
42 * This implementation will establish the websocket connection once the connection to the remote server was complete.
43 *
44 * To know once a handshake was done you can intercept the
45 * {@link ChannelInboundHandler#userEventTriggered(ChannelHandlerContext, Object)} and check if the event was of type
46 * {@link ClientHandshakeStateEvent#HANDSHAKE_ISSUED} or {@link ClientHandshakeStateEvent#HANDSHAKE_COMPLETE}.
47 */
48 public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
49 private final WebSocketClientHandshaker handshaker;
50 private final WebSocketClientProtocolConfig clientConfig;
51
52 /**
53 * Returns the used handshaker
54 */
55 public WebSocketClientHandshaker handshaker() {
56 return handshaker;
57 }
58
59 /**
60 * Events that are fired to notify about handshake status
61 */
62 public enum ClientHandshakeStateEvent {
63 /**
64 * The Handshake was timed out
65 */
66 HANDSHAKE_TIMEOUT,
67
68 /**
69 * The Handshake was started but the server did not response yet to the request
70 */
71 HANDSHAKE_ISSUED,
72
73 /**
74 * The Handshake was complete successful and so the channel was upgraded to websockets
75 */
76 HANDSHAKE_COMPLETE
77 }
78
79 /**
80 * Base constructor
81 *
82 * @param clientConfig
83 * Client protocol configuration.
84 */
85 public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig) {
86 super(checkNotNull(clientConfig, "clientConfig").dropPongFrames(),
87 clientConfig.sendCloseFrame(), clientConfig.forceCloseTimeoutMillis());
88 this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
89 clientConfig.webSocketUri(),
90 clientConfig.version(),
91 clientConfig.subprotocol(),
92 clientConfig.allowExtensions(),
93 clientConfig.customHeaders(),
94 clientConfig.maxFramePayloadLength(),
95 clientConfig.performMasking(),
96 clientConfig.allowMaskMismatch(),
97 clientConfig.forceCloseTimeoutMillis(),
98 clientConfig.absoluteUpgradeUrl(),
99 clientConfig.generateOriginHeader()
100 );
101 this.clientConfig = clientConfig;
102 }
103
104 /**
105 * Base constructor
106 *
107 * @param handshaker
108 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
109 * was established to the remote peer.
110 * @param clientConfig
111 * Client protocol configuration.
112 */
113 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker,
114 WebSocketClientProtocolConfig clientConfig) {
115 super(checkNotNull(clientConfig, "clientConfig").dropPongFrames(),
116 clientConfig.sendCloseFrame(), clientConfig.forceCloseTimeoutMillis());
117 this.handshaker = handshaker;
118 this.clientConfig = clientConfig;
119 }
120
121 /**
122 * Base constructor
123 *
124 * @param webSocketURL
125 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
126 * sent to this URL.
127 * @param version
128 * Version of web socket specification to use to connect to the server
129 * @param subprotocol
130 * Sub protocol request sent to the server.
131 * @param customHeaders
132 * Map of custom headers to add to the client request
133 * @param maxFramePayloadLength
134 * Maximum length of a frame's payload
135 * @param handleCloseFrames
136 * {@code true} if close frames should not be forwarded and just close the channel
137 * @param performMasking
138 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
139 * with the websocket specifications. Client applications that communicate with a non-standard server
140 * which doesn't require masking might set this to false to achieve a higher performance.
141 * @param allowMaskMismatch
142 * When set to true, frames which are not masked properly according to the standard will still be
143 * accepted.
144 */
145 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
146 boolean allowExtensions, HttpHeaders customHeaders,
147 int maxFramePayloadLength, boolean handleCloseFrames,
148 boolean performMasking, boolean allowMaskMismatch) {
149 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
150 handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
151 }
152
153 /**
154 * Base constructor
155 *
156 * @param webSocketURL
157 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
158 * sent to this URL.
159 * @param version
160 * Version of web socket specification to use to connect to the server
161 * @param subprotocol
162 * Sub protocol request sent to the server.
163 * @param customHeaders
164 * Map of custom headers to add to the client request
165 * @param maxFramePayloadLength
166 * Maximum length of a frame's payload
167 * @param handleCloseFrames
168 * {@code true} if close frames should not be forwarded and just close the channel
169 * @param performMasking
170 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
171 * with the websocket specifications. Client applications that communicate with a non-standard server
172 * which doesn't require masking might set this to false to achieve a higher performance.
173 * @param allowMaskMismatch
174 * When set to true, frames which are not masked properly according to the standard will still be
175 * accepted.
176 * @param handshakeTimeoutMillis
177 * Handshake timeout in mills, when handshake timeout, will trigger user
178 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
179 */
180 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
181 boolean allowExtensions, HttpHeaders customHeaders,
182 int maxFramePayloadLength, boolean handleCloseFrames, boolean performMasking,
183 boolean allowMaskMismatch, long handshakeTimeoutMillis) {
184 this(WebSocketClientHandshakerFactory.newHandshaker(webSocketURL, version, subprotocol,
185 allowExtensions, customHeaders, maxFramePayloadLength,
186 performMasking, allowMaskMismatch),
187 handleCloseFrames, handshakeTimeoutMillis);
188 }
189
190 /**
191 * Base constructor
192 *
193 * @param webSocketURL
194 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
195 * sent to this URL.
196 * @param version
197 * Version of web socket specification to use to connect to the server
198 * @param subprotocol
199 * Sub protocol request sent to the server.
200 * @param customHeaders
201 * Map of custom headers to add to the client request
202 * @param maxFramePayloadLength
203 * Maximum length of a frame's payload
204 * @param handleCloseFrames
205 * {@code true} if close frames should not be forwarded and just close the channel
206 */
207 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
208 boolean allowExtensions, HttpHeaders customHeaders,
209 int maxFramePayloadLength, boolean handleCloseFrames) {
210 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
211 handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
212 }
213
214 /**
215 * Base constructor
216 *
217 * @param webSocketURL
218 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
219 * sent to this URL.
220 * @param version
221 * Version of web socket specification to use to connect to the server
222 * @param subprotocol
223 * Sub protocol request sent to the server.
224 * @param customHeaders
225 * Map of custom headers to add to the client request
226 * @param maxFramePayloadLength
227 * Maximum length of a frame's payload
228 * @param handleCloseFrames
229 * {@code true} if close frames should not be forwarded and just close the channel
230 * @param handshakeTimeoutMillis
231 * Handshake timeout in mills, when handshake timeout, will trigger user
232 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
233 */
234 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
235 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
236 boolean handleCloseFrames, long handshakeTimeoutMillis) {
237 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
238 handleCloseFrames, DEFAULT_PERFORM_MASKING, DEFAULT_ALLOW_MASK_MISMATCH, handshakeTimeoutMillis);
239 }
240
241 /**
242 * Base constructor
243 *
244 * @param webSocketURL
245 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
246 * sent to this URL.
247 * @param version
248 * Version of web socket specification to use to connect to the server
249 * @param subprotocol
250 * Sub protocol request sent to the server.
251 * @param customHeaders
252 * Map of custom headers to add to the client request
253 * @param maxFramePayloadLength
254 * Maximum length of a frame's payload
255 */
256 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
257 boolean allowExtensions, HttpHeaders customHeaders,
258 int maxFramePayloadLength) {
259 this(webSocketURL, version, subprotocol, allowExtensions,
260 customHeaders, maxFramePayloadLength, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
261 }
262
263 /**
264 * Base constructor
265 *
266 * @param webSocketURL
267 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
268 * sent to this URL.
269 * @param version
270 * Version of web socket specification to use to connect to the server
271 * @param subprotocol
272 * Sub protocol request sent to the server.
273 * @param customHeaders
274 * Map of custom headers to add to the client request
275 * @param maxFramePayloadLength
276 * Maximum length of a frame's payload
277 * @param handshakeTimeoutMillis
278 * Handshake timeout in mills, when handshake timeout, will trigger user
279 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
280 */
281 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
282 boolean allowExtensions, HttpHeaders customHeaders,
283 int maxFramePayloadLength, long handshakeTimeoutMillis) {
284 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
285 maxFramePayloadLength, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
286 }
287
288 /**
289 * Base constructor
290 *
291 * @param handshaker
292 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
293 * was established to the remote peer.
294 * @param handleCloseFrames
295 * {@code true} if close frames should not be forwarded and just close the channel
296 */
297 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames) {
298 this(handshaker, handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
299 }
300
301 /**
302 * Base constructor
303 *
304 * @param handshaker
305 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
306 * was established to the remote peer.
307 * @param handleCloseFrames
308 * {@code true} if close frames should not be forwarded and just close the channel
309 * @param handshakeTimeoutMillis
310 * Handshake timeout in mills, when handshake timeout, will trigger user
311 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
312 */
313 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
314 long handshakeTimeoutMillis) {
315 this(handshaker, handleCloseFrames, DEFAULT_DROP_PONG_FRAMES, handshakeTimeoutMillis);
316 }
317
318 /**
319 * Base constructor
320 *
321 * @param handshaker
322 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
323 * was established to the remote peer.
324 * @param handleCloseFrames
325 * {@code true} if close frames should not be forwarded and just close the channel
326 * @param dropPongFrames
327 * {@code true} if pong frames should not be forwarded
328 */
329 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
330 boolean dropPongFrames) {
331 this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
332 }
333
334 /**
335 * Base constructor
336 *
337 * @param handshaker
338 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
339 * was established to the remote peer.
340 * @param handleCloseFrames
341 * {@code true} if close frames should not be forwarded and just close the channel
342 * @param dropPongFrames
343 * {@code true} if pong frames should not be forwarded
344 * @param handshakeTimeoutMillis
345 * Handshake timeout in mills, when handshake timeout, will trigger user
346 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
347 */
348 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
349 boolean dropPongFrames, long handshakeTimeoutMillis) {
350 this(handshaker, handleCloseFrames, dropPongFrames, handshakeTimeoutMillis, DEFAULT_WITH_UTF8_VALIDATOR);
351 }
352
353 /**
354 * Base constructor
355 *
356 * @param handshaker
357 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
358 * was established to the remote peer.
359 * @param handleCloseFrames
360 * {@code true} if close frames should not be forwarded and just close the channel
361 * @param dropPongFrames
362 * {@code true} if pong frames should not be forwarded
363 * @param handshakeTimeoutMillis
364 * Handshake timeout in mills, when handshake timeout, will trigger user
365 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
366 * @param withUTF8Validator
367 * {@code true} if UTF8 validation of text frames should be enabled
368 */
369 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
370 boolean dropPongFrames, long handshakeTimeoutMillis,
371 boolean withUTF8Validator) {
372 super(dropPongFrames);
373 this.handshaker = handshaker;
374 this.clientConfig = WebSocketClientProtocolConfig.newBuilder()
375 .handleCloseFrames(handleCloseFrames)
376 .handshakeTimeoutMillis(handshakeTimeoutMillis)
377 .withUTF8Validator(withUTF8Validator)
378 .build();
379 }
380
381 /**
382 * Base constructor
383 *
384 * @param handshaker
385 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
386 * was established to the remote peer.
387 */
388 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker) {
389 this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
390 }
391
392 /**
393 * Base constructor
394 *
395 * @param handshaker
396 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
397 * was established to the remote peer.
398 * @param handshakeTimeoutMillis
399 * Handshake timeout in mills, when handshake timeout, will trigger user
400 * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
401 */
402 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) {
403 this(handshaker, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
404 }
405
406 @Override
407 protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
408 if (clientConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) {
409 ctx.close();
410 return;
411 }
412 super.decode(ctx, frame, out);
413 }
414
415 @Override
416 protected WebSocketClientHandshakeException buildHandshakeException(String message) {
417 return new WebSocketClientHandshakeException(message);
418 }
419
420 @Override
421 public void handlerAdded(ChannelHandlerContext ctx) {
422 ChannelPipeline cp = ctx.pipeline();
423 if (cp.get(WebSocketClientProtocolHandshakeHandler.class) == null) {
424 // Add the WebSocketClientProtocolHandshakeHandler before this one.
425 ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(),
426 new WebSocketClientProtocolHandshakeHandler(handshaker, clientConfig.handshakeTimeoutMillis()));
427 }
428 if (clientConfig.withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
429 // Add the UFT8 checking before this one.
430 ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
431 new Utf8FrameValidator());
432 }
433 }
434 }