diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala index b2d440a172..9aa3dbde5d 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala @@ -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 diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpRequestParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpRequestParser.scala index 7afccab3cb..17511784c9 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpRequestParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpRequestParser.scala @@ -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") } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpResponseParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpResponseParser.scala index 4751f68123..23dbc2a3c5 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpResponseParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpResponseParser.scala @@ -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") + } \ No newline at end of file diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala index 3a7a4efe89..c04c068c79 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala @@ -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 diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala index dfee0ba651..5d35284a43 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala @@ -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)())