diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncodableEntry.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncodableEntry.java new file mode 100644 index 00000000000..18bd0045276 --- /dev/null +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncodableEntry.java @@ -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; + } +} diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackContext.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackContext.java index 5ddbcade186..b71000bced2 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackContext.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackContext.java @@ -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); } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java index 4c72fd4a882..884b5a39153 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java @@ -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(); } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java index 391eea5b7a0..f2af43745aa 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java @@ -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 DO_NOT_HUFFMAN = + public static final EnumSet DO_NOT_HUFFMAN = EnumSet.of( HttpHeader.AUTHORIZATION, HttpHeader.CONTENT_MD5, HttpHeader.PROXY_AUTHENTICATE, HttpHeader.PROXY_AUTHORIZATION); - static final EnumSet DO_NOT_INDEX = + public static final EnumSet 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 NEVER_INDEX = + public static final EnumSet 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 _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 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 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; } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackFieldPreEncoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackFieldPreEncoder.java index 7913408d39a..fc1de12cff4 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackFieldPreEncoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackFieldPreEncoder.java @@ -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); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncodedFieldSection.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncodedFieldSection.java index 3ce71bbe340..ada55867acf 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncodedFieldSection.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncodedFieldSection.java @@ -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; - } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java index 90ac0c8793b..56c6e741735 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java @@ -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 _fieldMap = new HashMap<>(); @@ -39,25 +39,21 @@ public class DynamicTable { private int streamId; private final List 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); } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/Entry.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/Entry.java index d5af1d41b89..ca2e5c8d86b 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/Entry.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/Entry.java @@ -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();