diff --git a/spray-can-tests/src/test/scala/spray/can/parsing/RequestParserSpec.scala b/spray-can-tests/src/test/scala/spray/can/parsing/RequestParserSpec.scala index 11510bcb9c..648f1ab0f9 100644 --- a/spray-can-tests/src/test/scala/spray/can/parsing/RequestParserSpec.scala +++ b/spray-can-tests/src/test/scala/spray/can/parsing/RequestParserSpec.scala @@ -37,6 +37,7 @@ class RequestParserSpec extends Specification { spray.can.parsing.max-content-length = 4000000000 spray.can.parsing.incoming-auto-chunking-threshold-size = 20""") val system = ActorSystem(Utils.actorSystemNameFrom(getClass), testConf) + val BOLT = HttpMethods.register(HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true)) "The request parsing logic" should { "properly parse a request" in { @@ -118,15 +119,24 @@ class RequestParserSpec extends Specification { parse(parser)("DEFGH") === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(4)), "ABCD", "EFGH", false) } - "reject requests with Content-Length > Int.MaxSize" in { + + "with a custom HTTP method" in { + parse { + """BOLT / HTTP/1.0 + | + |""" + } === (BOLT, Uri("/"), `HTTP/1.0`, Nil, "", "", true) + } + + "with Content-Length > Int.MaxSize if autochunking is enabled" in { val request = """PUT /resource/yes HTTP/1.1 |Content-length: 2147483649 |Host: x | |""" - val parser = new HttpRequestPartParser(ParserSettings(system).copy(autoChunkingThreshold = Long.MaxValue))() - parse(parser)(request) === (400: StatusCode, "Content-Length > Int.MaxSize not supported for non-(auto)-chunked requests") + val parser = newParser + parse(parser)(request) === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(2147483649L)), "", false) } } @@ -196,6 +206,7 @@ class RequestParserSpec extends Specification { (GET, Uri("/data"), `HTTP/1.1`, List(`Content-Length`(25), Host("ping"), `Content-Type`(`application/pdf`)), "rest", false) } + "request start if complete message is already available" in { val parser = newParser parse(parser)(start(25) + "rest1rest2rest3rest4rest5") === @@ -211,6 +222,7 @@ class RequestParserSpec extends Specification { parse(parser)("rest") === ("rest", "", "", false) () } + "request end" in { val parser = newParser parse(parser)(start(25) + "rest") @@ -223,27 +235,6 @@ class RequestParserSpec extends Specification { parse(parser)("\0GET /data HTTP/1.1") === ("", Nil, "GET /data HTTP/1.1", false) // next parse run produced end parse(parser)("GET /data HTTP/1.1") === Result.NeedMoreData // start of next request } - "don't reject requests with Content-Length > Int.MaxSize" in { - val request = - """PUT /resource/yes HTTP/1.1 - |Content-length: 2147483649 - |Host: x - | - |""" - val parser = newParser - parse(parser)(request) === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(2147483649L)), "", false) - } - "reject requests with Content-Length > Long.MaxSize" in { - // content-length = (Long.MaxValue + 1) * 10, which is 0 when calculated overflow - val request = - """PUT /resource/yes HTTP/1.1 - |Content-length: 92233720368547758080 - |Host: x - | - |""" - val parser = newParser - parse(parser)(request) === (400: StatusCode, "Illegal `Content-Length` header value") - } } "reject a message chunk with" in { @@ -294,8 +285,8 @@ class RequestParserSpec extends Specification { "reject a request with" in { "an illegal HTTP method" in { - parse("get") === (NotImplemented, "Unsupported HTTP method") - parse("GETX") === (NotImplemented, "Unsupported HTTP method") + parse("get ") === (NotImplemented, "Unsupported HTTP method: get") + parse("GETX ") === (NotImplemented, "Unsupported HTTP method: GETX") } "two Content-Length headers" in { @@ -346,6 +337,29 @@ class RequestParserSpec extends Specification { |abc""" } === (BadRequest, "Illegal `Content-Length` header value") } + + "with Content-Length > Int.MaxSize if autochunking is disabled" in { + val request = + """PUT /resource/yes HTTP/1.1 + |Content-length: 2147483649 + |Host: x + | + |""" + val parser = new HttpRequestPartParser(ParserSettings(system).copy(autoChunkingThreshold = Long.MaxValue))() + parse(parser)(request) === (400: StatusCode, "Content-Length > Int.MaxSize not supported for non-(auto)-chunked requests") + } + + "with Content-Length > Long.MaxSize" in { + // content-length = (Long.MaxValue + 1) * 10, which is 0 when calculated overflow + val request = + """PUT /resource/yes HTTP/1.1 + |Content-length: 92233720368547758080 + |Host: x + | + |""" + val parser = newParser + parse(parser)(request) === (400: StatusCode, "Illegal `Content-Length` header value") + } } } diff --git a/spray-can/src/main/scala/spray/can/parsing/HttpRequestPartParser.scala b/spray-can/src/main/scala/spray/can/parsing/HttpRequestPartParser.scala index 24fb42e878..f7663145e8 100644 --- a/spray-can/src/main/scala/spray/can/parsing/HttpRequestPartParser.scala +++ b/spray-can/src/main/scala/spray/can/parsing/HttpRequestPartParser.scala @@ -16,6 +16,7 @@ package spray.can.parsing +import java.lang.{ StringBuilder ⇒ JStringBuilder } import scala.annotation.tailrec import akka.util.CompactByteString import spray.http._ @@ -44,15 +45,26 @@ class HttpRequestPartParser(_settings: ParserSettings)(_headerParser: HttpHeader } def parseMethod(input: CompactByteString): Int = { - def badMethod = throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method")) + @tailrec def parseCustomMethod(ix: Int = 0, sb: JStringBuilder = new JStringBuilder(16)): Int = + if (ix < 16) { // hard-coded maximum custom method length + byteChar(input, ix) match { + case ' ' ⇒ + HttpMethods.getForKey(sb.toString) match { + case Some(m) ⇒ method = m; ix + 1 + case None ⇒ parseCustomMethod(Int.MaxValue, sb) + } + case c ⇒ parseCustomMethod(ix + 1, sb.append(c)) + } + } else throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString)) + @tailrec def parseMethod(meth: HttpMethod, ix: Int = 1): Int = if (ix == meth.value.length) if (byteChar(input, ix) == ' ') { method = meth ix + 1 - } else badMethod + } else parseCustomMethod() else if (byteChar(input, ix) == meth.value.charAt(ix)) parseMethod(meth, ix + 1) - else badMethod + else parseCustomMethod() byteChar(input, 0) match { case 'G' ⇒ parseMethod(GET) @@ -60,13 +72,14 @@ class HttpRequestPartParser(_settings: ParserSettings)(_headerParser: HttpHeader case 'O' ⇒ parseMethod(POST, 2) case 'U' ⇒ parseMethod(PUT, 2) case 'A' ⇒ parseMethod(PATCH, 2) - case _ ⇒ badMethod + case _ ⇒ parseCustomMethod() } case 'D' ⇒ parseMethod(DELETE) case 'H' ⇒ parseMethod(HEAD) case 'O' ⇒ parseMethod(OPTIONS) case 'T' ⇒ parseMethod(TRACE) - case _ ⇒ badMethod + case 'C' ⇒ parseMethod(CONNECT) + case _ ⇒ parseCustomMethod() } } diff --git a/spray-can/src/main/scala/spray/can/rendering/RequestRenderingComponent.scala b/spray-can/src/main/scala/spray/can/rendering/RequestRenderingComponent.scala index c9c4372d62..8538d6db41 100644 --- a/spray-can/src/main/scala/spray/can/rendering/RequestRenderingComponent.scala +++ b/spray-can/src/main/scala/spray/can/rendering/RequestRenderingComponent.scala @@ -63,7 +63,7 @@ trait RequestRenderingComponent { def renderRequest(request: HttpRequest): Unit = { renderRequestStart(request) val bodyLength = request.entity.data.length - if (bodyLength > 0 || request.method.entityAccepted) r ~~ `Content-Length` ~~ bodyLength ~~ CrLf + if (bodyLength > 0 || request.method.isEntityAccepted) r ~~ `Content-Length` ~~ bodyLength ~~ CrLf r ~~ CrLf ~~ request.entity.data } diff --git a/spray-http/src/main/scala/spray/http/HttpEncoding.scala b/spray-http/src/main/scala/spray/http/HttpEncoding.scala index 7d96321d5a..b7f8e7172e 100644 --- a/spray-http/src/main/scala/spray/http/HttpEncoding.scala +++ b/spray-http/src/main/scala/spray/http/HttpEncoding.scala @@ -25,7 +25,7 @@ case class HttpEncoding private[http] (value: String) extends HttpEncodingRange } object HttpEncoding { - def custom(value: String) = apply(value) + def custom(value: String): HttpEncoding = apply(value) } // see http://www.iana.org/assignments/http-parameters/http-parameters.xml diff --git a/spray-http/src/main/scala/spray/http/HttpMethod.scala b/spray-http/src/main/scala/spray/http/HttpMethod.scala index 1733c842ae..a75624d054 100644 --- a/spray-http/src/main/scala/spray/http/HttpMethod.scala +++ b/spray-http/src/main/scala/spray/http/HttpMethod.scala @@ -19,12 +19,12 @@ package spray.http /** * @param isSafe true if the resource should not be altered on the server * @param isIdempotent true if requests can be safely (& automatically) repeated - * @param entityAccepted true if meaning of request entities is properly defined + * @param isEntityAccepted true if meaning of request entities is properly defined */ case class HttpMethod private[http] (value: String, isSafe: Boolean, isIdempotent: Boolean, - entityAccepted: Boolean) extends LazyValueBytesRenderable { + isEntityAccepted: Boolean) extends LazyValueBytesRenderable { // for faster equality checks we use the hashcode of the method name (and make sure it's distinct during registration) private[http] val fingerprint = value.## @@ -38,20 +38,29 @@ case class HttpMethod private[http] (value: String, } } +object HttpMethod { + def custom(value: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = { + require(value.nonEmpty, "value must be non-empty") + require(!safe || idempotent, "An HTTP method cannot be safe without being idempotent") + apply(value, safe, idempotent, entityAccepted) + } +} + object HttpMethods extends ObjectRegistry[String, HttpMethod] { - private def register(method: HttpMethod): HttpMethod = { + def register(method: HttpMethod): HttpMethod = { registry.values foreach { m ⇒ if (m.fingerprint == method.fingerprint) sys.error("Method fingerprint collision") } register(method.value, method) } // format: OFF - val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , entityAccepted = false)) - val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , entityAccepted = false)) - val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , entityAccepted = false)) - val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , entityAccepted = true)) - val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, entityAccepted = true)) - val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, entityAccepted = true)) - val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , entityAccepted = true)) - val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , entityAccepted = false)) + val CONNECT = register(HttpMethod("CONNECT", isSafe = false, isIdempotent = false, isEntityAccepted = false)) + val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , isEntityAccepted = false)) + val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) + val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) + val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , isEntityAccepted = true)) + val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, isEntityAccepted = true)) + val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, isEntityAccepted = true)) + val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , isEntityAccepted = true)) + val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) // format: ON }