This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch 1.4.x
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/1.4.x by this push:
new bcd5fd41c http/2: bound incoming header blocks with
max-header-list-size (#1247)
bcd5fd41c is described below
commit bcd5fd41cda60b4934d538dcb82168ee9dc39a5f
Author: PJ Fanning <[email protected]>
AuthorDate: Sun Aug 30 19:27:17 2026 +0100
http/2: bound incoming header blocks with max-header-list-size (#1247)
* http/2: bound incoming header blocks with max-header-list-size (#1216)
Motivation:
The HTTP/2 header decompression stage had no upper bound on the incoming
side. The HPACK decoder was constructed with
Http2Protocol.InitialMaxHeaderListSize (Int.MaxValue) and the header block
fragments of a HEADERS frame and its CONTINUATION frames were accumulated
until END_HEADERS was seen, so the memory used for a single header block
was limited only by what the peer chose to send. Neither endpoint
advertised SETTINGS_MAX_HEADER_LIST_SIZE, so a peer had no way of knowing
what it may send either.
Modification:
Add a `max-header-list-size` setting (64 KiB by default) to
`pekko.http.server.http2` and `pekko.http.client.http2` and pass it to
`HeaderDecompression`, which now
* constructs the HPACK decoder with that limit and checks the truncation
result of `Decoder.endHeaderBlock()`, which was previously ignored,
* applies the same limit to the accumulated header block fragments,
accounting each fragment with its frame header size so that the number
of empty CONTINUATION frames per header block is bounded as well,
* fails the connection with GOAWAY(ENHANCE_YOUR_CALM) when the limit is
exceeded.
The configured value is advertised to the peer in the initial SETTINGS
frame.
Result:
The memory used for a single incoming header block is bounded by the
configured limit on both the server and the client side, and peers are
told about the limit up front.
Tests:
- sbt "http2-tests/testOnly
org.apache.pekko.http.impl.engine.http2.Http2ServerSpec" - pass, 5 new tests
- sbt http2-tests/test - pass
- sbt http-core/test - pass (HostConnectionPoolSpec flaked in the full run,
passes on its own)
- sbt +http-core/mimaReportBinaryIssues - pass
- sbt http-core/scalafmt http2-tests/Test/scalafmt - clean
- sbt http-core/headerCreateAll - no changes
References:
None - bounds the memory used for incoming HTTP/2 header blocks
* Create http2-max-header-list-size.excludes
* docs: the max-header-list-size settings ship in 1.4.1, not 2.0.0
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* fix: drop main-only changes that the backport pulled in
`ParsedHeadersFrame` has no error-info field on 1.4.x and the parsing
exception is not routed through it, so the header decompression stage
keeps 1.4.x's four-argument frame and its IOException-only handling,
along with `ByteStringInputStream`. Only the header list size checks are
new. `RequestParsingSpec` keeps unpacking `futureValue` exceptions, since
a malformed request still fails the stream here.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../http2-max-header-list-size.excludes | 24 ++++++++
http-core/src/main/resources/reference.conf | 30 ++++++++++
.../http/impl/engine/http2/Http2Blueprint.scala | 8 +--
.../pekko/http/impl/engine/http2/Http2Demux.scala | 3 +-
.../engine/http2/hpack/HeaderDecompression.scala | 43 +++++++++++---
.../javadsl/settings/Http2ClientSettings.scala | 14 +++++
.../javadsl/settings/Http2ServerSettings.scala | 14 +++++
.../scaladsl/settings/Http2ServerSettings.scala | 35 +++++++++++
.../http/impl/engine/http2/Http2ServerSpec.scala | 69 ++++++++++++++++++++++
.../impl/engine/http2/RequestParsingSpec.scala | 2 +-
10 files changed, 229 insertions(+), 13 deletions(-)
diff --git
a/http-core/src/main/mima-filters/1.4.x.backwards.excludes/http2-max-header-list-size.excludes
b/http-core/src/main/mima-filters/1.4.x.backwards.excludes/http2-max-header-list-size.excludes
new file mode 100644
index 000000000..b069604f7
--- /dev/null
+++
b/http-core/src/main/mima-filters/1.4.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 (1.4.1)
+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 9fe7894fa..8b47a7719 100644
--- a/http-core/src/main/resources/reference.conf
+++ b/http-core/src/main/resources/reference.conf
@@ -243,6 +243,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
@@ -438,6 +453,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 f732085b0..7ed03461c 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 f05bbb446..aff2b5c84 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 a22e7c6c4..fc7aafb36 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
@@ -35,13 +35,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")
@@ -50,8 +60,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)
@@ -94,11 +104,13 @@ private[http2] final class
HeaderDecompression(masterHeaderParser: HttpHeaderPar
}
try {
decoder.decode(ByteStringInputStream(payload), Receiver)
- 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))
+ if (truncated) headerListSizeExceeded(streamId)
+ else push(eventsOut, ParsedHeadersFrame(streamId, endStream,
headers.result(), prioInfo))
} catch {
- case ex: IOException =>
+ case _: IOException =>
// this is signalled by the decoder when it failed, we want to
react to this by rendering a GOAWAY frame
fail(eventsOut,
new
Http2Compliance.Http2ProtocolException(ErrorCode.COMPRESSION_ERROR,
"Decompression failed."))
@@ -109,6 +121,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)
@@ -122,14 +135,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 =>
@@ -137,6 +157,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 1ac4be45a..7f7428cea 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 1.4.1
+ */
+ def maxHeaderListSize: Int
+
+ /**
+ * @since 1.4.1
+ */
+ 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 e9830fb6b..0ec10020c 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 1.4.1
+ */
+ def getMaxHeaderListSize: Int = maxHeaderListSize
+
+ /**
+ * @since 1.4.1
+ */
+ 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 e2c7eac5d..8bf3a1790 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 1.4.1
+ */
+ def maxHeaderListSize: Int
+
+ /**
+ * @since 1.4.1
+ */
+ 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 1.4.1
+ */
+ def maxHeaderListSize: Int
+
+ /**
+ * @since 1.4.1
+ */
+ 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 cd3f459be..95892435e 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
@@ -17,6 +17,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
@@ -191,6 +192,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 b05334c5d..57579b500 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
@@ -54,7 +54,7 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer
with Inside with Insp
RequestParsing.parseRequest(headerParser, serverSettings, attributes)
try Source.single(frame)
- .via(new HeaderDecompression(headerParser, parserSettings))
+ .via(new HeaderDecompression(headerParser, parserSettings,
serverSettings.http2Settings.maxHeaderListSize))
.map { // emulate demux
case headers: ParsedHeadersFrame =>
Http2SubStream(
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]