View Javadoc
1   /*
2    * Copyright 2021 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.ChannelHandlerContext;
20  import io.netty.channel.socket.ChannelInputShutdownEvent;
21  import io.netty.handler.codec.ByteToMessageDecoder;
22  import io.netty.handler.codec.quic.QuicStreamChannel;
23  import io.netty.util.AsciiString;
24  import org.jetbrains.annotations.Nullable;
25  
26  import java.util.List;
27  
28  import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError;
29  import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR;
30  import static io.netty.handler.codec.http3.QpackUtil.MAX_UNSIGNED_INT;
31  import static io.netty.handler.codec.http3.QpackUtil.decodePrefixedIntegerAsInt;
32  import static io.netty.util.internal.ObjectUtil.checkInRange;
33  
34  final class QpackEncoderHandler extends ByteToMessageDecoder {
35      private static final QpackException INVALID_LENGTH_STRING_LITERAL =
36              QpackException.newStatic(QpackEncoderHandler.class, "decodeStringLiteral(...)",
37                      "QPACK - invalid length for STRING_LITERAL");
38      private static final QpackException STRING_LITERAL_TOO_LARGE =
39              QpackException.newStatic(QpackEncoderHandler.class, "checkStringLiteralLength(...)",
40                      "QPACK - string literal exceeds the maximum dynamic table capacity");
41      private final QpackHuffmanDecoder huffmanDecoder;
42      private final QpackDecoder qpackDecoder;
43      // No single name/value string literal can ever be inserted into the dynamic table if it alone already
44      // exceeds the maximum table capacity, so this also bounds the amount of data that decode(...) will ever
45      // buffer for a single string literal while waiting for more bytes to arrive.
46      private final long maxTableCapacity;
47      private boolean discard;
48  
49      QpackEncoderHandler(@Nullable Long maxTableCapacity, QpackDecoder qpackDecoder) {
50          this.maxTableCapacity = checkInRange(
51                  maxTableCapacity == null ? 0 : maxTableCapacity, 0, MAX_UNSIGNED_INT, "maxTableCapacity");
52          huffmanDecoder = new QpackHuffmanDecoder();
53          this.qpackDecoder = qpackDecoder;
54      }
55  
56      @Override
57      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> __) throws Exception {
58          if (!in.isReadable()) {
59              return;
60          }
61          if (discard) {
62              in.skipBytes(in.readableBytes());
63              return;
64          }
65  
66          byte b = in.getByte(in.readerIndex());
67  
68          // 4.3.1. Set Dynamic Table Capacity
69          //
70          //   0   1   2   3   4   5   6   7
71          //+---+---+---+---+---+---+---+---+
72          //| 0 | 0 | 1 |   Capacity (5+)   |
73          //+---+---+---+-------------------+
74          if ((b & 0b1110_0000) == 0b0010_0000) {
75              // new capacity
76              try {
77                  long capacity = QpackUtil.decodePrefixedInteger(in, 5);
78                  if (capacity < 0) {
79                      // Not enough readable bytes
80                      return;
81                  }
82  
83                  qpackDecoder.setDynamicTableCapacity(capacity);
84              } catch (QpackException e) {
85                  handleDecodeFailure(ctx, e, "setDynamicTableCapacity failed.");
86              }
87              return;
88          }
89  
90          final QpackAttributes qpackAttributes = Http3.getQpackAttributes(ctx.channel().parent());
91          assert qpackAttributes != null;
92          if (!qpackAttributes.dynamicTableDisabled() && !qpackAttributes.decoderStreamAvailable()) {
93              // We need the decoder stream to update the decoder with these instructions.
94              return;
95          }
96          final QuicStreamChannel decoderStream = qpackAttributes.decoderStream();
97  
98          // 4.3.2. Insert With Name Reference
99          //
100         //      0   1   2   3   4   5   6   7
101         //   +---+---+---+---+---+---+---+---+
102         //   | 1 | T |    Name Index (6+)    |
103         //   +---+---+-----------------------+
104         //   | H |     Value Length (7+)     |
105         //   +---+---------------------------+
106         //   |  Value String (Length bytes)  |
107         //   +-------------------------------+
108         if ((b & 0b1000_0000) == 0b1000_0000) {
109             int readerIndex = in.readerIndex();
110             // T == 1 implies static table index.
111             // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference
112             final boolean isStaticTableIndex = QpackUtil.firstByteEquals(in, (byte) 0b1100_0000);
113             final int nameIdx = decodePrefixedIntegerAsInt(in, 6);
114             if (nameIdx < 0) {
115                 // Not enough readable bytes
116                 return;
117             }
118 
119             try {
120                 CharSequence value = decodeLiteralValue(in);
121                 if (value == null) {
122                     // Reset readerIndex
123                     in.readerIndex(readerIndex);
124                     // Not enough readable bytes
125                     return;
126                 }
127                 qpackDecoder.insertWithNameReference(decoderStream, isStaticTableIndex, nameIdx,
128                         value);
129             } catch (QpackException e) {
130                 handleDecodeFailure(ctx, e, "insertWithNameReference failed.");
131             }
132             return;
133         }
134         // 4.3.3. Insert With Literal Name
135         //
136         //      0   1   2   3   4   5   6   7
137         //   +---+---+---+---+---+---+---+---+
138         //   | 0 | 1 | H | Name Length (5+)  |
139         //   +---+---+---+-------------------+
140         //   |  Name String (Length bytes)   |
141         //   +---+---------------------------+
142         //   | H |     Value Length (7+)     |
143         //   +---+---------------------------+
144         //   |  Value String (Length bytes)  |
145         //   +-------------------------------+
146         if ((b & 0b1100_0000) == 0b0100_0000) {
147             int readerIndex = in.readerIndex();
148             final boolean nameHuffEncoded = QpackUtil.firstByteEquals(in, (byte) 0b0110_0000);
149             int nameLength = decodePrefixedIntegerAsInt(in, 5);
150             if (nameLength < 0) {
151                 // Reset readerIndex
152                 in.readerIndex(readerIndex);
153                 // Not enough readable bytes
154                 return;
155             }
156 
157             try {
158                 checkStringLiteralLength(nameHuffEncoded, nameLength);
159                 if (in.readableBytes() < nameLength) {
160                     // Reset readerIndex
161                     in.readerIndex(readerIndex);
162                     // Not enough readable bytes
163                     return;
164                 }
165 
166                 CharSequence name = decodeStringLiteral(in, nameHuffEncoded, nameLength);
167                 CharSequence value = decodeLiteralValue(in);
168                 if (value == null) {
169                     // Reset readerIndex
170                     in.readerIndex(readerIndex);
171                     // Not enough readable bytes
172                     return;
173                 }
174                 qpackDecoder.insertLiteral(decoderStream, name, value);
175             } catch (QpackException e) {
176                 handleDecodeFailure(ctx, e, "insertLiteral failed.");
177             }
178             return;
179         }
180         // 4.3.4. Duplicate
181         //
182         //      0   1   2   3   4   5   6   7
183         //   +---+---+---+---+---+---+---+---+
184         //   | 0 | 0 | 0 |    Index (5+)     |
185         //   +---+---+---+-------------------+
186         if ((b & 0b1110_0000) == 0b0000_0000) {
187             int readerIndex = in.readerIndex();
188             int index = decodePrefixedIntegerAsInt(in, 5);
189             if (index < 0) {
190                 // Reset readerIndex
191                 in.readerIndex(readerIndex);
192                 // Not enough readable bytes
193                 return;
194             }
195             try {
196                 qpackDecoder.duplicate(decoderStream, index);
197             } catch (QpackException e) {
198                 handleDecodeFailure(ctx, e, "duplicate failed.");
199             }
200             return;
201         }
202 
203         discard = true;
204         Http3CodecUtils.connectionError(ctx, Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR,
205                 "Unknown encoder instruction '" + b + "'.",  false);
206     }
207 
208     @Override
209     public void channelReadComplete(ChannelHandlerContext ctx) {
210         ctx.fireChannelReadComplete();
211 
212         // QPACK streams should always be processed, no matter what the user is doing in terms of configuration
213         // and AUTO_READ.
214         Http3CodecUtils.readIfNoAutoRead(ctx);
215     }
216 
217     @Override
218     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
219         if (evt instanceof ChannelInputShutdownEvent) {
220             // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams
221             Http3CodecUtils.criticalStreamClosed(ctx);
222         }
223         ctx.fireUserEventTriggered(evt);
224     }
225 
226     @Override
227     public void channelInactive(ChannelHandlerContext ctx) {
228         // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams
229         Http3CodecUtils.criticalStreamClosed(ctx);
230         ctx.fireChannelInactive();
231     }
232 
233     private void handleDecodeFailure(ChannelHandlerContext ctx, QpackException cause, String message) {
234         discard = true;
235         connectionError(ctx, new Http3Exception(QPACK_ENCODER_STREAM_ERROR, message, cause), true);
236     }
237 
238     @Nullable
239     private CharSequence decodeLiteralValue(ByteBuf in) throws QpackException {
240         int readerIndex = in.readerIndex();
241         int valueLength = decodePrefixedIntegerAsInt(in, 7);
242         if (valueLength < 0) {
243             // Not enough readable bytes
244             return null;
245         }
246         final boolean valueHuffEncoded = QpackUtil.byteEquals(in, readerIndex, (byte) 0b1000_0000);
247         checkStringLiteralLength(valueHuffEncoded, valueLength);
248         if (in.readableBytes() < valueLength) {
249             // Not enough readable bytes
250             return null;
251         }
252         return decodeStringLiteral(in, valueHuffEncoded, valueLength);
253     }
254 
255     private void checkStringLiteralLength(boolean huffmanEncoded, int length) throws QpackException {
256         // A string literal that is larger than the maximum dynamic table capacity can never be inserted into the
257         // dynamic table, so there is no reason to buffer it. This also guards against a peer declaring an
258         // (attacker-controlled) length of up to Integer.MAX_VALUE and forcing this handler to accumulate up to
259         // ~2 GiB per string literal before giving up.
260         //
261         // The `maxTableCapacity` is the decoded bound. If the value is huffman encoded, then
262         // inflate the limit by the max possible huffman expansion.
263         // Actual, precise table size is checked in `QpackDecoderDynamicTable.add()`.
264         final long limit = huffmanEncoded ? (maxTableCapacity * 8 + 4) / 5 : maxTableCapacity;
265         if (length > limit) {
266             throw STRING_LITERAL_TOO_LARGE;
267         }
268     }
269 
270     private CharSequence decodeStringLiteral(ByteBuf in, boolean huffmanEncoded, int length)
271             throws QpackException {
272         if (huffmanEncoded) {
273             return huffmanDecoder.decode(in, length);
274         }
275         if (in.readableBytes() < length) {
276             throw INVALID_LENGTH_STRING_LITERAL;
277         }
278         byte[] buf = new byte[length];
279         in.readBytes(buf);
280         return new AsciiString(buf, false);
281     }
282 }