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