Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
! http: add CONNECT method and support for custom HTTP methods, closes
Browse files Browse the repository at this point in the history
…#428

We don't want to add even even more (and comparatively rarely used) predefined methods in order to avoid namespace pollution (an `import HttpMethods._` is probably quite frequent).

The breaking change comes from a naming cleanup:
`HttpMethod::entityAccepted` is now named `HttpMethod::isEntityAccepted` for consistency with the other boolean members.
  • Loading branch information
sirthias committed Sep 25, 2013
1 parent 8f941d5 commit 5d78dae
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 44 deletions.
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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") ===
Expand All @@ -211,6 +222,7 @@ class RequestParserSpec extends Specification {
parse(parser)("rest") === ("rest", "", "", false)
()
}

"request end" in {
val parser = newParser
parse(parser)(start(25) + "rest")
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
}

Expand Down
Expand Up @@ -16,6 +16,7 @@

package spray.can.parsing

import java.lang.{ StringBuilder JStringBuilder }
import scala.annotation.tailrec
import akka.util.CompactByteString
import spray.http._
Expand Down Expand Up @@ -44,29 +45,41 @@ 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)
case 'P' byteChar(input, 1) match {
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()
}
}

Expand Down
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion spray-http/src/main/scala/spray/http/HttpEncoding.scala
Expand Up @@ -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
Expand Down
31 changes: 20 additions & 11 deletions spray-http/src/main/scala/spray/http/HttpMethod.scala
Expand Up @@ -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.##

Expand All @@ -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
}

0 comments on commit 5d78dae

Please sign in to comment.