Improve exceptions from QPACK

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2021-09-13 20:50:52 +10:00 committed by Simone Bordet
parent 3d8433b58c
commit 63e4f1a074
10 changed files with 162 additions and 90 deletions

View File

@ -28,12 +28,16 @@ import org.eclipse.jetty.http3.qpack.internal.parser.EncodedFieldSection;
import org.eclipse.jetty.http3.qpack.internal.table.DynamicTable;
import org.eclipse.jetty.http3.qpack.internal.table.Entry;
import org.eclipse.jetty.http3.qpack.internal.table.StaticTable;
import org.eclipse.jetty.http3.qpack.internal.util.EncodingException;
import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECODER_STREAM_ERROR;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECOMPRESSION_FAILED;
/**
* Qpack Decoder
* <p>This is not thread safe and may only be called by 1 thread at a time.</p>
@ -98,18 +102,18 @@ public class QpackDecoder implements Dumpable
// If the buffer is big, don't even think about decoding it
if (buffer.remaining() > _maxHeaderSize)
throw new QpackException.SessionException("431 Request Header Fields too large");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "header_too_large");
_integerDecoder.setPrefix(8);
int encodedInsertCount = _integerDecoder.decodeInt(buffer);
if (encodedInsertCount < 0)
throw new QpackException.CompressionException("Could not parse Required Insert Count");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "invalid_required_insert_count");
_integerDecoder.setPrefix(7);
boolean signBit = (buffer.get(buffer.position()) & 0x80) != 0;
int deltaBase = _integerDecoder.decodeInt(buffer);
if (deltaBase < 0)
throw new QpackException.CompressionException("Could not parse Delta Base");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "invalid_delta_base");
// Decode the Required Insert Count using the DynamicTable state.
DynamicTable dynamicTable = _context.getDynamicTable();
@ -117,24 +121,33 @@ public class QpackDecoder implements Dumpable
int maxDynamicTableSize = dynamicTable.getCapacity();
int requiredInsertCount = decodeInsertCount(encodedInsertCount, insertCount, maxDynamicTableSize);
// Parse the buffer into an Encoded Field Section.
int base = signBit ? requiredInsertCount - deltaBase - 1 : requiredInsertCount + deltaBase;
EncodedFieldSection encodedFieldSection = new EncodedFieldSection(streamId, handler, requiredInsertCount, base, buffer);
try
{
// Parse the buffer into an Encoded Field Section.
int base = signBit ? requiredInsertCount - deltaBase - 1 : requiredInsertCount + deltaBase;
EncodedFieldSection encodedFieldSection = new EncodedFieldSection(streamId, handler, requiredInsertCount, base, buffer);
// Decode it straight away if we can, otherwise add it to the list of EncodedFieldSections.
if (requiredInsertCount <= insertCount)
{
MetaData metaData = encodedFieldSection.decode(_context, _maxHeaderSize);
if (LOG.isDebugEnabled())
LOG.debug("Decoded: streamId={}, metadata={}", streamId, metaData);
_metaDataNotifications.add(new MetaDataNotification(streamId, metaData, handler));
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
// Decode it straight away if we can, otherwise add it to the list of EncodedFieldSections.
if (requiredInsertCount <= insertCount)
{
MetaData metaData = encodedFieldSection.decode(_context, _maxHeaderSize);
if (LOG.isDebugEnabled())
LOG.debug("Decoded: streamId={}, metadata={}", streamId, metaData);
_metaDataNotifications.add(new MetaDataNotification(streamId, metaData, handler));
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Deferred Decoding: streamId={}, encodedFieldSection={}", streamId, encodedFieldSection);
_encodedFieldSections.add(encodedFieldSection);
}
}
else
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("Deferred Decoding: streamId={}, encodedFieldSection={}", streamId, encodedFieldSection);
_encodedFieldSections.add(encodedFieldSection);
notifyInstructionHandler();
notifyMetaDataHandler();
throw t;
}
boolean hadMetaData = !_metaDataNotifications.isEmpty();
@ -145,13 +158,23 @@ public class QpackDecoder implements Dumpable
public void parseInstructionBuffer(ByteBuffer buffer) throws QpackException
{
while (BufferUtil.hasContent(buffer))
try
{
_parser.parse(buffer);
while (BufferUtil.hasContent(buffer))
{
_parser.parse(buffer);
}
}
catch (EncodingException e)
{
// There was an error decoding the instruction.
throw new QpackException.SessionException(QPACK_DECODER_STREAM_ERROR, e.getMessage(), e);
}
finally
{
notifyInstructionHandler();
notifyMetaDataHandler();
}
notifyInstructionHandler();
notifyMetaDataHandler();
}
private void checkEncodedFieldSections() throws QpackException
@ -230,7 +253,7 @@ public class QpackDecoder implements Dumpable
int maxEntries = maxTableCapacity / 32;
int fullRange = 2 * maxEntries;
if (encInsertCount > fullRange)
throw new QpackException.CompressionException("encInsertCount > fullRange");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "encInsertCount_greater_than_fullRange");
// MaxWrapped is the largest possible value of ReqInsertCount that is 0 mod 2 * MaxEntries.
int maxValue = totalNumInserts + maxEntries;
@ -241,13 +264,13 @@ public class QpackDecoder implements Dumpable
if (reqInsertCount > maxValue)
{
if (reqInsertCount <= fullRange)
throw new QpackException.CompressionException("reqInsertCount <= fullRange");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "reqInsertCount_less_than_or_equal_to_fullRange");
reqInsertCount -= fullRange;
}
// Value of 0 must be encoded as 0.
if (reqInsertCount == 0)
throw new QpackException.CompressionException("reqInsertCount == 0");
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "reqInsertCount_is_zero");
return reqInsertCount;
}

View File

@ -42,6 +42,9 @@ import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http3.qpack.QpackException.H3_GENERAL_PROTOCOL_ERROR;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_ENCODER_STREAM_ERROR;
public class QpackEncoder implements Dumpable
{
private static final Logger LOG = LoggerFactory.getLogger(QpackEncoder.class);
@ -180,7 +183,7 @@ public class QpackEncoder implements Dumpable
String name = field.getName();
char firstChar = name.charAt(0);
if (firstChar <= ' ')
throw new QpackException.StreamException("Invalid header name: '%s'", name);
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, String.format("Invalid header name: '%s'", name));
}
}
@ -313,7 +316,7 @@ public class QpackEncoder implements Dumpable
int insertCount = _context.getDynamicTable().getInsertCount();
if (_knownInsertCount + increment > insertCount)
throw new QpackException.StreamException("KnownInsertCount incremented over InsertCount");
throw new QpackException.SessionException(QPACK_ENCODER_STREAM_ERROR, "KnownInsertCount incremented over InsertCount");
_knownInsertCount += increment;
}
@ -324,7 +327,7 @@ public class QpackEncoder implements Dumpable
StreamInfo streamInfo = _streamInfoMap.get(streamId);
if (streamInfo == null)
throw new QpackException.StreamException("No StreamInfo for " + streamId);
throw new QpackException.SessionException(QPACK_ENCODER_STREAM_ERROR, "No StreamInfo for " + streamId);
// The KnownInsertCount should be updated to the earliest sent RequiredInsertCount on that stream.
StreamInfo.SectionInfo sectionInfo = streamInfo.acknowledge();
@ -343,7 +346,7 @@ public class QpackEncoder implements Dumpable
StreamInfo streamInfo = _streamInfoMap.remove(streamId);
if (streamInfo == null)
throw new QpackException.StreamException("No StreamInfo for " + streamId);
throw new QpackException.SessionException(QPACK_ENCODER_STREAM_ERROR, "No StreamInfo for " + streamId);
// Release all referenced entries outstanding on the stream that was cancelled.
for (StreamInfo.SectionInfo sectionInfo : streamInfo)

View File

@ -16,9 +16,22 @@ package org.eclipse.jetty.http3.qpack;
@SuppressWarnings("serial")
public abstract class QpackException extends Exception
{
QpackException(String messageFormat, Object... args)
public static final int QPACK_DECOMPRESSION_FAILED = 0x200;
public static final int QPACK_ENCODER_STREAM_ERROR = 0x201;
public static final int QPACK_DECODER_STREAM_ERROR = 0x202;
public static final int H3_GENERAL_PROTOCOL_ERROR = 0x0101;
private final int _errorCode;
QpackException(int errorCode, String messageFormat, Throwable cause)
{
super(String.format(messageFormat, args));
super(messageFormat, cause);
_errorCode = errorCode;
}
public int getErrorCode()
{
return _errorCode;
}
/**
@ -30,9 +43,14 @@ public abstract class QpackException extends Exception
*/
public static class StreamException extends QpackException
{
public StreamException(String messageFormat, Object... args)
public StreamException(int errorCode, String messageFormat)
{
super(messageFormat, args);
this(errorCode, messageFormat, null);
}
public StreamException(int errorCode, String messageFormat, Throwable cause)
{
super(errorCode, messageFormat, cause);
}
}
@ -43,17 +61,14 @@ public abstract class QpackException extends Exception
*/
public static class SessionException extends QpackException
{
public SessionException(String messageFormat, Object... args)
public SessionException(int errorCode, String message)
{
super(messageFormat, args);
this(errorCode, message, null);
}
}
public static class CompressionException extends SessionException
{
public CompressionException(String messageFormat, Object... args)
public SessionException(int errorCode, String message, Throwable cause)
{
super(messageFormat, args);
super(errorCode, message, cause);
}
}
}

View File

@ -23,6 +23,8 @@ import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.qpack.QpackException;
import static org.eclipse.jetty.http3.qpack.QpackException.H3_GENERAL_PROTOCOL_ERROR;
public class MetaDataBuilder
{
private final int _maxSize;
@ -72,12 +74,12 @@ public class MetaDataBuilder
HttpHeader header = field.getHeader();
String name = field.getName();
if (name == null || name.length() == 0)
throw new QpackException.SessionException("Header size 0");
throw new QpackException.SessionException(QpackException.QPACK_DECOMPRESSION_FAILED, "Header size 0");
String value = field.getValue();
int fieldSize = name.length() + (value == null ? 0 : value.length());
_size += fieldSize + 32;
if (_size > _maxSize)
throw new QpackException.SessionException("Header size %d > %d", _size, _maxSize);
throw new QpackException.SessionException(QpackException.QPACK_DECOMPRESSION_FAILED, String.format("Header size %d > %d", _size, _maxSize));
if (field instanceof StaticTableHttpField)
{
@ -198,7 +200,7 @@ public class MetaDataBuilder
protected void streamException(String messageFormat, Object... args)
{
QpackException.StreamException stream = new QpackException.StreamException(messageFormat, args);
QpackException.StreamException stream = new QpackException.StreamException(QpackException.QPACK_DECOMPRESSION_FAILED, String.format(messageFormat, args));
if (_streamException == null)
_streamException = stream;
else
@ -227,7 +229,7 @@ public class MetaDataBuilder
}
if (_request && _response)
throw new QpackException.StreamException("Request and Response headers");
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "Request and Response headers");
HttpFields.Mutable fields = _fields;
try
@ -235,14 +237,14 @@ public class MetaDataBuilder
if (_request)
{
if (_method == null)
throw new QpackException.StreamException("No Method");
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "No Method");
boolean isConnect = HttpMethod.CONNECT.is(_method);
if (!isConnect || _protocol != null)
{
if (_scheme == null)
throw new QpackException.StreamException("No Scheme");
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "No Scheme");
if (_path == null)
throw new QpackException.StreamException("No Path");
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "No Path");
}
if (isConnect)
return new MetaData.ConnectRequest(_scheme, _authority, _path, fields, _protocol);
@ -259,7 +261,7 @@ public class MetaDataBuilder
if (_response)
{
if (_status == null)
throw new QpackException.StreamException("No Status");
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "No Status");
return new MetaData.Response(HttpVersion.HTTP_3, _status, fields, _contentLength);
}

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.http3.qpack.internal.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http3.qpack.QpackException;
import org.eclipse.jetty.http3.qpack.internal.util.EncodingException;
import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser;
import org.eclipse.jetty.http3.qpack.internal.util.NBitStringParser;
@ -69,7 +70,7 @@ public class DecoderInstructionParser
_integerParser = new NBitIntegerParser();
}
public void parse(ByteBuffer buffer) throws QpackException
public void parse(ByteBuffer buffer) throws QpackException, EncodingException
{
if (buffer == null || !buffer.hasRemaining())
return;
@ -123,7 +124,7 @@ public class DecoderInstructionParser
}
}
private void parseInsertNameWithReference(ByteBuffer buffer) throws QpackException
private void parseInsertNameWithReference(ByteBuffer buffer) throws QpackException, EncodingException
{
while (true)
{
@ -162,7 +163,7 @@ public class DecoderInstructionParser
}
}
private void parseInsertWithLiteralName(ByteBuffer buffer) throws QpackException
private void parseInsertWithLiteralName(ByteBuffer buffer) throws QpackException, EncodingException
{
while (true)
{

View File

@ -23,12 +23,15 @@ import org.eclipse.jetty.http3.qpack.QpackDecoder;
import org.eclipse.jetty.http3.qpack.QpackException;
import org.eclipse.jetty.http3.qpack.internal.QpackContext;
import org.eclipse.jetty.http3.qpack.internal.metadata.MetaDataBuilder;
import org.eclipse.jetty.http3.qpack.internal.util.EncodingException;
import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser;
import org.eclipse.jetty.http3.qpack.internal.util.NBitStringParser;
import org.eclipse.jetty.util.BufferUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECOMPRESSION_FAILED;
public class EncodedFieldSection
{
public static final Logger LOG = LoggerFactory.getLogger(EncodedFieldSection.class);
@ -49,22 +52,29 @@ public class EncodedFieldSection
_base = base;
_handler = handler;
while (content.hasRemaining())
try
{
EncodedField encodedField;
byte firstByte = content.get(content.position());
if ((firstByte & 0x80) != 0)
encodedField = parseIndexedField(content);
else if ((firstByte & 0x40) != 0)
encodedField = parseNameReference(content);
else if ((firstByte & 0x20) != 0)
encodedField = parseLiteralField(content);
else if ((firstByte & 0x10) != 0)
encodedField = parseIndexedFieldPostBase(content);
else
encodedField = parseNameReferencePostBase(content);
while (content.hasRemaining())
{
EncodedField encodedField;
byte firstByte = content.get(content.position());
if ((firstByte & 0x80) != 0)
encodedField = parseIndexedField(content);
else if ((firstByte & 0x40) != 0)
encodedField = parseNameReference(content);
else if ((firstByte & 0x20) != 0)
encodedField = parseLiteralField(content);
else if ((firstByte & 0x10) != 0)
encodedField = parseIndexedFieldPostBase(content);
else
encodedField = parseNameReferencePostBase(content);
_encodedFields.add(encodedField);
_encodedFields.add(encodedField);
}
}
catch (EncodingException e)
{
throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, e.getMessage(), e);
}
}
@ -97,28 +107,28 @@ public class EncodedFieldSection
return metaDataBuilder.build();
}
private EncodedField parseIndexedField(ByteBuffer buffer) throws QpackException
private EncodedField parseIndexedField(ByteBuffer buffer) throws EncodingException
{
byte firstByte = buffer.get(buffer.position());
boolean dynamicTable = (firstByte & 0x40) == 0;
_integerParser.setPrefix(6);
int index = _integerParser.decodeInt(buffer);
if (index < 0)
throw new QpackException.CompressionException("Invalid Index");
throw new EncodingException("invalid_index");
return new IndexedField(dynamicTable, index);
}
private EncodedField parseIndexedFieldPostBase(ByteBuffer buffer) throws QpackException
private EncodedField parseIndexedFieldPostBase(ByteBuffer buffer) throws EncodingException
{
_integerParser.setPrefix(4);
int index = _integerParser.decodeInt(buffer);
if (index < 0)
throw new QpackException.CompressionException("Invalid Index");
throw new EncodingException("Invalid Index");
return new PostBaseIndexedField(index);
}
private EncodedField parseNameReference(ByteBuffer buffer) throws QpackException
private EncodedField parseNameReference(ByteBuffer buffer) throws EncodingException
{
LOG.info("parseLiteralFieldLineWithNameReference: " + BufferUtil.toDetailString(buffer));
@ -129,17 +139,17 @@ public class EncodedFieldSection
_integerParser.setPrefix(4);
int nameIndex = _integerParser.decodeInt(buffer);
if (nameIndex < 0)
throw new QpackException.CompressionException("Invalid Name Index");
throw new EncodingException("invalid_name_index");
_stringParser.setPrefix(8);
String value = _stringParser.decode(buffer);
if (value == null)
throw new QpackException.CompressionException("Incomplete Value");
throw new EncodingException("incomplete_value");
return new IndexedNameField(allowEncoding, dynamicTable, nameIndex, value);
}
private EncodedField parseNameReferencePostBase(ByteBuffer buffer) throws QpackException
private EncodedField parseNameReferencePostBase(ByteBuffer buffer) throws EncodingException
{
byte firstByte = buffer.get(buffer.position());
boolean allowEncoding = (firstByte & 0x08) != 0;
@ -147,17 +157,17 @@ public class EncodedFieldSection
_integerParser.setPrefix(3);
int nameIndex = _integerParser.decodeInt(buffer);
if (nameIndex < 0)
throw new QpackException.CompressionException("Invalid Index");
throw new EncodingException("invalid_index");
_stringParser.setPrefix(8);
String value = _stringParser.decode(buffer);
if (value == null)
throw new QpackException.CompressionException("Invalid Value");
throw new EncodingException("invalid_value");
return new PostBaseIndexedNameField(allowEncoding, nameIndex, value);
}
private EncodedField parseLiteralField(ByteBuffer buffer) throws QpackException
private EncodedField parseLiteralField(ByteBuffer buffer) throws EncodingException
{
byte firstByte = buffer.get(buffer.position());
boolean allowEncoding = (firstByte & 0x10) != 0;
@ -165,12 +175,12 @@ public class EncodedFieldSection
_stringParser.setPrefix(4);
String name = _stringParser.decode(buffer);
if (name == null)
throw new QpackException.CompressionException("Invalid Name");
throw new EncodingException("invalid_name");
_stringParser.setPrefix(8);
String value = _stringParser.decode(buffer);
if (value == null)
throw new QpackException.CompressionException("Invalid Value");
throw new EncodingException("invalid_value");
return new LiteralField(allowEncoding, name, value);
}

View File

@ -21,7 +21,6 @@ import java.util.List;
import java.util.Map;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http3.qpack.QpackException;
import org.eclipse.jetty.util.component.Dumpable;
public class DynamicTable implements Iterable<Entry>, Dumpable
@ -134,10 +133,10 @@ public class DynamicTable implements Iterable<Entry>, Dumpable
* @param absoluteIndex the absolute index of the entry in the table.
* @return the entry with the absolute index.
*/
public Entry getAbsolute(int absoluteIndex) throws QpackException
public Entry getAbsolute(int absoluteIndex)
{
if (absoluteIndex < 0)
throw new QpackException.CompressionException("Invalid Index");
throw new IllegalArgumentException("Invalid Index");
if (_entries.isEmpty())
throw new IllegalArgumentException("Invalid Index");

View File

@ -0,0 +1,22 @@
//
// ========================================================================
// 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.internal.util;
public class EncodingException extends Exception
{
public EncodingException(String message)
{
super(message);
}
}

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.http3.qpack.internal.util;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http3.qpack.QpackException;
import org.eclipse.jetty.util.Utf8StringBuilder;
public class HuffmanDecoder
@ -39,7 +38,7 @@ public class HuffmanDecoder
_length = length;
}
public String decode(ByteBuffer buffer) throws QpackException.CompressionException
public String decode(ByteBuffer buffer) throws EncodingException
{
for (; _count < _length; _count++)
{
@ -58,7 +57,7 @@ public class HuffmanDecoder
if (rowsym[_node] == EOS)
{
reset();
throw new QpackException.CompressionException("EOS in content");
throw new EncodingException("eos_in_content");
}
// terminal node
@ -89,7 +88,7 @@ public class HuffmanDecoder
}
if ((c >> (8 - _bits)) != requiredPadding)
throw new QpackException.CompressionException("Incorrect padding");
throw new EncodingException("incorrect_padding");
_node = lastNode;
break;
@ -103,7 +102,7 @@ public class HuffmanDecoder
if (_node != 0)
{
reset();
throw new QpackException.CompressionException("Bad termination");
throw new EncodingException("bad_termination");
}
String value = _utf8.toString();

View File

@ -15,8 +15,6 @@ package org.eclipse.jetty.http3.qpack.internal.util;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http3.qpack.QpackException;
public class NBitStringParser
{
private final NBitIntegerParser _integerParser;
@ -50,7 +48,7 @@ public class NBitStringParser
_prefix = prefix;
}
public String decode(ByteBuffer buffer) throws QpackException
public String decode(ByteBuffer buffer) throws EncodingException
{
while (true)
{