Merge pull request #18049 from spray/w/16472-max-content-size-for-chunked
=htc #16472 apply max-content-length setting to Chunked and CloseDelimited entities as well
This commit is contained in:
commit
343d64050d
5 changed files with 154 additions and 14 deletions
|
|
@ -183,7 +183,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
|
|||
} else continue(input, bodyStart)(parseFixedLengthBody(remainingBodyBytes, isLastMessage))
|
||||
}
|
||||
|
||||
def parseChunk(input: ByteString, offset: Int, isLastMessage: Boolean): StateResult = {
|
||||
def parseChunk(input: ByteString, offset: Int, isLastMessage: Boolean, totalBytesRead: Long): StateResult = {
|
||||
@tailrec def parseTrailer(extension: String, lineStart: Int, headers: List[HttpHeader] = Nil,
|
||||
headerCount: Int = 0): StateResult = {
|
||||
var errorInfo: ErrorInfo = null
|
||||
|
|
@ -208,11 +208,13 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
|
|||
}
|
||||
|
||||
def parseChunkBody(chunkSize: Int, extension: String, cursor: Int): StateResult =
|
||||
if (chunkSize > 0) {
|
||||
if (totalBytesRead + chunkSize > settings.maxContentLength)
|
||||
failWithChunkedEntityTooLong(totalBytesRead + chunkSize)
|
||||
else if (chunkSize > 0) {
|
||||
val chunkBodyEnd = cursor + chunkSize
|
||||
def result(terminatorLen: Int) = {
|
||||
emit(EntityChunk(HttpEntity.Chunk(input.slice(cursor, chunkBodyEnd).compact, extension)))
|
||||
Trampoline(_ ⇒ parseChunk(input, chunkBodyEnd + terminatorLen, isLastMessage))
|
||||
Trampoline(_ ⇒ parseChunk(input, chunkBodyEnd + terminatorLen, isLastMessage, totalBytesRead + chunkSize))
|
||||
}
|
||||
byteChar(input, chunkBodyEnd) match {
|
||||
case '\r' if byteChar(input, chunkBodyEnd + 1) == '\n' ⇒ result(2)
|
||||
|
|
@ -243,7 +245,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
|
|||
|
||||
try parseSize(offset, 0)
|
||||
catch {
|
||||
case NotEnoughDataException ⇒ continue(input, offset)(parseChunk(_, _, isLastMessage))
|
||||
case NotEnoughDataException ⇒ continue(input, offset)(parseChunk(_, _, isLastMessage, totalBytesRead))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +285,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
|
|||
setCompletionHandling(CompletionOk)
|
||||
terminate()
|
||||
}
|
||||
def failWithChunkedEntityTooLong(totalBytesRead: Long): StateResult
|
||||
|
||||
def terminate(): StateResult = {
|
||||
terminated = true
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ private[http] class HttpRequestParser(_settings: ParserSettings,
|
|||
}
|
||||
if (contentLength > maxContentLength)
|
||||
failMessageStart(RequestEntityTooLarge,
|
||||
s"Request Content-Length $contentLength exceeds the configured limit of $maxContentLength")
|
||||
summary = s"Request Content-Length of $contentLength bytes exceeds the configured limit of $maxContentLength bytes",
|
||||
detail = "Consider increasing the value of akka.http.server.parsing.max-content-length")
|
||||
else if (contentLength == 0) {
|
||||
emitRequestStart(emptyEntity(cth))
|
||||
setCompletionHandling(HttpMessageParser.CompletionOk)
|
||||
|
|
@ -187,10 +188,16 @@ private[http] class HttpRequestParser(_settings: ParserSettings,
|
|||
if (te.isChunked) {
|
||||
if (clh.isEmpty) {
|
||||
emitRequestStart(chunkedEntity(cth, expect100continueHandling), completedHeaders)
|
||||
parseChunk(input, bodyStart, closeAfterResponseCompletion)
|
||||
parseChunk(input, bodyStart, closeAfterResponseCompletion, totalBytesRead = 0L)
|
||||
} else failMessageStart("A chunked request must not contain a Content-Length header.")
|
||||
} else parseEntity(completedHeaders, protocol, input, bodyStart, clh, cth, teh = None,
|
||||
expect100continue, hostHeaderPresent, closeAfterResponseCompletion)
|
||||
}
|
||||
} else failMessageStart("Request is missing required `Host` header")
|
||||
|
||||
def failWithChunkedEntityTooLong(totalBytesRead: Long): StateResult =
|
||||
failEntityStream(
|
||||
summary = s"Aggregated data length of chunked request entity of $totalBytesRead " +
|
||||
s"bytes exceeds the configured limit of $maxContentLength bytes",
|
||||
detail = "Consider increasing the value of akka.http.server.parsing.max-content-length")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser:
|
|||
case None ⇒ clh match {
|
||||
case Some(`Content-Length`(contentLength)) ⇒
|
||||
if (contentLength > maxContentLength)
|
||||
failMessageStart(s"Response Content-Length $contentLength exceeds the configured limit of $maxContentLength")
|
||||
failMessageStart(
|
||||
summary = s"Response Content-Length of $contentLength bytes exceeds the configured limit of $maxContentLength bytes",
|
||||
detail = "Consider increasing the value of akka.http.client.parsing.max-content-length")
|
||||
else if (contentLength == 0) finishEmptyResponse()
|
||||
else if (contentLength < input.size - bodyStart) {
|
||||
val cl = contentLength.toInt
|
||||
|
|
@ -113,7 +115,7 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser:
|
|||
HttpEntity.CloseDelimited(contentType(cth), data)
|
||||
}
|
||||
setCompletionHandling(HttpMessageParser.CompletionOk)
|
||||
parseToCloseBody(input, bodyStart)
|
||||
parseToCloseBody(input, bodyStart, totalBytesRead = 0)
|
||||
}
|
||||
|
||||
case Some(te) ⇒
|
||||
|
|
@ -121,7 +123,7 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser:
|
|||
if (te.isChunked) {
|
||||
if (clh.isEmpty) {
|
||||
emitResponseStart(chunkedEntity(cth), completedHeaders)
|
||||
parseChunk(input, bodyStart, closeAfterResponseCompletion)
|
||||
parseChunk(input, bodyStart, closeAfterResponseCompletion, totalBytesRead = 0L)
|
||||
} else failMessageStart("A chunked response must not contain a Content-Length header.")
|
||||
} else parseEntity(completedHeaders, protocol, input, bodyStart, clh, cth, teh = None,
|
||||
expect100continue, hostHeaderPresent, closeAfterResponseCompletion)
|
||||
|
|
@ -130,9 +132,24 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser:
|
|||
}
|
||||
|
||||
// currently we do not check for `settings.maxContentLength` overflow
|
||||
def parseToCloseBody(input: ByteString, bodyStart: Int): StateResult = {
|
||||
if (input.length > bodyStart)
|
||||
emit(EntityPart(input.drop(bodyStart).compact))
|
||||
continue(parseToCloseBody)
|
||||
def parseToCloseBody(input: ByteString, bodyStart: Int, totalBytesRead: Long): StateResult = {
|
||||
val newTotalBytes = totalBytesRead + math.max(0, input.length - bodyStart)
|
||||
if (newTotalBytes > settings.maxContentLength)
|
||||
failEntityStream(
|
||||
summary = s"Aggregated data length of close-delimited response entity of $newTotalBytes " +
|
||||
s"bytes exceeds the configured limit of $maxContentLength bytes",
|
||||
detail = "Consider increasing the value of akka.http.client.parsing.max-content-length")
|
||||
else {
|
||||
if (input.length > bodyStart)
|
||||
emit(EntityPart(input.drop(bodyStart).compact))
|
||||
continue(parseToCloseBody(_, _, newTotalBytes))
|
||||
}
|
||||
}
|
||||
|
||||
def failWithChunkedEntityTooLong(totalBytesRead: Long): StateResult =
|
||||
failEntityStream(
|
||||
summary = s"Aggregated data length of chunked response entity of $totalBytesRead " +
|
||||
s"bytes exceeds the configured limit of $maxContentLength bytes",
|
||||
detail = "Consider increasing the value of akka.http.client.parsing.max-content-length")
|
||||
|
||||
}
|
||||
|
|
@ -400,6 +400,54 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
|""" should parseToError(400: StatusCode, ErrorInfo("`Content-Length` header value must not exceed 63-bit integer range"))
|
||||
}
|
||||
|
||||
"with entity length > max-content-length" - {
|
||||
"for Default entity" in new Test {
|
||||
"""PUT /resource/yes HTTP/1.1
|
||||
|Content-length: 101
|
||||
|Host: x
|
||||
|
|
||||
|""" should parseToError(413: StatusCode,
|
||||
ErrorInfo("Request Content-Length of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.server.parsing.max-content-length"))
|
||||
|
||||
override protected def parserSettings: ParserSettings = super.parserSettings.copy(maxContentLength = 100)
|
||||
}
|
||||
|
||||
"for Chunked entity" in new Test {
|
||||
def request(dataElements: ByteString*) = HttpRequest(PUT, "/", List(Host("x")),
|
||||
HttpEntity.Chunked(`application/octet-stream`, source(dataElements.map(ChunkStreamPart(_)): _*)))
|
||||
|
||||
Seq(
|
||||
"""PUT / HTTP/1.1
|
||||
|Transfer-Encoding: chunked
|
||||
|Host: x
|
||||
|
|
||||
|65
|
||||
|abc""") should generalMultiParseTo(Right(request()),
|
||||
Left(
|
||||
EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of chunked request entity of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.server.parsing.max-content-length"))))
|
||||
|
||||
Seq(
|
||||
"""PUT / HTTP/1.1
|
||||
|Transfer-Encoding: chunked
|
||||
|Host: x
|
||||
|
|
||||
|1
|
||||
|a
|
||||
|""",
|
||||
"""64
|
||||
|a""") should generalMultiParseTo(Right(request(ByteString("a"))),
|
||||
Left(EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of chunked request entity of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.server.parsing.max-content-length"))))
|
||||
|
||||
override protected def parserSettings: ParserSettings = super.parserSettings.copy(maxContentLength =
|
||||
100)
|
||||
}
|
||||
}
|
||||
|
||||
"with an illegal entity" in new Test {
|
||||
"""HEAD /resource/yes HTTP/1.1
|
||||
|Content-length: 3
|
||||
|
|
|
|||
|
|
@ -215,6 +215,71 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
Seq("HTTP/1.1 204 12345678", "90123456789012\r\n") should generalMultiParseTo(Left(
|
||||
MessageStartError(400: StatusCode, ErrorInfo("Response reason phrase exceeds the configured limit of 21 characters"))))
|
||||
}
|
||||
|
||||
"with entity length > max-content-length" - {
|
||||
def response(dataElements: ByteString*) = HttpResponse(200, Nil,
|
||||
HttpEntity.Chunked(`application/octet-stream`, Source(dataElements.map(ChunkStreamPart(_)).toVector)))
|
||||
|
||||
"for Default entity" in new Test {
|
||||
Seq("""HTTP/1.1 200 OK
|
||||
|Content-length: 101
|
||||
|
|
||||
|""") should generalMultiParseTo(Left(
|
||||
MessageStartError(400: StatusCode,
|
||||
ErrorInfo(
|
||||
"Response Content-Length of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.client.parsing.max-content-length"))))
|
||||
|
||||
override protected def parserSettings: ParserSettings = super.parserSettings.copy(maxContentLength = 100)
|
||||
}
|
||||
|
||||
"for CloseDelimited entity" in new Test {
|
||||
Seq(
|
||||
"""HTTP/1.1 200 OK
|
||||
|
|
||||
|abcdef""") should generalMultiParseTo(Right(response()),
|
||||
Left(EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of close-delimited response entity of 6 bytes exceeds the configured limit of 5 bytes",
|
||||
"Consider increasing the value of akka.http.client.parsing.max-content-length"))))
|
||||
|
||||
Seq(
|
||||
"""HTTP/1.1 200 OK
|
||||
|
|
||||
|a""", "bcdef") should generalMultiParseTo(Right(response(ByteString("a"))),
|
||||
Left(EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of close-delimited response entity of 6 bytes exceeds the configured limit of 5 bytes",
|
||||
"Consider increasing the value of akka.http.client.parsing.max-content-length"))))
|
||||
|
||||
override protected def parserSettings: ParserSettings = super.parserSettings.copy(maxContentLength = 5)
|
||||
}
|
||||
|
||||
"for Chunked entity" in new Test {
|
||||
Seq(
|
||||
"""HTTP/1.1 200 OK
|
||||
|Transfer-Encoding: chunked
|
||||
|
|
||||
|65
|
||||
|abc""") should generalMultiParseTo(Right(response()),
|
||||
Left(EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of chunked response entity of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.client.parsing.max-content-length"))))
|
||||
|
||||
Seq(
|
||||
"""HTTP/1.1 200 OK
|
||||
|Transfer-Encoding: chunked
|
||||
|
|
||||
|1
|
||||
|a
|
||||
|""",
|
||||
"""64
|
||||
|a""") should generalMultiParseTo(Right(response(ByteString("a"))),
|
||||
Left(EntityStreamError(
|
||||
ErrorInfo("Aggregated data length of chunked response entity of 101 bytes exceeds the configured limit of 100 bytes",
|
||||
"Consider increasing the value of akka.http.client.parsing.max-content-length"))))
|
||||
|
||||
override protected def parserSettings: ParserSettings = super.parserSettings.copy(maxContentLength = 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,7 +349,7 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
Await.result(future, 500.millis)
|
||||
}
|
||||
|
||||
def parserSettings: ParserSettings = ParserSettings(system)
|
||||
protected def parserSettings: ParserSettings = ParserSettings(system)
|
||||
|
||||
def newParserStage(requestMethod: HttpMethod = GET) = {
|
||||
val parser = new HttpResponseParser(parserSettings, HttpHeaderParser(parserSettings)())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue