Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/3 QPACK - do not expect section ack for zero required insert count #7802

Merged
merged 4 commits into from May 15, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -166,7 +166,8 @@ public boolean decode(long streamId, ByteBuffer buffer, Handler handler) throws
if (LOG.isDebugEnabled())
LOG.debug("Decoded: streamId={}, metadata={}", streamId, metaData);
_metaDataNotifications.add(new MetaDataNotification(streamId, metaData, handler));
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
if (requiredInsertCount > 0)
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
}
else
{
Expand Down Expand Up @@ -236,15 +237,17 @@ private void checkEncodedFieldSections() throws QpackException
int insertCount = _context.getDynamicTable().getInsertCount();
for (EncodedFieldSection encodedFieldSection : _encodedFieldSections)
{
if (encodedFieldSection.getRequiredInsertCount() <= insertCount)
int requiredInsertCount = encodedFieldSection.getRequiredInsertCount();
if (requiredInsertCount <= insertCount)
{
long streamId = encodedFieldSection.getStreamId();
MetaData metaData = encodedFieldSection.decode(_context, _maxHeaderSize);
if (LOG.isDebugEnabled())
LOG.debug("Decoded: streamId={}, metadata={}", streamId, metaData);

_metaDataNotifications.add(new MetaDataNotification(streamId, metaData, encodedFieldSection.getHandler()));
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
if (requiredInsertCount > 0)
_instructions.add(new SectionAcknowledgmentInstruction(streamId));
}
}
}
Expand Down
Expand Up @@ -193,7 +193,15 @@ public void encode(ByteBuffer buffer, long streamId, MetaData metadata) throws Q
requiredInsertCount = entryRequiredInsertCount;
}

// We should not expect section acknowledgements for 0 required insert count.
sectionInfo.setRequiredInsertCount(requiredInsertCount);
if (requiredInsertCount == 0)
{
streamInfo.remove(sectionInfo);
if (streamInfo.isEmpty())
_streamInfoMap.remove(streamId);
}

int base = dynamicTable.getBase();
int encodedInsertCount = encodeInsertCount(requiredInsertCount, dynamicTable.getCapacity());
boolean signBit = base < requiredInsertCount;
Expand Down
Expand Up @@ -16,6 +16,10 @@
import java.nio.ByteBuffer;
import java.util.List;

import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.NullByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
Expand All @@ -27,6 +31,23 @@

public class QpackTestUtil
{
public static ByteBuffer toBuffer(Instruction... instructions)
{
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(new NullByteBufferPool());
for (Instruction instruction : instructions)
{
instruction.encode(lease);
}
ByteBuffer combinedBuffer = BufferUtil.allocate(Math.toIntExact(lease.getTotalLength()));
BufferUtil.clearToFill(combinedBuffer);
for (ByteBuffer buffer : lease.getByteBuffers())
{
combinedBuffer.put(buffer);
}
BufferUtil.flipToFlush(combinedBuffer, 0);
return combinedBuffer;
}

public static Matcher<String> equalsHex(String expectedString)
{
expectedString = expectedString.replaceAll("\\s+", "");
Expand Down Expand Up @@ -56,4 +77,43 @@ public static String toHexString(Instruction instruction)
{
return BufferUtil.toHexString(toBuffer(List.of(instruction)));
}

public static ByteBuffer encode(QpackEncoder encoder, long streamId, MetaData metaData) throws QpackException
{
ByteBuffer buffer = BufferUtil.allocate(1024);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, streamId, metaData);
BufferUtil.flipToFlush(buffer, 0);
return buffer;
}

public static HttpFields.Mutable toHttpFields(HttpField field)
{
return HttpFields.build().add(field);
}

public static MetaData toMetaData(String name, String value)
{
return toMetaData(toHttpFields(new HttpField(name, value)));
}

public static MetaData toMetaData(String method, String path, String scheme)
{
return toMetaData(method, path, scheme, null);
}

public static MetaData toMetaData(String method, String path, String scheme, HttpFields.Mutable fields)
{
if (fields == null)
fields = HttpFields.build();
fields.put(":scheme", scheme);
fields.put(":method", method);
fields.put(":path", path);
return new MetaData(HttpVersion.HTTP_3, fields);
}

public static MetaData toMetaData(HttpFields fields)
{
return new MetaData(HttpVersion.HTTP_3, fields);
}
}
@@ -0,0 +1,79 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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 org.eclipse.jetty.http3.qpack.internal.instruction.SectionAcknowledgmentInstruction;
import org.eclipse.jetty.util.BufferUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.eclipse.jetty.http3.qpack.QpackTestUtil.encode;
import static org.eclipse.jetty.http3.qpack.QpackTestUtil.toBuffer;
import static org.eclipse.jetty.http3.qpack.QpackTestUtil.toMetaData;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class SectionAcknowledgmentTest
{
private static final int MAX_BLOCKED_STREAMS = 5;
private static final int MAX_HEADER_SIZE = 1024;

private QpackEncoder _encoder;
private QpackDecoder _decoder;
private TestDecoderHandler _decoderHandler;
private TestEncoderHandler _encoderHandler;

@BeforeEach
public void before()
{
_encoderHandler = new TestEncoderHandler();
_decoderHandler = new TestDecoderHandler();
_encoder = new QpackEncoder(_encoderHandler, MAX_BLOCKED_STREAMS);
_decoder = new QpackDecoder(_decoderHandler, MAX_HEADER_SIZE);
}

@Test
public void testSectionAcknowledgmentForZeroRequiredInsertCountOnDecoder() throws Exception
{
// Encode a header with only a value contained in the static table.
ByteBuffer buffer = encode(_encoder, 0, toMetaData("GET", "/", "http"));

// No instruction since no addition to table.
Instruction instruction = _encoderHandler.getInstruction();
assertNull(instruction);

// Decoding should generate no instruction.
_decoder.decode(0, buffer, _decoderHandler);
instruction = _decoderHandler.getInstruction();
assertNull(instruction);
}

@Test
public void testSectionAcknowledgmentForZeroRequiredInsertCountOnEncoder() throws Exception
{
// Encode a header with only a value contained in the static table.
ByteBuffer buffer = encode(_encoder, 0, toMetaData("GET", "/", "http"));
System.err.println(BufferUtil.toDetailString(buffer));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the System.err..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Parsing a section ack instruction on the encoder when we are not expecting it should result in QPACK_DECODER_STREAM_ERROR.
SectionAcknowledgmentInstruction instruction = new SectionAcknowledgmentInstruction(0);
ByteBuffer instructionBuffer = toBuffer(instruction);
QpackException error = assertThrows(QpackException.class, () -> _encoder.parseInstructions(instructionBuffer));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to test for QpackException.StreamException here to be sure it's not a SESSION error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a SessionException and thats what I would expect it to be. The rfc says it must be treated "as a connection error".

I have added this check to the test case.

assertThat(error.getMessage(), containsString("No StreamInfo for 0"));
}
}