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