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  
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.channel.ChannelOutboundHandler;
23  import io.netty.channel.ChannelPromise;
24  import io.netty.handler.codec.MessageToMessageDecoder;
25  import io.netty.util.ReferenceCountUtil;
26  import io.netty.util.concurrent.Future;
27  import io.netty.util.concurrent.PromiseNotifier;
28  
29  import java.net.SocketAddress;
30  import java.nio.channels.ClosedChannelException;
31  import java.util.List;
32  import java.util.concurrent.TimeUnit;
33  
34  abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame>
35          implements ChannelOutboundHandler {
36  
37      private final boolean dropPongFrames;
38      private final WebSocketCloseStatus closeStatus;
39      private final long forceCloseTimeoutMillis;
40      private ChannelPromise closeSent;
41      private Future<?> forceCloseTimeoutTask;
42  
43      /**
44       * Creates a new {@link WebSocketProtocolHandler} that will <i>drop</i> {@link PongWebSocketFrame}s.
45       */
46      WebSocketProtocolHandler() {
47          this(true);
48      }
49  
50      /**
51       * Creates a new {@link WebSocketProtocolHandler}, given a parameter that determines whether or not to drop {@link
52       * PongWebSocketFrame}s.
53       *
54       * @param dropPongFrames
55       *            {@code true} if {@link PongWebSocketFrame}s should be dropped
56       */
57      WebSocketProtocolHandler(boolean dropPongFrames) {
58          this(dropPongFrames, null, WebSocketServerProtocolConfig.DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
59      }
60  
61      WebSocketProtocolHandler(boolean dropPongFrames,
62                               WebSocketCloseStatus closeStatus,
63                               long forceCloseTimeoutMillis) {
64          this.dropPongFrames = dropPongFrames;
65          this.closeStatus = closeStatus;
66          this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
67      }
68  
69      @Override
70      protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
71          if (frame instanceof PingWebSocketFrame) {
72              frame.content().retain();
73              ctx.writeAndFlush(new PongWebSocketFrame(frame.content()));
74              readIfNeeded(ctx);
75              return;
76          }
77          if (frame instanceof PongWebSocketFrame && dropPongFrames) {
78              readIfNeeded(ctx);
79              return;
80          }
81  
82          out.add(frame.retain());
83      }
84  
85      private static void readIfNeeded(ChannelHandlerContext ctx) {
86          if (!ctx.channel().config().isAutoRead()) {
87              ctx.read();
88          }
89      }
90  
91      @Override
92      public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) throws Exception {
93          if (closeStatus == null || !ctx.channel().isActive()) {
94              ctx.close(promise);
95          } else {
96              if (closeSent == null) {
97                  write(ctx, new CloseWebSocketFrame(closeStatus), ctx.newPromise());
98              }
99              flush(ctx);
100             closeSent.addListener(new ChannelFutureListener() {
101                 @Override
102                 public void operationComplete(ChannelFuture future) {
103                     ctx.close(promise);
104                 }
105             });
106         }
107     }
108 
109     @Override
110     public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
111         if (closeSent != null) {
112             ReferenceCountUtil.release(msg);
113             promise.setFailure(new ClosedChannelException());
114         } else if (msg instanceof CloseWebSocketFrame) {
115             closeSent(promise.unvoid());
116             ctx.write(msg).addListener(new PromiseNotifier<Void, ChannelFuture>(false, closeSent));
117         } else {
118             ctx.write(msg, promise);
119         }
120     }
121 
122     @Override
123     public void flush(ChannelHandlerContext ctx) throws Exception {
124         ctx.flush();
125         // Give the flush a chance to complete synchronously before arming the deadline, so a write that
126         // finishes immediately (e.g. EmbeddedChannel, or a fast socket) never races the force-close timer.
127         if (closeSent != null) {
128             applyCloseSentTimeout(ctx);
129         }
130     }
131 
132     /**
133      * Records the {@link ChannelPromise} used to write the outgoing close frame. Every code path that initiates
134      * the close handshake must call this, and must subsequently give
135      * {@link #applyCloseSentTimeout(ChannelHandlerContext)} a chance to run (directly, or indirectly via
136      * {@link #flush(ChannelHandlerContext)}) so the channel is guaranteed to close even if the outbound write
137      * never completes (e.g. the peer stops reading and the socket send buffer fills up).
138      */
139     void closeSent(ChannelPromise promise) {
140         if (closeSent != null) {
141             // Already sending (or already sent) a close frame, e.g. a peer that sends more than one CLOSE
142             // frame. Keep the original promise (and the deadline already armed for it) authoritative, and
143             // just cascade its outcome onto the new one instead of losing track of the original.
144             closeSent.addListener(new PromiseNotifier<Void, ChannelFuture>(false, promise));
145             return;
146         }
147         closeSent = promise;
148     }
149 
150     void applyCloseSentTimeout(final ChannelHandlerContext ctx) {
151         if (forceCloseTimeoutTask != null || closeSent.isDone() || forceCloseTimeoutMillis < 0) {
152             return;
153         }
154 
155         final Future<?> timeoutTask = ctx.executor().schedule(new Runnable() {
156             @Override
157             public void run() {
158                 if (!closeSent.isDone()) {
159                     closeSent.tryFailure(buildHandshakeException("send close frame timed out"));
160                     // Do not rely on some other listener eventually closing the channel once closeSent
161                     // completes (e.g. write(CloseWebSocketFrame) without a subsequent close() call attaches
162                     // none): close it here so the deadline is enforced unconditionally.
163                     ctx.close();
164                 }
165             }
166         }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS);
167         forceCloseTimeoutTask = timeoutTask;
168 
169         closeSent.addListener(new ChannelFutureListener() {
170             @Override
171             public void operationComplete(ChannelFuture future) {
172                 timeoutTask.cancel(false);
173             }
174         });
175     }
176 
177     /**
178      * Returns a {@link WebSocketHandshakeException} that depends on which client or server pipeline
179      * this handler belongs. Should be overridden in implementation otherwise a default exception is used.
180      */
181     protected WebSocketHandshakeException buildHandshakeException(String message) {
182         return new WebSocketHandshakeException(message);
183     }
184 
185     @Override
186     public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
187                      ChannelPromise promise) throws Exception {
188         ctx.bind(localAddress, promise);
189     }
190 
191     @Override
192     public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
193                         SocketAddress localAddress, ChannelPromise promise) throws Exception {
194         ctx.connect(remoteAddress, localAddress, promise);
195     }
196 
197     @Override
198     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
199             throws Exception {
200         ctx.disconnect(promise);
201     }
202 
203     @Override
204     public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
205         ctx.deregister(promise);
206     }
207 
208     @Override
209     public void read(ChannelHandlerContext ctx) throws Exception {
210         ctx.read();
211     }
212 
213     @Override
214     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
215         ctx.fireExceptionCaught(cause);
216         ctx.close();
217     }
218 }