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:
drewhk 2015-08-14 11:43:46 +02:00
commit 343d64050d
5 changed files with 154 additions and 14 deletions

View file

@ -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

View file

@ -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")
}

View file

@ -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")
}

View file

@ -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

View file

@ -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)())