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 
129         final int strLen = decodeInt(src, 7);
130         if (strLen > src.remaining()) {
131             throw new HPackException(UNEXPECTED_EOS);
132         }
133         if (src.hasArray()) {
134             final byte[] b = src.array();
135             final int off = src.position();
136             buffer.append(b, src.arrayOffset() + off, strLen);
137             src.position(off + strLen);
138         } else {
139             for (int i = 0; i < strLen; i++) {
140                 buffer.append(src.get());
141             }
142         }
143     }
144 
145     static void decodeHuffman(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
146 
147         final int strLen = decodeInt(src, 7);
148         if (strLen > src.remaining()) {
149             throw new HPackException(UNEXPECTED_EOS);
150         }
151         final int limit = src.limit();
152         src.limit(src.position() + strLen);
153         Huffman.DECODER.decode(buffer, src);
154         src.limit(limit);
155     }
156 
157     void decodeString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
158 
159         final int firstByte = peekByte(src);
160         if ((firstByte & 0x80) == 0x80) {
161             decodeHuffman(buffer, src);
162         } else {
163             decodePlainString(buffer, src);
164         }
165     }
166 
167     private void clearState() {
168 
169         if (this.tmpBuf != null) {
170             this.tmpBuf.clear();
171         }
172         if (this.charsetDecoder != null) {
173             this.charsetDecoder.reset();
174         }
175         this.contentBuf.clear();
176     }
177 
178     private void expandCapacity(final int capacity) {
179 
180         final CharBuffer previous = this.tmpBuf;
181         this.tmpBuf = CharBuffer.allocate(capacity);
182         previous.flip();
183         this.tmpBuf.put(previous);
184     }
185 
186     private void ensureCapacity(final int extra) {
187 
188         if (this.tmpBuf == null) {
189             this.tmpBuf = CharBuffer.allocate(Math.max(256, extra));
190         }
191         final int requiredCapacity = this.tmpBuf.remaining() + extra;
192         if (requiredCapacity > this.tmpBuf.capacity()) {
193             expandCapacity(requiredCapacity);
194         }
195     }
196 
197     int decodeString(final ByteBuffer src, final StringBuilder buf) throws HPackException, CharacterCodingException {
198 
199         clearState();
200         decodeString(this.contentBuf, src);
201         final int binaryLen = this.contentBuf.length();
202         if (this.charsetDecoder == null) {
203             buf.ensureCapacity(binaryLen);
204             for (int i = 0; i < binaryLen; i++) {
205                 buf.append((char) (this.contentBuf.byteAt(i) & 0xff));
206             }
207         } else {
208             final ByteBuffer in = ByteBuffer.wrap(this.contentBuf.array(), 0, binaryLen);
209             while (in.hasRemaining()) {
210                 ensureCapacity(in.remaining());
211                 final CoderResult result = this.charsetDecoder.decode(in, this.tmpBuf, true);
212                 if (result.isError()) {
213                     result.throwException();
214                 }
215             }
216             ensureCapacity(8);
217             final CoderResult result = this.charsetDecoder.flush(this.tmpBuf);
218             if (result.isError()) {
219                 result.throwException();
220             }
221             this.tmpBuf.flip();
222             buf.append(this.tmpBuf);
223         }
224         return binaryLen;
225     }
226 
227     HPackHeader decodeLiteralHeader(
228             final ByteBuffer src,
229             final HPackRepresentation representation) throws HPackException, CharacterCodingException {
230 
231         final int n = representation == HPackRepresentation.WITH_INDEXING ? 6 : 4;
232         final int index = decodeInt(src, n);
233         final String name;
234         final int nameLen;
235         if (index == 0) {
236             final StringBuilder buf = new StringBuilder();
237             nameLen = decodeString(src, buf);
238             name = buf.toString();
239         } else {
240             final HPackHeader existing =  this.dynamicTable.getHeader(index);
241             if (existing == null) {
242                 throw new HPackException("Invalid header index");
243             }
244             name = existing.getName();
245             nameLen = existing.getNameLen();
246         }
247         final StringBuilder buf = new StringBuilder();
248         final int valueLen = decodeString(src, buf);
249         final String value = buf.toString();
250         final HPackHeaderpack/HPackHeader.html#HPackHeader">HPackHeader header = new HPackHeader(name, nameLen, value, valueLen, representation == HPackRepresentation.NEVER_INDEXED);
251         if (representation == HPackRepresentation.WITH_INDEXING) {
252             this.dynamicTable.add(header);
253         }
254         return header;
255     }
256 
257     HPackHeader decodeIndexedHeader(final ByteBuffer src) throws HPackException {
258 
259         final int index = decodeInt(src, 7);
260         final HPackHeader existing =  this.dynamicTable.getHeader(index);
261         if (existing == null) {
262             throw new HPackException("Invalid header index");
263         }
264         return existing;
265     }
266 
267     public Header decodeHeader(final ByteBuffer src) throws HPackException {
268         final HPackHeader header = decodeHPackHeader(src);
269         return header != null ? new BasicHeader(header.getName(), header.getValue(), header.isSensitive()) : null;
270     }
271 
272     HPackHeader decodeHPackHeader(final ByteBuffer src) throws HPackException {
273         try {
274             while (src.hasRemaining()) {
275                 final int b = peekByte(src);
276                 if ((b & 0x80) == 0x80) {
277                     return decodeIndexedHeader(src);
278                 } else if ((b & 0xc0) == 0x40) {
279                     return decodeLiteralHeader(src, HPackRepresentation.WITH_INDEXING);
280                 } else if ((b & 0xf0) == 0x00) {
281                     return decodeLiteralHeader(src, HPackRepresentation.WITHOUT_INDEXING);
282                 } else if ((b & 0xf0) == 0x10) {
283                     return decodeLiteralHeader(src, HPackRepresentation.NEVER_INDEXED);
284                 } else if ((b & 0xe0) == 0x20) {
285                     final int maxSize = decodeInt(src, 5);
286                     this.dynamicTable.setMaxSize(Math.min(this.maxTableSize, maxSize));
287                 } else {
288                     throw new HPackException("Unexpected header first byte: 0x" + Integer.toHexString(b));
289                 }
290             }
291             return null;
292         } catch (final CharacterCodingException ex) {
293             throw new HPackException(ex.getMessage(), ex);
294         }
295     }
296 
297     public List<Header> decodeHeaders(final ByteBuffer src) throws HPackException {
298         final boolean enforceSizeLimit = maxListSize < Integer.MAX_VALUE;
299         int listSize = 0;
300 
301         final List<Header> list = new ArrayList<>();
302         while (src.hasRemaining()) {
303             final HPackHeader header = decodeHPackHeader(src);
304             if (header == null) {
305                 break;
306             }
307             if (enforceSizeLimit) {
308                 listSize += header.getTotalSize();
309                 if (listSize >= maxListSize) {
310                     throw new HeaderListConstraintException("Maximum header list size exceeded");
311                 }
312             }
313             list.add(new BasicHeader(header.getName(), header.getValue(), header.isSensitive()));
314         }
315         return list;
316     }
317 
318     public int getMaxTableSize() {
319         return this.maxTableSize;
320     }
321 
322     public void setMaxTableSize(final int maxTableSize) {
323         Args.notNegative(maxTableSize, "Max table size");
324         this.maxTableSize = maxTableSize;
325         this.dynamicTable.setMaxSize(maxTableSize);
326     }
327 
328     public int getMaxListSize() {
329         return maxListSize;
330     }
331 
332     public void setMaxListSize(final int maxListSize) {
333         Args.notNegative(maxListSize, "Max list size");
334         this.maxListSize = maxListSize;
335     }
336 
337 }