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.netty5.handler.codec.http.websocketx;
17
18 import io.netty5.channel.ChannelHandler;
19 import io.netty5.channel.ChannelHandlerContext;
20 import io.netty5.channel.ChannelPipeline;
21 import io.netty5.handler.codec.http.HttpHeaders;
22 import io.netty5.util.Resource;
23
24 import java.net.URI;
25 import java.util.Objects;
26
27 import static io.netty5.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_ALLOW_MASK_MISMATCH;
28 import static io.netty5.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_DROP_PONG_FRAMES;
29 import static io.netty5.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_HANDLE_CLOSE_FRAMES;
30 import static io.netty5.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_PERFORM_MASKING;
31 import static io.netty5.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS;
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 ChannelHandler#channelInboundEvent(ChannelHandlerContext, Object)} and check if the event was of type
45 * {@link WebSocketHandshakeCompletionEvent}.
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 * Base constructor
60 *
61 * @param clientConfig
62 * Client protocol configuration.
63 */
64 public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig) {
65 super(Objects.requireNonNull(clientConfig, "clientConfig").dropPongFrames(),
66 clientConfig.sendCloseFrame(), clientConfig.forceCloseTimeoutMillis());
67 this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
68 clientConfig.webSocketUri(),
69 clientConfig.version(),
70 clientConfig.subprotocol(),
71 clientConfig.allowExtensions(),
72 clientConfig.customHeaders(),
73 clientConfig.maxFramePayloadLength(),
74 clientConfig.performMasking(),
75 clientConfig.allowMaskMismatch(),
76 clientConfig.forceCloseTimeoutMillis(),
77 clientConfig.absoluteUpgradeUrl()
78 );
79 this.clientConfig = clientConfig;
80 }
81
82 /**
83 * Base constructor
84 *
85 * @param webSocketURL
86 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
87 * sent to this URL.
88 * @param version
89 * Version of web socket specification to use to connect to the server
90 * @param subprotocol
91 * Sub protocol request sent to the server.
92 * @param customHeaders
93 * Map of custom headers to add to the client request
94 * @param maxFramePayloadLength
95 * Maximum length of a frame's payload
96 * @param handleCloseFrames
97 * {@code true} if close frames should not be forwarded and just close the channel
98 * @param performMasking
99 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
100 * with the websocket specifications. Client applications that communicate with a non-standard server
101 * which doesn't require masking might set this to false to achieve a higher performance.
102 * @param allowMaskMismatch
103 * When set to true, frames which are not masked properly according to the standard will still be
104 * accepted.
105 */
106 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
107 boolean allowExtensions, HttpHeaders customHeaders,
108 int maxFramePayloadLength, boolean handleCloseFrames,
109 boolean performMasking, boolean allowMaskMismatch) {
110 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
111 handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
112 }
113
114 /**
115 * Base constructor
116 *
117 * @param webSocketURL
118 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
119 * sent to this URL.
120 * @param version
121 * Version of web socket specification to use to connect to the server
122 * @param subprotocol
123 * Sub protocol request sent to the server.
124 * @param customHeaders
125 * Map of custom headers to add to the client request
126 * @param maxFramePayloadLength
127 * Maximum length of a frame's payload
128 * @param handleCloseFrames
129 * {@code true} if close frames should not be forwarded and just close the channel
130 * @param performMasking
131 * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
132 * with the websocket specifications. Client applications that communicate with a non-standard server
133 * which doesn't require masking might set this to false to achieve a higher performance.
134 * @param allowMaskMismatch
135 * When set to true, frames which are not masked properly according to the standard will still be
136 * accepted.
137 * @param handshakeTimeoutMillis
138 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
139 * event {@link WebSocketClientHandshakeCompletionEvent} with a
140 * {@link WebSocketHandshakeTimeoutException}.
141 */
142 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
143 boolean allowExtensions, HttpHeaders customHeaders,
144 int maxFramePayloadLength, boolean handleCloseFrames, boolean performMasking,
145 boolean allowMaskMismatch, long handshakeTimeoutMillis) {
146 this(WebSocketClientHandshakerFactory.newHandshaker(webSocketURL, version, subprotocol,
147 allowExtensions, customHeaders, maxFramePayloadLength,
148 performMasking, allowMaskMismatch),
149 handleCloseFrames, handshakeTimeoutMillis);
150 }
151
152 /**
153 * Base constructor
154 *
155 * @param webSocketURL
156 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
157 * sent to this URL.
158 * @param version
159 * Version of web socket specification to use to connect to the server
160 * @param subprotocol
161 * Sub protocol request sent to the server.
162 * @param customHeaders
163 * Map of custom headers to add to the client request
164 * @param maxFramePayloadLength
165 * Maximum length of a frame's payload
166 * @param handleCloseFrames
167 * {@code true} if close frames should not be forwarded and just close the channel
168 */
169 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
170 boolean allowExtensions, HttpHeaders customHeaders,
171 int maxFramePayloadLength, boolean handleCloseFrames) {
172 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
173 handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
174 }
175
176 /**
177 * Base constructor
178 *
179 * @param webSocketURL
180 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
181 * sent to this URL.
182 * @param version
183 * Version of web socket specification to use to connect to the server
184 * @param subprotocol
185 * Sub protocol request sent to the server.
186 * @param customHeaders
187 * Map of custom headers to add to the client request
188 * @param maxFramePayloadLength
189 * Maximum length of a frame's payload
190 * @param handleCloseFrames
191 * {@code true} if close frames should not be forwarded and just close the channel
192 * @param handshakeTimeoutMillis
193 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
194 * event {@link WebSocketClientHandshakeCompletionEvent} with a
195 * {@link WebSocketHandshakeTimeoutException}.
196 */
197 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
198 boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
199 boolean handleCloseFrames, long handshakeTimeoutMillis) {
200 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
201 handleCloseFrames, DEFAULT_PERFORM_MASKING, DEFAULT_ALLOW_MASK_MISMATCH, handshakeTimeoutMillis);
202 }
203
204 /**
205 * Base constructor
206 *
207 * @param webSocketURL
208 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
209 * sent to this URL.
210 * @param version
211 * Version of web socket specification to use to connect to the server
212 * @param subprotocol
213 * Sub protocol request sent to the server.
214 * @param customHeaders
215 * Map of custom headers to add to the client request
216 * @param maxFramePayloadLength
217 * Maximum length of a frame's payload
218 */
219 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
220 boolean allowExtensions, HttpHeaders customHeaders,
221 int maxFramePayloadLength) {
222 this(webSocketURL, version, subprotocol, allowExtensions,
223 customHeaders, maxFramePayloadLength, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
224 }
225
226 /**
227 * Base constructor
228 *
229 * @param webSocketURL
230 * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
231 * sent to this URL.
232 * @param version
233 * Version of web socket specification to use to connect to the server
234 * @param subprotocol
235 * Sub protocol request sent to the server.
236 * @param customHeaders
237 * Map of custom headers to add to the client request
238 * @param maxFramePayloadLength
239 * Maximum length of a frame's payload
240 * @param handshakeTimeoutMillis
241 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
242 * event {@link WebSocketClientHandshakeCompletionEvent} with a
243 * {@link WebSocketHandshakeTimeoutException}.
244 */
245 public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
246 boolean allowExtensions, HttpHeaders customHeaders,
247 int maxFramePayloadLength, long handshakeTimeoutMillis) {
248 this(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
249 maxFramePayloadLength, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
250 }
251
252 /**
253 * Base constructor
254 *
255 * @param handshaker
256 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
257 * was established to the remote peer.
258 * @param handleCloseFrames
259 * {@code true} if close frames should not be forwarded and just close the channel
260 */
261 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames) {
262 this(handshaker, handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
263 }
264
265 /**
266 * Base constructor
267 *
268 * @param handshaker
269 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
270 * was established to the remote peer.
271 * @param handleCloseFrames
272 * {@code true} if close frames should not be forwarded and just close the channel
273 * @param handshakeTimeoutMillis
274 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
275 * event {@link WebSocketClientHandshakeCompletionEvent} with a
276 * {@link WebSocketHandshakeTimeoutException}.
277 */
278 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
279 long handshakeTimeoutMillis) {
280 this(handshaker, handleCloseFrames, DEFAULT_DROP_PONG_FRAMES, handshakeTimeoutMillis);
281 }
282
283 /**
284 * Base constructor
285 *
286 * @param handshaker
287 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
288 * was established to the remote peer.
289 * @param handleCloseFrames
290 * {@code true} if close frames should not be forwarded and just close the channel
291 * @param dropPongFrames
292 * {@code true} if pong frames should not be forwarded
293 */
294 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
295 boolean dropPongFrames) {
296 this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
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 * @param handshakeTimeoutMillis
310 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
311 * event {@link WebSocketClientHandshakeCompletionEvent} with a
312 * {@link WebSocketHandshakeTimeoutException}.
313 */
314 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
315 boolean dropPongFrames, long handshakeTimeoutMillis) {
316 super(dropPongFrames);
317 this.handshaker = handshaker;
318 this.clientConfig = WebSocketClientProtocolConfig.newBuilder()
319 .handleCloseFrames(handleCloseFrames)
320 .handshakeTimeoutMillis(handshakeTimeoutMillis)
321 .build();
322 }
323
324 /**
325 * Base constructor
326 *
327 * @param handshaker
328 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
329 * was established to the remote peer.
330 */
331 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker) {
332 this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
333 }
334
335 /**
336 * Base constructor
337 *
338 * @param handshaker
339 * The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
340 * was established to the remote peer.
341 * @param handshakeTimeoutMillis
342 * Handshake timeout in mills, when handshake timeout, will trigger an inbound channel
343 * event {@link WebSocketClientHandshakeCompletionEvent} with a
344 * {@link WebSocketHandshakeTimeoutException}.
345 */
346 public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) {
347 this(handshaker, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
348 }
349
350 @Override
351 protected void decodeAndClose(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
352 if (clientConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) {
353 Resource.dispose(frame);
354 ctx.close();
355 return;
356 }
357 super.decodeAndClose(ctx, frame);
358 }
359
360 @Override
361 protected WebSocketClientHandshakeException buildHandshakeException(String message) {
362 return new WebSocketClientHandshakeException(message);
363 }
364
365 @Override
366 public void handlerAdded(ChannelHandlerContext ctx) {
367 ChannelPipeline cp = ctx.pipeline();
368 if (cp.get(WebSocketClientProtocolHandshakeHandler.class) == null) {
369 // Add the WebSocketClientProtocolHandshakeHandler before this one.
370 ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(),
371 new WebSocketClientProtocolHandshakeHandler(handshaker, clientConfig.handshakeTimeoutMillis()));
372 }
373 if (cp.get(Utf8FrameValidator.class) == null) {
374 // Add the UFT8 checking before this one.
375 ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
376 new Utf8FrameValidator());
377 }
378 }
379 }