View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http2.hpack;
29  
30  import java.nio.ByteBuffer;
31  import java.nio.CharBuffer;
32  import java.nio.charset.CharacterCodingException;
33  import java.nio.charset.Charset;
34  import java.nio.charset.CharsetDecoder;
35  import java.nio.charset.CoderResult;
36  import java.nio.charset.StandardCharsets;
37  import java.util.ArrayList;
38  import java.util.List;
39  
40  import org.apache.hc.core5.annotation.Internal;
41  import org.apache.hc.core5.http.Header;
42  import org.apache.hc.core5.http.message.BasicHeader;
43  import org.apache.hc.core5.util.Args;
44  import org.apache.hc.core5.util.ByteArrayBuffer;
45  
46  /**
47   * HPACK decoder.
48   *
49   * @since 5.0
50   */
51  @Internal
52  public final class HPackDecoder {
53  
54      private static final String UNEXPECTED_EOS = "Unexpected end of HPACK data";
55      private static final String MAX_LIMIT_EXCEEDED = "Max integer exceeded";
56  
57      private final InboundDynamicTable dynamicTable;
58      private final ByteArrayBuffer contentBuf;
59      private final CharsetDecoder charsetDecoder;
60      private CharBuffer tmpBuf;
61      private int maxTableSize;
62      private int maxListSize;
63  
64      HPackDecoder(final InboundDynamicTable dynamicTable, final CharsetDecoder charsetDecoder) {
65          this.dynamicTable = dynamicTable != null ? dynamicTable : new InboundDynamicTable();
66          this.contentBuf = new ByteArrayBuffer(256);
67          this.charsetDecoder = charsetDecoder;
68          this.maxTableSize = dynamicTable != null ? dynamicTable.getMaxSize() : Integer.MAX_VALUE;
69          this.maxListSize = Integer.MAX_VALUE;
70      }
71  
72      HPackDecoder(final InboundDynamicTable dynamicTable, final Charset charset) {
73          this(dynamicTable, charset != null && !StandardCharsets.US_ASCII.equals(charset) ? charset.newDecoder() : null);
74      }
75  
76      public HPackDecoder(final Charset charset) {
77          this(new InboundDynamicTable(), charset);
78      }
79  
80      public HPackDecoder(final CharsetDecoder charsetDecoder) {
81          this(new InboundDynamicTable(), charsetDecoder);
82      }
83  
84      static int readByte(final ByteBuffer src) throws HPackException {
85  
86          if (!src.hasRemaining()) {
87              throw new HPackException(UNEXPECTED_EOS);
88          }
89          return src.get() & 0xff;
90      }
91  
92      static int peekByte(final ByteBuffer src) throws HPackException {
93  
94          if (!src.hasRemaining()) {
95              throw new HPackException(UNEXPECTED_EOS);
96          }
97          final int pos = src.position();
98          final int b = src.get() & 0xff;
99          src.position(pos);
100         return b;
101     }
102 
103     static int decodeInt(final ByteBuffer src, final int n) throws HPackException {
104 
105         final int nbits = 0xff >>> (8 - n);
106         int value = readByte(src) & nbits;
107         if (value < nbits) {
108             return value;
109         }
110         int m = 0;
111         while (m < 32) {
112             final int b = readByte(src);
113             if ((b & 0x80) != 0) {
114                 value += (b & 0x7f) << m;
115                 m += 7;
116             } else {
117                 if (m == 28 && (b & 0xf8) != 0) {
118                     break;
119                 }
120                 value += b << m;
121                 return value;
122             }
123         }
124         throw new HPackException(MAX_LIMIT_EXCEEDED);
125     }
126 
127     static void decodePlainString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
128         final int strLen = decodeInt(src, 7);
129         final int remaining = src.remaining();
130         if (strLen > remaining) {
131             throw new HPackException(UNEXPECTED_EOS);
132         }
133         final int originalLimit = src.limit();
134         src.limit(originalLimit - (remaining - strLen));
135         buffer.append(src);
136         src.limit(originalLimit);
137     }
138 
139     static void decodeHuffman(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
140 
141         final int strLen = decodeInt(src, 7);
142         if (strLen > src.remaining()) {
143             throw new HPackException(UNEXPECTED_EOS);
144         }
145         final int limit = src.limit();
146         src.limit(src.position() + strLen);
147         Huffman.DECODER.decode(buffer, src);
148         src.limit(limit);
149     }
150 
151     void decodeString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
152 
153         final int firstByte = peekByte(src);
154         if ((firstByte & 0x80) == 0x80) {
155             decodeHuffman(buffer, src);
156         } else {
157             decodePlainString(buffer, src);
158         }
159     }
160 
161     private void clearState() {
162 
163         if (this.tmpBuf != null) {
164             this.tmpBuf.clear();
165         }
166         if (this.charsetDecoder != null) {
167             this.charsetDecoder.reset();
168         }
169         this.contentBuf.clear();
170     }
171 
172     private void expandCapacity(final int capacity) {
173 
174         final CharBuffer previous = this.tmpBuf;
175         this.tmpBuf = CharBuffer.allocate(capacity);
176         previous.flip();
177         this.tmpBuf.put(previous);
178     }
179 
180     private void ensureCapacity(final int extra) {
181 
182         if (this.tmpBuf == null) {
183             this.tmpBuf = CharBuffer.allocate(Math.max(256, extra));
184         }
185         final int requiredCapacity = this.tmpBuf.remaining() + extra;
186         if (requiredCapacity > this.tmpBuf.capacity()) {
187             expandCapacity(requiredCapacity);
188         }
189     }
190 
191     int decodeString(final ByteBuffer src, final StringBuilder buf) throws HPackException, CharacterCodingException {
192 
193         clearState();
194         decodeString(this.contentBuf, src);
195         final int binaryLen = this.contentBuf.length();
196         if (this.charsetDecoder == null) {
197             buf.ensureCapacity(binaryLen);
198             for (int i = 0; i < binaryLen; i++) {
199                 buf.append((char) (this.contentBuf.byteAt(i) & 0xff));
200             }
201         } else {
202             final ByteBuffer in = ByteBuffer.wrap(this.contentBuf.array(), 0, binaryLen);
203             while (in.hasRemaining()) {
204                 ensureCapacity(in.remaining());
205                 final CoderResult result = this.charsetDecoder.decode(in, this.tmpBuf, true);
206                 if (result.isError()) {
207                     result.throwException();
208                 }
209             }
210             ensureCapacity(8);
211             final CoderResult result = this.charsetDecoder.flush(this.tmpBuf);
212             if (result.isError()) {
213                 result.throwException();
214             }
215             this.tmpBuf.flip();
216             buf.append(this.tmpBuf);
217         }
218         return binaryLen;
219     }
220 
221     HPackHeader decodeLiteralHeader(
222             final ByteBuffer src,
223             final HPackRepresentation representation) throws HPackException, CharacterCodingException {
224 
225         final int n = representation == HPackRepresentation.WITH_INDEXING ? 6 : 4;
226         final int index = decodeInt(src, n);
227         final String name;
228         final int nameLen;
229         if (index == 0) {
230             final StringBuilder buf = new StringBuilder();
231             nameLen = decodeString(src, buf);
232             name = buf.toString();
233         } else {
234             final HPackHeader existing =  this.dynamicTable.getHeader(index);
235             if (existing == null) {
236                 throw new HPackException("Invalid header index");
237             }
238             name = existing.getName();
239             nameLen = existing.getNameLen();
240         }
241         final StringBuilder buf = new StringBuilder();
242         final int valueLen = decodeString(src, buf);
243         final String value = buf.toString();
244         final HPackHeaderpack/HPackHeader.html#HPackHeader">HPackHeader header = new HPackHeader(name, nameLen, value, valueLen, representation == HPackRepresentation.NEVER_INDEXED);
245         if (representation == HPackRepresentation.WITH_INDEXING) {
246             this.dynamicTable.add(header);
247         }
248         return header;
249     }
250 
251     HPackHeader decodeIndexedHeader(final ByteBuffer src) throws HPackException {
252 
253         final int index = decodeInt(src, 7);
254         final HPackHeader existing =  this.dynamicTable.getHeader(index);
255         if (existing == null) {
256             throw new HPackException("Invalid header index");
257         }
258         return existing;
259     }
260 
261     public Header decodeHeader(final ByteBuffer src) throws HPackException {
262         final HPackHeader header = decodeHPackHeader(src);
263         return header != null ? new BasicHeader(header.getName(), header.getValue(), header.isSensitive()) : null;
264     }
265 
266     HPackHeader decodeHPackHeader(final ByteBuffer src) throws HPackException {
267         try {
268             while (src.hasRemaining()) {
269                 final int b = peekByte(src);
270                 if ((b & 0x80) == 0x80) {
271                     return decodeIndexedHeader(src);
272                 } else if ((b & 0xc0) == 0x40) {
273                     return decodeLiteralHeader(src, HPackRepresentation.WITH_INDEXING);
274                 } else if ((b & 0xf0) == 0x00) {
275                     return decodeLiteralHeader(src, HPackRepresentation.WITHOUT_INDEXING);
276                 } else if ((b & 0xf0) == 0x10) {
277                     return decodeLiteralHeader(src, HPackRepresentation.NEVER_INDEXED);
278                 } else if ((b & 0xe0) == 0x20) {
279                     final int maxSize = decodeInt(src, 5);
280                     this.dynamicTable.setMaxSize(Math.min(this.maxTableSize, maxSize));
281                 } else {
282                     throw new HPackException("Unexpected header first byte: 0x" + Integer.toHexString(b));
283                 }
284             }
285             return null;
286         } catch (final CharacterCodingException ex) {
287             throw new HPackException(ex.getMessage(), ex);
288         }
289     }
290 
291     public List<Header> decodeHeaders(final ByteBuffer src) throws HPackException {
292         final boolean enforceSizeLimit = maxListSize < Integer.MAX_VALUE;
293         int listSize = 0;
294 
295         final List<Header> list = new ArrayList<>();
296         while (src.hasRemaining()) {
297             final HPackHeader header = decodeHPackHeader(src);
298             if (header == null) {
299                 break;
300             }
301             if (enforceSizeLimit) {
302                 listSize += header.getTotalSize();
303                 if (listSize >= maxListSize) {
304                     throw new HeaderListConstraintException("Maximum header list size exceeded");
305                 }
306             }
307             list.add(new BasicHeader(header.getName(), header.getValue(), header.isSensitive()));
308         }
309         return list;
310     }
311 
312     public int getMaxTableSize() {
313         return this.maxTableSize;
314     }
315 
316     public void setMaxTableSize(final int maxTableSize) {
317         Args.notNegative(maxTableSize, "Max table size");
318         this.maxTableSize = maxTableSize;
319         this.dynamicTable.setMaxSize(maxTableSize);
320     }
321 
322     public int getMaxListSize() {
323         return maxListSize;
324     }
325 
326     public void setMaxListSize(final int maxListSize) {
327         Args.notNegative(maxListSize, "Max list size");
328         this.maxListSize = maxListSize;
329     }
330 
331 }