org.reaktivity.nukleus.http2.internal.hpack.HpackContext Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nukleus-http Show documentation
Show all versions of nukleus-http Show documentation
HTTP Nukleus Implementation
/**
* Copyright 2016-2020 The Reaktivity Project
*
* The Reaktivity Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.reaktivity.nukleus.http2.internal.hpack;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
public class HpackContext
{
private static final DirectBuffer EMPTY_VALUE = new UnsafeBuffer(new byte[0]);
private static final HeaderField[] STATIC_TABLE =
{
/* 0 */ new HeaderField((String) null, null),
/* 1 */ new HeaderField(":authority", null),
/* 2 */ new HeaderField(":method", "GET"),
/* 3 */ new HeaderField(":method", "POST"),
/* 4 */ new HeaderField(":path", "/"),
/* 5 */ new HeaderField(":path", "/index.html"),
/* 6 */ new HeaderField(":scheme", "http"),
/* 7 */ new HeaderField(":scheme", "https"),
/* 8 */ new HeaderField(":status", "200"),
/* 9 */ new HeaderField(":status", "204"),
/* 10 */ new HeaderField(":status", "206"),
/* 11 */ new HeaderField(":status", "304"),
/* 12 */ new HeaderField(":status", "400"),
/* 13 */ new HeaderField(":status", "404"),
/* 14 */ new HeaderField(":status", "500"),
/* 15 */ new HeaderField("accept-charset", null),
/* 16 */ new HeaderField("accept-encoding", "gzip, deflate"),
/* 17 */ new HeaderField("accept-language", null),
/* 18 */ new HeaderField("accept-ranges", null),
/* 19 */ new HeaderField("accept", null),
/* 20 */ new HeaderField("access-control-allow-origin", null),
/* 21 */ new HeaderField("age", null),
/* 22 */ new HeaderField("allow", null),
/* 23 */ new HeaderField("authorization", null),
/* 24 */ new HeaderField("cache-control", null),
/* 25 */ new HeaderField("content-disposition", null),
/* 26 */ new HeaderField("content-encoding", null),
/* 27 */ new HeaderField("content-language", null),
/* 28 */ new HeaderField("content-length", null),
/* 29 */ new HeaderField("content-location", null),
/* 30 */ new HeaderField("content-range", null),
/* 31 */ new HeaderField("content-type", null),
/* 32 */ new HeaderField("cookie", null),
/* 33 */ new HeaderField("date", null),
/* 34 */ new HeaderField("etag", null),
/* 35 */ new HeaderField("expect", null),
/* 36 */ new HeaderField("expires", null),
/* 37 */ new HeaderField("from", null),
/* 38 */ new HeaderField("host", null),
/* 39 */ new HeaderField("if-match", null),
/* 40 */ new HeaderField("if-modified-since", null),
/* 41 */ new HeaderField("if-none-match", null),
/* 42 */ new HeaderField("if-range", null),
/* 43 */ new HeaderField("if-unmodified-since", null),
/* 44 */ new HeaderField("last-modified", null),
/* 45 */ new HeaderField("link", null),
/* 46 */ new HeaderField("location", null),
/* 47 */ new HeaderField("max-forwards", null),
/* 48 */ new HeaderField("proxy-authenticate", null),
/* 49 */ new HeaderField("proxy-authorization", null),
/* 50 */ new HeaderField("range", null),
/* 51 */ new HeaderField("referer", null),
/* 52 */ new HeaderField("refresh", null),
/* 53 */ new HeaderField("retry-after", null),
/* 54 */ new HeaderField("server", null),
/* 55 */ new HeaderField("set-cookie", null),
/* 56 */ new HeaderField("strict-transport-security", null),
/* 57 */ new HeaderField("transfer-encoding", null),
/* 58 */ new HeaderField("user-agent", null),
/* 59 */ new HeaderField("vary", null),
/* 60 */ new HeaderField("via", null),
/* 61 */ new HeaderField("www-authenticate", null)
};
private static final int STATIC_TABLE_LENGTH = STATIC_TABLE.length;
public static final DirectBuffer CONNECTION = new UnsafeBuffer("connection".getBytes(UTF_8));
public static final DirectBuffer TE = new UnsafeBuffer("te".getBytes(UTF_8));
public static final DirectBuffer TRAILERS = new UnsafeBuffer("trailers".getBytes(UTF_8));
public static final DirectBuffer KEEP_ALIVE = new UnsafeBuffer("keep-alive".getBytes(UTF_8));
public static final DirectBuffer PROXY_CONNECTION = new UnsafeBuffer("proxy-connection".getBytes(UTF_8));
public static final DirectBuffer UPGRADE = new UnsafeBuffer("upgrade".getBytes(UTF_8));
public static final DirectBuffer DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN = new UnsafeBuffer("*".getBytes(UTF_8));
// Dynamic table. Entries are added at the end (since it is in reverse order,
// need to calculate the index accordingly)
/* private */ final List table = new ArrayList<>();
/* private */ int tableSize;
// No need to update the following index maps for decoding context
private final boolean encoding;
// name --> uniquie id (stable across evictions) for dynamic entries.
// Used during encoding
private final Map name2Index = new HashMap<>();
// (name, value) --> uniquie id (stable across evictions) for dynamic entries.
// Used during encoding
private final Map namevalue2Index = new HashMap<>();
private int maxTableSize;
// Keeps track of number of evictions and used in calculation of unique id
// (No need to worry about overflow as it takes many years to overflow in practice)
private long noEvictions;
private static final class HeaderField
{
private final DirectBuffer name;
private final DirectBuffer value;
private int size;
HeaderField(String name, String value)
{
this(buffer(name), buffer(value));
}
HeaderField(DirectBuffer name, DirectBuffer value)
{
this.name = requireNonNull(name);
this.value = requireNonNull(value);
this.size = name.capacity() + value.capacity() + 32;
}
private static DirectBuffer buffer(String str)
{
return str == null ? EMPTY_VALUE : new UnsafeBuffer(str.getBytes(UTF_8));
}
}
public HpackContext()
{
this(4096, true);
}
public HpackContext(int maxTableSize, boolean encoding)
{
this.maxTableSize = maxTableSize;
this.encoding = encoding;
}
void add(String name, String value)
{
DirectBuffer nameBuffer = new UnsafeBuffer(name.getBytes(UTF_8));
DirectBuffer valueBuffer = new UnsafeBuffer(value.getBytes(UTF_8));
add(nameBuffer, valueBuffer);
}
public void add(DirectBuffer nameBuffer, DirectBuffer valueBuffer)
{
HeaderField header = new HeaderField(nameBuffer, valueBuffer);
// See if the header can be added to dynamic table. Calculate the
// number of entries to be evicted to make space in the table.
int noEntries = 0;
int wouldbeSize = tableSize + header.size;
while (noEntries < table.size() && wouldbeSize > maxTableSize)
{
wouldbeSize -= table.get(noEntries).size;
noEntries++;
}
if (noEntries > 0)
{
evict(noEntries);
}
// After evicting older entries, add the current one if space available
boolean spaceAvailable = wouldbeSize <= maxTableSize;
if (spaceAvailable)
{
if (encoding)
{
long id = noEvictions + table.size();
name2Index.putIfAbsent(header.name, id);
NameValue nameValue = new NameValue(header.name, header.value);
namevalue2Index.putIfAbsent(nameValue, id);
}
table.add(header);
tableSize += header.size;
}
}
public void updateSize(int newMaxTableSize)
{
if (newMaxTableSize < maxTableSize)
{
// Calculate the number of entries to be evicted so that table size is
// under new max table size
int wouldbeSize = maxTableSize;
int noEntries = 0;
while (noEntries < table.size() && wouldbeSize > newMaxTableSize)
{
wouldbeSize -= table.get(noEntries).size;
noEntries++;
}
if (noEntries > 0)
{
evict(noEntries);
}
}
this.maxTableSize = newMaxTableSize;
}
// Evicts older entries from dynamic table
private void evict(int noEntries)
{
for (int i = 0; i < noEntries; i++)
{
HeaderField header = table.get(i);
tableSize -= header.size;
if (encoding)
{
Long id = noEvictions + i;
if (id.equals(name2Index.get(header.name)))
{
name2Index.remove(header.name, id);
}
NameValue nameValue = new NameValue(header.name, header.value);
if (id.equals(namevalue2Index.get(nameValue)))
{
namevalue2Index.remove(nameValue, id);
}
}
}
table.subList(0, noEntries).clear();
noEvictions += noEntries;
}
// @return true if the index is valid
// false otherwise
public boolean valid(int index)
{
return index != 0 && index < STATIC_TABLE.length + table.size();
}
String name(int index)
{
DirectBuffer nameBuffer = nameBuffer(index);
return nameBuffer.getStringWithoutLengthUtf8(0, nameBuffer.capacity());
}
public DirectBuffer nameBuffer(int index)
{
if (!valid(index))
{
throw new IllegalArgumentException("Invalid index = " + index + " in HPACK context");
}
return index < STATIC_TABLE.length
? STATIC_TABLE[index].name
: table.get(table.size() - (index - STATIC_TABLE_LENGTH) - 1).name;
}
String value(int index)
{
DirectBuffer valueBuffer = valueBuffer(index);
return valueBuffer.getStringWithoutLengthUtf8(0, valueBuffer.capacity());
}
public DirectBuffer valueBuffer(int index)
{
if (!valid(index))
{
throw new IllegalArgumentException("Invalid index = " + index + " in HPACK context");
}
return index < STATIC_TABLE.length
? STATIC_TABLE[index].value
: table.get(table.size() - (index - STATIC_TABLE_LENGTH) - 1).value;
}
int index(String name)
{
DirectBuffer nameBuffer = new UnsafeBuffer(name.getBytes(UTF_8));
return index(nameBuffer);
}
public int index(DirectBuffer name)
{
int index = staticIndex(name);
// If there is no entry in static table, look in dynamic table
if (index == -1)
{
Long id = name2Index.get(name);
index = (id != null) ? idToIndex(id) : -1;
}
return index;
}
int index(String name, String value)
{
DirectBuffer nameBuffer = new UnsafeBuffer(name.getBytes(UTF_8));
DirectBuffer valueBuffer = new UnsafeBuffer(value.getBytes(UTF_8));
return index(nameBuffer, valueBuffer);
}
public int index(DirectBuffer name, DirectBuffer value)
{
int index = staticIndex(name, value);
// If there is no entry in static table, look in dynamic table
if (index == -1)
{
Long id = namevalue2Index.get(new NameValue(name, value));
return (id != null) ? idToIndex(id) : -1;
}
return index;
}
private int idToIndex(long id)
{
return (int) (STATIC_TABLE_LENGTH + table.size() - (id - noEvictions) - 1);
}
private static final class NameValue
{
private final DirectBuffer name;
private final DirectBuffer value;
private final int hash;
NameValue(DirectBuffer name, DirectBuffer value)
{
this.name = name;
this.value = value;
this.hash = Objects.hash(name, value);
}
@Override
public boolean equals(Object obj)
{
if (obj instanceof NameValue)
{
NameValue other = (NameValue) obj;
return Objects.equals(name, other.name) && Objects.equals(value, other.value);
}
return false;
}
@Override
public int hashCode()
{
return hash;
}
}
/*
* Index in static table for (name, value). There aren't many entries with value
* in the static table, so simple loop for the entries corresponding to the given name.
*
* @return index in static table if present
* -1 otherwise
*/
private static int staticIndex(DirectBuffer name, DirectBuffer value)
{
if (name.equals(STATIC_TABLE[16].name) && value.equals(STATIC_TABLE[16].value))
{
return 16; // accept-encoding: gzip, deflate
}
int start = -1, end = -1;
if (name.getByte(0) == ':')
{
int index = staticIndex(name);
switch (index)
{
case 2: // :method
start = 2; // GET
end = 3; // POST
break;
case 4: // :path
start = 4; // /
end = 5; // /index.html
break;
case 6: // :scheme
start = 6; // http
end = 7; // https
break;
case 8: // :status
start = 8; // 200, 204, 206, 304,
end = 14; // 400, 404, 500
break;
}
if (start != -1)
{
for (int i = start; i <= end; i++)
{
if (value.equals(STATIC_TABLE[i].value))
{
return i;
}
}
}
}
return -1;
}
/*
* Index in static table for the name. There aren't many entries, so will use
* the length and last byte in the name to look up the entry.
*
* @return index in static table if present
* -1 otherwise
*/
private static int staticIndex(DirectBuffer name)
{
switch (name.capacity())
{
case 3: return staticIndex3(name);
case 4: return staticIndex4(name);
case 5: return staticIndex5(name);
case 6: return staticIndex6(name);
case 7: return staticIndex7(name);
case 8: return staticIndex8(name);
case 10: return staticIndex10(name);
case 11: return staticIndex11(name);
case 12: return staticIndex12(name);
case 13: return staticIndex13(name);
case 14: return staticIndex14(name);
case 15: return staticIndex15(name);
case 16: return staticIndex16(name);
case 17: return staticIndex17(name);
case 18: return staticIndex18(name);
case 19: return staticIndex19(name);
case 25: return staticIndex25(name);
case 27: return staticIndex27(name);
default: return -1;
}
}
// Index in static table for the given name of length 3
private static int staticIndex3(DirectBuffer name)
{
switch (name.getByte(2))
{
case 'a':
if (STATIC_TABLE[60].name.equals(name)) // via
{
return 60;
}
break;
case 'e':
if (STATIC_TABLE[21].name.equals(name)) // age
{
return 21;
}
break;
}
return -1;
}
// Index in static table for the given name of length 4
private static int staticIndex4(DirectBuffer name)
{
switch (name.getByte(3))
{
case 'e':
if (STATIC_TABLE[33].name.equals(name)) // date
{
return 33;
}
break;
case 'g':
if (STATIC_TABLE[34].name.equals(name)) // etag
{
return 34;
}
break;
case 'k':
if (STATIC_TABLE[45].name.equals(name)) // link
{
return 45;
}
break;
case 'm':
if (STATIC_TABLE[37].name.equals(name)) // from
{
return 37;
}
break;
case 't':
if (STATIC_TABLE[38].name.equals(name)) // host
{
return 38;
}
break;
case 'y':
if (STATIC_TABLE[59].name.equals(name)) // vary
{
return 59;
}
break;
}
return -1;
}
// Index in static table for the given name of length 5
private static int staticIndex5(DirectBuffer name)
{
switch (name.getByte(4))
{
case 'e':
if (STATIC_TABLE[50].name.equals(name)) // range
{
return 50;
}
break;
case 'h':
if (STATIC_TABLE[4].name.equals(name)) // path
{
return 4;
}
break;
case 'w':
if (STATIC_TABLE[22].name.equals(name)) // allow
{
return 22;
}
break;
}
return -1;
}
// Index in static table for the given name of length 6
private static int staticIndex6(DirectBuffer name)
{
switch (name.getByte(5))
{
case 'e':
if (STATIC_TABLE[32].name.equals(name)) // cookie
{
return 32;
}
break;
case 'r':
if (STATIC_TABLE[54].name.equals(name)) // server
{
return 54;
}
break;
case 't':
if (STATIC_TABLE[19].name.equals(name)) // accept
{
return 19;
}
if (STATIC_TABLE[35].name.equals(name)) // expect
{
return 35;
}
break;
}
return -1;
}
// Index in static table for the given name of length 7
private static int staticIndex7(DirectBuffer name)
{
switch (name.getByte(6))
{
case 'd':
if (STATIC_TABLE[2].name.equals(name)) // :method
{
return 2;
}
break;
case 'e':
if (STATIC_TABLE[6].name.equals(name)) // :scheme
{
return 6;
}
break;
case 'h':
if (STATIC_TABLE[52].name.equals(name)) // refresh
{
return 52;
}
break;
case 'r':
if (STATIC_TABLE[51].name.equals(name)) // referer
{
return 51;
}
break;
case 's':
if (STATIC_TABLE[8].name.equals(name)) // :status
{
return 8;
}
if (STATIC_TABLE[36].name.equals(name)) // expires
{
return 36;
}
break;
}
return -1;
}
// Index in static table for the given name of length 8
private static int staticIndex8(DirectBuffer name)
{
switch (name.getByte(7))
{
case 'e':
if (STATIC_TABLE[42].name.equals(name)) // if-range
{
return 42;
}
break;
case 'h':
if (STATIC_TABLE[39].name.equals(name)) // if-match
{
return 39;
}
break;
case 'n':
if (STATIC_TABLE[46].name.equals(name)) // location
{
return 46;
}
break;
}
return -1;
}
// Index in static table for the given name of length 10
private static int staticIndex10(DirectBuffer name)
{
switch (name.getByte(9))
{
case 'e':
if (STATIC_TABLE[55].name.equals(name)) // set-cookie
{
return 55;
}
break;
case 't':
if (STATIC_TABLE[58].name.equals(name)) // user-agent
{
return 58;
}
break;
case 'y':
if (STATIC_TABLE[1].name.equals(name)) // :authority
{
return 1;
}
break;
}
return -1;
}
// Index in static table for the given name of length 11
private static int staticIndex11(DirectBuffer name)
{
return (name.getByte(10) == 'r' && STATIC_TABLE[53].name.equals(name)) ? 53 : -1; // retry-after
}
// Index in static table for the given name of length 12
private static int staticIndex12(DirectBuffer name)
{
switch (name.getByte(11))
{
case 'e':
if (STATIC_TABLE[31].name.equals(name)) // content-type
{
return 31;
}
break;
case 's':
if (STATIC_TABLE[47].name.equals(name)) // max-forwards
{
return 47;
}
break;
}
return -1;
}
// Index in static table for the given name of length 13
private static int staticIndex13(DirectBuffer name)
{
switch (name.getByte(12))
{
case 'd':
if (STATIC_TABLE[44].name.equals(name)) // last-modified
{
return 44;
}
break;
case 'e':
if (STATIC_TABLE[30].name.equals(name)) // content-range
{
return 30;
}
break;
case 'h':
if (STATIC_TABLE[41].name.equals(name)) // if-none-match
{
return 41;
}
break;
case 'l':
if (STATIC_TABLE[24].name.equals(name)) // cache-control
{
return 24;
}
break;
case 'n':
if (STATIC_TABLE[23].name.equals(name)) // authorization
{
return 23;
}
break;
case 's':
if (STATIC_TABLE[18].name.equals(name)) // accept-ranges
{
return 18;
}
break;
}
return -1;
}
// Index in static table for the given name of length 14
private static int staticIndex14(DirectBuffer name)
{
switch (name.getByte(13))
{
case 'h':
if (STATIC_TABLE[28].name.equals(name)) // content-length
{
return 28;
}
break;
case 't':
if (STATIC_TABLE[15].name.equals(name)) // accept-charset
{
return 15;
}
break;
}
return -1;
}
// Index in static table for the given name of length 15
private static int staticIndex15(DirectBuffer name)
{
switch (name.getByte(14))
{
case 'e':
if (STATIC_TABLE[17].name.equals(name)) // accept-language
{
return 17;
}
break;
case 'g':
if (STATIC_TABLE[16].name.equals(name)) // accept-encoding
{
return 16;
}
break;
}
return -1;
}
// Index in static table for the given name of length 16
private static int staticIndex16(DirectBuffer name)
{
switch (name.getByte(15))
{
case 'e':
if (STATIC_TABLE[27].name.equals(name)) // content-language
{
return 27;
}
if (STATIC_TABLE[61].name.equals(name)) // www-authenticate
{
return 61;
}
break;
case 'g':
if (STATIC_TABLE[26].name.equals(name)) // content-encoding
{
return 26;
}
break;
case 'n':
if (STATIC_TABLE[29].name.equals(name)) // content-location
{
return 29;
}
}
return -1;
}
// Index in static table for the given name of length 17
private static int staticIndex17(DirectBuffer name)
{
switch (name.getByte(16))
{
case 'e':
if (STATIC_TABLE[40].name.equals(name)) // if-modified-since
{
return 40;
}
break;
case 'g':
if (STATIC_TABLE[57].name.equals(name)) // transfer-encoding
{
return 57;
}
break;
}
return -1;
}
// Index in static table for the given name of length 18
private static int staticIndex18(DirectBuffer name)
{
return (name.getByte(17) == 'e' && STATIC_TABLE[48].name.equals(name)) ? 48 : -1; // proxy-authenticate
}
// Index in static table for the given name of length 19
private static int staticIndex19(DirectBuffer name)
{
switch (name.getByte(18))
{
case 'e':
if (STATIC_TABLE[43].name.equals(name)) // if-unmodified-since
{
return 43;
}
break;
case 'n':
if (STATIC_TABLE[25].name.equals(name)) // content-disposition
{
return 25;
}
if (STATIC_TABLE[49].name.equals(name)) // proxy-authorization
{
return 49;
}
}
return -1;
}
// Index in static table for the given name of length 25
private static int staticIndex25(DirectBuffer name)
{
return (name.getByte(24) == 'y' && STATIC_TABLE[56].name.equals(name)) ? 56 : -1; // strict-transport-security
}
// Index in static table for the given name of length 27
private static int staticIndex27(DirectBuffer name)
{
return (name.getByte(26) == 'n' && STATIC_TABLE[20].name.equals(name)) ? 20 : -1; // access-control-allow-origi
}
}