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 }