Skip to content

Commit

Permalink
improve: table size update must come first in header block
Browse files Browse the repository at this point in the history
According to RFC 7531, section 4.2, HPACK dynamic table size update must
happen at the beginning of the header block.

This change will increase the number of green cases in h2spec, which other
HTTP/2 implementation such as hyper/h2(Rust) or Crystal http/2 uses
in order to check spec conformance.

References

- https://datatracker.ietf.org/doc/html/rfc7541#section-4.2
- netty/netty#12988
- https://github.com/summerwind/h2spec
- https://github.com/hyperium/h2?tab=readme-ov-file#features
- https://github.com/ysbaddaden/http2
  • Loading branch information
i10416 committed Nov 21, 2024
1 parent 9532496 commit 48cff61
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 15 deletions.
33 changes: 23 additions & 10 deletions hpack/shared/src/main/scala/org/http4s/hpack/Decoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@
*/
package org.http4s.hpack;

import java.io.IOException;
import java.io.InputStream;

import org.http4s.hpack.HpackUtil.IndexType;
import org.http4s.hpack.HeaderField.HEADER_ENTRY_OVERHEAD
import org.http4s.hpack.HpackUtil.IndexType

import org.http4s.hpack.HeaderField.HEADER_ENTRY_OVERHEAD;
import java.io.IOException
import java.io.InputStream;

private[http4s] final class Decoder(dynamicTable: DynamicTable) {

Expand All @@ -48,6 +47,11 @@ private[http4s] final class Decoder(dynamicTable: DynamicTable) {
new IOException("invalid max dynamic table size");
private[this] val MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED =
new IOException("max dynamic table size change required");
private[this] val UNEXPECTED_DYNAMIC_TABLE_SIZE_UPDATE =
new IOException(
"Dynamic table size update must happen "
+ "at the beginning of the header block"
)

private[this] val EMPTY = Array.emptyByteArray;

Expand Down Expand Up @@ -103,6 +107,8 @@ private[http4s] final class Decoder(dynamicTable: DynamicTable) {
*/
@throws[IOException]
def decode(in: InputStream, headerListener: HeaderListener): Unit = {
var seenOnlyDynamicTableSizeUpdate = true

while (in.available() > 0) {
var b: Byte = 0
import State._
Expand All @@ -115,6 +121,7 @@ private[http4s] final class Decoder(dynamicTable: DynamicTable) {
}
if (b < 0) {
// Indexed Header Field
seenOnlyDynamicTableSizeUpdate = false
index = b & 0x7f;
if (index == 0) {
throw ILLEGAL_INDEX_VALUE;
Expand All @@ -125,6 +132,7 @@ private[http4s] final class Decoder(dynamicTable: DynamicTable) {
}
} else if ((b & 0x40) == 0x40) {
// Literal Header Field with Incremental Indexing
seenOnlyDynamicTableSizeUpdate = false
indexType = IndexType.INCREMENTAL;
index = b & 0x3f;
if (index == 0) {
Expand All @@ -138,15 +146,20 @@ private[http4s] final class Decoder(dynamicTable: DynamicTable) {
}
} else if ((b & 0x20) == 0x20) {
// Dynamic Table Size Update
index = b & 0x1f;
if (index == 0x1f) {
state = State.READ_MAX_DYNAMIC_TABLE_SIZE;
if (seenOnlyDynamicTableSizeUpdate) {
index = b & 0x1f;
if (index == 0x1f) {
state = State.READ_MAX_DYNAMIC_TABLE_SIZE;
} else {
setDynamicTableSize(index);
state = State.READ_HEADER_REPRESENTATION;
}
} else {
setDynamicTableSize(index);
state = State.READ_HEADER_REPRESENTATION;
throw UNEXPECTED_DYNAMIC_TABLE_SIZE_UPDATE
}
} else {
// Literal Header Field without Indexing / never Indexed
seenOnlyDynamicTableSizeUpdate = false
indexType = if ((b & 0x10) == 0x10) IndexType.NEVER else IndexType.NONE;
index = b & 0x0f;
if (index == 0) {
Expand Down
20 changes: 15 additions & 5 deletions hpack/shared/src/test/scala/org/http4s/hpack/DecoderTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ package org.http4s.hpack

import cats.kernel.Eq
import cats.syntax.all._

import java.io.ByteArrayInputStream
import java.io.IOException
import org.junit.Before
import org.junit.Test
import org.http4s.hpack.HpackUtil.ISO_8859_1
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

import java.io.ByteArrayInputStream
import java.io.IOException

object DecoderTest {
private val MAX_HEADER_SIZE = 8192
Expand Down Expand Up @@ -142,6 +142,16 @@ class DecoderTest {
decode("2081")
}

@Test(expected = classOf[IOException])
@throws[IOException]
def testDynamicTableSizeUpdateAfterTheBeginningOfTheBlock(): Unit =
decode("8120")

@Test(expected = classOf[IOException])
@throws[Exception]
def testDynamicTableSizeUpdateAfterTheBeginningOfTheBlockLong(): Unit =
decode("813FE11F")

@Test(expected = classOf[IOException])
@throws[Exception]
def testTooLargeDynamicTableSizeUpdate(): Unit = {
Expand Down

0 comments on commit 48cff61

Please sign in to comment.