diff --git a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/http2-max-header-list-size.excludes b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/http2-max-header-list-size.excludes new file mode 100644 index 0000000000..461457482a --- /dev/null +++ b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/http2-max-header-list-size.excludes @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# new max-header-list-size setting for HTTP/2 +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.maxHeaderListSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ClientSettings.withMaxHeaderListSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.getMaxHeaderListSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.settings.Http2ServerSettings.withMaxHeaderListSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ClientSettings.maxHeaderListSize") +ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.scaladsl.settings.Http2ServerSettings.maxHeaderListSize") diff --git a/http-core/src/main/resources/reference.conf b/http-core/src/main/resources/reference.conf index 1c7019879b..0767de86bd 100644 --- a/http-core/src/main/resources/reference.conf +++ b/http-core/src/main/resources/reference.conf @@ -239,6 +239,21 @@ pekko.http { # the connection was established but before it received our SETTINGS. max-concurrent-streams = 256 + # The maximum size of the header list (the sum of the sizes of the decompressed header names and values) that + # this endpoint is prepared to accept, in bytes. The value is advertised to the peer using the + # SETTINGS_MAX_HEADER_LIST_SIZE setting. A header block that decompresses to more than this amount is rejected + # with a GOAWAY(ENHANCE_YOUR_CALM) frame instead of being buffered. + # + # The same limit is applied to the accumulated header block fragments carried by a HEADERS frame and its + # subsequent CONTINUATION frames, so that the memory used for a header block that the peer never completes + # (END_HEADERS is never set) stays bounded. Each fragment is accounted with its frame header size on top of + # its payload size, which also bounds the number of empty fragments that are accepted for one header block. + # + # Note that peers calculate the header list size with an extra overhead of 32 octets per header field (see + # RFC 9113, section 6.5.2) while this implementation only counts the actual name and value bytes, so the + # effective limit for a well-behaved peer is somewhat stricter than the configured value. + max-header-list-size = 64 KiB + # The maximum number of bytes to receive from a request entity in a single chunk. # # The reasoning to limit that amount (instead of delivering all buffered data for a stream) is that @@ -473,6 +488,21 @@ pekko.http { # the connection was established but before it received our SETTINGS. max-concurrent-streams = 256 + # The maximum size of the header list (the sum of the sizes of the decompressed header names and values) that + # this endpoint is prepared to accept, in bytes. The value is advertised to the peer using the + # SETTINGS_MAX_HEADER_LIST_SIZE setting. A header block that decompresses to more than this amount is rejected + # with a GOAWAY(ENHANCE_YOUR_CALM) frame instead of being buffered. + # + # The same limit is applied to the accumulated header block fragments carried by a HEADERS frame and its + # subsequent CONTINUATION frames, so that the memory used for a header block that the peer never completes + # (END_HEADERS is never set) stays bounded. Each fragment is accounted with its frame header size on top of + # its payload size, which also bounds the number of empty fragments that are accepted for one header block. + # + # Note that peers calculate the header list size with an extra overhead of 32 octets per header field (see + # RFC 9113, section 6.5.2) while this implementation only counts the actual name and value bytes, so the + # effective limit for a well-behaved peer is somewhat stricter than the configured value. + max-header-list-size = 64 KiB + # The maximum number of bytes to receive from a request entity in a single chunk. # # The reasoning to limit that amount (instead of delivering all buffered data for a stream) is that diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala index 31454417f6..aa7de7bda2 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala @@ -127,7 +127,7 @@ private[http] object Http2Blueprint { httpLayer(settings, log, dateHeaderRendering) atopKeepRight serverDemux(settings.http2Settings, initialDemuxerSettings, upgraded) atop FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames) atop // enable for debugging - hpackCoding(masterHttpHeaderParser, settings.parserSettings) + hpackCoding(masterHttpHeaderParser, settings.parserSettings, settings.http2Settings.maxHeaderListSize) val frameTypesForThrottle = getFrameTypesForThrottle(settings.http2Settings) @@ -153,7 +153,7 @@ private[http] object Http2Blueprint { httpLayerClient(masterHttpHeaderParser, settings, log)).atop( clientDemux(settings.http2Settings, masterHttpHeaderParser)).atop( FrameLogger.logFramesIfEnabled(settings.http2Settings.logFrames)).atop( // enable for debugging - hpackCoding(masterHttpHeaderParser, settings.parserSettings)).atop( + hpackCoding(masterHttpHeaderParser, settings.parserSettings, settings.http2Settings.maxHeaderListSize)).atop( framingClient(log)).atop( errorHandling(log)).atop( idleTimeoutIfConfigured(settings.idleTimeout)) @@ -247,11 +247,11 @@ private[http] object Http2Blueprint { * TODO: introduce another FrameEvent type that exclude HeadersFrame and ContinuationFrame from * reaching the higher-level. */ - def hpackCoding(masterHttpHeaderParser: HttpHeaderParser, parserSettings: ParserSettings) + def hpackCoding(masterHttpHeaderParser: HttpHeaderParser, parserSettings: ParserSettings, maxHeaderListSize: Int) : BidiFlow[FrameEvent, FrameEvent, FrameEvent, FrameEvent, NotUsed] = BidiFlow.fromFlows( Flow[FrameEvent].via(HeaderCompression), - Flow[FrameEvent].via(new HeaderDecompression(masterHttpHeaderParser, parserSettings))) + Flow[FrameEvent].via(new HeaderDecompression(masterHttpHeaderParser, parserSettings, maxHeaderListSize))) /** * Creates substreams for every stream and manages stream state machines diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala index df829c20bc..9e63961c9f 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Demux.scala @@ -295,7 +295,8 @@ private[http2] abstract class Http2Demux(http2Settings: Http2CommonSettings, // enforced immediately even before the acknowledgement is received. // Reminder: the receiver of a SETTINGS frame must process them in the order they are received. val initialLocalSettings: immutable.Seq[Setting] = immutable.Seq( - Setting(SettingIdentifier.SETTINGS_MAX_CONCURRENT_STREAMS, http2Settings.maxConcurrentStreams)) ++ + Setting(SettingIdentifier.SETTINGS_MAX_CONCURRENT_STREAMS, http2Settings.maxConcurrentStreams), + Setting(SettingIdentifier.SETTINGS_MAX_HEADER_LIST_SIZE, http2Settings.maxHeaderListSize)) ++ immutable.Seq(Setting(SettingIdentifier.SETTINGS_ENABLE_PUSH, 0)).filter(_ => !isServer) // only on client override def preStart(): Unit = { diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala index 6d992e36b0..6aad6e0857 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala @@ -36,13 +36,23 @@ import scala.collection.immutable.VectorBuilder * INTERNAL API * * Can be used on server and client side. + * + * @param maxHeaderListSize the maximum size of a decoded header list. The same limit is applied to the accumulated + * header block fragments of a HEADERS frame and its CONTINUATION frames, so that the memory + * used for a header block that the peer never completes stays bounded + * (see RFC 9113, section 10.5). */ @InternalApi -private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderParser, parserSettings: ParserSettings) +private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderParser, parserSettings: ParserSettings, + maxHeaderListSize: Int) extends GraphStage[FlowShape[FrameEvent, FrameEvent]] { val UTF8 = StandardCharsets.UTF_8 val US_ASCII = StandardCharsets.US_ASCII + // Each fragment is accounted with the size of its frame header on top of its payload size. Without that, empty + // CONTINUATION frames would never add to the accumulated header block and their number would be unbounded. + private val FrameHeaderSize = 9 + val eventsIn = Inlet[FrameEvent]("HeaderDecompression.eventsIn") val eventsOut = Outlet[FrameEvent]("HeaderDecompression.eventsOut") @@ -51,8 +61,8 @@ private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderPar def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new HandleOrPassOnStage[FrameEvent, FrameEvent](shape) { val httpHeaderParser = masterHeaderParser.createShallowCopy() - val decoder = new pekko.http.shaded.com.twitter.hpack.Decoder(Http2Protocol.InitialMaxHeaderListSize, - Http2Protocol.InitialMaxHeaderTableSize) + val decoder = + new pekko.http.shaded.com.twitter.hpack.Decoder(maxHeaderListSize, Http2Protocol.InitialMaxHeaderTableSize) become(Idle) @@ -99,9 +109,11 @@ private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderPar val stream = payload.compact.asInputStream try { decoder.decode(stream, Receiver) // only compact ByteString supports InputStream with mark/reset - decoder.endHeaderBlock() // TODO: do we have to check the result here? + // the decoder stops emitting headers as soon as the limit is exceeded and reports that here + val truncated = decoder.endHeaderBlock() - push(eventsOut, ParsedHeadersFrame(streamId, endStream, headers.result(), prioInfo, None)) + if (truncated) headerListSizeExceeded(streamId) + else push(eventsOut, ParsedHeadersFrame(streamId, endStream, headers.result(), prioInfo, None)) } catch { case ex: ParsingException => // push details further and let RequestErrorFlow handle responding with bad request @@ -119,6 +131,7 @@ private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderPar val handleEvent: PartialFunction[FrameEvent, Unit] = { case HeadersFrame(streamId, endStream, endHeaders, fragment, prioInfo) => if (endHeaders) parseAndEmit(streamId, endStream, fragment, prioInfo) + else if (exceedsMaxHeaderListSize(0, fragment)) headerListSizeExceeded(streamId) else { become(new ReceivingHeaders(streamId, endStream, fragment, prioInfo)) pull(eventsIn) @@ -132,14 +145,21 @@ private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderPar class ReceivingHeaders(streamId: Int, endStream: Boolean, initiallyReceivedData: ByteString, priorityInfo: Option[PriorityFrame]) extends State { var receivedData = initiallyReceivedData + // includes the frame headers of the fragments received so far, see `FrameHeaderSize` + var accountedSize: Long = FrameHeaderSize + initiallyReceivedData.size val handleEvent: PartialFunction[FrameEvent, Unit] = { case ContinuationFrame(`streamId`, endHeaders, payload) => - if (endHeaders) { + if (exceedsMaxHeaderListSize(accountedSize, payload)) + // Neither the HPACK decoder nor any of the checks further down the line run before the header block + // is complete, so this is the only place where the size of an unfinished header block is bounded. + headerListSizeExceeded(streamId) + else if (endHeaders) { parseAndEmit(streamId, endStream, receivedData ++ payload, priorityInfo) become(Idle) } else { receivedData ++= payload + accountedSize += FrameHeaderSize + payload.size pull(eventsIn) } case x => @@ -147,6 +167,15 @@ private[http2] final class HeaderDecompression(masterHeaderParser: HttpHeaderPar } } + def exceedsMaxHeaderListSize(accountedSize: Long, payload: ByteString): Boolean = + accountedSize + FrameHeaderSize + payload.size > maxHeaderListSize + + def headerListSizeExceeded(streamId: Int): Unit = + fail(eventsOut, + new Http2ProtocolException( + ErrorCode.ENHANCE_YOUR_CALM, + s"Header block of stream $streamId exceeded the configured max-header-list-size of $maxHeaderListSize bytes")) + def protocolError(msg: String): Unit = failStage(new Http2ProtocolException(msg)) } } diff --git a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala index 1ac4be45aa..bfcf87be7d 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ClientSettings.scala @@ -34,6 +34,20 @@ trait Http2ClientSettings { self: scaladsl.settings.Http2ClientSettings.Http2Cli def maxConcurrentStreams: Int def withMaxConcurrentStreams(newValue: Int): Http2ClientSettings = copy(maxConcurrentStreams = newValue) + /** + * The maximum size of a decoded header list that this endpoint is prepared to accept, in bytes. The value is + * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same limit is applied to the accumulated + * header block fragments of a HEADERS frame and its CONTINUATION frames. + * + * @since 2.0.0 + */ + def maxHeaderListSize: Int + + /** + * @since 2.0.0 + */ + def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = copy(maxHeaderListSize = newValue) + def outgoingControlFrameBufferSize: Int def withOutgoingControlFrameBufferSize(newValue: Int): Http2ClientSettings = copy(outgoingControlFrameBufferSize = newValue) diff --git a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala index a0493aeb4e..1c9165e168 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/javadsl/settings/Http2ServerSettings.scala @@ -42,6 +42,20 @@ trait Http2ServerSettings { def getMaxConcurrentStreams: Int = maxConcurrentStreams def withMaxConcurrentStreams(newValue: Int): Http2ServerSettings + /** + * The maximum size of a decoded header list that this endpoint is prepared to accept, in bytes. The value is + * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same limit is applied to the accumulated + * header block fragments of a HEADERS frame and its CONTINUATION frames. + * + * @since 2.0.0 + */ + def getMaxHeaderListSize: Int = maxHeaderListSize + + /** + * @since 2.0.0 + */ + def withMaxHeaderListSize(newValue: Int): Http2ServerSettings + def getOutgoingControlFrameBufferSize: Int = outgoingControlFrameBufferSize def withOutgoingControlFrameBufferSize(newValue: Int): Http2ServerSettings diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala index a239f0d5fc..19847648fd 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/settings/Http2ServerSettings.scala @@ -41,6 +41,7 @@ private[http] trait Http2CommonSettings { def logFrames: Boolean def maxConcurrentStreams: Int + def maxHeaderListSize: Int def outgoingControlFrameBufferSize: Int def pingInterval: FiniteDuration @@ -90,6 +91,20 @@ trait Http2ServerSettings extends javadsl.settings.Http2ServerSettings with Http def maxConcurrentStreams: Int override def withMaxConcurrentStreams(newValue: Int): Http2ServerSettings = copy(maxConcurrentStreams = newValue) + /** + * The maximum size of a decoded header list that this endpoint is prepared to accept, in bytes. The value is + * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same limit is applied to the accumulated + * header block fragments of a HEADERS frame and its CONTINUATION frames. + * + * @since 2.0.0 + */ + def maxHeaderListSize: Int + + /** + * @since 2.0.0 + */ + override def withMaxHeaderListSize(newValue: Int): Http2ServerSettings = copy(maxHeaderListSize = newValue) + def outgoingControlFrameBufferSize: Int override def withOutgoingControlFrameBufferSize(newValue: Int): Http2ServerSettings = copy(outgoingControlFrameBufferSize = newValue) @@ -129,6 +144,7 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { private[http] case class Http2ServerSettingsImpl( maxConcurrentStreams: Int, + maxHeaderListSize: Int, requestEntityChunkSize: Int, incomingConnectionLevelBufferSize: Int, incomingStreamLevelBufferSize: Int, @@ -144,6 +160,7 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { internalSettings: Option[Http2InternalServerSettings]) extends Http2ServerSettings { require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0") + require(maxHeaderListSize > 0, "max-header-list-size must be > 0") require(requestEntityChunkSize > 0, "request-entity-chunk-size must be > 0") require(incomingConnectionLevelBufferSize > 0, "incoming-connection-level-buffer-size must be > 0") require(incomingStreamLevelBufferSize > 0, "incoming-stream-level-buffer-size must be > 0") @@ -161,6 +178,7 @@ object Http2ServerSettings extends SettingsCompanion[Http2ServerSettings] { extends pekko.http.impl.util.SettingsCompanionImpl[Http2ServerSettingsImpl]("pekko.http.server.http2") { def fromSubConfig(root: Config, c: Config): Http2ServerSettingsImpl = Http2ServerSettingsImpl( maxConcurrentStreams = c.getInt("max-concurrent-streams"), + maxHeaderListSize = c.getIntBytes("max-header-list-size"), requestEntityChunkSize = c.getIntBytes("request-entity-chunk-size"), incomingConnectionLevelBufferSize = c.getIntBytes("incoming-connection-level-buffer-size"), incomingStreamLevelBufferSize = c.getIntBytes("incoming-stream-level-buffer-size"), @@ -205,6 +223,20 @@ trait Http2ClientSettings extends javadsl.settings.Http2ClientSettings with Http def maxConcurrentStreams: Int override def withMaxConcurrentStreams(newValue: Int): Http2ClientSettings = copy(maxConcurrentStreams = newValue) + /** + * The maximum size of a decoded header list that this endpoint is prepared to accept, in bytes. The value is + * advertised to the peer via SETTINGS_MAX_HEADER_LIST_SIZE and the same limit is applied to the accumulated + * header block fragments of a HEADERS frame and its CONTINUATION frames. + * + * @since 2.0.0 + */ + def maxHeaderListSize: Int + + /** + * @since 2.0.0 + */ + override def withMaxHeaderListSize(newValue: Int): Http2ClientSettings = copy(maxHeaderListSize = newValue) + def outgoingControlFrameBufferSize: Int override def withOutgoingControlFrameBufferSize(newValue: Int): Http2ClientSettings = copy(outgoingControlFrameBufferSize = newValue) @@ -244,6 +276,7 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { private[http] case class Http2ClientSettingsImpl( maxConcurrentStreams: Int, + maxHeaderListSize: Int, requestEntityChunkSize: Int, incomingConnectionLevelBufferSize: Int, incomingStreamLevelBufferSize: Int, @@ -258,6 +291,7 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { internalSettings: Option[Http2InternalClientSettings]) extends Http2ClientSettings with javadsl.settings.Http2ClientSettings { require(maxConcurrentStreams >= 0, "max-concurrent-streams must be >= 0") + require(maxHeaderListSize > 0, "max-header-list-size must be > 0") require(requestEntityChunkSize > 0, "request-entity-chunk-size must be > 0") require(incomingConnectionLevelBufferSize > 0, "incoming-connection-level-buffer-size must be > 0") require(incomingStreamLevelBufferSize > 0, "incoming-stream-level-buffer-size must be > 0") @@ -272,6 +306,7 @@ object Http2ClientSettings extends SettingsCompanion[Http2ClientSettings] { extends pekko.http.impl.util.SettingsCompanionImpl[Http2ClientSettingsImpl]("pekko.http.client.http2") { def fromSubConfig(root: Config, c: Config): Http2ClientSettingsImpl = Http2ClientSettingsImpl( maxConcurrentStreams = c.getInt("max-concurrent-streams"), + maxHeaderListSize = c.getIntBytes("max-header-list-size"), requestEntityChunkSize = c.getIntBytes("request-entity-chunk-size"), incomingConnectionLevelBufferSize = c.getIntBytes("incoming-connection-level-buffer-size"), incomingStreamLevelBufferSize = c.getIntBytes("incoming-stream-level-buffer-size"), diff --git a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala index 573a963351..1d478726da 100644 --- a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala +++ b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala @@ -23,6 +23,7 @@ import org.apache.pekko import pekko.NotUsed import pekko.http.impl.engine.http2.FrameEvent._ import pekko.http.impl.engine.http2.Http2Protocol.{ ErrorCode, Flags, FrameType, SettingIdentifier } +import pekko.http.impl.engine.http2.framing.FrameRenderer import pekko.http.impl.engine.server.{ HttpAttributes, ServerTerminator } import pekko.http.impl.engine.ws.ByteStringSinkProbe import pekko.http.scaladsl.client.RequestBuilding.Get @@ -188,6 +189,74 @@ class Http2ServerSpec extends Http2SpecWithMaterializer(""" headerPayload shouldBe HPackSpecExamples.C61FirstResponseWithHuffman }) + "reject an unfinished header block that grows beyond max-header-list-size".inAssertAllStagesStopped( + new TestSetup with RequestResponseProbes { + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxHeaderListSize(4096)) + + val headerBlock = HPackSpecExamples.C41FirstRequestWithHuffman + network.sendHEADERS(1, endStream = true, endHeaders = false, headerBlock) + + // the peer never sets END_HEADERS, so neither the HPACK decoder nor request dispatch ever run + val fragment = ByteString(new Array[Byte](1024)) + (1 to 5).foreach(_ => network.sendCONTINUATION(1, endHeaders = false, fragment)) + + user.requestIn.ensureSubscription() + user.requestIn.expectNoMessage(100.millis) + + val (_, errorCode) = network.expectGOAWAY() + errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM) + }) + "reject an unfinished header block made up of empty CONTINUATION frames".inAssertAllStagesStopped( + new TestSetup with RequestResponseProbes { + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxHeaderListSize(256)) + + val headerBlock = HPackSpecExamples.C41FirstRequestWithHuffman + network.sendHEADERS(1, endStream = true, endHeaders = false, headerBlock) + + // empty fragments don't grow the header block but are accounted with their frame header size, so their + // number is bounded as well (sent in one go because the connection is failed in between) + network.sendBytes((1 to 64).map(_ => + FrameRenderer.render(ContinuationFrame(1, endHeaders = false, ByteString.empty))).reduce(_ ++ _)) + + user.requestIn.ensureSubscription() + user.requestIn.expectNoMessage(100.millis) + + val (_, errorCode) = network.expectGOAWAY() + errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM) + }) + "reject a header block that decodes to more than max-header-list-size".inAssertAllStagesStopped( + new TestSetup with RequestResponseProbes { + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxHeaderListSize(1024)) + + val request = HttpRequest( + uri = "http://www.example.com/", + headers = RawHeader("big-header", "x" * 2000) :: Nil) + network.sendHEADERS(1, endStream = true, endHeaders = true, network.encodeRequestHeaders(request)) + + user.requestIn.ensureSubscription() + user.requestIn.expectNoMessage(100.millis) + + val (_, errorCode) = network.expectGOAWAY() + errorCode should ===(ErrorCode.ENHANCE_YOUR_CALM) + }) + "accept a header block that stays within max-header-list-size".inAssertAllStagesStopped( + new TestSetup with RequestResponseProbes { + override def settings: ServerSettings = super.settings.mapHttp2Settings(_.withMaxHeaderListSize(1024)) + + val request = + HttpRequest(uri = "http://www.example.com/", headers = RawHeader("small-header", "x" * 100) :: Nil) + network.sendHEADERS(1, endStream = true, endHeaders = true, network.encodeRequestHeaders(request)) + + user.expectRequest().headers should contain(RawHeader("small-header", "x" * 100)) + }) + + "advertise SETTINGS_MAX_HEADER_LIST_SIZE to the peer" in + new TestSetupWithoutHandshake with RequestResponseProbes { + network.sendBytes(Http2Protocol.ClientConnectionPreface) + network.expectSETTINGS().settings should contain( + Setting(SettingIdentifier.SETTINGS_MAX_HEADER_LIST_SIZE, settings.http2Settings.maxHeaderListSize)) + } + "fail if Http2StreamIdHeader missing" in pending "automatically add `Date` header" in pending diff --git a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala index 9c692d5775..0d4d2f23b7 100644 --- a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala +++ b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala @@ -59,7 +59,7 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp attributes) Source.single(frame) - .via(new HeaderDecompression(headerParser, parserSettings)) + .via(new HeaderDecompression(headerParser, parserSettings, serverSettings.http2Settings.maxHeaderListSize)) .map { // emulate demux case headers: ParsedHeadersFrame => Http2SubStream(