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