// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** * HPACK - Header Compression for HTTP/2 * <p>This class maintains the compression context for a single HTTP/2 * connection. Specifically it holds the static and dynamic Header Field Tables * and the associated sizes and limits. * </p> * <p>It is compliant with draft 11 of the specification</p> */ public class HpackContext { public static final Logger LOG = Log.getLogger(HpackContext.class); private static final String EMPTY = ""; public static final String[][] STATIC_TABLE = { {null,null}, /* 1 */ {":authority",EMPTY}, /* 2 */ {":method","GET"}, /* 3 */ {":method","POST"}, /* 4 */ {":path","/"}, /* 5 */ {":path","/index.html"}, /* 6 */ {":scheme","http"}, /* 7 */ {":scheme","https"}, /* 8 */ {":status","200"}, /* 9 */ {":status","204"}, /* 10 */ {":status","206"}, /* 11 */ {":status","304"}, /* 12 */ {":status","400"}, /* 13 */ {":status","404"}, /* 14 */ {":status","500"}, /* 15 */ {"accept-charset",EMPTY}, /* 16 */ {"accept-encoding","gzip, deflate"}, /* 17 */ {"accept-language",EMPTY}, /* 18 */ {"accept-ranges",EMPTY}, /* 19 */ {"accept",EMPTY}, /* 20 */ {"access-control-allow-origin",EMPTY}, /* 21 */ {"age",EMPTY}, /* 22 */ {"allow",EMPTY}, /* 23 */ {"authorization",EMPTY}, /* 24 */ {"cache-control",EMPTY}, /* 25 */ {"content-disposition",EMPTY}, /* 26 */ {"content-encoding",EMPTY}, /* 27 */ {"content-language",EMPTY}, /* 28 */ {"content-length",EMPTY}, /* 29 */ {"content-location",EMPTY}, /* 30 */ {"content-range",EMPTY}, /* 31 */ {"content-type",EMPTY}, /* 32 */ {"cookie",EMPTY}, /* 33 */ {"date",EMPTY}, /* 34 */ {"etag",EMPTY}, /* 35 */ {"expect",EMPTY}, /* 36 */ {"expires",EMPTY}, /* 37 */ {"from",EMPTY}, /* 38 */ {"host",EMPTY}, /* 39 */ {"if-match",EMPTY}, /* 40 */ {"if-modified-since",EMPTY}, /* 41 */ {"if-none-match",EMPTY}, /* 42 */ {"if-range",EMPTY}, /* 43 */ {"if-unmodified-since",EMPTY}, /* 44 */ {"last-modified",EMPTY}, /* 45 */ {"link",EMPTY}, /* 46 */ {"location",EMPTY}, /* 47 */ {"max-forwards",EMPTY}, /* 48 */ {"proxy-authenticate",EMPTY}, /* 49 */ {"proxy-authorization",EMPTY}, /* 50 */ {"range",EMPTY}, /* 51 */ {"referer",EMPTY}, /* 52 */ {"refresh",EMPTY}, /* 53 */ {"retry-after",EMPTY}, /* 54 */ {"server",EMPTY}, /* 55 */ {"set-cookie",EMPTY}, /* 56 */ {"strict-transport-security",EMPTY}, /* 57 */ {"transfer-encoding",EMPTY}, /* 58 */ {"user-agent",EMPTY}, /* 59 */ {"vary",EMPTY}, /* 60 */ {"via",EMPTY}, /* 61 */ {"www-authenticate",EMPTY}, }; private static final Map<HttpField,Entry> __staticFieldMap = new HashMap<>(); private static final Trie<StaticEntry> __staticNameMap = new ArrayTernaryTrie<>(true,512); private static final StaticEntry[] __staticTableByHeader = new StaticEntry[HttpHeader.UNKNOWN.ordinal()]; private static final StaticEntry[] __staticTable=new StaticEntry[STATIC_TABLE.length]; public static final int STATIC_SIZE = STATIC_TABLE.length-1; static { Set<String> added = new HashSet<>(); for (int i=1;i<STATIC_TABLE.length;i++) { StaticEntry entry=null; String name = STATIC_TABLE[i][0]; String value = STATIC_TABLE[i][1]; HttpHeader header = HttpHeader.CACHE.get(name); if (header!=null && value!=null) { switch (header) { case C_METHOD: { HttpMethod method = HttpMethod.CACHE.get(value); if (method!=null) entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,method)); break; } case C_SCHEME: { HttpScheme scheme = HttpScheme.CACHE.get(value); if (scheme!=null) entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,scheme)); break; } case C_STATUS: { entry=new StaticEntry(i,new StaticTableHttpField(header,name,value,Integer.valueOf(value))); break; } default: break; } } if (entry==null) entry=new StaticEntry(i,header==null?new HttpField(STATIC_TABLE[i][0],value):new HttpField(header,name,value)); __staticTable[i]=entry; if (entry._field.getValue()!=null) __staticFieldMap.put(entry._field,entry); if (!added.contains(entry._field.getName())) { added.add(entry._field.getName()); __staticNameMap.put(entry._field.getName(),entry); if (__staticNameMap.get(entry._field.getName())==null) throw new IllegalStateException("name trie too small"); } } for (HttpHeader h : HttpHeader.values()) { StaticEntry entry = __staticNameMap.get(h.asString()); if (entry!=null) __staticTableByHeader[h.ordinal()]=entry; } } private int _maxDynamicTableSizeInBytes; private int _dynamicTableSizeInBytes; private final DynamicTable _dynamicTable; private final Map<HttpField,Entry> _fieldMap = new HashMap<>(); private final Map<String,Entry> _nameMap = new HashMap<>(); HpackContext(int maxDynamicTableSize) { _maxDynamicTableSizeInBytes=maxDynamicTableSize; int guesstimateEntries = 10+maxDynamicTableSize/(32+10+10); _dynamicTable=new DynamicTable(guesstimateEntries); if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] created max=%d",hashCode(),maxDynamicTableSize)); } public void resize(int newMaxDynamicTableSize) { if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d",hashCode(),_maxDynamicTableSizeInBytes,newMaxDynamicTableSize)); _maxDynamicTableSizeInBytes=newMaxDynamicTableSize; _dynamicTable.evict(); } public Entry get(HttpField field) { Entry entry = _fieldMap.get(field); if (entry==null) entry=__staticFieldMap.get(field); return entry; } public Entry get(String name) { Entry entry = __staticNameMap.get(name); if (entry!=null) return entry; return _nameMap.get(StringUtil.asciiToLowerCase(name)); } public Entry get(int index) { if (index<=STATIC_SIZE) return __staticTable[index]; return _dynamicTable.get(index); } public Entry get(HttpHeader header) { Entry e = __staticTableByHeader[header.ordinal()]; if (e==null) return get(header.asString()); return e; } public static Entry getStatic(HttpHeader header) { return __staticTableByHeader[header.ordinal()]; } public Entry add(HttpField field) { Entry entry=new Entry(field); int size = entry.getSize(); if (size>_maxDynamicTableSizeInBytes) { if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] !added size %d>%d",hashCode(),size,_maxDynamicTableSizeInBytes)); return null; } _dynamicTableSizeInBytes+=size; _dynamicTable.add(entry); _fieldMap.put(field,entry); _nameMap.put(StringUtil.asciiToLowerCase(field.getName()),entry); if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] added %s",hashCode(),entry)); _dynamicTable.evict(); return entry; } /** * @return Current dynamic table size in entries */ public int size() { return _dynamicTable.size(); } /** * @return Current Dynamic table size in Octets */ public int getDynamicTableSize() { return _dynamicTableSizeInBytes; } /** * @return Max Dynamic table size in Octets */ public int getMaxDynamicTableSize() { return _maxDynamicTableSizeInBytes; } public int index(Entry entry) { if (entry._slot<0) return 0; if (entry.isStatic()) return entry._slot; return _dynamicTable.index(entry); } public static int staticIndex(HttpHeader header) { if (header==null) return 0; Entry entry=__staticNameMap.get(header.asString()); if (entry==null) return 0; return entry._slot; } @Override public String toString() { return String.format("HpackContext@%x{entries=%d,size=%d,max=%d}",hashCode(),_dynamicTable.size(),_dynamicTableSizeInBytes,_maxDynamicTableSizeInBytes); } private class DynamicTable { Entry[] _entries; int _size; int _offset; int _growby; private DynamicTable(int initCapacity) { _entries=new Entry[initCapacity]; _growby=initCapacity; } public void add(Entry entry) { if (_size==_entries.length) { Entry[] entries = new Entry[_entries.length+_growby]; for (int i=0;i<_size;i++) { int slot = (_offset+i)%_entries.length; entries[i]=_entries[slot]; entries[i]._slot=i; } _entries=entries; _offset=0; } int slot=(_size++ + _offset)%_entries.length; _entries[slot]=entry; entry._slot=slot; } public int index(Entry entry) { return STATIC_SIZE + _size-(entry._slot-_offset+_entries.length)%_entries.length; } public Entry get(int index) { int d = index-STATIC_SIZE-1; if (d<0 || d>=_size) return null; int slot = (_offset+_size-d-1)%_entries.length; return _entries[slot]; } public int size() { return _size; } private void evict() { while (_dynamicTableSizeInBytes>_maxDynamicTableSizeInBytes) { Entry entry = _entries[_offset]; _entries[_offset]=null; _offset = (_offset+1)%_entries.length; _size--; if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] evict %s",HpackContext.this.hashCode(),entry)); _dynamicTableSizeInBytes-=entry.getSize(); entry._slot=-1; _fieldMap.remove(entry.getHttpField()); String lc=StringUtil.asciiToLowerCase(entry.getHttpField().getName()); if (entry==_nameMap.get(lc)) _nameMap.remove(lc); } if (LOG.isDebugEnabled()) LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d",HpackContext.this.hashCode(),_dynamicTable.size(),_dynamicTableSizeInBytes,_maxDynamicTableSizeInBytes)); } } public static class Entry { final HttpField _field; int _slot; // The index within it's array Entry() { _slot=-1; _field=null; } Entry(HttpField field) { _field=field; } public int getSize() { String value = _field.getValue(); return 32 + _field.getName().length() + (value == null ? 0 : value.length()); } public HttpField getHttpField() { return _field; } public boolean isStatic() { return false; } public byte[] getStaticHuffmanValue() { return null; } public String toString() { return String.format("{%s,%d,%s,%x}",isStatic()?"S":"D",_slot,_field,hashCode()); } } public static class StaticEntry extends Entry { private final byte[] _huffmanValue; private final byte _encodedField; StaticEntry(int index,HttpField field) { super(field); _slot=index; String value = field.getValue(); if (value!=null && value.length()>0) { int huffmanLen = Huffman.octetsNeeded(value); int lenLen = NBitInteger.octectsNeeded(7,huffmanLen); _huffmanValue = new byte[1+lenLen+huffmanLen]; ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); // Indicate Huffman buffer.put((byte)0x80); // Add huffman length NBitInteger.encode(buffer,7,huffmanLen); // Encode value Huffman.encode(buffer,value); } else _huffmanValue=null; _encodedField=(byte)(0x80|index); } @Override public boolean isStatic() { return true; } @Override public byte[] getStaticHuffmanValue() { return _huffmanValue; } public byte getEncodedField() { return _encodedField; } } }