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