org.eclipse.jetty.http2.hpack.HpackContext Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.util.Arrays;
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.http.compression.HuffmanEncoder;
import org.eclipse.jetty.http.compression.NBitIntegerEncoder;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* HPACK - Header Compression for HTTP/2
* 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.
*
* It is compliant with draft 11 of the specification
*/
public class HpackContext
{
private static final Logger LOG = LoggerFactory.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 __staticFieldMap = new HashMap<>();
private static final Index __staticNameMap;
private static final StaticEntry[] __staticTableByHeader = new StaticEntry[HttpHeader.values().length];
private static final StaticEntry[] __staticTable = new StaticEntry[STATIC_TABLE.length];
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
public static final int DEFAULT_MAX_TABLE_CAPACITY = 4096;
static
{
Index.Builder staticNameMapBuilder = new Index.Builder().caseSensitive(false);
Set 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, 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());
staticNameMapBuilder.with(entry._field.getName(), entry);
}
}
__staticNameMap = staticNameMapBuilder.build();
for (HttpHeader h : HttpHeader.values())
{
StaticEntry entry = __staticNameMap.get(h.asString());
if (entry != null)
__staticTableByHeader[h.ordinal()] = entry;
}
}
private final DynamicTable _dynamicTable;
private final Map _fieldMap = new HashMap<>();
private final Map _nameMap = new HashMap<>();
private int _maxTableSize;
private int _tableSize;
HpackContext(int maxTableSize)
{
_maxTableSize = maxTableSize;
int guesstimateEntries = 10 + maxTableSize / (32 + 10 + 10);
_dynamicTable = new DynamicTable(guesstimateEntries);
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] created max=%d", hashCode(), maxTableSize));
}
public void resize(int newMaxDynamicTableSize)
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _maxTableSize, newMaxDynamicTableSize));
_maxTableSize = 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 > _maxTableSize)
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] !added size %d>%d", hashCode(), size, _maxTableSize));
_dynamicTable.evictAll();
return null;
}
_tableSize += size;
_dynamicTable.add(entry);
_fieldMap.put(field, entry);
_nameMap.put(field.getLowerCaseName(), 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 _tableSize;
}
/**
* @return Max Dynamic table size in Octets
*/
public int getMaxDynamicTableSize()
{
return _maxTableSize;
}
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(), _tableSize, _maxTableSize);
}
private class DynamicTable
{
private Entry[] _entries;
private final int _growby;
private int _size;
private int _offset;
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 (_tableSize > _maxTableSize)
{
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));
_tableSize -= entry.getSize();
entry._slot = -1;
_fieldMap.remove(entry.getHttpField());
String lc = entry.getHttpField().getLowerCaseName();
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(), _tableSize, _maxTableSize));
}
private void evictAll()
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] evictAll", HpackContext.this.hashCode()));
if (size() > 0)
{
_fieldMap.clear();
_nameMap.clear();
_offset = 0;
_size = 0;
_tableSize = 0;
Arrays.fill(_entries, null);
}
}
}
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;
}
@Override
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 = HuffmanEncoder.octetsNeeded(value);
if (huffmanLen < 0)
throw new IllegalStateException("bad value");
int lenLen = NBitIntegerEncoder.octetsNeeded(7, huffmanLen);
_huffmanValue = new byte[lenLen + huffmanLen];
ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue);
// Indicate Huffman
buffer.put((byte)0x80);
// Add huffman length
NBitIntegerEncoder.encode(buffer, 7, huffmanLen);
// Encode value
HuffmanEncoder.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;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy