Write the basic implementation for the QpackEncoder.
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
ae4f33ed9e
commit
5b178d16b7
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.qpack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http3.qpack.table.Entry;
|
||||
|
||||
class EncodableEntry
|
||||
{
|
||||
private final Entry _referencedEntry;
|
||||
private final Entry _referencedName;
|
||||
private final HttpField _field;
|
||||
|
||||
public EncodableEntry(Entry entry)
|
||||
{
|
||||
// We want to reference the entry directly.
|
||||
_referencedEntry = entry;
|
||||
_referencedName = null;
|
||||
_field = null;
|
||||
}
|
||||
|
||||
public EncodableEntry(Entry nameEntry, HttpField field)
|
||||
{
|
||||
// We want to reference the name and use a literal value.
|
||||
_referencedEntry = null;
|
||||
_referencedName = nameEntry;
|
||||
_field = field;
|
||||
}
|
||||
|
||||
public EncodableEntry(HttpField field)
|
||||
{
|
||||
// We want to use a literal name and value.
|
||||
_referencedEntry = null;
|
||||
_referencedName = null;
|
||||
_field = field;
|
||||
}
|
||||
|
||||
public void encode(ByteBuffer buffer, int base)
|
||||
{
|
||||
// TODO: When should we ever encode with post-base indexes?
|
||||
// We are currently always using the base as the start of the current dynamic table entries.
|
||||
if (_referencedEntry != null)
|
||||
{
|
||||
byte staticBit = _referencedEntry.isStatic() ? (byte)0x40 : (byte)0x00;
|
||||
buffer.put((byte)(0x80 | staticBit));
|
||||
int relativeIndex = base - _referencedEntry.getIndex();
|
||||
NBitInteger.encode(buffer, relativeIndex, 6);
|
||||
}
|
||||
else if (_referencedName != null)
|
||||
{
|
||||
byte allowIntermediary = 0x00; // TODO: this is 0x20 bit, when should this be set?
|
||||
byte staticBit = _referencedName.isStatic() ? (byte)0x10 : (byte)0x00;
|
||||
String value = (Objects.requireNonNull(_field).getValue() == null) ? "" : _field.getValue();
|
||||
boolean huffman = QpackEncoder.shouldHuffmanEncode(_referencedName.getHttpField());
|
||||
|
||||
// Encode the prefix.
|
||||
buffer.put((byte)(0x40 | allowIntermediary | staticBit));
|
||||
int relativeIndex = base - _referencedName.getIndex();
|
||||
NBitInteger.encode(buffer, relativeIndex, 4);
|
||||
|
||||
// Encode the value.
|
||||
if (huffman)
|
||||
{
|
||||
buffer.put((byte)0x80);
|
||||
NBitInteger.encode(buffer, Huffman.octetsNeeded(value), 7);
|
||||
Huffman.encode(buffer, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.put((byte)0x00);
|
||||
NBitInteger.encode(buffer, value.length(), 7);
|
||||
buffer.put(value.getBytes());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
byte allowIntermediary = 0x00; // TODO: this is 0x10 bit, when should this be set?
|
||||
boolean huffman = QpackEncoder.shouldHuffmanEncode(Objects.requireNonNull(_field));
|
||||
String name = _field.getName();
|
||||
String value = (_field.getValue() == null) ? "" : _field.getValue();
|
||||
|
||||
// Encode the prefix code and the name.
|
||||
if (huffman)
|
||||
{
|
||||
buffer.put((byte)(0x28 | allowIntermediary));
|
||||
NBitInteger.encode(buffer, Huffman.octetsNeeded(name), 3);
|
||||
Huffman.encode(buffer, name);
|
||||
buffer.put((byte)0x80);
|
||||
NBitInteger.encode(buffer, Huffman.octetsNeeded(value), 7);
|
||||
Huffman.encode(buffer, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: What charset should we be using? (this applies to the instruction generators as well).
|
||||
buffer.put((byte)(0x20 | allowIntermediary));
|
||||
NBitInteger.encode(buffer, name.length(), 3);
|
||||
buffer.put(name.getBytes());
|
||||
buffer.put((byte)0x00);
|
||||
NBitInteger.encode(buffer, value.length(), 7);
|
||||
buffer.put(value.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getRequiredInsertCount()
|
||||
{
|
||||
if (_referencedEntry != null)
|
||||
return _referencedEntry.getIndex();
|
||||
else if (_referencedName != null)
|
||||
return _referencedName.getIndex();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -36,12 +36,18 @@ public class QpackContext
|
|||
private static final StaticTable __staticTable = new StaticTable();
|
||||
private final DynamicTable _dynamicTable;
|
||||
|
||||
QpackContext(int maxDynamicTableSize)
|
||||
public QpackContext()
|
||||
{
|
||||
_dynamicTable = new DynamicTable();
|
||||
_dynamicTable.setCapacity(maxDynamicTableSize);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(String.format("HdrTbl[%x] created max=%d", hashCode(), maxDynamicTableSize));
|
||||
LOG.debug(String.format("HdrTbl[%x] created", hashCode()));
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public QpackContext(int maxDynamicTableSize)
|
||||
{
|
||||
this();
|
||||
_dynamicTable.setCapacity(maxDynamicTableSize);
|
||||
}
|
||||
|
||||
public DynamicTable getDynamicTable()
|
||||
|
@ -93,7 +99,17 @@ public class QpackContext
|
|||
|
||||
public Entry add(HttpField field)
|
||||
{
|
||||
return _dynamicTable.add(new Entry(field));
|
||||
Entry entry = new Entry(field);
|
||||
_dynamicTable.add(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public boolean canReference(Entry entry)
|
||||
{
|
||||
if (entry.isStatic())
|
||||
return true;
|
||||
|
||||
return _dynamicTable.canReference(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,7 +133,7 @@ public class QpackContext
|
|||
*/
|
||||
public int getMaxDynamicTableSize()
|
||||
{
|
||||
return _dynamicTable.getMaxSize();
|
||||
return _dynamicTable.getCapacity();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,11 +141,8 @@ public class QpackContext
|
|||
*/
|
||||
public int index(Entry entry)
|
||||
{
|
||||
if (entry.getIndex() < 0)
|
||||
return 0;
|
||||
if (entry.isStatic())
|
||||
return entry.getIndex();
|
||||
|
||||
return _dynamicTable.index(entry);
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ public class QpackDecoder
|
|||
// Decode the Required Insert Count using the DynamicTable state.
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
int insertCount = dynamicTable.getInsertCount();
|
||||
int maxDynamicTableSize = dynamicTable.getMaxSize();
|
||||
int maxDynamicTableSize = dynamicTable.getCapacity();
|
||||
int requiredInsertCount = decodeInsertCount(encodedInsertCount, insertCount, maxDynamicTableSize);
|
||||
|
||||
// Parse the buffer into an Encoded Field Section.
|
||||
|
@ -145,8 +145,7 @@ public class QpackDecoder
|
|||
Entry entry = dynamicTable.get(duplicate.getIndex());
|
||||
|
||||
// Add the new Entry to the DynamicTable.
|
||||
if (dynamicTable.add(entry) == null)
|
||||
throw new QpackException.StreamException("No space in DynamicTable");
|
||||
dynamicTable.add(entry);
|
||||
_handler.onInstruction(new InsertCountIncrementInstruction(1));
|
||||
checkEncodedFieldSections();
|
||||
}
|
||||
|
@ -159,8 +158,7 @@ public class QpackDecoder
|
|||
|
||||
// Add the new Entry to the DynamicTable.
|
||||
Entry entry = new Entry(new HttpField(referencedEntry.getHttpField().getHeader(), referencedEntry.getHttpField().getName(), value));
|
||||
if (dynamicTable.add(entry) == null)
|
||||
throw new QpackException.StreamException("No space in DynamicTable");
|
||||
dynamicTable.add(entry);
|
||||
_handler.onInstruction(new InsertCountIncrementInstruction(1));
|
||||
checkEncodedFieldSections();
|
||||
}
|
||||
|
@ -172,8 +170,7 @@ public class QpackDecoder
|
|||
Entry entry = new Entry(new HttpField(name, value));
|
||||
|
||||
// Add the new Entry to the DynamicTable.
|
||||
if (dynamicTable.add(entry) == null)
|
||||
throw new QpackException.StreamException("No space in DynamicTable");
|
||||
dynamicTable.add(entry);
|
||||
_handler.onInstruction(new InsertCountIncrementInstruction(1));
|
||||
checkEncodedFieldSections();
|
||||
}
|
||||
|
|
|
@ -14,25 +14,26 @@
|
|||
package org.eclipse.jetty.http3.qpack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.http3.qpack.generator.IndexedNameEntryInstruction;
|
||||
import org.eclipse.jetty.http3.qpack.generator.Instruction;
|
||||
import org.eclipse.jetty.http3.qpack.generator.LiteralNameEntryInstruction;
|
||||
import org.eclipse.jetty.http3.qpack.table.DynamicTable;
|
||||
import org.eclipse.jetty.http3.qpack.table.Entry;
|
||||
import org.eclipse.jetty.http3.qpack.table.StaticEntry;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -40,13 +41,13 @@ public class QpackEncoder
|
|||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QpackEncoder.class);
|
||||
private static final HttpField[] STATUSES = new HttpField[599];
|
||||
static final EnumSet<HttpHeader> DO_NOT_HUFFMAN =
|
||||
public static final EnumSet<HttpHeader> DO_NOT_HUFFMAN =
|
||||
EnumSet.of(
|
||||
HttpHeader.AUTHORIZATION,
|
||||
HttpHeader.CONTENT_MD5,
|
||||
HttpHeader.PROXY_AUTHENTICATE,
|
||||
HttpHeader.PROXY_AUTHORIZATION);
|
||||
static final EnumSet<HttpHeader> DO_NOT_INDEX =
|
||||
public static final EnumSet<HttpHeader> DO_NOT_INDEX =
|
||||
EnumSet.of(
|
||||
// HttpHeader.C_PATH, // TODO more data needed
|
||||
// HttpHeader.DATE, // TODO more data needed
|
||||
|
@ -66,7 +67,7 @@ public class QpackEncoder
|
|||
HttpHeader.LAST_MODIFIED,
|
||||
HttpHeader.SET_COOKIE,
|
||||
HttpHeader.SET_COOKIE2);
|
||||
static final EnumSet<HttpHeader> NEVER_INDEX =
|
||||
public static final EnumSet<HttpHeader> NEVER_INDEX =
|
||||
EnumSet.of(
|
||||
HttpHeader.AUTHORIZATION,
|
||||
HttpHeader.SET_COOKIE,
|
||||
|
@ -90,46 +91,69 @@ public class QpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
public interface Handler
|
||||
{
|
||||
void onInstruction(Instruction instruction);
|
||||
}
|
||||
|
||||
private final Handler _handler;
|
||||
private final QpackContext _context;
|
||||
private final boolean _debug;
|
||||
private int _remoteMaxDynamicTableSize;
|
||||
private int _localMaxDynamicTableSize;
|
||||
private int _maxHeaderListSize;
|
||||
private int _headerListSize;
|
||||
private final int _maxBlockedStreams;
|
||||
private final Map<Integer, AtomicInteger> _blockedStreams = new HashMap<>();
|
||||
private boolean _validateEncoding = true;
|
||||
|
||||
@Deprecated
|
||||
public QpackEncoder()
|
||||
{
|
||||
this(4096, 4096, -1);
|
||||
this(null, -1, -1);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public QpackEncoder(int localMaxDynamicTableSize)
|
||||
{
|
||||
this(localMaxDynamicTableSize, 4096, -1);
|
||||
this(null, -1, -1);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public QpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize)
|
||||
{
|
||||
this(localMaxDynamicTableSize, remoteMaxDynamicTableSize, -1);
|
||||
this(null, -1, -1);
|
||||
}
|
||||
|
||||
public QpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize, int maxHeaderListSize)
|
||||
public QpackEncoder(Handler handler, int dynamicTableSize, int maxBlockedStreams)
|
||||
{
|
||||
_context = new QpackContext(remoteMaxDynamicTableSize);
|
||||
_remoteMaxDynamicTableSize = remoteMaxDynamicTableSize;
|
||||
_localMaxDynamicTableSize = localMaxDynamicTableSize;
|
||||
_maxHeaderListSize = maxHeaderListSize;
|
||||
_debug = LOG.isDebugEnabled();
|
||||
_handler = handler;
|
||||
_context = new QpackContext();
|
||||
_maxBlockedStreams = maxBlockedStreams;
|
||||
|
||||
// TODO: Fix.
|
||||
_context.getDynamicTable().setCapacity(dynamicTableSize);
|
||||
}
|
||||
|
||||
public int getMaxHeaderListSize()
|
||||
private boolean acquireBlockedStream(int streamId)
|
||||
{
|
||||
return _maxHeaderListSize;
|
||||
AtomicInteger atomicInteger = _blockedStreams.get(streamId);
|
||||
if (atomicInteger == null && (_blockedStreams.size() > _maxBlockedStreams))
|
||||
return false;
|
||||
|
||||
if (atomicInteger == null)
|
||||
{
|
||||
atomicInteger = new AtomicInteger();
|
||||
_blockedStreams.put(streamId, atomicInteger);
|
||||
}
|
||||
|
||||
atomicInteger.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setMaxHeaderListSize(int maxHeaderListSize)
|
||||
private void releaseBlockedStream(int streamId)
|
||||
{
|
||||
_maxHeaderListSize = maxHeaderListSize;
|
||||
AtomicInteger atomicInteger = _blockedStreams.get(streamId);
|
||||
if (atomicInteger == null)
|
||||
throw new IllegalArgumentException("Invalid Stream ID");
|
||||
|
||||
if (atomicInteger.decrementAndGet() == 0)
|
||||
_blockedStreams.remove(streamId);
|
||||
}
|
||||
|
||||
public QpackContext getQpackContext()
|
||||
|
@ -137,16 +161,6 @@ public class QpackEncoder
|
|||
return _context;
|
||||
}
|
||||
|
||||
public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize)
|
||||
{
|
||||
_remoteMaxDynamicTableSize = remoteMaxDynamicTableSize;
|
||||
}
|
||||
|
||||
public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize)
|
||||
{
|
||||
_localMaxDynamicTableSize = localMaxDynamicTableSize;
|
||||
}
|
||||
|
||||
public boolean isValidateEncoding()
|
||||
{
|
||||
return _validateEncoding;
|
||||
|
@ -157,337 +171,124 @@ public class QpackEncoder
|
|||
_validateEncoding = validateEncoding;
|
||||
}
|
||||
|
||||
public void encode(ByteBuffer buffer, MetaData metadata) throws QpackException
|
||||
public static boolean shouldIndex(HttpField httpField)
|
||||
{
|
||||
try
|
||||
return !DO_NOT_INDEX.contains(httpField.getHeader());
|
||||
}
|
||||
|
||||
public static boolean shouldHuffmanEncode(HttpField httpField)
|
||||
{
|
||||
return !DO_NOT_HUFFMAN.contains(httpField.getHeader());
|
||||
}
|
||||
|
||||
public void encode(int streamId, ByteBuffer buffer, MetaData metadata) throws QpackException
|
||||
{
|
||||
HttpFields httpFields = metadata.getFields();
|
||||
|
||||
// Verify that we can encode without errors.
|
||||
if (isValidateEncoding() && httpFields != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(String.format("CtxTbl[%x] encoding", _context.hashCode()));
|
||||
|
||||
HttpFields fields = metadata.getFields();
|
||||
// Verify that we can encode without errors.
|
||||
if (isValidateEncoding() && fields != null)
|
||||
for (HttpField field : httpFields)
|
||||
{
|
||||
for (HttpField field : fields)
|
||||
{
|
||||
String name = field.getName();
|
||||
char firstChar = name.charAt(0);
|
||||
if (firstChar <= ' ' || firstChar == ':')
|
||||
throw new QpackException.StreamException("Invalid header name: '%s'", name);
|
||||
}
|
||||
String name = field.getName();
|
||||
char firstChar = name.charAt(0);
|
||||
if (firstChar <= ' ' || firstChar == ':')
|
||||
throw new QpackException.StreamException("Invalid header name: '%s'", name);
|
||||
}
|
||||
|
||||
_headerListSize = 0;
|
||||
int pos = buffer.position();
|
||||
|
||||
// Check the dynamic table sizes!
|
||||
int maxDynamicTableSize = Math.min(_remoteMaxDynamicTableSize, _localMaxDynamicTableSize);
|
||||
if (maxDynamicTableSize != _context.getMaxDynamicTableSize())
|
||||
encodeMaxDynamicTableSize(buffer, maxDynamicTableSize);
|
||||
|
||||
// Add Request/response meta fields
|
||||
if (metadata.isRequest())
|
||||
{
|
||||
MetaData.Request request = (MetaData.Request)metadata;
|
||||
|
||||
String method = request.getMethod();
|
||||
HttpMethod httpMethod = method == null ? null : HttpMethod.fromString(method);
|
||||
HttpField methodField = C_METHODS.get(httpMethod);
|
||||
encode(buffer, methodField == null ? new HttpField(HttpHeader.C_METHOD, method) : methodField);
|
||||
encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority()));
|
||||
boolean isConnect = HttpMethod.CONNECT.is(request.getMethod());
|
||||
String protocol = request.getProtocol();
|
||||
if (!isConnect || protocol != null)
|
||||
{
|
||||
String scheme = request.getURI().getScheme();
|
||||
encode(buffer, HttpScheme.HTTPS.is(scheme) ? C_SCHEME_HTTPS : C_SCHEME_HTTP);
|
||||
encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery()));
|
||||
if (protocol != null)
|
||||
encode(buffer,new HttpField(HttpHeader.C_PROTOCOL,protocol));
|
||||
}
|
||||
}
|
||||
else if (metadata.isResponse())
|
||||
{
|
||||
MetaData.Response response = (MetaData.Response)metadata;
|
||||
int code = response.getStatus();
|
||||
HttpField status = code < STATUSES.length ? STATUSES[code] : null;
|
||||
if (status == null)
|
||||
status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code);
|
||||
encode(buffer, status);
|
||||
}
|
||||
|
||||
// Remove fields as specified in RFC 7540, 8.1.2.2.
|
||||
if (fields != null)
|
||||
{
|
||||
// Remove the headers specified in the Connection header,
|
||||
// for example: Connection: Close, TE, Upgrade, Custom.
|
||||
Set<String> hopHeaders = null;
|
||||
for (String value : fields.getCSV(HttpHeader.CONNECTION, false))
|
||||
{
|
||||
if (hopHeaders == null)
|
||||
hopHeaders = new HashSet<>();
|
||||
hopHeaders.add(StringUtil.asciiToLowerCase(value));
|
||||
}
|
||||
|
||||
boolean contentLengthEncoded = false;
|
||||
for (HttpField field : fields)
|
||||
{
|
||||
HttpHeader header = field.getHeader();
|
||||
if (header != null && IGNORED_HEADERS.contains(header))
|
||||
continue;
|
||||
if (header == HttpHeader.TE)
|
||||
{
|
||||
if (field.contains("trailers"))
|
||||
encode(buffer, TE_TRAILERS);
|
||||
continue;
|
||||
}
|
||||
String name = field.getLowerCaseName();
|
||||
if (hopHeaders != null && hopHeaders.contains(name))
|
||||
continue;
|
||||
if (header == HttpHeader.CONTENT_LENGTH)
|
||||
contentLengthEncoded = true;
|
||||
encode(buffer, field);
|
||||
}
|
||||
|
||||
if (!contentLengthEncoded)
|
||||
{
|
||||
long contentLength = metadata.getContentLength();
|
||||
if (contentLength >= 0)
|
||||
encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength)));
|
||||
}
|
||||
}
|
||||
|
||||
// Check size
|
||||
if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.warn("Header list size too large {} > {} metadata={}", _headerListSize, _maxHeaderListSize, metadata);
|
||||
else
|
||||
LOG.warn("Header list size too large {} > {}", _headerListSize, _maxHeaderListSize);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos));
|
||||
}
|
||||
catch (QpackException x)
|
||||
|
||||
int requiredInsertCount = 0;
|
||||
List<EncodableEntry> encodableEntries = new ArrayList<>();
|
||||
if (httpFields != null)
|
||||
{
|
||||
throw x;
|
||||
for (HttpField field : httpFields)
|
||||
{
|
||||
EncodableEntry entry = encode(streamId, field);
|
||||
encodableEntries.add(entry);
|
||||
|
||||
// Update the required InsertCount.
|
||||
int entryRequiredInsertCount = entry.getRequiredInsertCount();
|
||||
if (entryRequiredInsertCount > requiredInsertCount)
|
||||
requiredInsertCount = entryRequiredInsertCount;
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
int base = dynamicTable.getBase();
|
||||
int encodedInsertCount = encodeInsertCount(requiredInsertCount, dynamicTable.getInsertCount());
|
||||
boolean signBit = base < requiredInsertCount;
|
||||
int deltaBase = signBit ? requiredInsertCount - base - 1 : base - requiredInsertCount;
|
||||
|
||||
// Encode the Field Section Prefix into the ByteBuffer.
|
||||
buffer.put((byte)0x00);
|
||||
NBitInteger.encode(buffer, encodedInsertCount, 8);
|
||||
buffer.put(signBit ? (byte)0x01 : (byte)0x00);
|
||||
NBitInteger.encode(buffer, deltaBase, 7);
|
||||
|
||||
// Encode the field lines into the ByteBuffer.
|
||||
for (EncodableEntry entry : encodableEntries)
|
||||
{
|
||||
QpackException.SessionException failure = new QpackException.SessionException("Could not qpack encode %s", metadata);
|
||||
failure.initCause(x);
|
||||
throw failure;
|
||||
entry.encode(buffer, base);
|
||||
}
|
||||
}
|
||||
|
||||
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)
|
||||
private EncodableEntry encode(int streamId, HttpField field)
|
||||
{
|
||||
if (maxDynamicTableSize > _remoteMaxDynamicTableSize)
|
||||
throw new IllegalArgumentException();
|
||||
buffer.put((byte)0x20);
|
||||
NBitInteger.encode(buffer, 5, maxDynamicTableSize);
|
||||
_context.resize(maxDynamicTableSize);
|
||||
}
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
|
||||
public void encode(ByteBuffer buffer, HttpField field)
|
||||
{
|
||||
if (field.getValue() == null)
|
||||
field = new HttpField(field.getHeader(), field.getName(), "");
|
||||
|
||||
int fieldSize = field.getName().length() + field.getValue().length();
|
||||
_headerListSize += fieldSize + 32;
|
||||
// TODO:
|
||||
// 1. The field.getHeader() could be null.
|
||||
// 3. Handle pre-encoded HttpFields.
|
||||
// 4. Someone still needs to generate the HTTP/3 pseudo headers. (this should be independent of HTTP/3 though?)
|
||||
|
||||
String encoding = null;
|
||||
|
||||
// Is there an index entry for the field?
|
||||
Entry entry = _context.get(field);
|
||||
if (entry != null)
|
||||
if (entry != null && _context.canReference(entry))
|
||||
{
|
||||
// This is a known indexed field, send as static or dynamic indexed.
|
||||
if (entry.isStatic())
|
||||
// TODO: we may want to duplicate the entry if it is in the eviction zone?
|
||||
// then we would also need to reference this entry, is that okay?
|
||||
entry.reference();
|
||||
return new EncodableEntry(entry);
|
||||
}
|
||||
|
||||
Entry nameEntry = _context.get(field.getName());
|
||||
boolean canReferenceName = nameEntry != null && _context.canReference(nameEntry);
|
||||
|
||||
Entry newEntry = new Entry(field);
|
||||
if (shouldIndex(field) && (newEntry.getSize() <= dynamicTable.getSpace()))
|
||||
{
|
||||
dynamicTable.add(newEntry);
|
||||
|
||||
boolean huffman = shouldHuffmanEncode(field);
|
||||
if (canReferenceName)
|
||||
{
|
||||
buffer.put(((StaticEntry)entry).getEncodedField());
|
||||
if (_debug)
|
||||
encoding = "IdxFieldS1";
|
||||
boolean isDynamic = !nameEntry.isStatic();
|
||||
int nameIndex = _context.index(nameEntry);
|
||||
_handler.onInstruction(new IndexedNameEntryInstruction(isDynamic, nameIndex, huffman, field.getValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
int index = _context.index(entry);
|
||||
buffer.put((byte)0x80);
|
||||
NBitInteger.encode(buffer, 7, index);
|
||||
if (_debug)
|
||||
encoding = "IdxField" + (entry.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(7, index));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown field entry, so we will have to send literally, but perhaps add an index.
|
||||
final boolean indexed;
|
||||
|
||||
// Do we know its name?
|
||||
HttpHeader header = field.getHeader();
|
||||
|
||||
// Select encoding strategy
|
||||
if (header == null)
|
||||
{
|
||||
// Select encoding strategy for unknown header names
|
||||
Entry name = _context.get(field.getName());
|
||||
|
||||
if (field instanceof PreEncodedHttpField)
|
||||
{
|
||||
int i = buffer.position();
|
||||
((PreEncodedHttpField)field).putTo(buffer, HttpVersion.HTTP_2);
|
||||
byte b = buffer.get(i);
|
||||
indexed = b < 0 || b >= 0x40;
|
||||
if (_debug)
|
||||
encoding = indexed ? "PreEncodedIdx" : "PreEncoded";
|
||||
}
|
||||
else if (name == null && fieldSize < _context.getMaxDynamicTableSize())
|
||||
{
|
||||
// unknown name and value that will fit in dynamic table, so let's index
|
||||
// this just in case it is the first time we have seen a custom name or a
|
||||
// custom field. Unless the name is once only, this is worthwhile
|
||||
indexed = true;
|
||||
encodeName(buffer, (byte)0x40, 6, field.getName(), null);
|
||||
encodeValue(buffer, true, field.getValue());
|
||||
if (_debug)
|
||||
encoding = "LitHuffNHuffVIdx";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Known name, but different value.
|
||||
// This is probably a custom field with changing value, so don't index.
|
||||
indexed = false;
|
||||
encodeName(buffer, (byte)0x00, 4, field.getName(), null);
|
||||
encodeValue(buffer, true, field.getValue());
|
||||
if (_debug)
|
||||
encoding = "LitHuffNHuffV!Idx";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Select encoding strategy for known header names
|
||||
Entry name = _context.get(header);
|
||||
|
||||
if (field instanceof PreEncodedHttpField)
|
||||
{
|
||||
// Preencoded field
|
||||
int i = buffer.position();
|
||||
((PreEncodedHttpField)field).putTo(buffer, HttpVersion.HTTP_2);
|
||||
byte b = buffer.get(i);
|
||||
indexed = b < 0 || b >= 0x40;
|
||||
if (_debug)
|
||||
encoding = indexed ? "PreEncodedIdx" : "PreEncoded";
|
||||
}
|
||||
else if (DO_NOT_INDEX.contains(header))
|
||||
{
|
||||
// Non indexed field
|
||||
indexed = false;
|
||||
boolean neverIndex = NEVER_INDEX.contains(header);
|
||||
boolean huffman = !DO_NOT_HUFFMAN.contains(header);
|
||||
encodeName(buffer, neverIndex ? (byte)0x10 : (byte)0x00, 4, header.asString(), name);
|
||||
encodeValue(buffer, huffman, field.getValue());
|
||||
|
||||
if (_debug)
|
||||
encoding = "Lit" +
|
||||
((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(4, _context.index(name))))) +
|
||||
(huffman ? "HuffV" : "LitV") +
|
||||
(neverIndex ? "!!Idx" : "!Idx");
|
||||
}
|
||||
else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && !"0".equals(field.getValue()))
|
||||
{
|
||||
// The field is too large or a non zero content length, so do not index.
|
||||
indexed = false;
|
||||
encodeName(buffer, (byte)0x00, 4, header.asString(), name);
|
||||
encodeValue(buffer, true, field.getValue());
|
||||
if (_debug)
|
||||
encoding = "Lit" +
|
||||
((name == null) ? "HuffN" : "IdxNS" + (1 + NBitInteger.octectsNeeded(4, _context.index(name)))) +
|
||||
"HuffV!Idx";
|
||||
}
|
||||
else
|
||||
{
|
||||
// indexed
|
||||
indexed = true;
|
||||
boolean huffman = !DO_NOT_HUFFMAN.contains(header);
|
||||
encodeName(buffer, (byte)0x40, 6, header.asString(), name);
|
||||
encodeValue(buffer, huffman, field.getValue());
|
||||
if (_debug)
|
||||
encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(6, _context.index(name))))) +
|
||||
(huffman ? "HuffVIdx" : "LitVIdx");
|
||||
}
|
||||
_handler.onInstruction(new LiteralNameEntryInstruction(huffman, field.getName(), huffman, field.getValue()));
|
||||
}
|
||||
|
||||
// If we want the field referenced, then we add it to our table and reference set.
|
||||
if (indexed)
|
||||
_context.add(field);
|
||||
// We might be able to risk blocking the decoder stream and reference this immediately.
|
||||
if (acquireBlockedStream(streamId))
|
||||
return new EncodableEntry(newEntry);
|
||||
}
|
||||
|
||||
if (_debug)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString(buffer.duplicate().flip()));
|
||||
}
|
||||
if (canReferenceName)
|
||||
return new EncodableEntry(nameEntry, field);
|
||||
return new EncodableEntry(field);
|
||||
}
|
||||
|
||||
private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry)
|
||||
public static int encodeInsertCount(int reqInsertCount, int maxTableCapacity)
|
||||
{
|
||||
buffer.put(mask);
|
||||
if (entry == null)
|
||||
{
|
||||
// leave name index bits as 0
|
||||
// Encode the name always with lowercase huffman
|
||||
buffer.put((byte)0x80);
|
||||
NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name));
|
||||
Huffman.encodeLC(buffer, name);
|
||||
}
|
||||
else
|
||||
{
|
||||
NBitInteger.encode(buffer, bits, _context.index(entry));
|
||||
}
|
||||
}
|
||||
if (reqInsertCount == 0)
|
||||
return 0;
|
||||
|
||||
static void encodeValue(ByteBuffer buffer, boolean huffman, String value)
|
||||
{
|
||||
if (huffman)
|
||||
{
|
||||
// huffman literal value
|
||||
buffer.put((byte)0x80);
|
||||
|
||||
int needed = Huffman.octetsNeeded(value);
|
||||
if (needed >= 0)
|
||||
{
|
||||
NBitInteger.encode(buffer, 7, needed);
|
||||
Huffman.encode(buffer, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not iso_8859_1
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes));
|
||||
Huffman.encode(buffer, bytes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// add literal assuming iso_8859_1
|
||||
buffer.put((byte)0x00).mark();
|
||||
NBitInteger.encode(buffer, 7, value.length());
|
||||
for (int i = 0; i < value.length(); i++)
|
||||
{
|
||||
char c = value.charAt(i);
|
||||
if (c < ' ' || c > 127)
|
||||
{
|
||||
// Not iso_8859_1, so re-encode as UTF-8
|
||||
buffer.reset();
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
NBitInteger.encode(buffer, 7, bytes.length);
|
||||
buffer.put(bytes, 0, bytes.length);
|
||||
return;
|
||||
}
|
||||
buffer.put((byte)c);
|
||||
}
|
||||
}
|
||||
int maxEntries = maxTableCapacity / 32;
|
||||
return (reqInsertCount % (2 * maxEntries)) + 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,8 @@ public class QpackFieldPreEncoder implements HttpFieldPreEncoder
|
|||
Huffman.encodeLC(buffer, name);
|
||||
}
|
||||
|
||||
QpackEncoder.encodeValue(buffer, huffman, value);
|
||||
// TODO: I think we can only encode referencing the static table or with literal representations.
|
||||
// QpackEncoder.encodeValue(buffer, huffman, value);
|
||||
|
||||
BufferUtil.flipToFlush(buffer, 0);
|
||||
return BufferUtil.toArray(buffer);
|
||||
|
|
|
@ -270,15 +270,4 @@ public class EncodedFieldSection
|
|||
return new HttpField(field.getHeader(), field.getName(), _value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to QpackEncoder.
|
||||
@SuppressWarnings("unused")
|
||||
public static int encodeInsertCount(int reqInsertCount, int maxTableCapacity)
|
||||
{
|
||||
if (reqInsertCount == 0)
|
||||
return 0;
|
||||
|
||||
int maxEntries = maxTableCapacity / 32;
|
||||
return (reqInsertCount % (2 * maxEntries)) + 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import org.eclipse.jetty.http3.qpack.QpackException;
|
|||
public class DynamicTable
|
||||
{
|
||||
public static final int FIRST_INDEX = StaticTable.STATIC_SIZE + 1;
|
||||
private int _maxCapacity;
|
||||
private int _capacity;
|
||||
private int _size;
|
||||
private int _absoluteIndex;
|
||||
|
||||
private final Map<HttpField, Entry> _fieldMap = new HashMap<>();
|
||||
|
@ -39,25 +39,21 @@ public class DynamicTable
|
|||
{
|
||||
private int streamId;
|
||||
private final List<Integer> referencedEntries = new ArrayList<>();
|
||||
private int potentiallyBlockedStreams = 0;
|
||||
private int potentiallyBlockedStreams;
|
||||
}
|
||||
|
||||
public DynamicTable()
|
||||
{
|
||||
}
|
||||
|
||||
public Entry add(Entry entry)
|
||||
public void add(Entry entry)
|
||||
{
|
||||
evict();
|
||||
|
||||
int size = entry.getSize();
|
||||
if (size + _capacity > _maxCapacity)
|
||||
{
|
||||
if (QpackContext.LOG.isDebugEnabled())
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] !added size %d>%d", hashCode(), size, _maxCapacity));
|
||||
return null;
|
||||
}
|
||||
_capacity += size;
|
||||
int entrySize = entry.getSize();
|
||||
if (entrySize + _size > _capacity)
|
||||
throw new IllegalStateException("No available space");
|
||||
_size += entrySize;
|
||||
|
||||
// Set the Entries absolute index which will never change.
|
||||
entry.setIndex(_absoluteIndex++);
|
||||
|
@ -67,7 +63,6 @@ public class DynamicTable
|
|||
|
||||
if (QpackContext.LOG.isDebugEnabled())
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] added %s", hashCode(), entry));
|
||||
return entry;
|
||||
}
|
||||
|
||||
public int index(Entry entry)
|
||||
|
@ -98,14 +93,26 @@ public class DynamicTable
|
|||
return _fieldMap.get(field);
|
||||
}
|
||||
|
||||
public int getBase()
|
||||
{
|
||||
if (_entries.size() == 0)
|
||||
return _absoluteIndex;
|
||||
return _entries.get(0).getIndex();
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return _size;
|
||||
}
|
||||
|
||||
public int getCapacity()
|
||||
{
|
||||
return _capacity;
|
||||
}
|
||||
|
||||
public int getMaxSize()
|
||||
public int getSpace()
|
||||
{
|
||||
return _maxCapacity;
|
||||
return _capacity - _size;
|
||||
}
|
||||
|
||||
public int getNumEntries()
|
||||
|
@ -121,16 +128,16 @@ public class DynamicTable
|
|||
public void setCapacity(int capacity)
|
||||
{
|
||||
if (QpackContext.LOG.isDebugEnabled())
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _maxCapacity, capacity));
|
||||
_maxCapacity = capacity;
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _capacity, capacity));
|
||||
_capacity = capacity;
|
||||
evict();
|
||||
}
|
||||
|
||||
private boolean canReference(Entry entry)
|
||||
public boolean canReference(Entry entry)
|
||||
{
|
||||
int evictionThreshold = getEvictionThreshold();
|
||||
int lowestReferencableIndex = -1;
|
||||
int remainingCapacity = _capacity;
|
||||
int remainingCapacity = _size;
|
||||
for (int i = 0; i < _entries.size(); i++)
|
||||
{
|
||||
if (remainingCapacity <= evictionThreshold)
|
||||
|
@ -151,7 +158,7 @@ public class DynamicTable
|
|||
for (Entry e : _entries)
|
||||
{
|
||||
// We only evict when the table is getting full.
|
||||
if (_capacity < evictionThreshold)
|
||||
if (_size < evictionThreshold)
|
||||
return;
|
||||
|
||||
// We can only evict if there are no references outstanding to this entry.
|
||||
|
@ -170,21 +177,21 @@ public class DynamicTable
|
|||
if (e == _nameMap.get(name))
|
||||
_nameMap.remove(name);
|
||||
|
||||
_capacity -= e.getSize();
|
||||
_size -= e.getSize();
|
||||
}
|
||||
|
||||
if (QpackContext.LOG.isDebugEnabled())
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d", hashCode(), getNumEntries(), _capacity, _maxCapacity));
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d", hashCode(), getNumEntries(), _size, _capacity));
|
||||
}
|
||||
|
||||
private int getEvictionThreshold()
|
||||
{
|
||||
return _maxCapacity * 3 / 4;
|
||||
return _capacity * 3 / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x{entries=%d,size=%d,max=%d}", getClass().getSimpleName(), hashCode(), getNumEntries(), _capacity, _maxCapacity);
|
||||
return String.format("%s@%x{entries=%d,size=%d,max=%d}", getClass().getSimpleName(), hashCode(), getNumEntries(), _size, _capacity);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,13 @@ package org.eclipse.jetty.http3.qpack.table;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
||||
public class Entry
|
||||
{
|
||||
private final HttpField _field;
|
||||
private int _absoluteIndex;
|
||||
private AtomicInteger _referenceCount = new AtomicInteger(0);
|
||||
private final AtomicInteger _referenceCount = new AtomicInteger(0);
|
||||
|
||||
public Entry()
|
||||
{
|
||||
|
@ -41,8 +42,7 @@ public class Entry
|
|||
|
||||
public int getSize()
|
||||
{
|
||||
String value = _field.getValue();
|
||||
return 32 + _field.getName().length() + (value == null ? 0 : value.length());
|
||||
return 32 + StringUtil.getLength(_field.getName()) + StringUtil.getLength(_field.getValue());
|
||||
}
|
||||
|
||||
public void setIndex(int index)
|
||||
|
@ -60,6 +60,11 @@ public class Entry
|
|||
return _field;
|
||||
}
|
||||
|
||||
public void reference()
|
||||
{
|
||||
_referenceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
public int getReferenceCount()
|
||||
{
|
||||
return _referenceCount.get();
|
||||
|
|
Loading…
Reference in New Issue