View Javadoc
1   /*
2    * Copyright 2020 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.http3;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandler;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelInboundHandlerAdapter;
22  import io.netty.channel.socket.ChannelInputShutdownEvent;
23  import io.netty.handler.codec.quic.QuicChannel;
24  import io.netty.handler.codec.quic.QuicStreamChannel;
25  import io.netty.handler.codec.quic.QuicStreamType;
26  import io.netty.util.ReferenceCountUtil;
27  import io.netty.util.concurrent.Future;
28  import io.netty.util.concurrent.GenericFutureListener;
29  import org.jetbrains.annotations.Nullable;
30  
31  import java.nio.channels.ClosedChannelException;
32  
33  import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure;
34  import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError;
35  import static io.netty.handler.codec.http3.Http3CodecUtils.criticalStreamClosed;
36  import static io.netty.handler.codec.http3.Http3ErrorCode.H3_FRAME_UNEXPECTED;
37  import static io.netty.handler.codec.http3.Http3ErrorCode.H3_ID_ERROR;
38  import static io.netty.handler.codec.http3.Http3ErrorCode.H3_MISSING_SETTINGS;
39  import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR;
40  import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS;
41  import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY;
42  import static io.netty.handler.codec.http3.QpackUtil.toIntOrThrow;
43  import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
44  
45  final class Http3ControlStreamInboundHandler extends Http3FrameTypeInboundValidationHandler<Http3ControlStreamFrame> {
46      final boolean server;
47      private final ChannelHandler controlFrameHandler;
48      private final QpackEncoder qpackEncoder;
49      private final Http3ControlStreamOutboundHandler remoteControlStreamHandler;
50      private boolean firstFrameRead;
51      private Long receivedGoawayId;
52      private Long receivedMaxPushId;
53  
54      Http3ControlStreamInboundHandler(boolean server, @Nullable ChannelHandler controlFrameHandler,
55                                       QpackEncoder qpackEncoder,
56                                       Http3ControlStreamOutboundHandler remoteControlStreamHandler) {
57          super(Http3ControlStreamFrame.class);
58          this.server = server;
59          this.controlFrameHandler = controlFrameHandler;
60          this.qpackEncoder = qpackEncoder;
61          this.remoteControlStreamHandler = remoteControlStreamHandler;
62      }
63  
64      boolean isServer() {
65          return server;
66      }
67  
68      boolean isGoAwayReceived() {
69          return receivedGoawayId != null;
70      }
71  
72      long maxPushIdReceived() {
73          return receivedMaxPushId == null ? -1 : receivedMaxPushId;
74      }
75  
76      private boolean forwardControlFrames() {
77          return controlFrameHandler != null;
78      }
79  
80      @Override
81      public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
82          super.handlerAdded(ctx);
83          // The user want's to be notified about control frames, add the handler to the pipeline.
84          if (controlFrameHandler != null) {
85              ctx.pipeline().addLast(controlFrameHandler);
86          }
87      }
88  
89      @Override
90      void readFrameDiscarded(ChannelHandlerContext ctx, Object discardedFrame) {
91          if (!firstFrameRead && !(discardedFrame instanceof Http3SettingsFrame)) {
92              connectionError(ctx, Http3ErrorCode.H3_MISSING_SETTINGS, "Missing settings frame.", forwardControlFrames());
93          }
94      }
95  
96      @Override
97      void channelRead(ChannelHandlerContext ctx, Http3ControlStreamFrame frame) throws QpackException {
98          boolean isSettingsFrame = frame instanceof Http3SettingsFrame;
99          if (!firstFrameRead && !isSettingsFrame) {
100             connectionError(ctx, H3_MISSING_SETTINGS, "Missing settings frame.", forwardControlFrames());
101             ReferenceCountUtil.release(frame);
102             return;
103         }
104         if (firstFrameRead && isSettingsFrame) {
105             connectionError(ctx, H3_FRAME_UNEXPECTED, "Second settings frame received.", forwardControlFrames());
106             ReferenceCountUtil.release(frame);
107             return;
108         }
109         firstFrameRead = true;
110 
111         final boolean valid;
112         if (isSettingsFrame) {
113             valid = handleHttp3SettingsFrame(ctx, (Http3SettingsFrame) frame);
114         } else if (frame instanceof Http3GoAwayFrame) {
115             valid = handleHttp3GoAwayFrame(ctx, (Http3GoAwayFrame) frame);
116         } else if (frame instanceof Http3MaxPushIdFrame) {
117             valid = handleHttp3MaxPushIdFrame(ctx, (Http3MaxPushIdFrame) frame);
118         } else if (frame instanceof Http3CancelPushFrame) {
119             valid = handleHttp3CancelPushFrame(ctx, (Http3CancelPushFrame) frame);
120         } else {
121             // We don't need to do any special handling for Http3UnknownFrames as we either pass these to the next#
122             // handler or release these directly.
123             assert frame instanceof Http3UnknownFrame;
124             valid = true;
125         }
126 
127         if (!valid || controlFrameHandler == null) {
128             ReferenceCountUtil.release(frame);
129             return;
130         }
131 
132         // The user did specify ChannelHandler that should be notified about control stream frames.
133         // Let's forward the frame so the user can do something with it.
134         ctx.fireChannelRead(frame);
135     }
136 
137     private boolean handleHttp3SettingsFrame(ChannelHandlerContext ctx, Http3SettingsFrame settingsFrame)
138             throws QpackException {
139         final QuicChannel quicChannel = (QuicChannel) ctx.channel().parent();
140         final QpackAttributes qpackAttributes = Http3.getQpackAttributes(quicChannel);
141         assert qpackAttributes != null;
142         final GenericFutureListener<Future<? super QuicStreamChannel>> closeOnFailure = future -> {
143             if (!future.isSuccess()) {
144                 criticalStreamClosed(ctx);
145             }
146         };
147         if (qpackAttributes.dynamicTableDisabled()) {
148             qpackEncoder.configureDynamicTable(qpackAttributes, 0, 0);
149             return true;
150         }
151         quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL,
152                 new QPackEncoderStreamInitializer(qpackEncoder, qpackAttributes,
153                         settingsFrame.getOrDefault(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0),
154                         settingsFrame.getOrDefault(HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, 0)))
155                 .addListener(closeOnFailure);
156         quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, new QPackDecoderStreamInitializer(qpackAttributes))
157                 .addListener(closeOnFailure);
158         return true;
159     }
160 
161     private boolean handleHttp3GoAwayFrame(ChannelHandlerContext ctx, Http3GoAwayFrame goAwayFrame) {
162         long id = goAwayFrame.id();
163         if (!server && id % 4 != 0) {
164             connectionError(ctx, H3_FRAME_UNEXPECTED, "GOAWAY received with ID of non-request stream.",
165                     forwardControlFrames());
166             return false;
167         }
168         if (receivedGoawayId != null && id > receivedGoawayId) {
169             connectionError(ctx, H3_ID_ERROR,
170                     "GOAWAY received with ID larger than previously received.", forwardControlFrames());
171             return false;
172         }
173         receivedGoawayId = id;
174         return true;
175     }
176 
177     private boolean handleHttp3MaxPushIdFrame(ChannelHandlerContext ctx, Http3MaxPushIdFrame frame) {
178         long id = frame.id();
179         if (!server) {
180             connectionError(ctx, H3_FRAME_UNEXPECTED, "MAX_PUSH_ID received by client.",
181                     forwardControlFrames());
182             return false;
183         }
184         if (receivedMaxPushId != null && id < receivedMaxPushId) {
185             connectionError(ctx, H3_ID_ERROR, "MAX_PUSH_ID reduced limit.", forwardControlFrames());
186             return false;
187         }
188         receivedMaxPushId = id;
189         return true;
190     }
191 
192     private boolean handleHttp3CancelPushFrame(ChannelHandlerContext ctx, Http3CancelPushFrame cancelPushFrame) {
193         final Long maxPushId = server ? receivedMaxPushId : remoteControlStreamHandler.sentMaxPushId();
194         if (maxPushId == null || maxPushId < cancelPushFrame.id()) {
195             connectionError(ctx, H3_ID_ERROR, "CANCEL_PUSH received with an ID greater than MAX_PUSH_ID.",
196                     forwardControlFrames());
197             return false;
198         }
199         return true;
200     }
201 
202     @Override
203     public void channelReadComplete(ChannelHandlerContext ctx) {
204         ctx.fireChannelReadComplete();
205 
206         // control streams should always be processed, no matter what the user is doing in terms of
207         // configuration and AUTO_READ.
208         Http3CodecUtils.readIfNoAutoRead(ctx);
209     }
210 
211     @Override
212     public boolean isSharable() {
213         // Not sharable as it keeps state.
214         return false;
215     }
216 
217     @Override
218     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
219         if (evt instanceof ChannelInputShutdownEvent) {
220             // See https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#section-4.2
221             criticalStreamClosed(ctx);
222         }
223         ctx.fireUserEventTriggered(evt);
224     }
225 
226     private abstract static class AbstractQPackStreamInitializer extends ChannelInboundHandlerAdapter {
227         private final int streamType;
228         protected final QpackAttributes attributes;
229 
230         AbstractQPackStreamInitializer(int streamType, QpackAttributes attributes) {
231             this.streamType = streamType;
232             this.attributes = attributes;
233         }
234 
235         @Override
236         public final void channelActive(ChannelHandlerContext ctx) {
237             // We need to write the streamType into the stream before doing anything else.
238             // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1
239             // Just allocate 8 bytes which would be the max needed.
240             ByteBuf buffer = ctx.alloc().buffer(8);
241             Http3CodecUtils.writeVariableLengthInteger(buffer, streamType);
242             closeOnFailure(ctx.writeAndFlush(buffer));
243             streamAvailable(ctx);
244             ctx.fireChannelActive();
245         }
246 
247         @Override
248         public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
249             streamClosed(ctx);
250             if (evt instanceof ChannelInputShutdownEvent) {
251                 // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
252                 criticalStreamClosed(ctx);
253             }
254             ctx.fireUserEventTriggered(evt);
255         }
256 
257         @Override
258         public void channelInactive(ChannelHandlerContext ctx) {
259             streamClosed(ctx);
260             // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
261             criticalStreamClosed(ctx);
262             ctx.fireChannelInactive();
263         }
264 
265         protected abstract void streamAvailable(ChannelHandlerContext ctx);
266 
267         protected abstract void streamClosed(ChannelHandlerContext ctx);
268     }
269 
270     private static final class QPackEncoderStreamInitializer extends AbstractQPackStreamInitializer {
271         private static final ClosedChannelException ENCODER_STREAM_INACTIVE =
272                 unknownStackTrace(new ClosedChannelException(), ClosedChannelException.class, "streamClosed()");
273         private final QpackEncoder encoder;
274         private final long maxTableCapacity;
275         private final long blockedStreams;
276 
277         QPackEncoderStreamInitializer(QpackEncoder encoder, QpackAttributes attributes, long maxTableCapacity,
278                                       long blockedStreams) {
279             super(Http3CodecUtils.HTTP3_QPACK_ENCODER_STREAM_TYPE, attributes);
280             this.encoder = encoder;
281             this.maxTableCapacity = maxTableCapacity;
282             this.blockedStreams = blockedStreams;
283         }
284 
285         @Override
286         protected void streamAvailable(ChannelHandlerContext ctx) {
287             final QuicStreamChannel stream = (QuicStreamChannel) ctx.channel();
288             attributes.encoderStream(stream);
289 
290             try {
291                 encoder.configureDynamicTable(attributes, maxTableCapacity, toIntOrThrow(blockedStreams));
292             } catch (QpackException e) {
293                 connectionError(ctx, new Http3Exception(QPACK_ENCODER_STREAM_ERROR,
294                         "Dynamic table configuration failed.", e), true);
295             }
296         }
297 
298         @Override
299         protected void streamClosed(ChannelHandlerContext ctx) {
300             attributes.encoderStreamInactive(ENCODER_STREAM_INACTIVE);
301         }
302     }
303 
304     private static final class QPackDecoderStreamInitializer extends AbstractQPackStreamInitializer {
305         private static final ClosedChannelException DECODER_STREAM_INACTIVE =
306                 unknownStackTrace(new ClosedChannelException(), ClosedChannelException.class, "streamClosed()");
307         private QPackDecoderStreamInitializer(QpackAttributes attributes) {
308             super(Http3CodecUtils.HTTP3_QPACK_DECODER_STREAM_TYPE, attributes);
309         }
310 
311         @Override
312         protected void streamAvailable(ChannelHandlerContext ctx) {
313             attributes.decoderStream((QuicStreamChannel) ctx.channel());
314         }
315 
316         @Override
317         protected void streamClosed(ChannelHandlerContext ctx) {
318             attributes.decoderStreamInactive(DECODER_STREAM_INACTIVE);
319         }
320     }
321 }