diff --git a/akka-http-core/src/main/java/akka/http/model/japi/headers/Age.java b/akka-http-core/src/main/java/akka/http/model/japi/headers/Age.java new file mode 100644 index 0000000000..5a66630c98 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/model/japi/headers/Age.java @@ -0,0 +1,17 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.model.japi.headers; + +/** + * Model for the `Age` header. + * Specification: http://tools.ietf.org/html/rfc7234#section-5.1 + */ +public abstract class Age extends akka.http.model.HttpHeader { + public abstract long deltaSeconds(); + + public static Age create(long deltaSeconds) { + return new akka.http.model.headers.Age(deltaSeconds); + } +} diff --git a/akka-http-core/src/main/java/akka/http/model/japi/headers/Expires.java b/akka-http-core/src/main/java/akka/http/model/japi/headers/Expires.java new file mode 100644 index 0000000000..14db6909a2 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/model/japi/headers/Expires.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.model.japi.headers; + +import akka.http.model.japi.DateTime; + +/** + * Model for the `Expires` header. + * Specification: http://tools.ietf.org/html/rfc7234#section-5.3 + */ +public abstract class Expires extends akka.http.model.HttpHeader { + public abstract DateTime date(); + + public static Expires create(DateTime date) { + return new akka.http.model.headers.Expires(((akka.http.util.DateTime) date)); + } +} diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala index 1e21b6afba..1e5204b18c 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala @@ -277,6 +277,13 @@ final case class `Access-Control-Request-Method`(method: HttpMethod) extends jap protected def companion = `Access-Control-Request-Method` } +// http://tools.ietf.org/html/rfc7234#section-5.1 +object Age extends ModeledCompanion +final case class Age(deltaSeconds: Long) extends japi.headers.Age with ModeledHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds + protected def companion = Age +} + // http://tools.ietf.org/html/rfc7231#section-7.4.1 object Allow extends ModeledCompanion { def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*)) @@ -393,6 +400,13 @@ final case class ETag(etag: EntityTag) extends japi.headers.ETag with ModeledHea protected def companion = ETag } +// http://tools.ietf.org/html/rfc7234#section-5.3 +object Expires extends ModeledCompanion +final case class Expires(date: DateTime) extends japi.headers.Expires with ModeledHeader { + def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) + protected def companion = Expires +} + // http://tools.ietf.org/html/rfc7232#section-3.1 object `If-Match` extends ModeledCompanion { val `*` = `If-Match`(EntityTagRange.`*`) diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/HeaderParser.scala b/akka-http-core/src/main/scala/akka/http/model/parser/HeaderParser.scala index 2c9597e911..38073c510a 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/HeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/HeaderParser.scala @@ -71,6 +71,7 @@ object HeaderParser { "access-control-request-headers", "access-control-request-method", "accept", + "age", "allow", "authorization", "cache-control", @@ -84,6 +85,7 @@ object HeaderParser { "date", "etag", "expect", + "expires", "host", "if-match", "if-modified-since", diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala b/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala index d3268012eb..8aa73022d5 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala @@ -61,6 +61,9 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA httpMethodDef ~ EOI ~> (`Access-Control-Request-Method`(_)) } + // http://tools.ietf.org/html/rfc7234#section-5.1 + def age = rule { `delta-seconds` ~ EOI ~> (Age(_)) } + // http://tools.ietf.org/html/rfc7231#section-7.4.1 def allow = rule { zeroOrMore(httpMethodDef).separatedBy(listSep) ~ EOI ~> (Allow(_)) @@ -109,6 +112,9 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA ignoreCase("100-continue") ~ OWS ~ push(Expect.`100-continue`) } + // http://tools.ietf.org/html/rfc7234#section-5.3 + def `expires` = rule { `HTTP-date` ~ EOI ~> (Expires(_)) } + // http://tools.ietf.org/html/rfc7230#section-5.4 // We don't accept scoped IPv6 addresses as they should not appear in the Host header, // see also https://issues.apache.org/bugzilla/show_bug.cgi?id=35122 (WONTFIX in Apache 2 issue) and diff --git a/akka-http-core/src/test/scala/akka/http/ClientServerSpec.scala b/akka-http-core/src/test/scala/akka/http/ClientServerSpec.scala index 602b849209..c27fb5c429 100644 --- a/akka-http-core/src/test/scala/akka/http/ClientServerSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/ClientServerSpec.scala @@ -114,11 +114,11 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { val serverOutSub = serverOut.expectSubscription() serverOutSub.expectRequest() - serverOutSub.sendNext(HttpResponse(206, List(RawHeader("Age", "42")), chunkedEntity)) + serverOutSub.sendNext(HttpResponse(206, List(Age(42)), chunkedEntity)) val clientInSub = clientIn.expectSubscription() clientInSub.request(1) - val HttpResponse(StatusCodes.PartialContent, List(RawHeader("Age", "42"), Server(_), Date(_)), + val HttpResponse(StatusCodes.PartialContent, List(Age(42), Server(_), Date(_)), Chunked(`chunkedContentType`, chunkStream2), HttpProtocols.`HTTP/1.1`) = clientIn.expectNext() Await.result(chunkStream2.grouped(1000).runWith(Sink.head), 100.millis) shouldEqual chunks diff --git a/akka-http-core/src/test/scala/akka/http/engine/parsing/HttpHeaderParserSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/parsing/HttpHeaderParserSpec.scala index 09b37e16cf..85551512bd 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/parsing/HttpHeaderParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/parsing/HttpHeaderParserSpec.scala @@ -110,19 +110,20 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll check { """ ┌─\r-\n- EmptyHeader | | ┌─c-h-a-r-s-e-t-:- (accept-charset) - | | | | ┌─e-n-c-o-d-i-n-g-:- (accept-encoding) - | | | └─l-a-n-g-u-a-g-e-:- (accept-language) - | | ┌─p-t---r-a-n-g-e-s-:- (accept-ranges) + | | ┌─p-t---e-n-c-o-d-i-n-g-:- (accept-encoding) + | | | | | ┌─l-a-n-g-u-a-g-e-:- (accept-language) + | | | | └─r-a-n-g-e-s-:- (accept-ranges) | | | | ┌─\r-\n- Accept: */* | | | └─:-(accept)- -*-/-*-\r-\n- Accept: */* - | | | ┌─c-r-e-d-e-n-t-i-a-l-s-:- (access-control-allow-credentials) - | | | ┌─h-e-a-d-e-r-s-:- (access-control-allow-headers) - | | | ┌─a-l-l-o-w---m-e-t-h-o-d-s-:- (access-control-allow-methods) + | | | ┌─a-l-l-o-w---c-r-e-d-e-n-t-i-a-l-s-:- (access-control-allow-credentials) + | | | | | | ┌─h-e-a-d-e-r-s-:- (access-control-allow-headers) + | | | | | | ┌─m-e-t-h-o-d-s-:- (access-control-allow-methods) | | | | | └─o-r-i-g-i-n-:- (access-control-allow-origin) - | | | | └─e-x-p-o-s-e---h-e-a-d-e-r-s-:- (access-control-expose-headers) - | ┌─a-c-c-e-s-s---c-o-n-t-r-o-l---m-a-x---a-g-e-:- (access-control-max-age) - | | | | ┌─h-e-a-d-e-r-s-:- (access-control-request-headers) - | | | └─r-e-q-u-e-s-t---m-e-t-h-o-d-:- (access-control-request-method) + | | | | | ┌─e-x-p-o-s-e---h-e-a-d-e-r-s-:- (access-control-expose-headers) + | | | | └─m-a-x---a-g-e-:- (access-control-max-age) + | ┌─a-c-c-e-s-s---c-o-n-t-r-o-l---r-e-q-u-e-s-t---h-e-a-d-e-r-s-:- (access-control-request-headers) + | | | └─m-e-t-h-o-d-:- (access-control-request-method) + | | | ┌─g-e-:- (age) | | └─l-l-o-w-:- (allow) | | └─u-t-h-o-r-i-z-a-t-i-o-n-:- (authorization) | | ┌─a-c-h-e---c-o-n-t-r-o-l-:-(cache-control)- -m-a-x---a-g-e-=-0-\r-\n- Cache-Control: max-age=0 @@ -138,7 +139,8 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll |-c-o-o-k-i-e-:- (cookie) | | ┌─d-a-t-e-:- (date) | | | ┌─t-a-g-:- (etag) - | | ┌─e-x-p-e-c-t-:-(expect)- -1-0-0---c-o-n-t-i-n-u-e-\r-\n- Expect: 100-continue + | | | | ┌─e-c-t-:-(expect)- -1-0-0---c-o-n-t-i-n-u-e-\r-\n- Expect: 100-continue + | | ┌─e-x-p-i-r-e-s-:- (expires) | | | └─h-o-s-t-:- (host) | | | ┌─a-t-c-h-:- (if-match) | | ┌─i-f---m-o-d-i-f-i-e-d---s-i-n-c-e-:- (if-modified-since) @@ -160,7 +162,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll | └─x---f-o-r-w-a-r-d-e-d---f-o-r-:- (x-forwarded-for) |""" -> parser.formatTrie } - parser.formatSizes shouldEqual "592 nodes, 40 branchData rows, 55 values" + parser.formatSizes shouldEqual "602 nodes, 42 branchData rows, 57 values" parser.contentHistogram shouldEqual Map("connection" -> 3, "Content-Length" -> 1, "accept" -> 2, "cache-control" -> 2, "expect" -> 1) } @@ -232,8 +234,8 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll } randomHeaders.take(300).foldLeft(0) { case (acc, rawHeader) ⇒ acc + parseAndCache(rawHeader.toString + "\r\nx", rawHeader) - } shouldEqual 100 // number of cache hits - parser.formatSizes shouldEqual "3050 nodes, 114 branchData rows, 255 values" + } shouldEqual 99 // number of cache hits + parser.formatSizes shouldEqual "3040 nodes, 115 branchData rows, 255 values" } "continue parsing modelled headers even if the overall cache capacity is reached" in new TestSetup() { @@ -245,7 +247,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll randomHostHeaders.take(300).foldLeft(0) { case (acc, header) ⇒ acc + parseAndCache(header.toString + "\r\nx", header) } shouldEqual 12 // number of cache hits - parser.formatSizes shouldEqual "756 nodes, 49 branchData rows, 67 values" + parser.formatSizes shouldEqual "766 nodes, 51 branchData rows, 69 values" } "continue parsing raw headers even if the header-specific cache capacity is reached" in new TestSetup() { diff --git a/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala index d62f4a7acf..b6eb5deace 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala @@ -57,7 +57,7 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "POST request, a few headers (incl. a custom Host header) and no body" in new TestSetup() { HttpRequest(POST, "/abc/xyz", List( RawHeader("X-Fancy", "naa"), - RawHeader("Age", "0"), + Age(0), Host("spray.io", 9999))) should renderTo { """POST /abc/xyz HTTP/1.1 |X-Fancy: naa diff --git a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala index 5ee67e2e25..9ca918b5de 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala @@ -55,7 +55,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "status 304 and a few headers" in new TestSetup() { - HttpResponse(304, List(RawHeader("X-Fancy", "of course"), RawHeader("Age", "0"))) should renderTo { + HttpResponse(304, List(RawHeader("X-Fancy", "of course"), Age(0))) should renderTo { """HTTP/1.1 304 Not Modified |X-Fancy: of course |Age: 0 @@ -80,7 +80,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll ResponseRenderingContext( requestMethod = HttpMethods.HEAD, response = HttpResponse( - headers = List(RawHeader("Age", "30"), Connection("Keep-Alive")), + headers = List(Age(30), Connection("Keep-Alive")), entity = "Small f*ck up overhere!")) should renderTo( """HTTP/1.1 200 OK |Age: 30 @@ -95,7 +95,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "a response with a Strict body," - { "status 400 and a few headers" in new TestSetup() { - HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), "Small f*ck up overhere!") should renderTo { + HttpResponse(400, List(Age(30), Connection("Keep-Alive")), "Small f*ck up overhere!") should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 |Server: akka-http/1.0.0 @@ -108,7 +108,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "status 400, a few headers and a body with an explicitly suppressed Content Type header" in new TestSetup() { - HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), + HttpResponse(400, List(Age(30), Connection("Keep-Alive")), HttpEntity(contentType = ContentTypes.NoContentType, "Small f*ck up overhere!")) should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 @@ -136,7 +136,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "a response with a Default (streamed with explicit content-length body," - { "status 400 and a few headers" in new TestSetup() { - HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), + HttpResponse(400, List(Age(30), Connection("Keep-Alive")), entity = Default(contentType = ContentTypes.`text/plain(UTF-8)`, 23, source(ByteString("Small f*ck up overhere!")))) should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 @@ -193,7 +193,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "a chunked response" - { "with empty entity" in new TestSetup() { pending // Disabled until #15981 is fixed - HttpResponse(200, List(RawHeader("Age", "30")), + HttpResponse(200, List(Age(30)), Chunked(ContentTypes.NoContentType, source())) should renderTo { """HTTP/1.1 200 OK |Age: 30 @@ -206,7 +206,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "with empty entity but non-default Content-Type" in new TestSetup() { pending // Disabled until #15981 is fixed - HttpResponse(200, List(RawHeader("Age", "30")), + HttpResponse(200, List(Age(30)), Chunked(ContentTypes.`application/json`, source())) should renderTo { """HTTP/1.1 200 OK |Age: 30 @@ -238,7 +238,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "with one chunk and an explicit LastChunk" in new TestSetup() { HttpResponse(entity = Chunked(ContentTypes.`text/plain(UTF-8)`, source(Chunk(ByteString("body123"), """key=value;another="tl;dr""""), - LastChunk("foo=bar", List(RawHeader("Age", "30"), RawHeader("Cache-Control", "public")))))) should renderTo { + LastChunk("foo=bar", List(Age(30), RawHeader("Cache-Control", "public")))))) should renderTo { """HTTP/1.1 200 OK |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT @@ -292,7 +292,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll requestProtocol = HttpProtocols.`HTTP/1.0`, response = HttpResponse(entity = Chunked(ContentTypes.`text/plain(UTF-8)`, source(Chunk(ByteString("body123"), """key=value;another="tl;dr""""), - LastChunk("foo=bar", List(RawHeader("Age", "30"), RawHeader("Cache-Control", "public"))))))) should renderTo( + LastChunk("foo=bar", List(Age(30), RawHeader("Cache-Control", "public"))))))) should renderTo( """HTTP/1.1 200 OK |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT diff --git a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala index e3a5424457..b863cd0071 100644 --- a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala @@ -114,6 +114,10 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Accept-Language: es-419, es" =!= `Accept-Language`(Language("es", "419"), Language("es")) } + "Age" in { + "Age: 3600" =!= Age(3600) + } + "Allow" in { "Allow: " =!= Allow() "Allow: GET, PUT" =!= Allow(GET, PUT) @@ -237,6 +241,11 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Expect: 100-continue" =!= Expect.`100-continue` } + "Expires" in { + "Expires: Wed, 13 Jul 2011 08:12:31 GMT" =!= Expires(DateTime(2011, 7, 13, 8, 12, 31)) + "Expires: 0" =!= Expires(DateTime.MinValue).renderedTo("Wed, 01 Jan 1800 00:00:00 GMT") + } + "Host" in { "Host: www.spray.io:8080" =!= Host("www.spray.io", 8080) "Host: spray.io" =!= Host("spray.io") diff --git a/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala index aaf8207e35..b71a6f648d 100644 --- a/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala @@ -65,7 +65,7 @@ class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll wi |Content-type: text/xml |Age: 12 |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( - Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(RawHeader("Age", "12")))) + Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12)))) } "one non-empty part" in { Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",