View Javadoc
1   /*
2    * Copyright 2015 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  
17  /*
18   * Copyright 2014 Twitter, Inc.
19   *
20   * Licensed under the Apache License, Version 2.0 (the "License");
21   * you may not use this file except in compliance with the License.
22   * You may obtain a copy of the License at
23   *
24   *     https://www.apache.org/licenses/LICENSE-2.0
25   *
26   * Unless required by applicable law or agreed to in writing, software
27   * distributed under the License is distributed on an "AS IS" BASIS,
28   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29   * See the License for the specific language governing permissions and
30   * limitations under the License.
31   */
32  package io.netty.handler.codec.http2;
33  
34  import io.netty.buffer.ByteBuf;
35  import io.netty.handler.codec.http2.HpackUtil.IndexType;
36  import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
37  import io.netty.util.AsciiString;
38  import io.netty.util.CharsetUtil;
39  
40  import java.util.Map;
41  
42  import static io.netty.handler.codec.http2.HpackUtil.equalsConstantTime;
43  import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime;
44  import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
45  import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_LIST_SIZE;
46  import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE;
47  import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_HEADER_LIST_SIZE;
48  import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_HEADER_TABLE_SIZE;
49  import static io.netty.handler.codec.http2.Http2CodecUtil.headerListSizeExceeded;
50  import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
51  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
52  import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
53  import static java.lang.Math.max;
54  import static java.lang.Math.min;
55  
56  /**
57   * An HPACK encoder.
58   *
59   * <p>Implementation note:  This class is security sensitive, and depends on users correctly identifying their headers
60   * as security sensitive or not.  If a header is considered not sensitive, methods names "insensitive" are used which
61   * are fast, but don't provide any security guarantees.
62   */
63  final class HpackEncoder {
64      static final int NOT_FOUND = -1;
65      static final int HUFF_CODE_THRESHOLD = 512;
66      // a hash map of header fields keyed by header name
67      private final NameEntry[] nameEntries;
68  
69      // a hash map of header fields keyed by header name and value
70      private final NameValueEntry[] nameValueEntries;
71  
72      private final NameValueEntry head = new NameValueEntry(-1, AsciiString.EMPTY_STRING,
73        AsciiString.EMPTY_STRING, Integer.MAX_VALUE, null);
74  
75      private NameValueEntry latest = head;
76  
77      private final HpackHuffmanEncoder hpackHuffmanEncoder = new HpackHuffmanEncoder();
78      private final byte hashMask;
79      private final boolean ignoreMaxHeaderListSize;
80      private final int huffCodeThreshold;
81      private long size;
82      private long maxHeaderTableSize;
83      private long maxHeaderListSize;
84  
85      private boolean pendingHeaderTableSizeUpdate;
86      private long pendingMinHeaderTableSize = Long.MAX_VALUE;
87  
88      /**
89       * Creates a new encoder.
90       */
91      HpackEncoder() {
92          this(false);
93      }
94  
95      /**
96       * Creates a new encoder.
97       */
98      HpackEncoder(boolean ignoreMaxHeaderListSize) {
99          this(ignoreMaxHeaderListSize, 64, HUFF_CODE_THRESHOLD);
100     }
101 
102     /**
103      * Creates a new encoder.
104      */
105     HpackEncoder(boolean ignoreMaxHeaderListSize, int arraySizeHint, int huffCodeThreshold) {
106         this.ignoreMaxHeaderListSize = ignoreMaxHeaderListSize;
107         maxHeaderTableSize = DEFAULT_HEADER_TABLE_SIZE;
108         maxHeaderListSize = MAX_HEADER_LIST_SIZE;
109         // Enforce a bound of [2, 128] because hashMask is a byte. The max possible value of hashMask is one less
110         // than the length of this array, and we want the mask to be > 0. There is also simply an
111         // upper limit on how many entries can be safely held, as nameEntries will have duplicate
112         // entries every time a header name is used, and thus form a long collision chain.
113         nameEntries = new NameEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))];
114         nameValueEntries = new NameValueEntry[nameEntries.length];
115         hashMask = (byte) (nameEntries.length - 1);
116         this.huffCodeThreshold = huffCodeThreshold;
117     }
118 
119     /**
120      * Encode the header field into the header block.
121      * <p>
122      * <strong>The given {@link CharSequence}s must be immutable!</strong>
123      */
124     public void encodeHeaders(int streamId, ByteBuf out, Http2Headers headers, SensitivityDetector sensitivityDetector)
125       throws Http2Exception {
126         if (ignoreMaxHeaderListSize) {
127             encodeHeadersIgnoreMaxHeaderListSize(out, headers, sensitivityDetector);
128         } else {
129             encodeHeadersEnforceMaxHeaderListSize(streamId, out, headers, sensitivityDetector);
130         }
131     }
132 
133     private void encodeHeadersEnforceMaxHeaderListSize(int streamId, ByteBuf out, Http2Headers headers,
134                                                        SensitivityDetector sensitivityDetector)
135       throws Http2Exception {
136         long headerSize = 0;
137         // To ensure we stay consistent with our peer check the size is valid before we potentially modify HPACK state.
138         for (Map.Entry<CharSequence, CharSequence> header : headers) {
139             CharSequence name = header.getKey();
140             CharSequence value = header.getValue();
141             // OK to increment now and check for bounds after because this value is limited to unsigned int and will not
142             // overflow.
143             headerSize += HpackHeaderField.sizeOf(name, value);
144             if (headerSize > maxHeaderListSize) {
145                 headerListSizeExceeded(streamId, maxHeaderListSize, false);
146             }
147         }
148         encodeHeadersIgnoreMaxHeaderListSize(out, headers, sensitivityDetector);
149     }
150 
151     private void encodeHeadersIgnoreMaxHeaderListSize(ByteBuf out, Http2Headers headers,
152                                                       SensitivityDetector sensitivityDetector) {
153         if (this.pendingHeaderTableSizeUpdate) {
154             this.pendingHeaderTableSizeUpdate = false;
155             // Casting to integer is safe as we verified the table size is a valid unsigned int in
156             // setMaxHeaderTableSize().
157             if (this.pendingMinHeaderTableSize < this.maxHeaderTableSize) {
158                 // The table size shrunk and increased again.
159                 encodeInteger(out, 0x20, 5, this.pendingMinHeaderTableSize);
160             }
161             this.pendingMinHeaderTableSize = Long.MAX_VALUE;
162             encodeInteger(out, 0x20, 5, this.maxHeaderTableSize);
163         }
164         for (Map.Entry<CharSequence, CharSequence> header : headers) {
165             CharSequence name = header.getKey();
166             CharSequence value = header.getValue();
167             encodeHeader(out, name, value, sensitivityDetector.isSensitive(name, value),
168               HpackHeaderField.sizeOf(name, value));
169         }
170     }
171 
172     /**
173      * Encode the header field into the header block.
174      * <p>
175      * <strong>The given {@link CharSequence}s must be immutable!</strong>
176      */
177     private void encodeHeader(ByteBuf out, CharSequence name, CharSequence value, boolean sensitive, long headerSize) {
178         // If the header value is sensitive then it must never be indexed
179         if (sensitive) {
180             int nameIndex = getNameIndex(name);
181             encodeLiteral(out, name, value, IndexType.NEVER, nameIndex);
182             return;
183         }
184 
185         // If the peer will only use the static table
186         if (maxHeaderTableSize == 0) {
187             int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
188             if (staticTableIndex == HpackStaticTable.NOT_FOUND) {
189                 int nameIndex = HpackStaticTable.getIndex(name);
190                 encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
191             } else {
192                 encodeInteger(out, 0x80, 7, staticTableIndex);
193             }
194             return;
195         }
196 
197         // If the headerSize is greater than the max table size then it must be encoded literally
198         if (headerSize > maxHeaderTableSize) {
199             int nameIndex = getNameIndex(name);
200             encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
201             return;
202         }
203 
204         int nameHash = AsciiString.hashCode(name);
205         int valueHash = AsciiString.hashCode(value);
206         NameValueEntry headerField = getEntryInsensitive(name, nameHash, value, valueHash);
207         if (headerField != null) {
208             // Section 6.1. Indexed Header Field Representation
209             encodeInteger(out, 0x80, 7, getIndexPlusOffset(headerField.counter));
210         } else {
211             int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
212             if (staticTableIndex != HpackStaticTable.NOT_FOUND) {
213                 // Section 6.1. Indexed Header Field Representation
214                 encodeInteger(out, 0x80, 7, staticTableIndex);
215             } else {
216                 ensureCapacity(headerSize);
217                 encodeAndAddEntries(out, name, nameHash, value, valueHash);
218                 size += headerSize;
219             }
220         }
221     }
222 
223     private void encodeAndAddEntries(ByteBuf out, CharSequence name, int nameHash, CharSequence value, int valueHash) {
224         int staticTableIndex = HpackStaticTable.getIndex(name);
225         int nextCounter = latestCounter() - 1;
226         if (staticTableIndex == HpackStaticTable.NOT_FOUND) {
227             NameEntry e = getEntry(name, nameHash);
228             if (e == null) {
229                 encodeLiteral(out, name, value, IndexType.INCREMENTAL, NOT_FOUND);
230                 addNameEntry(name, nameHash, nextCounter);
231                 addNameValueEntry(name, value, nameHash, valueHash, nextCounter);
232             } else {
233                 encodeLiteral(out, name, value, IndexType.INCREMENTAL, getIndexPlusOffset(e.counter));
234                 addNameValueEntry(e.name, value, nameHash, valueHash, nextCounter);
235 
236                 // The name entry should always point to the latest counter.
237                 e.counter = nextCounter;
238             }
239         } else {
240             encodeLiteral(out, name, value, IndexType.INCREMENTAL, staticTableIndex);
241             // use the name from the static table to optimize memory usage.
242             addNameValueEntry(
243               HpackStaticTable.getEntry(staticTableIndex).name, value, nameHash, valueHash, nextCounter);
244         }
245     }
246 
247     /**
248      * Set the maximum table size.
249      */
250     public void setMaxHeaderTableSize(long maxHeaderTableSize) throws Http2Exception {
251         if (maxHeaderTableSize < MIN_HEADER_TABLE_SIZE || maxHeaderTableSize > MAX_HEADER_TABLE_SIZE) {
252             throw connectionError(PROTOCOL_ERROR, "Header Table Size must be >= %d and <= %d but was %d",
253               MIN_HEADER_TABLE_SIZE, MAX_HEADER_TABLE_SIZE, maxHeaderTableSize);
254         }
255         // While the receiver may allow a larger table, it is important to cap the local impact of
256         // hash collisions and memory use. This allows the default 4 KiB table when using the
257         // default 64 arraySizeHint.
258         maxHeaderTableSize = Math.min(maxHeaderTableSize, nameEntries.length * 64);
259         if (this.maxHeaderTableSize == maxHeaderTableSize) {
260             return;
261         }
262         this.pendingHeaderTableSizeUpdate = true;
263         this.pendingMinHeaderTableSize = Math.min(this.pendingMinHeaderTableSize, maxHeaderTableSize);
264         this.maxHeaderTableSize = maxHeaderTableSize;
265         ensureCapacity(0);
266     }
267 
268     /**
269      * Return the maximum table size.
270      */
271     public long getMaxHeaderTableSize() {
272         return maxHeaderTableSize;
273     }
274 
275     public void setMaxHeaderListSize(long maxHeaderListSize) throws Http2Exception {
276         if (maxHeaderListSize < MIN_HEADER_LIST_SIZE || maxHeaderListSize > MAX_HEADER_LIST_SIZE) {
277             throw connectionError(PROTOCOL_ERROR, "Header List Size must be >= %d and <= %d but was %d",
278               MIN_HEADER_LIST_SIZE, MAX_HEADER_LIST_SIZE, maxHeaderListSize);
279         }
280         this.maxHeaderListSize = maxHeaderListSize;
281     }
282 
283     public long getMaxHeaderListSize() {
284         return maxHeaderListSize;
285     }
286 
287     /**
288      * Encode integer according to <a href="https://tools.ietf.org/html/rfc7541#section-5.1">Section 5.1</a>.
289      */
290     private static void encodeInteger(ByteBuf out, int mask, int n, int i) {
291         encodeInteger(out, mask, n, (long) i);
292     }
293 
294     /**
295      * Encode integer according to <a href="https://tools.ietf.org/html/rfc7541#section-5.1">Section 5.1</a>.
296      */
297     private static void encodeInteger(ByteBuf out, int mask, int n, long i) {
298         assert n >= 0 && n <= 8 : "N: " + n;
299         int nbits = 0xFF >>> 8 - n;
300         if (i < nbits) {
301             out.writeByte((int) (mask | i));
302         } else {
303             out.writeByte(mask | nbits);
304             long length = i - nbits;
305             for (; (length & ~0x7F) != 0; length >>>= 7) {
306                 out.writeByte((int) (length & 0x7F | 0x80));
307             }
308             out.writeByte((int) length);
309         }
310     }
311 
312     /**
313      * Encode string literal according to Section 5.2.
314      */
315     private void encodeStringLiteral(ByteBuf out, CharSequence string) {
316         int huffmanLength;
317         if (string.length() >= huffCodeThreshold
318           && (huffmanLength = hpackHuffmanEncoder.getEncodedLength(string)) < string.length()) {
319             encodeInteger(out, 0x80, 7, huffmanLength);
320             hpackHuffmanEncoder.encode(out, string);
321         } else {
322             encodeInteger(out, 0x00, 7, string.length());
323             if (string instanceof AsciiString) {
324                 // Fast-path
325                 AsciiString asciiString = (AsciiString) string;
326                 out.writeBytes(asciiString.array(), asciiString.arrayOffset(), asciiString.length());
327             } else {
328                 // Only ASCII is allowed in http2 headers, so it is fine to use this.
329                 // https://tools.ietf.org/html/rfc7540#section-8.1.2
330                 out.writeCharSequence(string, CharsetUtil.ISO_8859_1);
331             }
332         }
333     }
334 
335     /**
336      * Encode literal header field according to Section 6.2.
337      */
338     private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value, IndexType indexType,
339                                int nameIndex) {
340         boolean nameIndexValid = nameIndex != NOT_FOUND;
341         switch (indexType) {
342             case INCREMENTAL:
343                 encodeInteger(out, 0x40, 6, nameIndexValid ? nameIndex : 0);
344                 break;
345             case NONE:
346                 encodeInteger(out, 0x00, 4, nameIndexValid ? nameIndex : 0);
347                 break;
348             case NEVER:
349                 encodeInteger(out, 0x10, 4, nameIndexValid ? nameIndex : 0);
350                 break;
351             default:
352                 throw new Error("should not reach here");
353         }
354         if (!nameIndexValid) {
355             encodeStringLiteral(out, name);
356         }
357         encodeStringLiteral(out, value);
358     }
359 
360     private int getNameIndex(CharSequence name) {
361         int index = HpackStaticTable.getIndex(name);
362         if (index != HpackStaticTable.NOT_FOUND) {
363             return index;
364         }
365         NameEntry e = getEntry(name, AsciiString.hashCode(name));
366         return e == null ? NOT_FOUND : getIndexPlusOffset(e.counter);
367     }
368 
369     /**
370      * Ensure that the dynamic table has enough room to hold 'headerSize' more bytes. Removes the
371      * oldest entry from the dynamic table until sufficient space is available.
372      */
373     private void ensureCapacity(long headerSize) {
374         while (maxHeaderTableSize - size < headerSize) {
375             remove();
376         }
377     }
378 
379     /**
380      * Return the number of header fields in the dynamic table. Exposed for testing.
381      */
382     int length() {
383         return isEmpty() ? 0 : getIndex(head.after.counter);
384     }
385 
386     /**
387      * Return the size of the dynamic table. Exposed for testing.
388      */
389     long size() {
390         return size;
391     }
392 
393     /**
394      * Return the header field at the given index. Exposed for testing.
395      */
396     HpackHeaderField getHeaderField(int index) {
397         NameValueEntry entry = head;
398         while (index++ < length()) {
399             entry = entry.after;
400         }
401         return entry;
402     }
403 
404     /**
405      * Returns the header entry with the lowest index value for the header field. Returns null if
406      * header field is not in the dynamic table.
407      */
408     private NameValueEntry getEntryInsensitive(CharSequence name, int nameHash, CharSequence value, int valueHash) {
409         int h = hash(nameHash, valueHash);
410         for (NameValueEntry e = nameValueEntries[bucket(h)]; e != null; e = e.next) {
411             if (e.hash == h && equalsVariableTime(value, e.value) && equalsVariableTime(name, e.name)) {
412                 return e;
413             }
414         }
415         return null;
416     }
417 
418     /**
419      * Returns the lowest index value for the header field name in the dynamic table. Returns -1 if
420      * the header field name is not in the dynamic table.
421      */
422     private NameEntry getEntry(CharSequence name, int nameHash) {
423         for (NameEntry e = nameEntries[bucket(nameHash)]; e != null; e = e.next) {
424             if (e.hash == nameHash && equalsConstantTime(name, e.name) != 0) {
425                 return e;
426             }
427         }
428         return null;
429     }
430 
431     private int getIndexPlusOffset(int counter) {
432         return getIndex(counter) + HpackStaticTable.length;
433     }
434 
435     /**
436      * Compute the index into the dynamic table given the counter in the header entry.
437      */
438     private int getIndex(int counter) {
439         return counter - latestCounter() + 1;
440     }
441 
442     private int latestCounter() {
443         return latest.counter;
444     }
445 
446     private void addNameEntry(CharSequence name, int nameHash, int nextCounter) {
447         int bucket = bucket(nameHash);
448         nameEntries[bucket] = new NameEntry(nameHash, name, nextCounter, nameEntries[bucket]);
449     }
450 
451     private void addNameValueEntry(CharSequence name, CharSequence value,
452                                    int nameHash, int valueHash, int nextCounter) {
453         int hash = hash(nameHash, valueHash);
454         int bucket = bucket(hash);
455         NameValueEntry e = new NameValueEntry(hash, name, value, nextCounter, nameValueEntries[bucket]);
456         nameValueEntries[bucket] = e;
457         latest.after = e;
458         latest = e;
459     }
460 
461     /**
462      * Remove the oldest header field from the dynamic table.
463      */
464     private void remove() {
465         NameValueEntry eldest = head.after;
466         removeNameValueEntry(eldest);
467         removeNameEntryMatchingCounter(eldest.name, eldest.counter);
468         head.after = eldest.after;
469         eldest.unlink();
470         size -= eldest.size();
471         if (isEmpty()) {
472             latest = head;
473         }
474     }
475 
476     private boolean isEmpty() {
477         return size == 0;
478     }
479 
480     private void removeNameValueEntry(NameValueEntry eldest) {
481         int bucket = bucket(eldest.hash);
482         NameValueEntry e = nameValueEntries[bucket];
483         if (e == eldest) {
484             nameValueEntries[bucket] = eldest.next;
485         } else {
486             while (e.next != eldest) {
487                 e = e.next;
488             }
489             e.next = eldest.next;
490         }
491     }
492 
493     private void removeNameEntryMatchingCounter(CharSequence name, int counter) {
494         int hash = AsciiString.hashCode(name);
495         int bucket = bucket(hash);
496         NameEntry e = nameEntries[bucket];
497         if (e == null) {
498             return;
499         }
500         if (counter == e.counter) {
501             nameEntries[bucket] = e.next;
502             e.unlink();
503         } else {
504             NameEntry prev = e;
505             e = e.next;
506             while (e != null) {
507                 if (counter == e.counter) {
508                     prev.next = e.next;
509                     e.unlink();
510                     break;
511                 }
512                 prev = e;
513                 e = e.next;
514             }
515         }
516     }
517 
518     /**
519      * Returns the bucket of the hash table for the hash code h.
520      */
521     private int bucket(int h) {
522         return h & hashMask;
523     }
524 
525     private static int hash(int nameHash, int valueHash) {
526         return 31 * nameHash + valueHash;
527     }
528 
529     private static final class NameEntry {
530         NameEntry next;
531 
532         final CharSequence name;
533 
534         final int hash;
535 
536         // This is used to compute the index in the dynamic table.
537         int counter;
538 
539         NameEntry(int hash, CharSequence name, int counter, NameEntry next) {
540             this.hash = hash;
541             this.name = name;
542             this.counter = counter;
543             this.next = next;
544         }
545 
546         void unlink() {
547             next = null; // null references to prevent nepotism in generational GC.
548         }
549     }
550 
551     private static final class NameValueEntry extends HpackHeaderField {
552         // This field comprises the linked list used for implementing the eviction policy.
553         NameValueEntry after;
554 
555         NameValueEntry next;
556 
557         // hash of both name and value
558         final int hash;
559 
560         // This is used to compute the index in the dynamic table.
561         final int counter;
562 
563         NameValueEntry(int hash, CharSequence name, CharSequence value, int counter, NameValueEntry next) {
564             super(name, value);
565             this.next = next;
566             this.hash = hash;
567             this.counter = counter;
568         }
569 
570         void unlink() {
571             after = null; // null references to prevent nepotism in generational GC.
572             next = null;
573         }
574     }
575 }