View Javadoc
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 }