Permalink
Browse files

fix spray-can version

  • Loading branch information...
1 parent c8b524c commit a63d1e61ee03dd864ac571355727f75f8102ae18 @dukeboard committed Nov 8, 2011
Showing with 189 additions and 155 deletions.
  1. +7 −2 org.kevoree.extra.macwidgets/pom.xml
  2. +1 −1 org.kevoree.extra.spray/pom.xml
  3. +2 −2 org.kevoree.extra.spray/server-example/src/main/resources/akka.conf
  4. +1 −1 org.kevoree.extra.spray/server-example/src/main/scala/cc/spray/can/example/Main.scala
  5. +2 −6 org.kevoree.extra.spray/server-example/src/main/scala/cc/spray/can/example/TestService.scala
  6. +0 −16 org.kevoree.extra.spray/server-example/src/main/scala/cc/spray/can/example/akka.conf
  7. +0 −25 org.kevoree.extra.spray/server-example/src/main/scala/cc/spray/can/example/logback.xml
  8. +16 −0 org.kevoree.extra.spray/spray-can/notes/0.9.0.markdown
  9. +9 −0 org.kevoree.extra.spray/spray-can/notes/0.9.1.markdown
  10. +2 −0 org.kevoree.extra.spray/spray-can/notes/about.markdown
  11. +1 −2 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/DateTime.scala
  12. +5 −1 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/HttpClient.scala
  13. +1 −1 ...ray/spray-can/src/main/scala/cc/spray/can/{HighLevelHttpClient.scala → HttpDialogComponent.scala}
  14. +12 −8 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/HttpPeer.scala
  15. +5 −3 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/HttpServer.scala
  16. +20 −25 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/MessageParser.scala
  17. +27 −21 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/MessagePreparer.scala
  18. +7 −7 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/ServerConfig.scala
  19. +2 −5 org.kevoree.extra.spray/spray-can/src/main/scala/cc/spray/can/package.scala
  20. +1 −1 org.kevoree.extra.spray/spray-can/src/test/resources/logback.xml
  21. +11 −0 org.kevoree.extra.spray/spray-can/src/test/scala/cc/spray/can/HttpClientServerSpec.scala
  22. +21 −12 org.kevoree.extra.spray/spray-can/src/test/scala/cc/spray/can/RequestParserSpec.scala
  23. +36 −16 org.kevoree.extra.spray/spray-can/src/test/scala/cc/spray/can/ResponsePreparerSpec.scala
@@ -1,6 +1,5 @@
-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.kevoree.extra</groupId>
<artifactId>org.kevoree.extra.macwidgets</artifactId>
@@ -23,6 +22,12 @@
<artifactId>maven-bundle-plugin</artifactId>
<version>2.3.5</version>
<extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>com.explodingpixels.*,com.jgoodies.forms.*</Export-Package>
+ <Import-Package>!com.explodingpixels.*,!com.jgoodies.forms.*,*</Import-Package>
+ </instructions>
+ </configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -12,7 +12,7 @@
<artifactId>org.kevoree.extra.spray</artifactId>
<packaging>bundle</packaging>
- <version>0.9.0</version>
+ <version>0.9.1</version>
<name>Kevoree :: Extra :: Spray</name>
<dependencies>
@@ -9,8 +9,8 @@ akka {
spray-can {
server {
port = 8080
- service-actor-id = "spray-root-service"
- timeout-actor-id = "spray-root-service" # we want to handle timeouts with the same service actor
+ service-actor-id = "test-endpoint"
+ timeout-actor-id = "test-endpoint" # we want to handle timeouts with the same service actor
request-timeout = 2000 # require all requests to be completed within 2 seconds
}
}
@@ -28,7 +28,7 @@ object Main extends App {
SupervisorConfig(
OneForOneStrategy(List(classOf[Exception]), 3, 100),
List(
- Supervise(Actor.actorOf(new TestService("spray-root-service")), Permanent),
+ Supervise(Actor.actorOf(new TestService("test-endpoint")), Permanent),
Supervise(Actor.actorOf(new HttpServer()), Permanent)
)
)
@@ -21,18 +21,14 @@ import org.slf4j.LoggerFactory
import HttpMethods._
import java.util.concurrent.TimeUnit
import akka.actor.{PoisonPill, Scheduler, Kill, Actor}
-import java.net.URL
class TestService(id: String) extends Actor {
val log = LoggerFactory.getLogger(getClass)
self.id = id
protected def receive = {
- case RequestContext(HttpRequest(GET, "/?myName=dididi", headers, body, _), _, responder) =>
-
-
-
+ case RequestContext(HttpRequest(GET, "/", _, _, _), _, responder) =>
responder.complete(index)
case RequestContext(HttpRequest(GET, "/ping", _, _, _), _, responder) =>
@@ -81,7 +77,7 @@ class TestService(id: String) extends Actor {
case RequestContext(HttpRequest(_, _, _, _, _), _, responder) =>
responder.complete(response("Unknown resource!", 404))
- case Timeout(method, uri, _, headers, _, complete) => complete {
+ case Timeout(method, uri, _, _, _, complete) => complete {
HttpResponse(status = 500).withBody("The " + method + " request to '" + uri + "' has timed out...")
}
}
@@ -1,16 +0,0 @@
-# akka config
-akka {
- version = "1.2" # Akka version, checked against the runtime version of Akka.
- event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
- event-handler-level = "DEBUG" # Options: ERROR, WARNING, INFO, DEBUG
-}
-
-# spray-can config
-spray-can {
- server {
- port = 8080
- service-actor-id = "spray-root-service"
- timeout-actor-id = "spray-root-service" # we want to handle timeouts with the same service actor
- request-timeout = 2000 # require all requests to be completed within 2 seconds
- }
-}
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<configuration>
-
- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
- <target>System.out</target>
- <encoder>
- <pattern>%date{MM/dd HH:mm:ss.SSS} %-5level[%.15thread] %logger{1} - %msg%n</pattern>
- </encoder>
- </appender>
-
- <!-- <appender name="FILE" class="ch.qos.logback.core.FileAppender">
- <file>akka.log</file>
- <append>false</append>
- <encoder>
- <pattern>%date{MM/dd HH:mm:ss} %-5level[%thread] %logger{1} - %msg%n</pattern>
- </encoder>
- </appender> -->
-
- <logger name="akka" level="INFO" />
-
- <root level="INFO">
- <appender-ref ref="CONSOLE"/>
- </root>
-
-</configuration>
@@ -0,0 +1,16 @@
+[spray-can](http://can.spray.cc) is a low-overhead, high-performance, fully asynchronous HTTP 1.1 server and client library
+implemented entirely in Scala on top of [Akka](http://akka.io).
+
+Both, the _spray-can_ server and the _spray-can_ client, sport the following features:
+
+* Low per-connection overhead for supporting thousands of concurrent connections
+* Efficient message parsing and processing logic for high throughput applications (> 50K requests/sec on ordinary consumer hardware)
+* Full support for HTTP/1.1 persistant connections
+* Full support for message pipelining
+* Full support for asynchronous HTTP streaming (i.e. "chunked" transfer encoding)
+* Akka-Actor and -Future based architecture for easy integration into your Akka applications
+* No dependencies except for JavaSE 6, Scala 2.9 and [Akka] 1.2 (actors module).
+
+This is the first public release.
+
+For more information please visit the project web site at <http://can.spray.cc>.
@@ -0,0 +1,9 @@
+This is a maintenance release:
+
+- Upgrade to [SBT][] 0.11.0
+- Fix #6 (HTTP/1.1 requests without Host header are not rejected)
+- Fix #7 (client: support for terminated-by-close responses broken)
+- Fix incorrect akka.conf keys for client configuration
+- Smaller cleanups
+
+ [SBT]: https://github.com/harrah/xsbt/wiki
@@ -0,0 +1,2 @@
+[spray-can](http://can.spray.cc) is a low-overhead, high-performance, fully asynchronous HTTP 1.1 server and client library
+implemented entirely in Scala on top of [Akka](http://akka.io).
@@ -1,6 +1,5 @@
/*
* Copyright (C) 2011 Mathias Doenitz
- * Based on code copyright (C) 2010-2011 by the BlueEyes Web Framework Team (http://github.com/jdegoes/blueeyes)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +20,7 @@ package cc.spray.can
* Immutable, fast and efficient Date + Time implementation without any dependencies.
* Does not support TimeZones, all DateTime values are always GMT based.
*/
-sealed trait DateTime extends Ordered[DateTime] {
+sealed abstract class DateTime extends Ordered[DateTime] {
/**
* The year.
*/
@@ -33,7 +33,7 @@ import HttpProtocols._
*/
case class Connect(host: String, port: Int = 80)
-object HttpClient extends HighLevelHttpClient {
+object HttpClient extends HttpDialogComponent {
private[can] class RequestMark // a unique object used to mark all parts of one chunked request
private[can] case class Send(
conn: ClientConnection,
@@ -242,6 +242,10 @@ class HttpClient(val config: ClientConfig = ClientConfig.fromAkkaConf) extends H
}
override protected def cleanClose(conn: Conn) {
+ conn.messageParser match {
+ case x: ToCloseBodyParser => handleCompleteMessage(conn, x.complete)
+ case _ =>
+ }
conn.closeAllPendingWithError("Server closed connection")
super.cleanClose(conn)
}
@@ -22,7 +22,7 @@ import org.slf4j.LoggerFactory
import akka.actor.{Actor, Scheduler}
import akka.dispatch.{DefaultCompletableFuture, Future}
-trait HighLevelHttpClient {
+trait HttpDialogComponent {
private lazy val log = LoggerFactory.getLogger(getClass)
/**
@@ -145,15 +145,19 @@ private[can] abstract class HttpPeer(threadName: String) extends Actor {
val conn = key.attachment.asInstanceOf[Conn]
@tailrec def parseReadBuffer() {
- val recurse = conn.messageParser.asInstanceOf[IntermediateParser].read(readBuffer) match {
- case x: IntermediateParser => conn.messageParser = x; false
- case x: CompleteMessageParser => handleCompleteMessage(conn, x); true
- case x: ChunkedStartParser => handleChunkedStart(conn, x); true
- case x: ChunkedChunkParser => handleChunkedChunk(conn, x); true
- case x: ChunkedEndParser => handleChunkedEnd(conn, x); true
- case x: ErrorParser => handleParseError(conn, x); false
+ conn.messageParser match {
+ case x: IntermediateParser =>
+ val recurse = x.read(readBuffer) match {
+ case x: IntermediateParser => conn.messageParser = x; false
+ case x: CompleteMessageParser => handleCompleteMessage(conn, x); true
+ case x: ChunkedStartParser => handleChunkedStart(conn, x); true
+ case x: ChunkedChunkParser => handleChunkedChunk(conn, x); true
+ case x: ChunkedEndParser => handleChunkedEnd(conn, x); true
+ case x: ErrorParser => handleParseError(conn, x); false
+ }
+ if (recurse && readBuffer.remaining > 0) parseReadBuffer()
+ case x: ErrorParser => handleParseError(conn, x)
}
- if (recurse && readBuffer.remaining > 0) parseReadBuffer()
}
protectIO("Read", conn) {
@@ -169,7 +169,7 @@ object HttpServer {
*
* An `HttpServer` also reacts to [[cc.spray.can.GetStats]] messages.
*/
-class HttpServer(val config: ServerConfig = ServerConfig())
+class HttpServer(val config: ServerConfig = ServerConfig.fromAkkaConf)
extends HttpPeer("spray-can-server") with ResponsePreparer {
import HttpServer._
@@ -407,12 +407,14 @@ class HttpServer(val config: ServerConfig = ServerConfig())
HttpResponse.verify(response)
require(response.protocol == `HTTP/1.1`, "Chunked responses must have protocol HTTP/1.1")
require(requestLine.protocol == `HTTP/1.1`, "Cannot reply with a chunked response to an HTTP/1.0 client")
- require(requestLine.method != HttpMethods.HEAD, "HEAD requests must not be answered by chunked responses")
if (mode.compareAndSet(UNCOMPLETED, STREAMING)) {
log.debug("Enqueueing start of chunked response")
val (buffers, close) = prepareChunkedResponseStart(requestLine, response, connectionHeader)
self ! new Respond(conn, buffers, false, responseNr, false, requestRecord)
- new DefaultChunkedResponder(close)
+ if (requestLine.method != HttpMethods.HEAD) new DefaultChunkedResponder(close) else new ChunkedResponder {
+ def sendChunk(chunk: MessageChunk) {}
+ def close(extensions: List[ChunkExtension], trailer: List[HttpHeader]) {}
+ }
} else throw new IllegalStateException {
mode.get match {
case COMPLETED => "The chunked response cannot be started since this request to '" + requestLine.uri + "' has already been completed"
@@ -198,45 +198,40 @@ private[can] class HeaderNameParser(config: MessageParserConfig, messageLine: Me
}
def headersComplete = {
@tailrec def traverse(remaining: List[HttpHeader], connection: Option[String], contentLength: Option[String],
- transferEncoding: Option[String]): MessageParser = {
+ transferEncoding: Option[String], hostHeaderPresent: Boolean): MessageParser = {
if (!remaining.isEmpty) {
remaining.head.name match {
case "Content-Length" =>
if (contentLength.isEmpty) {
- traverse(remaining.tail, connection, Some(remaining.head.value), transferEncoding)
- }
- else {
- ErrorParser("HTTP message must not contain more than one Content-Length header", 400)
- }
- case "Transfer-Encoding" => traverse(remaining.tail, connection, contentLength, Some(remaining.head.value))
- case "Connection" => traverse(remaining.tail, Some(remaining.head.value), contentLength, transferEncoding)
- case _ => traverse(remaining.tail, connection, contentLength, transferEncoding)
+ traverse(remaining.tail, connection, Some(remaining.head.value), transferEncoding, hostHeaderPresent)
+ } else ErrorParser("HTTP message must not contain more than one Content-Length header", 400)
+ case "Transfer-Encoding" => traverse(remaining.tail, connection, contentLength, Some(remaining.head.value), hostHeaderPresent)
+ case "Connection" => traverse(remaining.tail, Some(remaining.head.value), contentLength, transferEncoding, hostHeaderPresent)
+ case "Host" =>
+ if (!hostHeaderPresent) traverse(remaining.tail, connection, contentLength, transferEncoding, true)
+ else ErrorParser("HTTP message must not contain more than one Host header", 400)
+ case _ => traverse(remaining.tail, connection, contentLength, transferEncoding, hostHeaderPresent)
}
- } else {
- // rfc2616 sec. 4.4
- if (messageBodyDisallowed) {
+ } else messageLine match { // rfc2616 sec. 4.4
+ case x: RequestLine if x.protocol == `HTTP/1.1` && !hostHeaderPresent =>
+ ErrorParser("Host header required", 400)
+ case _ if messageBodyDisallowed =>
CompleteMessageParser(messageLine, headers, connection)
- }
- else if (transferEncoding.isDefined && transferEncoding.get != "identity") {
+ case _ if transferEncoding.isDefined && transferEncoding.get != "identity" =>
ChunkedStartParser(messageLine, headers, connection)
- }
- else if (contentLength.isDefined) {
+ case _ if contentLength.isDefined =>
contentLength.get match {
case "0" => CompleteMessageParser(messageLine, headers, connection)
case value => try {new FixedLengthBodyParser(config, messageLine, headers, connection, value.toInt)}
catch {case e: Exception => ErrorParser("Invalid Content-Length header value: " + e.getMessage)}
}
- } else {
- messageLine match {
- case _: RequestLine => CompleteMessageParser(messageLine, headers, connection)
- case x: StatusLine if connection == Some("close") || connection.isEmpty && x.protocol == `HTTP/1.0` =>
- new ToCloseBodyParser(config, messageLine, headers, connection)
- case _ => ErrorParser("Content-Length header or chunked transfer encoding required", 411)
- }
- }
+ case _: RequestLine => CompleteMessageParser(messageLine, headers, connection)
+ case x: StatusLine if connection == Some("close") || connection.isEmpty && x.protocol == `HTTP/1.0` =>
+ new ToCloseBodyParser(config, messageLine, headers, connection)
+ case _ => ErrorParser("Content-Length header or chunked transfer encoding required", 411)
}
}
- traverse(headers, None, None, None)
+ traverse(headers, None, None, None, false)
}
def messageBodyDisallowed = messageLine match {
case _: RequestLine => false // there can always be a body in a request
@@ -95,7 +95,8 @@ private[can] trait ResponsePreparer extends MessagePreparer {
reqConnectionHeader: Option[String]): (List[ByteBuffer], Boolean) = {
import response._
val (sb, close) = prepareResponseStart(requestLine, response, reqConnectionHeader)
- appendHeader("Content-Length", body.length.toString, sb)
+ // don't set a Content-Length header for non-keepalive HTTP/1.0 responses (rely on body end by connection close)
+ if (response.protocol == `HTTP/1.1` || !close) appendHeader("Content-Length", body.length.toString, sb)
appendLine(sb)
val bodyBufs = if (body.length == 0 || requestLine.method == HttpMethods.HEAD) Nil else ByteBuffer.wrap(body) :: Nil
(encode(sb) :: bodyBufs, close)
@@ -107,38 +108,43 @@ private[can] trait ResponsePreparer extends MessagePreparer {
val (sb, close) = prepareResponseStart(requestLine, response, reqConnectionHeader)
appendHeader("Transfer-Encoding", "chunked", sb)
appendLine(sb)
- val bodyBufs = if (body.length == 0) Nil else prepareChunk(Nil, body)
+ val bodyBufs = if (body.length == 0 || requestLine.method == HttpMethods.HEAD) Nil else prepareChunk(Nil, body)
(encode(sb) :: bodyBufs, close)
}
private def prepareResponseStart(requestLine: RequestLine, response: HttpResponse,
reqConnectionHeader: Option[String]) = {
import response._
- def appendConnectionHeader(sb: JStringBuilder)(connectionHeaderValue: Option[String]) = {
- if (connectionHeaderValue.isEmpty) requestLine.protocol match {
- case `HTTP/1.0` =>
- if (reqConnectionHeader.isEmpty || reqConnectionHeader.get != "Keep-Alive") true
- else {
- appendHeader("Connection", "Keep-Alive", sb)
- false
- }
- case `HTTP/1.1` =>
- if (reqConnectionHeader.isDefined && reqConnectionHeader.get == "close") {
- appendHeader("Connection", "close", sb)
- true
- } else false
- } else {
- connectionHeaderValue.get.contains("close")
+ def appendConnectionHeaderIfRequired(connectionHeaderValue: Option[String], sb: JStringBuilder) = {
+ requestLine.protocol match {
+ case `HTTP/1.0` => {
+ if (connectionHeaderValue.isEmpty) {
+ if (reqConnectionHeader.isDefined && reqConnectionHeader.get == "Keep-Alive") {
+ appendHeader("Connection", "Keep-Alive", sb)
+ false
+ } else true
+ } else !connectionHeaderValue.get.contains("Keep-Alive")
+ }
+ case `HTTP/1.1` => {
+ if (connectionHeaderValue.isEmpty) {
+ if (reqConnectionHeader.isDefined && reqConnectionHeader.get == "close") {
+ if (response.protocol == `HTTP/1.1`) appendHeader("Connection", "close", sb)
+ true
+ } else response.protocol == `HTTP/1.0`
+ } else connectionHeaderValue.get.contains("close")
+ }
}
}
val sb = new java.lang.StringBuilder(256)
- if (status == 200) sb.append("HTTP/1.1 200 OK\r\n")
- else appendLine(sb.append("HTTP/1.1 ").append(status).append(' ').append(HttpResponse.defaultReason(status)))
- val close = appendConnectionHeader(sb) {
- appendHeaders(headers, sb)
+ if (status == 200 && protocol == `HTTP/1.1`) {
+ sb.append("HTTP/1.1 200 OK\r\n")
+ } else appendLine {
+ sb.append(protocol.name).append(' ').append(status).append(' ').append(HttpResponse.defaultReason(status))
}
+ val connectionHeaderValue = appendHeaders(headers, sb)
+ val close = appendConnectionHeaderIfRequired(connectionHeaderValue, sb)
appendLine(sb.append(ServerHeaderPlusDateColonSP).append(dateTimeNow.toRfc1123DateTimeString))
(sb, close)
}
Oops, something went wrong.

0 comments on commit a63d1e6

Please sign in to comment.