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 004d94bc49..1c65e7a93f 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 @@ -34,8 +34,7 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser: if (requestMethodForCurrentResponse.isDefined) { var cursor = parseProtocol(input, offset) if (byteChar(input, cursor) == ' ') { - cursor = parseStatusCode(input, cursor + 1) - cursor = parseReason(input, cursor)() + cursor = parseStatus(input, cursor + 1) parseHeaderLines(input, cursor) } else badProtocol } else { @@ -50,13 +49,13 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser: def badProtocol = throw new ParsingException("The server-side HTTP version is not supported") - def parseStatusCode(input: ByteString, cursor: Int): Int = { + def parseStatus(input: ByteString, cursor: Int): Int = { def badStatusCode = throw new ParsingException("Illegal response status code") - def intValue(offset: Int): Int = { - val c = byteChar(input, cursor + offset) - if (CharacterClasses.DIGIT(c)) c - '0' else badStatusCode - } - if (byteChar(input, cursor + 3) == ' ') { + def parseStatusCode() = { + def intValue(offset: Int): Int = { + val c = byteChar(input, cursor + offset) + if (CharacterClasses.DIGIT(c)) c - '0' else badStatusCode + } val code = intValue(0) * 100 + intValue(1) * 10 + intValue(2) statusCode = code match { case 200 ⇒ StatusCodes.OK @@ -65,17 +64,22 @@ private[http] class HttpResponseParser(_settings: ParserSettings, _headerParser: case None ⇒ customStatusCodes(code) getOrElse badStatusCode } } - cursor + 4 + } + if (byteChar(input, cursor + 3) == ' ') { + parseStatusCode() + val startIdx = cursor + 4 + @tailrec def skipReason(idx: Int): Int = + if (idx - startIdx <= maxResponseReasonLength) + if (byteChar(input, idx) == '\r' && byteChar(input, idx + 1) == '\n') idx + 2 + else skipReason(idx + 1) + else throw new ParsingException("Response reason phrase exceeds the configured limit of " + + maxResponseReasonLength + " characters") + skipReason(startIdx) + } else if (byteChar(input, cursor + 3) == '\r' && byteChar(input, cursor + 4) == '\n') { + throw new ParsingException("Status code misses trailing space") } else badStatusCode } - @tailrec private def parseReason(input: ByteString, startIx: Int)(cursor: Int = startIx): Int = - if (cursor - startIx <= maxResponseReasonLength) - if (byteChar(input, cursor) == '\r' && byteChar(input, cursor + 1) == '\n') cursor + 2 - else parseReason(input, startIx)(cursor + 1) - else throw new ParsingException("Response reason phrase exceeds the configured limit of " + - maxResponseReasonLength + " characters") - // http://tools.ietf.org/html/rfc7230#section-3.3 def parseEntity(headers: List[HttpHeader], protocol: HttpProtocol, input: ByteString, bodyStart: Int, clh: Option[`Content-Length`], cth: Option[`Content-Type`], teh: Option[`Transfer-Encoding`], 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 59de26aae0..bd4adfded5 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 @@ -14,7 +14,6 @@ import org.scalatest.matchers.Matcher import akka.util.ByteString import akka.actor.ActorSystem import akka.stream.scaladsl._ -import akka.stream.scaladsl.FlattenStrategy import akka.stream.ActorMaterializer import akka.http.scaladsl.util.FastFuture._ import akka.http.impl.util._ @@ -79,6 +78,11 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { closeAfterResponseCompletion shouldEqual Seq(false) } + "a response with a missing reason phrase" in new Test { + "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n" should parseTo(HttpResponse(OK)) + closeAfterResponseCompletion shouldEqual Seq(false) + } + "a response funky `Transfer-Encoding` header" in new Test { override def parserSettings: ParserSettings = super.parserSettings.withCustomStatusCodes(ServerOnTheMove) @@ -227,6 +231,11 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { MessageStartError(400: StatusCode, ErrorInfo("Response reason phrase exceeds the configured limit of 21 characters")))) } + "with a missing reason phrase and no trailing space" in new Test { + Seq("HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n") should generalMultiParseTo(Left(MessageStartError( + 400: StatusCode, ErrorInfo("Status code misses trailing space")))) + } + "with entity length > max-content-length" - { def response(dataElements: ByteString*) = HttpResponse(200, Nil, HttpEntity.Chunked(`application/octet-stream`, Source(dataElements.map(ChunkStreamPart(_)).toVector)))