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