View Javadoc
1   /*
2    * Copyright 2019 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.http2;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelConfig;
21  import io.netty.channel.ChannelFuture;
22  import io.netty.channel.ChannelFutureListener;
23  import io.netty.channel.ChannelHandler;
24  import io.netty.channel.ChannelHandlerContext;
25  import io.netty.channel.ChannelPipeline;
26  import io.netty.channel.EventLoop;
27  import io.netty.channel.ServerChannel;
28  import io.netty.channel.socket.ChannelInputShutdownReadComplete;
29  import io.netty.channel.socket.ChannelOutputShutdownEvent;
30  import io.netty.handler.codec.http2.Http2FrameCodec.DefaultHttp2FrameStream;
31  import io.netty.handler.ssl.SslCloseCompletionEvent;
32  import io.netty.util.ReferenceCounted;
33  import io.netty.util.internal.ObjectUtil;
34  
35  import java.util.ArrayDeque;
36  import java.util.ArrayList;
37  import java.util.HashSet;
38  import java.util.Queue;
39  import java.util.Set;
40  import javax.net.ssl.SSLException;
41  
42  import static io.netty.handler.codec.http2.AbstractHttp2StreamChannel.CHANNEL_INPUT_SHUTDOWN_READ_COMPLETE_VISITOR;
43  import static io.netty.handler.codec.http2.AbstractHttp2StreamChannel.CHANNEL_OUTPUT_SHUTDOWN_EVENT_VISITOR;
44  import static io.netty.handler.codec.http2.AbstractHttp2StreamChannel.SSL_CLOSE_COMPLETION_EVENT_VISITOR;
45  import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
46  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
47  
48  /**
49   * An HTTP/2 handler that creates child channels for each stream. This handler must be used in combination
50   * with {@link Http2FrameCodec}.
51   *
52   * <p>When a new stream is created, a new {@link Http2StreamChannel} is created for it. Applications send and
53   * receive {@link Http2StreamFrame}s on the created channel. {@link ByteBuf}s cannot be processed by the channel;
54   * all writes that reach the head of the pipeline must be an instance of {@link Http2StreamFrame}. Writes that reach
55   * the head of the pipeline are processed directly by this handler and cannot be intercepted.
56   *
57   * <p>The child channel will be notified of user events that impact the stream, such as {@link
58   * Http2GoAwayFrame} and {@link Http2ResetFrame}, as soon as they occur. Although {@code
59   * Http2GoAwayFrame} and {@code Http2ResetFrame} signify that the remote is ignoring further
60   * communication, closing of the channel is delayed until any inbound queue is drained with {@link
61   * Channel#read()}, which follows the default behavior of channels in Netty. Applications are
62   * free to close the channel in response to such events if they don't have use for any queued
63   * messages. Any connection level events like {@link Http2SettingsFrame} and {@link Http2GoAwayFrame}
64   * will be processed internally and also propagated down the pipeline for other handlers to act on.
65   *
66   * <p>Outbound streams are supported via the {@link Http2StreamChannelBootstrap}.
67   *
68   * <p>{@link ChannelConfig#setMaxMessagesPerRead(int)} and {@link ChannelConfig#setAutoRead(boolean)} are supported.
69   *
70   * <h3>Reference Counting</h3>
71   *
72   * Some {@link Http2StreamFrame}s implement the {@link ReferenceCounted} interface, as they carry
73   * reference counted objects (e.g. {@link ByteBuf}s). The multiplex codec will call {@link ReferenceCounted#retain()}
74   * before propagating a reference counted object through the pipeline, and thus an application handler needs to release
75   * such an object after having consumed it. For more information on reference counting take a look at
76   * <a href="https://netty.io/wiki/reference-counted-objects.html">the reference counted docs.</a>
77   *
78   * <h3>Channel Events</h3>
79   *
80   * A child channel becomes active as soon as it is registered to an {@link EventLoop}. Therefore, an active channel
81   * does not map to an active HTTP/2 stream immediately. Only once a {@link Http2HeadersFrame} has been successfully sent
82   * or received, does the channel map to an active HTTP/2 stream. In case it is not possible to open a new HTTP/2 stream
83   * (i.e. due to the maximum number of active streams being exceeded), the child channel receives an exception
84   * indicating the cause and is closed immediately thereafter.
85   *
86   * <h3>Writability and Flow Control</h3>
87   *
88   * A child channel observes outbound/remote flow control via the channel's writability. A channel only becomes writable
89   * when it maps to an active HTTP/2 stream . A child channel does not know about the connection-level flow control
90   * window. {@link ChannelHandler}s are free to ignore the channel's writability, in which case the excessive writes will
91   * be buffered by the parent channel. It's important to note that only {@link Http2DataFrame}s are subject to
92   * HTTP/2 flow control.
93   *
94   * <h3>Closing a {@link Http2StreamChannel}</h3>
95   *
96   * Once you close a {@link Http2StreamChannel} a {@link Http2ResetFrame} will be sent to the remote peer with
97   * {@link Http2Error#CANCEL} if needed. If you want to close the stream with another {@link Http2Error} (due
98   * errors / limits) you should propagate a {@link Http2FrameStreamException} through the {@link ChannelPipeline}.
99   * Once it reaches the end of the {@link ChannelPipeline} it will automatically close the {@link Http2StreamChannel}
100  * and send a {@link Http2ResetFrame} with the unwrapped {@link Http2Error} set. Another possibility is to just
101  * directly write a {@link Http2ResetFrame} to the {@link Http2StreamChannel}l.
102  */
103 public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler {
104 
105     static final ChannelFutureListener CHILD_CHANNEL_REGISTRATION_LISTENER = new ChannelFutureListener() {
106         @Override
107         public void operationComplete(ChannelFuture future) {
108             registerDone(future);
109         }
110     };
111 
112     private final ChannelHandler inboundStreamHandler;
113     private final ChannelHandler upgradeStreamHandler;
114     private final Queue<AbstractHttp2StreamChannel> readCompletePendingQueue =
115             new MaxCapacityQueue<AbstractHttp2StreamChannel>(new ArrayDeque<AbstractHttp2StreamChannel>(8),
116                     // Choose 100 which is what is used most of the times as default.
117                     Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS);
118 
119     // Outbound child channels opened via Http2StreamChannelBootstrap that were not closed yet. Until their first
120     // HEADERS frame is written their stream is unknown to the connection, so closing the connection doesn't close them.
121     private final Set<AbstractHttp2StreamChannel> outboundStreamChannels = new HashSet<AbstractHttp2StreamChannel>();
122     // Shared by all outbound child channels: removes a child channel from outboundStreamChannels once it is closed.
123     private final ChannelFutureListener outboundStreamChannelCloseListener = new ChannelFutureListener() {
124         @Override
125         public void operationComplete(ChannelFuture future) {
126             outboundStreamChannels.remove(future.channel());
127         }
128     };
129     private boolean parentReadInProgress;
130     private int idCount;
131 
132     // Need to be volatile as accessed from within the Http2MultiplexHandlerStreamChannel in a multi-threaded fashion.
133     private volatile ChannelHandlerContext ctx;
134 
135     /**
136      * Creates a new instance
137      *
138      * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of
139      *                             the {@link Channel}s created for new inbound streams.
140      */
141     public Http2MultiplexHandler(ChannelHandler inboundStreamHandler) {
142         this(inboundStreamHandler, null);
143     }
144 
145     /**
146      * Creates a new instance
147      *
148      * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of
149      *                             the {@link Channel}s created for new inbound streams.
150      * @param upgradeStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of the
151      *                             upgraded {@link Channel}.
152      */
153     public Http2MultiplexHandler(ChannelHandler inboundStreamHandler, ChannelHandler upgradeStreamHandler) {
154         this.inboundStreamHandler = ObjectUtil.checkNotNull(inboundStreamHandler, "inboundStreamHandler");
155         this.upgradeStreamHandler = upgradeStreamHandler;
156     }
157 
158     static void registerDone(ChannelFuture future) {
159         // Handle any errors that occurred on the local thread while registering. Even though
160         // failures can happen after this point, they will be handled by the channel by closing the
161         // childChannel.
162         if (!future.isSuccess()) {
163             Channel childChannel = future.channel();
164             if (childChannel.isRegistered()) {
165                 childChannel.close();
166             } else {
167                 childChannel.unsafe().closeForcibly();
168             }
169         }
170     }
171 
172     @Override
173     protected void handlerAdded0(ChannelHandlerContext ctx) {
174         if (ctx.executor() != ctx.channel().eventLoop()) {
175             throw new IllegalStateException("EventExecutor must be EventLoop of Channel");
176         }
177         this.ctx = ctx;
178     }
179 
180     @Override
181     protected void handlerRemoved0(ChannelHandlerContext ctx) {
182         readCompletePendingQueue.clear();
183     }
184 
185     @Override
186     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
187         if (!outboundStreamChannels.isEmpty()) {
188             // The streams known to the connection were closed already. Close the outbound child channels whose
189             // stream was never created as well, so they see channelInactive() and their closeFuture() completes.
190             // Iterate over a copy, as closing a child channel removes it from outboundStreamChannels (see
191             // outboundStreamChannelCloseListener).
192             for (AbstractHttp2StreamChannel childChannel :
193                     new ArrayList<AbstractHttp2StreamChannel>(outboundStreamChannels)) {
194                 if (childChannel.stream().state() == Http2Stream.State.IDLE) {
195                     childChannel.unsafe().closeForcibly();
196                 }
197             }
198         }
199         ctx.fireChannelInactive();
200     }
201 
202     @Override
203     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
204         parentReadInProgress = true;
205         if (msg instanceof Http2StreamFrame) {
206             if (msg instanceof Http2WindowUpdateFrame) {
207                 // We dont want to propagate update frames to the user
208                 return;
209             }
210             Http2StreamFrame streamFrame = (Http2StreamFrame) msg;
211             DefaultHttp2FrameStream s =
212                     (DefaultHttp2FrameStream) streamFrame.stream();
213 
214             AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) s.attachment;
215             if (msg instanceof Http2ResetFrame || msg instanceof Http2PriorityFrame) {
216                 // Reset and Priority frames needs to be propagated via user events as these are not flow-controlled and
217                 // so must not be controlled by suppressing channel.read() on the child channel.
218                 channel.fireChildUserEventTriggered(msg);
219 
220                 // RST frames will also trigger closing of the streams which then will call
221                 // AbstractHttp2StreamChannel.streamClosed()
222             } else {
223                 channel.fireChildRead(streamFrame);
224             }
225             return;
226         }
227 
228         if (msg instanceof Http2GoAwayFrame) {
229             // goaway frames will also trigger closing of the streams which then will call
230             // AbstractHttp2StreamChannel.streamClosed()
231             onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) msg);
232         }
233 
234         // Send everything down the pipeline
235         ctx.fireChannelRead(msg);
236     }
237 
238     @Override
239     public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
240         if (ctx.channel().isWritable()) {
241             // While the writability state may change during iterating of the streams we just set all of the streams
242             // to writable to not affect fairness. These will be "limited" by their own watermarks in any case.
243             forEachActiveStream(AbstractHttp2StreamChannel.WRITABLE_VISITOR);
244         }
245 
246         ctx.fireChannelWritabilityChanged();
247     }
248 
249     @Override
250     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
251         if (evt instanceof Http2FrameStreamEvent) {
252             Http2FrameStreamEvent event = (Http2FrameStreamEvent) evt;
253             DefaultHttp2FrameStream stream = (DefaultHttp2FrameStream) event.stream();
254             if (event.type() == Http2FrameStreamEvent.Type.State) {
255                 switch (stream.state()) {
256                     case HALF_CLOSED_LOCAL:
257                         if (stream.id() != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) {
258                             // Ignore everything which was not caused by an upgrade
259                             break;
260                         }
261                         // fall-through
262                     case HALF_CLOSED_REMOTE:
263                         // fall-through
264                     case OPEN:
265                         if (stream.attachment != null) {
266                             // ignore if child channel was already created.
267                             break;
268                         }
269                         final AbstractHttp2StreamChannel ch;
270                         // We need to handle upgrades special when on the client side.
271                         if (stream.id() == Http2CodecUtil.HTTP_UPGRADE_STREAM_ID && !isServer(ctx)) {
272                             // We must have an upgrade handler or else we can't handle the stream
273                             if (upgradeStreamHandler == null) {
274                                 throw connectionError(INTERNAL_ERROR,
275                                         "Client is misconfigured for upgrade requests");
276                             }
277                             ch = new Http2MultiplexHandlerStreamChannel(stream, upgradeStreamHandler);
278                             ch.closeOutbound();
279                         } else {
280                             ch = new Http2MultiplexHandlerStreamChannel(stream, inboundStreamHandler);
281                         }
282                         ChannelFuture future = ctx.channel().eventLoop().register(ch);
283                         if (future.isDone()) {
284                             registerDone(future);
285                         } else {
286                             future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER);
287                         }
288                         break;
289                     case CLOSED:
290                         AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) stream.attachment;
291                         if (channel != null) {
292                             channel.streamClosed();
293                         }
294                         break;
295                     default:
296                         // ignore for now
297                         break;
298                 }
299             }
300             return;
301         }
302         if (evt == ChannelInputShutdownReadComplete.INSTANCE) {
303             forEachActiveStream(CHANNEL_INPUT_SHUTDOWN_READ_COMPLETE_VISITOR);
304         } else if (evt == ChannelOutputShutdownEvent.INSTANCE) {
305             forEachActiveStream(CHANNEL_OUTPUT_SHUTDOWN_EVENT_VISITOR);
306         } else if (evt == SslCloseCompletionEvent.SUCCESS) {
307             forEachActiveStream(SSL_CLOSE_COMPLETION_EVENT_VISITOR);
308         }
309         ctx.fireUserEventTriggered(evt);
310     }
311 
312     // TODO: This is most likely not the best way to expose this, need to think more about it.
313     Http2StreamChannel newOutboundStream() {
314         final Http2MultiplexHandlerStreamChannel childChannel =
315                 new Http2MultiplexHandlerStreamChannel((DefaultHttp2FrameStream) newStream(), null);
316         outboundStreamChannels.add(childChannel);
317         childChannel.closeFuture().addListener(outboundStreamChannelCloseListener);
318         return childChannel;
319     }
320 
321     @Override
322     public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) throws Exception {
323         if (cause instanceof Http2FrameStreamException) {
324             Http2FrameStreamException exception = (Http2FrameStreamException) cause;
325             Http2FrameStream stream = exception.stream();
326             AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
327                     ((DefaultHttp2FrameStream) stream).attachment;
328             try {
329                 childChannel.fireChildExceptionCaught(cause.getCause());
330             } finally {
331                 // Close with the correct error that causes this stream exception.
332                 // See https://github.com/netty/netty/issues/13235#issuecomment-1441994672
333                 childChannel.closeWithError(exception.error());
334             }
335             return;
336         }
337         if (cause instanceof Http2MultiplexActiveStreamsException) {
338             // Unwrap the cause that was used to create it and fire it for all the active streams.
339             fireExceptionCaughtForActiveStream(cause.getCause());
340             return;
341         }
342 
343         if (cause.getCause() instanceof SSLException) {
344             fireExceptionCaughtForActiveStream(cause);
345         }
346         ctx.fireExceptionCaught(cause);
347     }
348 
349     private void fireExceptionCaughtForActiveStream(final Throwable cause) throws Http2Exception {
350         forEachActiveStream(new Http2FrameStreamVisitor() {
351             @Override
352             public boolean visit(Http2FrameStream stream) {
353                 AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
354                         ((DefaultHttp2FrameStream) stream).attachment;
355                 childChannel.fireChildExceptionCaught(cause);
356                 return true;
357             }
358         });
359     }
360 
361     private static boolean isServer(ChannelHandlerContext ctx) {
362         return ctx.channel().parent() instanceof ServerChannel;
363     }
364 
365     private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) {
366         if (goAwayFrame.lastStreamId() == Integer.MAX_VALUE) {
367             // None of the streams can have an id greater than Integer.MAX_VALUE
368             return;
369         }
370         // Notify which streams were not processed by the remote peer and are safe to retry on another connection:
371         try {
372             final boolean server = isServer(ctx);
373             forEachActiveStream(new Http2FrameStreamVisitor() {
374                 @Override
375                 public boolean visit(Http2FrameStream stream) {
376                     final int streamId = stream.id();
377                     if (streamId > goAwayFrame.lastStreamId() && Http2CodecUtil.isStreamIdValid(streamId, server)) {
378                         final AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
379                                 ((DefaultHttp2FrameStream) stream).attachment;
380                         childChannel.fireChildUserEventTriggered(goAwayFrame.retainedDuplicate());
381                     }
382                     return true;
383                 }
384             });
385         } catch (Http2Exception e) {
386             ctx.fireExceptionCaught(e);
387             ctx.close();
388         }
389     }
390 
391     /**
392      * Notifies any child streams of the read completion.
393      */
394     @Override
395     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
396         processPendingReadCompleteQueue();
397         ctx.fireChannelReadComplete();
398     }
399 
400     private void processPendingReadCompleteQueue() {
401         parentReadInProgress = true;
402         // If we have many child channel we can optimize for the case when multiple call flush() in
403         // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple
404         // write calls on the socket which is expensive.
405         AbstractHttp2StreamChannel childChannel = readCompletePendingQueue.poll();
406         if (childChannel != null) {
407             try {
408                 do {
409                     childChannel.fireChildReadComplete();
410                     childChannel = readCompletePendingQueue.poll();
411                 } while (childChannel != null);
412             } finally {
413                 parentReadInProgress = false;
414                 readCompletePendingQueue.clear();
415                 ctx.flush();
416             }
417         } else {
418             parentReadInProgress = false;
419         }
420     }
421 
422     private final class Http2MultiplexHandlerStreamChannel extends AbstractHttp2StreamChannel {
423 
424         Http2MultiplexHandlerStreamChannel(DefaultHttp2FrameStream stream, ChannelHandler inboundHandler) {
425             super(stream, ++idCount, inboundHandler);
426         }
427 
428         @Override
429         protected boolean isParentReadInProgress() {
430             return parentReadInProgress;
431         }
432 
433         @Override
434         protected void addChannelToReadCompletePendingQueue() {
435             // If there is no space left in the queue, just keep on processing everything that is already
436             // stored there and try again.
437             while (!readCompletePendingQueue.offer(this)) {
438                 processPendingReadCompleteQueue();
439             }
440         }
441 
442         @Override
443         protected ChannelHandlerContext parentContext() {
444             return ctx;
445         }
446     }
447 }