diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java index 666dec3c87..b22043f312 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java @@ -34,4 +34,14 @@ public abstract class HttpHeader { * Returns !is(nameInLowerCase). */ public abstract boolean isNot(String nameInLowerCase); + + /** + * Returns true iff the header is to be rendered in requests. + */ + public abstract boolean renderInRequests(); + + /** + * Returns true iff the header is to be rendered in responses. + */ + public abstract boolean renderInResponses(); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java index 7313313a2e..f693b5a7bc 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java @@ -7,6 +7,4 @@ package akka.http.javadsl.model.headers; public abstract class CustomHeader extends akka.http.scaladsl.model.HttpHeader { public abstract String name(); public abstract String value(); - - protected abstract boolean suppressRendering(); } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala index 13b2f4fa11..06e208b54e 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala @@ -146,7 +146,7 @@ private[http] final class BodyPartParser(defaultContentType: ContentType, else if (doubleDash(input, ix)) terminate() else fail("Illegal multipart boundary in message content") - case HttpHeaderParser.EmptyHeader ⇒ parseEntity(headers.toList, contentType)(input, lineEnd) + case EmptyHeader ⇒ parseEntity(headers.toList, contentType)(input, lineEnd) case h: `Content-Type` ⇒ if (cth.isEmpty) parseHeaderLines(input, lineEnd, headers, headerCount + 1, Some(h)) @@ -261,6 +261,8 @@ private[http] object BodyPartParser { val boundaryChar = CharPredicate.Digit ++ CharPredicate.Alpha ++ "'()+_,-./:=? " private object BoundaryHeader extends HttpHeader { + def renderInRequests = false + def renderInResponses = false def name = "" def lowercaseName = "" def value = "" diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala index 010519f582..ceb5865e38 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala @@ -11,8 +11,8 @@ import scala.annotation.tailrec import akka.parboiled2.CharUtils import akka.util.ByteString import akka.http.impl.util._ -import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo, Uri } -import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo } +import akka.http.scaladsl.model.headers.{ EmptyHeader, RawHeader } import akka.http.impl.model.parser.HeaderParser import akka.http.impl.model.parser.CharacterClasses._ @@ -414,14 +414,6 @@ private[http] object HttpHeaderParser { def headerValueCacheLimit(headerName: String): Int } - object EmptyHeader extends HttpHeader { - def name = "" - def lowercaseName = "" - def value = "" - def render[R <: Rendering](r: R): r.type = r - override def toString = "EmptyHeader" - } - private def predefinedHeaders = Seq( "Accept: *", "Accept: */*", diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala index 227aeb5051..a160b5ddd4 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala @@ -136,7 +136,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser resultHeader match { case null ⇒ continue(input, lineStart)(parseHeaderLinesAux(headers, headerCount, ch, clh, cth, teh, e100c, hh)) - case HttpHeaderParser.EmptyHeader ⇒ + case EmptyHeader ⇒ val close = HttpMessage.connectionCloseExpected(protocol, ch) setCompletionHandling(CompletionIsEntityStreamError) parseEntity(headers.toList, protocol, input, lineEnd, clh, cth, teh, e100c, hh, close) @@ -206,7 +206,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser catch { case e: ParsingException ⇒ errorInfo = e.info; 0 } if (errorInfo eq null) { headerParser.resultHeader match { - case HttpHeaderParser.EmptyHeader ⇒ + case EmptyHeader ⇒ val lastChunk = if (extension.isEmpty && headers.isEmpty) HttpEntity.LastChunk else HttpEntity.LastChunk(extension, headers) emit(EntityChunk(lastChunk)) diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala index e7a91775f2..5b9e3c0043 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala @@ -78,8 +78,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` case x: `Raw-Request-URI` ⇒ // we never render this header renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) - case x: CustomHeader ⇒ - if (!x.suppressRendering) render(x) + case x: CustomHeader if x.renderInRequests ⇒ + render(x) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || @@ -88,7 +88,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) case x ⇒ - render(x) + if (x.renderInRequests) render(x) + else log.warning("HTTP header '{}' is not allowed in requests", x) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala index 693bd18931..586b64b836 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala @@ -161,8 +161,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser render(x) renderHeaders(tail, alwaysClose, connHeader, serverSeen = true, transferEncodingSeen, dateSeen) - case x: CustomHeader ⇒ - if (!x.suppressRendering) render(x) + case x: CustomHeader if x.renderInResponses ⇒ + render(x) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || @@ -171,7 +171,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) case x ⇒ - render(x) + if (x.renderInResponses) render(x) + else log.warning("HTTP header '{}' is not allowed in responses", x) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala index f1db3dcada..e736afaae2 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala @@ -12,7 +12,7 @@ private[http] final case class UpgradeToWebsocketResponseHeader(handler: Either[ extends InternalCustomHeader("UpgradeToWebsocketResponseHeader") private[http] abstract class InternalCustomHeader(val name: String) extends CustomHeader { - override def suppressRendering: Boolean = true - - def value(): String = "" + final def renderInRequests = false + final def renderInResponses = false + def value: String = "" } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala index 99e2d92e2b..335306b854 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala @@ -9,16 +9,12 @@ import java.net.InetSocketAddress import java.security.MessageDigest import java.util import javax.net.ssl.SSLSession - -import akka.stream.io.ScalaSessionAPI - import scala.reflect.ClassTag import scala.util.{ Failure, Success, Try } import scala.annotation.tailrec import scala.collection.immutable - import akka.parboiled2.util.Base64 - +import akka.stream.io.ScalaSessionAPI import akka.http.impl.util._ import akka.http.javadsl.{ model ⇒ jm } import akka.http.scaladsl.model._ @@ -41,6 +37,8 @@ sealed abstract class ModeledCompanion[T: ClassTag] extends Renderable { } sealed trait ModeledHeader extends HttpHeader with Serializable { + def renderInRequests: Boolean = false // default implementation + def renderInResponses: Boolean = false // default implementation def name: String = companion.name def value: String = renderValue(new StringRendering).get def lowercaseName: String = companion.lowercaseName @@ -49,6 +47,11 @@ sealed trait ModeledHeader extends HttpHeader with Serializable { protected def companion: ModeledCompanion[_] } +private[headers] sealed trait RequestHeader extends ModeledHeader { override def renderInRequests = true } +private[headers] sealed trait ResponseHeader extends ModeledHeader { override def renderInResponses = true } +private[headers] sealed trait RequestResponseHeader extends RequestHeader with ResponseHeader +private[headers] sealed trait SyntheticHeader extends ModeledHeader + /** * Superclass for user-defined custom headers defined by implementing `name` and `value`. * @@ -57,9 +60,6 @@ sealed trait ModeledHeader extends HttpHeader with Serializable { * as they allow the custom header to be matched from [[RawHeader]] and vice-versa. */ abstract class CustomHeader extends jm.headers.CustomHeader { - /** Override to return true if this header shouldn't be rendered */ - def suppressRendering: Boolean = false - def lowercaseName: String = name.toRootLowerCase final def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value } @@ -98,17 +98,243 @@ abstract class ModeledCustomHeader[H <: ModeledCustomHeader[H]] extends CustomHe def companion: ModeledCustomHeaderCompanion[H] final override def name = companion.name - final override def lowercaseName: String = name.toRootLowerCase + final override def lowercaseName = name.toRootLowerCase } import akka.http.impl.util.JavaMapping.Implicits._ +// http://tools.ietf.org/html/rfc7231#section-5.3.2 +object Accept extends ModeledCompanion[Accept] { + def apply(mediaRanges: MediaRange*): Accept = apply(immutable.Seq(mediaRanges: _*)) + implicit val mediaRangesRenderer = Renderer.defaultSeqRenderer[MediaRange] // cache +} +final case class Accept(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.Accept with RequestHeader { + import Accept.mediaRangesRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges + protected def companion = Accept + def acceptsAll = mediaRanges.exists(mr ⇒ mr.isWildcard && mr.qValue > 0f) + + /** Java API */ + def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava +} + +// http://tools.ietf.org/html/rfc7231#section-5.3.3 +object `Accept-Charset` extends ModeledCompanion[`Accept-Charset`] { + def apply(first: HttpCharsetRange, more: HttpCharsetRange*): `Accept-Charset` = apply(immutable.Seq(first +: more: _*)) + implicit val charsetRangesRenderer = Renderer.defaultSeqRenderer[HttpCharsetRange] // cache +} +final case class `Accept-Charset`(charsetRanges: immutable.Seq[HttpCharsetRange]) extends jm.headers.AcceptCharset + with RequestHeader { + require(charsetRanges.nonEmpty, "charsetRanges must not be empty") + import `Accept-Charset`.charsetRangesRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ charsetRanges + protected def companion = `Accept-Charset` + + /** Java API */ + def getCharsetRanges: Iterable[jm.HttpCharsetRange] = charsetRanges.asJava +} + +// http://tools.ietf.org/html/rfc7231#section-5.3.4 +object `Accept-Encoding` extends ModeledCompanion[`Accept-Encoding`] { + def apply(encodings: HttpEncodingRange*): `Accept-Encoding` = apply(immutable.Seq(encodings: _*)) + implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncodingRange] // cache +} +final case class `Accept-Encoding`(encodings: immutable.Seq[HttpEncodingRange]) extends jm.headers.AcceptEncoding + with RequestHeader { + import `Accept-Encoding`.encodingsRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings + protected def companion = `Accept-Encoding` + + /** Java API */ + def getEncodings: Iterable[jm.headers.HttpEncodingRange] = encodings.asJava +} + +// http://tools.ietf.org/html/rfc7231#section-5.3.5 +object `Accept-Language` extends ModeledCompanion[`Accept-Language`] { + def apply(first: LanguageRange, more: LanguageRange*): `Accept-Language` = apply(immutable.Seq(first +: more: _*)) + implicit val languagesRenderer = Renderer.defaultSeqRenderer[LanguageRange] // cache +} +final case class `Accept-Language`(languages: immutable.Seq[LanguageRange]) extends jm.headers.AcceptLanguage + with RequestHeader { + require(languages.nonEmpty, "languages must not be empty") + import `Accept-Language`.languagesRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ languages + protected def companion = `Accept-Language` + + /** Java API */ + def getLanguages: Iterable[jm.headers.LanguageRange] = languages.asJava +} + +// http://tools.ietf.org/html/rfc7233#section-2.3 +object `Accept-Ranges` extends ModeledCompanion[`Accept-Ranges`] { + def apply(rangeUnits: RangeUnit*): `Accept-Ranges` = apply(immutable.Seq(rangeUnits: _*)) + implicit val rangeUnitsRenderer = Renderer.defaultSeqRenderer[RangeUnit] // cache +} +final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends jm.headers.AcceptRanges + with RequestHeader { + import `Accept-Ranges`.rangeUnitsRenderer + def renderValue[R <: Rendering](r: R): r.type = if (rangeUnits.isEmpty) r ~~ "none" else r ~~ rangeUnits + protected def companion = `Accept-Ranges` + + /** Java API */ + def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava +} + +// http://www.w3.org/TR/cors/#access-control-allow-credentials-response-header +object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`] +final case class `Access-Control-Allow-Credentials`(allow: Boolean) + extends jm.headers.AccessControlAllowCredentials with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ allow.toString + protected def companion = `Access-Control-Allow-Credentials` +} + +// http://www.w3.org/TR/cors/#access-control-allow-headers-response-header +object `Access-Control-Allow-Headers` extends ModeledCompanion[`Access-Control-Allow-Headers`] { + def apply(headers: String*): `Access-Control-Allow-Headers` = apply(immutable.Seq(headers: _*)) + implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache +} +final case class `Access-Control-Allow-Headers`(headers: immutable.Seq[String]) + extends jm.headers.AccessControlAllowHeaders with RequestHeader { + import `Access-Control-Allow-Headers`.headersRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ headers + protected def companion = `Access-Control-Allow-Headers` + + /** Java API */ + def getHeaders: Iterable[String] = headers.asJava +} + +// http://www.w3.org/TR/cors/#access-control-allow-methods-response-header +object `Access-Control-Allow-Methods` extends ModeledCompanion[`Access-Control-Allow-Methods`] { + def apply(methods: HttpMethod*): `Access-Control-Allow-Methods` = apply(immutable.Seq(methods: _*)) + implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache +} +final case class `Access-Control-Allow-Methods`(methods: immutable.Seq[HttpMethod]) + extends jm.headers.AccessControlAllowMethods with RequestHeader { + import `Access-Control-Allow-Methods`.methodsRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ methods + protected def companion = `Access-Control-Allow-Methods` + + /** Java API */ + def getMethods: Iterable[jm.HttpMethod] = methods.asJava +} + +// http://www.w3.org/TR/cors/#access-control-allow-origin-response-header +object `Access-Control-Allow-Origin` extends ModeledCompanion[`Access-Control-Allow-Origin`] { + val `*` = forRange(HttpOriginRange.`*`) + val `null` = forRange(HttpOriginRange()) + def apply(origin: HttpOrigin) = forRange(HttpOriginRange(origin)) + + /** + * Creates an `Access-Control-Allow-Origin` header for the given origin range. + * + * CAUTION: Even though allowed by the spec (http://www.w3.org/TR/cors/#access-control-allow-origin-response-header) + * `Access-Control-Allow-Origin` headers with more than a single origin appear to be largely unsupported in the field. + * Make sure to thoroughly test such usages with all expected clients! + */ + def forRange(range: HttpOriginRange) = new `Access-Control-Allow-Origin`(range) +} +final case class `Access-Control-Allow-Origin` private (range: HttpOriginRange) + extends jm.headers.AccessControlAllowOrigin with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ range + protected def companion = `Access-Control-Allow-Origin` +} + +// http://www.w3.org/TR/cors/#access-control-expose-headers-response-header +object `Access-Control-Expose-Headers` extends ModeledCompanion[`Access-Control-Expose-Headers`] { + def apply(headers: String*): `Access-Control-Expose-Headers` = apply(immutable.Seq(headers: _*)) + implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache +} +final case class `Access-Control-Expose-Headers`(headers: immutable.Seq[String]) + extends jm.headers.AccessControlExposeHeaders with RequestHeader { + import `Access-Control-Expose-Headers`.headersRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ headers + protected def companion = `Access-Control-Expose-Headers` + + /** Java API */ + def getHeaders: Iterable[String] = headers.asJava +} + +// http://www.w3.org/TR/cors/#access-control-max-age-response-header +object `Access-Control-Max-Age` extends ModeledCompanion[`Access-Control-Max-Age`] +final case class `Access-Control-Max-Age`(deltaSeconds: Long) extends jm.headers.AccessControlMaxAge + with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds + protected def companion = `Access-Control-Max-Age` +} + +// http://www.w3.org/TR/cors/#access-control-request-headers-request-header +object `Access-Control-Request-Headers` extends ModeledCompanion[`Access-Control-Request-Headers`] { + def apply(headers: String*): `Access-Control-Request-Headers` = apply(immutable.Seq(headers: _*)) + implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache +} +final case class `Access-Control-Request-Headers`(headers: immutable.Seq[String]) + extends jm.headers.AccessControlRequestHeaders with RequestHeader { + import `Access-Control-Request-Headers`.headersRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ headers + protected def companion = `Access-Control-Request-Headers` + + /** Java API */ + def getHeaders: Iterable[String] = headers.asJava +} + +// http://www.w3.org/TR/cors/#access-control-request-method-request-header +object `Access-Control-Request-Method` extends ModeledCompanion[`Access-Control-Request-Method`] +final case class `Access-Control-Request-Method`(method: HttpMethod) extends jm.headers.AccessControlRequestMethod + with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ method + protected def companion = `Access-Control-Request-Method` +} + +// http://tools.ietf.org/html/rfc7234#section-5.1 +object Age extends ModeledCompanion[Age] +final case class Age(deltaSeconds: Long) extends jm.headers.Age with ResponseHeader { + 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[Allow] { + def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*)) + implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache +} +final case class Allow(methods: immutable.Seq[HttpMethod]) extends jm.headers.Allow with ResponseHeader { + import Allow.methodsRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ methods + protected def companion = Allow + + /** Java API */ + def getMethods: Iterable[jm.HttpMethod] = methods.asJava +} + +// http://tools.ietf.org/html/rfc7235#section-4.2 +object Authorization extends ModeledCompanion[Authorization] +final case class Authorization(credentials: HttpCredentials) extends jm.headers.Authorization with RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials + protected def companion = Authorization +} + +// http://tools.ietf.org/html/rfc7234#section-5.2 +object `Cache-Control` extends ModeledCompanion[`Cache-Control`] { + def apply(first: CacheDirective, more: CacheDirective*): `Cache-Control` = apply(immutable.Seq(first +: more: _*)) + implicit val directivesRenderer = Renderer.defaultSeqRenderer[CacheDirective] // cache +} +final case class `Cache-Control`(directives: immutable.Seq[CacheDirective]) extends jm.headers.CacheControl + with RequestResponseHeader { + require(directives.nonEmpty, "directives must not be empty") + import `Cache-Control`.directivesRenderer + def renderValue[R <: Rendering](r: R): r.type = r ~~ directives + protected def companion = `Cache-Control` + + /** Java API */ + def getDirectives: Iterable[jm.headers.CacheDirective] = directives.asJava +} + // http://tools.ietf.org/html/rfc7230#section-6.1 object Connection extends ModeledCompanion[Connection] { def apply(first: String, more: String*): Connection = apply(immutable.Seq(first +: more: _*)) implicit val tokensRenderer = Renderer.defaultSeqRenderer[String] // cache } -final case class Connection(tokens: immutable.Seq[String]) extends ModeledHeader { +final case class Connection(tokens: immutable.Seq[String]) extends RequestResponseHeader { require(tokens.nonEmpty, "tokens must not be empty") import Connection.tokensRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ tokens @@ -133,277 +359,20 @@ object `Content-Length` extends ModeledCompanion[`Content-Length`] * Instances of this class will only be created transiently during header parsing and will never appear * in HttpMessage.header. To access the Content-Length, see subclasses of HttpEntity. */ -final case class `Content-Length` private[http] (length: Long) extends ModeledHeader { +final case class `Content-Length` private[http] (length: Long) extends RequestResponseHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ length protected def companion = `Content-Length` } -// http://tools.ietf.org/html/rfc7231#section-5.1.1 -object Expect extends ModeledCompanion[Expect] { - val `100-continue` = new Expect() {} -} -sealed abstract case class Expect private () extends ModeledHeader { - final def renderValue[R <: Rendering](r: R): r.type = r ~~ "100-continue" - protected def companion = Expect -} - -// http://tools.ietf.org/html/rfc7230#section-5.4 -object Host extends ModeledCompanion[Host] { - def apply(authority: Uri.Authority): Host = apply(authority.host, authority.port) - def apply(address: InetSocketAddress): Host = apply(address.getHostString, address.getPort) - def apply(host: String): Host = apply(host, 0) - def apply(host: String, port: Int): Host = apply(Uri.Host(host), port) - val empty = Host("") -} -final case class Host(host: Uri.Host, port: Int = 0) extends jm.headers.Host with ModeledHeader { - import UriRendering.HostRenderer - require((port >> 16) == 0, "Illegal port: " + port) - def isEmpty = host.isEmpty - def renderValue[R <: Rendering](r: R): r.type = if (port > 0) r ~~ host ~~ ':' ~~ port else r ~~ host - protected def companion = Host - def equalsIgnoreCase(other: Host): Boolean = host.equalsIgnoreCase(other.host) && port == other.port -} - -// http://tools.ietf.org/html/rfc7233#section-3.2 -object `If-Range` extends ModeledCompanion[`If-Range`] { - def apply(tag: EntityTag): `If-Range` = apply(Left(tag)) - def apply(timestamp: DateTime): `If-Range` = apply(Right(timestamp)) -} -final case class `If-Range`(entityTagOrDateTime: Either[EntityTag, DateTime]) extends ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = - entityTagOrDateTime match { - case Left(tag) ⇒ r ~~ tag - case Right(dateTime) ⇒ dateTime.renderRfc1123DateTimeString(r) - } - protected def companion = `If-Range` -} - -final case class RawHeader(name: String, value: String) extends jm.headers.RawHeader { - val lowercaseName = name.toRootLowerCase - def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value -} -object RawHeader { - def unapply[H <: HttpHeader](customHeader: H): Option[(String, String)] = - Some(customHeader.name -> customHeader.value) -} - -// http://tools.ietf.org/html/rfc7231#section-5.3.2 -object Accept extends ModeledCompanion[Accept] { - def apply(mediaRanges: MediaRange*): Accept = apply(immutable.Seq(mediaRanges: _*)) - implicit val mediaRangesRenderer = Renderer.defaultSeqRenderer[MediaRange] // cache -} -final case class Accept(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.Accept with ModeledHeader { - import Accept.mediaRangesRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges - protected def companion = Accept - def acceptsAll = mediaRanges.exists(mr ⇒ mr.isWildcard && mr.qValue > 0f) - - /** Java API */ - def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava -} - -// http://tools.ietf.org/html/rfc7231#section-5.3.3 -object `Accept-Charset` extends ModeledCompanion[`Accept-Charset`] { - def apply(first: HttpCharsetRange, more: HttpCharsetRange*): `Accept-Charset` = apply(immutable.Seq(first +: more: _*)) - implicit val charsetRangesRenderer = Renderer.defaultSeqRenderer[HttpCharsetRange] // cache -} -final case class `Accept-Charset`(charsetRanges: immutable.Seq[HttpCharsetRange]) extends jm.headers.AcceptCharset with ModeledHeader { - require(charsetRanges.nonEmpty, "charsetRanges must not be empty") - import `Accept-Charset`.charsetRangesRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ charsetRanges - protected def companion = `Accept-Charset` - - /** Java API */ - def getCharsetRanges: Iterable[jm.HttpCharsetRange] = charsetRanges.asJava -} - -// http://tools.ietf.org/html/rfc7231#section-5.3.4 -object `Accept-Encoding` extends ModeledCompanion[`Accept-Encoding`] { - def apply(encodings: HttpEncodingRange*): `Accept-Encoding` = apply(immutable.Seq(encodings: _*)) - implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncodingRange] // cache -} -final case class `Accept-Encoding`(encodings: immutable.Seq[HttpEncodingRange]) extends jm.headers.AcceptEncoding with ModeledHeader { - import `Accept-Encoding`.encodingsRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings - protected def companion = `Accept-Encoding` - - /** Java API */ - def getEncodings: Iterable[jm.headers.HttpEncodingRange] = encodings.asJava -} - -// http://tools.ietf.org/html/rfc7231#section-5.3.5 -object `Accept-Language` extends ModeledCompanion[`Accept-Language`] { - def apply(first: LanguageRange, more: LanguageRange*): `Accept-Language` = apply(immutable.Seq(first +: more: _*)) - implicit val languagesRenderer = Renderer.defaultSeqRenderer[LanguageRange] // cache -} -final case class `Accept-Language`(languages: immutable.Seq[LanguageRange]) extends jm.headers.AcceptLanguage with ModeledHeader { - require(languages.nonEmpty, "languages must not be empty") - import `Accept-Language`.languagesRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ languages - protected def companion = `Accept-Language` - - /** Java API */ - def getLanguages: Iterable[jm.headers.LanguageRange] = languages.asJava -} - -// http://tools.ietf.org/html/rfc7233#section-2.3 -object `Accept-Ranges` extends ModeledCompanion[`Accept-Ranges`] { - def apply(rangeUnits: RangeUnit*): `Accept-Ranges` = apply(immutable.Seq(rangeUnits: _*)) - implicit val rangeUnitsRenderer = Renderer.defaultSeqRenderer[RangeUnit] // cache -} -final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends jm.headers.AcceptRanges with ModeledHeader { - import `Accept-Ranges`.rangeUnitsRenderer - def renderValue[R <: Rendering](r: R): r.type = if (rangeUnits.isEmpty) r ~~ "none" else r ~~ rangeUnits - protected def companion = `Accept-Ranges` - - /** Java API */ - def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava -} - -// http://www.w3.org/TR/cors/#access-control-allow-credentials-response-header -object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`] -final case class `Access-Control-Allow-Credentials`(allow: Boolean) extends jm.headers.AccessControlAllowCredentials with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = r ~~ allow.toString - protected def companion = `Access-Control-Allow-Credentials` -} - -// http://www.w3.org/TR/cors/#access-control-allow-headers-response-header -object `Access-Control-Allow-Headers` extends ModeledCompanion[`Access-Control-Allow-Headers`] { - def apply(headers: String*): `Access-Control-Allow-Headers` = apply(immutable.Seq(headers: _*)) - implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache -} -final case class `Access-Control-Allow-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlAllowHeaders with ModeledHeader { - import `Access-Control-Allow-Headers`.headersRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ headers - protected def companion = `Access-Control-Allow-Headers` - - /** Java API */ - def getHeaders: Iterable[String] = headers.asJava -} - -// http://www.w3.org/TR/cors/#access-control-allow-methods-response-header -object `Access-Control-Allow-Methods` extends ModeledCompanion[`Access-Control-Allow-Methods`] { - def apply(methods: HttpMethod*): `Access-Control-Allow-Methods` = apply(immutable.Seq(methods: _*)) - implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache -} -final case class `Access-Control-Allow-Methods`(methods: immutable.Seq[HttpMethod]) extends jm.headers.AccessControlAllowMethods with ModeledHeader { - import `Access-Control-Allow-Methods`.methodsRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ methods - protected def companion = `Access-Control-Allow-Methods` - - /** Java API */ - def getMethods: Iterable[jm.HttpMethod] = methods.asJava -} - -// http://www.w3.org/TR/cors/#access-control-allow-origin-response-header -object `Access-Control-Allow-Origin` extends ModeledCompanion[`Access-Control-Allow-Origin`] { - val `*` = forRange(HttpOriginRange.`*`) - val `null` = forRange(HttpOriginRange()) - def apply(origin: HttpOrigin) = forRange(HttpOriginRange(origin)) - - /** - * Creates an `Access-Control-Allow-Origin` header for the given origin range. - * - * CAUTION: Even though allowed by the spec (http://www.w3.org/TR/cors/#access-control-allow-origin-response-header) - * `Access-Control-Allow-Origin` headers with more than a single origin appear to be largely unsupported in the field. - * Make sure to thoroughly test such usages with all expected clients! - */ - def forRange(range: HttpOriginRange) = new `Access-Control-Allow-Origin`(range) -} -final case class `Access-Control-Allow-Origin` private (range: HttpOriginRange) extends jm.headers.AccessControlAllowOrigin with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = r ~~ range - protected def companion = `Access-Control-Allow-Origin` -} - -// http://www.w3.org/TR/cors/#access-control-expose-headers-response-header -object `Access-Control-Expose-Headers` extends ModeledCompanion[`Access-Control-Expose-Headers`] { - def apply(headers: String*): `Access-Control-Expose-Headers` = apply(immutable.Seq(headers: _*)) - implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache -} -final case class `Access-Control-Expose-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlExposeHeaders with ModeledHeader { - import `Access-Control-Expose-Headers`.headersRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ headers - protected def companion = `Access-Control-Expose-Headers` - - /** Java API */ - def getHeaders: Iterable[String] = headers.asJava -} - -// http://www.w3.org/TR/cors/#access-control-max-age-response-header -object `Access-Control-Max-Age` extends ModeledCompanion[`Access-Control-Max-Age`] -final case class `Access-Control-Max-Age`(deltaSeconds: Long) extends jm.headers.AccessControlMaxAge with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds - protected def companion = `Access-Control-Max-Age` -} - -// http://www.w3.org/TR/cors/#access-control-request-headers-request-header -object `Access-Control-Request-Headers` extends ModeledCompanion[`Access-Control-Request-Headers`] { - def apply(headers: String*): `Access-Control-Request-Headers` = apply(immutable.Seq(headers: _*)) - implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache -} -final case class `Access-Control-Request-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlRequestHeaders with ModeledHeader { - import `Access-Control-Request-Headers`.headersRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ headers - protected def companion = `Access-Control-Request-Headers` - - /** Java API */ - def getHeaders: Iterable[String] = headers.asJava -} - -// http://www.w3.org/TR/cors/#access-control-request-method-request-header -object `Access-Control-Request-Method` extends ModeledCompanion[`Access-Control-Request-Method`] -final case class `Access-Control-Request-Method`(method: HttpMethod) extends jm.headers.AccessControlRequestMethod with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = r ~~ method - protected def companion = `Access-Control-Request-Method` -} - -// http://tools.ietf.org/html/rfc7234#section-5.1 -object Age extends ModeledCompanion[Age] -final case class Age(deltaSeconds: Long) extends jm.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[Allow] { - def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*)) - implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache -} -final case class Allow(methods: immutable.Seq[HttpMethod]) extends jm.headers.Allow with ModeledHeader { - import Allow.methodsRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ methods - protected def companion = Allow - - /** Java API */ - def getMethods: Iterable[jm.HttpMethod] = methods.asJava -} - -// http://tools.ietf.org/html/rfc7235#section-4.2 -object Authorization extends ModeledCompanion[Authorization] -final case class Authorization(credentials: HttpCredentials) extends jm.headers.Authorization with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials - protected def companion = Authorization -} - -// http://tools.ietf.org/html/rfc7234#section-5.2 -object `Cache-Control` extends ModeledCompanion[`Cache-Control`] { - def apply(first: CacheDirective, more: CacheDirective*): `Cache-Control` = apply(immutable.Seq(first +: more: _*)) - implicit val directivesRenderer = Renderer.defaultSeqRenderer[CacheDirective] // cache -} -final case class `Cache-Control`(directives: immutable.Seq[CacheDirective]) extends jm.headers.CacheControl with ModeledHeader { - require(directives.nonEmpty, "directives must not be empty") - import `Cache-Control`.directivesRenderer - def renderValue[R <: Rendering](r: R): r.type = r ~~ directives - protected def companion = `Cache-Control` - - /** Java API */ - def getDirectives: Iterable[jm.headers.CacheDirective] = directives.asJava -} - // http://tools.ietf.org/html/rfc6266 object `Content-Disposition` extends ModeledCompanion[`Content-Disposition`] -final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty) extends jm.headers.ContentDisposition with ModeledHeader { - def renderValue[R <: Rendering](r: R): r.type = { r ~~ dispositionType; params foreach { case (k, v) ⇒ r ~~ "; " ~~ k ~~ '=' ~~# v }; r } +final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty) + extends jm.headers.ContentDisposition with RequestResponseHeader { + def renderValue[R <: Rendering](r: R): r.type = { + r ~~ dispositionType + params foreach { case (k, v) ⇒ r ~~ "; " ~~ k ~~ '=' ~~# v } + r + } protected def companion = `Content-Disposition` /** Java API */ @@ -415,7 +384,8 @@ object `Content-Encoding` extends ModeledCompanion[`Content-Encoding`] { def apply(first: HttpEncoding, more: HttpEncoding*): `Content-Encoding` = apply(immutable.Seq(first +: more: _*)) implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncoding] // cache } -final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) extends jm.headers.ContentEncoding with ModeledHeader { +final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) extends jm.headers.ContentEncoding + with RequestResponseHeader { require(encodings.nonEmpty, "encodings must not be empty") import `Content-Encoding`.encodingsRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings @@ -429,7 +399,8 @@ final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) exte object `Content-Range` extends ModeledCompanion[`Content-Range`] { def apply(byteContentRange: ByteContentRange): `Content-Range` = apply(RangeUnits.Bytes, byteContentRange) } -final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRange) extends jm.headers.ContentRange with ModeledHeader { +final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRange) extends jm.headers.ContentRange + with RequestResponseHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ ' ' ~~ contentRange protected def companion = `Content-Range` } @@ -440,7 +411,8 @@ object `Content-Type` extends ModeledCompanion[`Content-Type`] * Instances of this class will only be created transiently during header parsing and will never appear * in HttpMessage.header. To access the Content-Type, see subclasses of HttpEntity. */ -final case class `Content-Type` private[http] (contentType: ContentType) extends jm.headers.ContentType with ModeledHeader { +final case class `Content-Type` private[http] (contentType: ContentType) extends jm.headers.ContentType + with RequestResponseHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ contentType protected def companion = `Content-Type` } @@ -452,7 +424,7 @@ object Cookie extends ModeledCompanion[Cookie] { def apply(values: (String, String)*): Cookie = apply(values.map(HttpCookiePair(_)).toList) implicit val cookiePairsRenderer = Renderer.seqRenderer[HttpCookiePair](separator = "; ") // cache } -final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.headers.Cookie with ModeledHeader { +final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.headers.Cookie with RequestHeader { require(cookies.nonEmpty, "cookies must not be empty") import Cookie.cookiePairsRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ cookies @@ -464,42 +436,80 @@ final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.heade // http://tools.ietf.org/html/rfc7231#section-7.1.1.2 object Date extends ModeledCompanion[Date] -final case class Date(date: DateTime) extends jm.headers.Date with ModeledHeader { +final case class Date(date: DateTime) extends jm.headers.Date with RequestResponseHeader { def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) protected def companion = Date } +/** + * INTERNAL API + */ +private[headers] object EmptyCompanion extends ModeledCompanion[EmptyHeader.type] +/** + * INTERNAL API + */ +private[http] object EmptyHeader extends SyntheticHeader { + def renderValue[R <: Rendering](r: R): r.type = r + protected def companion: ModeledCompanion[EmptyHeader.type] = EmptyCompanion +} + // http://tools.ietf.org/html/rfc7232#section-2.3 object ETag extends ModeledCompanion[ETag] { def apply(tag: String, weak: Boolean = false): ETag = ETag(EntityTag(tag, weak)) } -final case class ETag(etag: EntityTag) extends jm.headers.ETag with ModeledHeader { +final case class ETag(etag: EntityTag) extends jm.headers.ETag with ResponseHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ etag protected def companion = ETag } +// http://tools.ietf.org/html/rfc7231#section-5.1.1 +object Expect extends ModeledCompanion[Expect] { + val `100-continue` = new Expect() {} +} +sealed abstract case class Expect private () extends RequestHeader { + final def renderValue[R <: Rendering](r: R): r.type = r ~~ "100-continue" + protected def companion = Expect +} + // http://tools.ietf.org/html/rfc7234#section-5.3 object Expires extends ModeledCompanion[Expires] -final case class Expires(date: DateTime) extends jm.headers.Expires with ModeledHeader { +final case class Expires(date: DateTime) extends jm.headers.Expires with ResponseHeader { def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) protected def companion = Expires } +// http://tools.ietf.org/html/rfc7230#section-5.4 +object Host extends ModeledCompanion[Host] { + def apply(authority: Uri.Authority): Host = apply(authority.host, authority.port) + def apply(address: InetSocketAddress): Host = apply(address.getHostString, address.getPort) + def apply(host: String): Host = apply(host, 0) + def apply(host: String, port: Int): Host = apply(Uri.Host(host), port) + val empty = Host("") +} +final case class Host(host: Uri.Host, port: Int = 0) extends jm.headers.Host with RequestHeader { + import UriRendering.HostRenderer + require((port >> 16) == 0, "Illegal port: " + port) + def isEmpty = host.isEmpty + def renderValue[R <: Rendering](r: R): r.type = if (port > 0) r ~~ host ~~ ':' ~~ port else r ~~ host + protected def companion = Host + def equalsIgnoreCase(other: Host): Boolean = host.equalsIgnoreCase(other.host) && port == other.port +} + // http://tools.ietf.org/html/rfc7232#section-3.1 object `If-Match` extends ModeledCompanion[`If-Match`] { val `*` = `If-Match`(EntityTagRange.`*`) def apply(first: EntityTag, more: EntityTag*): `If-Match` = `If-Match`(EntityTagRange(first +: more: _*)) } -final case class `If-Match`(m: EntityTagRange) extends jm.headers.IfMatch with ModeledHeader { +final case class `If-Match`(m: EntityTagRange) extends jm.headers.IfMatch with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ m protected def companion = `If-Match` } // http://tools.ietf.org/html/rfc7232#section-3.3 object `If-Modified-Since` extends ModeledCompanion[`If-Modified-Since`] -final case class `If-Modified-Since`(date: DateTime) extends jm.headers.IfModifiedSince with ModeledHeader { +final case class `If-Modified-Since`(date: DateTime) extends jm.headers.IfModifiedSince with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) protected def companion = `If-Modified-Since` } @@ -510,21 +520,35 @@ object `If-None-Match` extends ModeledCompanion[`If-None-Match`] { def apply(first: EntityTag, more: EntityTag*): `If-None-Match` = `If-None-Match`(EntityTagRange(first +: more: _*)) } -final case class `If-None-Match`(m: EntityTagRange) extends jm.headers.IfNoneMatch with ModeledHeader { +final case class `If-None-Match`(m: EntityTagRange) extends jm.headers.IfNoneMatch with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ m protected def companion = `If-None-Match` } +// http://tools.ietf.org/html/rfc7233#section-3.2 +object `If-Range` extends ModeledCompanion[`If-Range`] { + def apply(tag: EntityTag): `If-Range` = apply(Left(tag)) + def apply(timestamp: DateTime): `If-Range` = apply(Right(timestamp)) +} +final case class `If-Range`(entityTagOrDateTime: Either[EntityTag, DateTime]) extends RequestHeader { + def renderValue[R <: Rendering](r: R): r.type = + entityTagOrDateTime match { + case Left(tag) ⇒ r ~~ tag + case Right(dateTime) ⇒ dateTime.renderRfc1123DateTimeString(r) + } + protected def companion = `If-Range` +} + // http://tools.ietf.org/html/rfc7232#section-3.4 object `If-Unmodified-Since` extends ModeledCompanion[`If-Unmodified-Since`] -final case class `If-Unmodified-Since`(date: DateTime) extends jm.headers.IfUnmodifiedSince with ModeledHeader { +final case class `If-Unmodified-Since`(date: DateTime) extends jm.headers.IfUnmodifiedSince with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) protected def companion = `If-Unmodified-Since` } // http://tools.ietf.org/html/rfc7232#section-2.2 object `Last-Modified` extends ModeledCompanion[`Last-Modified`] -final case class `Last-Modified`(date: DateTime) extends jm.headers.LastModified with ModeledHeader { +final case class `Last-Modified`(date: DateTime) extends jm.headers.LastModified with ResponseHeader { def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) protected def companion = `Last-Modified` } @@ -535,7 +559,7 @@ object Link extends ModeledCompanion[Link] { def apply(values: LinkValue*): Link = apply(immutable.Seq(values: _*)) implicit val valuesRenderer = Renderer.defaultSeqRenderer[LinkValue] // cache } -final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link with ModeledHeader { +final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link with RequestResponseHeader { import Link.valuesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ values protected def companion = Link @@ -546,7 +570,7 @@ final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link // http://tools.ietf.org/html/rfc7231#section-7.1.2 object Location extends ModeledCompanion[Location] -final case class Location(uri: Uri) extends jm.headers.Location with ModeledHeader { +final case class Location(uri: Uri) extends jm.headers.Location with ResponseHeader { def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri } protected def companion = Location @@ -558,7 +582,7 @@ final case class Location(uri: Uri) extends jm.headers.Location with ModeledHead object Origin extends ModeledCompanion[Origin] { def apply(origins: HttpOrigin*): Origin = apply(immutable.Seq(origins: _*)) } -final case class Origin(origins: immutable.Seq[HttpOrigin]) extends jm.headers.Origin with ModeledHeader { +final case class Origin(origins: immutable.Seq[HttpOrigin]) extends jm.headers.Origin with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = if (origins.isEmpty) r ~~ "null" else r ~~ origins protected def companion = Origin @@ -571,7 +595,8 @@ object `Proxy-Authenticate` extends ModeledCompanion[`Proxy-Authenticate`] { def apply(first: HttpChallenge, more: HttpChallenge*): `Proxy-Authenticate` = apply(immutable.Seq(first +: more: _*)) implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache } -final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.ProxyAuthenticate with ModeledHeader { +final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.ProxyAuthenticate + with ResponseHeader { require(challenges.nonEmpty, "challenges must not be empty") import `Proxy-Authenticate`.challengesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges @@ -583,7 +608,8 @@ final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge]) // http://tools.ietf.org/html/rfc7235#section-4.4 object `Proxy-Authorization` extends ModeledCompanion[`Proxy-Authorization`] -final case class `Proxy-Authorization`(credentials: HttpCredentials) extends jm.headers.ProxyAuthorization with ModeledHeader { +final case class `Proxy-Authorization`(credentials: HttpCredentials) extends jm.headers.ProxyAuthorization + with RequestHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials protected def companion = `Proxy-Authorization` } @@ -594,7 +620,8 @@ object Range extends ModeledCompanion[Range] { def apply(ranges: immutable.Seq[ByteRange]): Range = Range(RangeUnits.Bytes, ranges) implicit val rangesRenderer = Renderer.defaultSeqRenderer[ByteRange] // cache } -final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) extends jm.headers.Range with ModeledHeader { +final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) extends jm.headers.Range + with RequestHeader { require(ranges.nonEmpty, "ranges must not be empty") import Range.rangesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ '=' ~~ ranges @@ -604,22 +631,33 @@ final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) e def getRanges: Iterable[jm.headers.ByteRange] = ranges.asJava } +final case class RawHeader(name: String, value: String) extends jm.headers.RawHeader { + def renderInRequests = true + def renderInResponses = true + val lowercaseName = name.toRootLowerCase + def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value +} +object RawHeader { + def unapply[H <: HttpHeader](customHeader: H): Option[(String, String)] = + Some(customHeader.name -> customHeader.value) +} + object `Raw-Request-URI` extends ModeledCompanion[`Raw-Request-URI`] -final case class `Raw-Request-URI`(uri: String) extends jm.headers.RawRequestURI with ModeledHeader { +final case class `Raw-Request-URI`(uri: String) extends jm.headers.RawRequestURI with SyntheticHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ uri protected def companion = `Raw-Request-URI` } object `Remote-Address` extends ModeledCompanion[`Remote-Address`] -final case class `Remote-Address`(address: RemoteAddress) extends jm.headers.RemoteAddress with ModeledHeader { +final case class `Remote-Address`(address: RemoteAddress) extends jm.headers.RemoteAddress with SyntheticHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ address protected def companion = `Remote-Address` } // http://tools.ietf.org/html/rfc7231#section-5.5.2 object Referer extends ModeledCompanion[Referer] -final case class Referer(uri: Uri) extends jm.headers.Referer with ModeledHeader { - require(uri.fragment == None, "Referer header URI must not contain a fragment") +final case class Referer(uri: Uri) extends jm.headers.Referer with RequestHeader { + require(uri.fragment.isEmpty, "Referer header URI must not contain a fragment") require(uri.authority.userinfo.isEmpty, "Referer header URI must not contain a userinfo component") def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri } @@ -649,9 +687,8 @@ private[http] object `Sec-WebSocket-Accept` extends ModeledCompanion[`Sec-WebSoc /** * INTERNAL API */ -private[http] final case class `Sec-WebSocket-Accept`(key: String) extends ModeledHeader { +private[http] final case class `Sec-WebSocket-Accept`(key: String) extends ResponseHeader { protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key - protected def companion = `Sec-WebSocket-Accept` } @@ -665,11 +702,11 @@ private[http] object `Sec-WebSocket-Extensions` extends ModeledCompanion[`Sec-We /** * INTERNAL API */ -private[http] final case class `Sec-WebSocket-Extensions`(extensions: immutable.Seq[WebsocketExtension]) extends ModeledHeader { +private[http] final case class `Sec-WebSocket-Extensions`(extensions: immutable.Seq[WebsocketExtension]) + extends ResponseHeader { require(extensions.nonEmpty, "Sec-WebSocket-Extensions.extensions must not be empty") import `Sec-WebSocket-Extensions`.extensionsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ extensions - protected def companion = `Sec-WebSocket-Extensions` } @@ -686,9 +723,8 @@ private[http] object `Sec-WebSocket-Key` extends ModeledCompanion[`Sec-WebSocket /** * INTERNAL API */ -private[http] final case class `Sec-WebSocket-Key`(key: String) extends ModeledHeader { +private[http] final case class `Sec-WebSocket-Key`(key: String) extends RequestHeader { protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key - protected def companion = `Sec-WebSocket-Key` /** @@ -708,11 +744,11 @@ private[http] object `Sec-WebSocket-Protocol` extends ModeledCompanion[`Sec-WebS /** * INTERNAL API */ -private[http] final case class `Sec-WebSocket-Protocol`(protocols: immutable.Seq[String]) extends ModeledHeader { +private[http] final case class `Sec-WebSocket-Protocol`(protocols: immutable.Seq[String]) + extends RequestResponseHeader { require(protocols.nonEmpty, "Sec-WebSocket-Protocol.protocols must not be empty") import `Sec-WebSocket-Protocol`.protocolsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols - protected def companion = `Sec-WebSocket-Protocol` } @@ -726,14 +762,13 @@ private[http] object `Sec-WebSocket-Version` extends ModeledCompanion[`Sec-WebSo /** * INTERNAL API */ -private[http] final case class `Sec-WebSocket-Version`(versions: immutable.Seq[Int]) extends ModeledHeader { +private[http] final case class `Sec-WebSocket-Version`(versions: immutable.Seq[Int]) + extends RequestResponseHeader { require(versions.nonEmpty, "Sec-WebSocket-Version.versions must not be empty") require(versions.forall(v ⇒ v >= 0 && v <= 255), s"Sec-WebSocket-Version.versions must be in the range 0 <= version <= 255 but were $versions") import `Sec-WebSocket-Version`.versionsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ versions - - def hasVersion(versionNumber: Int): Boolean = versions.exists(_ == versionNumber) - + def hasVersion(versionNumber: Int): Boolean = versions contains versionNumber protected def companion = `Sec-WebSocket-Version` } @@ -743,7 +778,7 @@ object Server extends ModeledCompanion[Server] { def apply(first: ProductVersion, more: ProductVersion*): Server = apply(immutable.Seq(first +: more: _*)) implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache } -final case class Server(products: immutable.Seq[ProductVersion]) extends jm.headers.Server with ModeledHeader { +final case class Server(products: immutable.Seq[ProductVersion]) extends jm.headers.Server with ResponseHeader { require(products.nonEmpty, "products must not be empty") import Server.productsRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ products @@ -755,7 +790,7 @@ final case class Server(products: immutable.Seq[ProductVersion]) extends jm.head // https://tools.ietf.org/html/rfc6265 object `Set-Cookie` extends ModeledCompanion[`Set-Cookie`] -final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie with ModeledHeader { +final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie with ResponseHeader { def renderValue[R <: Rendering](r: R): r.type = r ~~ cookie protected def companion = `Set-Cookie` } @@ -770,17 +805,14 @@ final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie w * akka.http.[client|server].parsing.tls-session-info-header = on * ``` */ -final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with ScalaSessionAPI { - override def suppressRendering: Boolean = true - override def toString = s"SSL-Session-Info($session)" - def name(): String = "SSL-Session-Info" - def value(): String = "" +object `Tls-Session-Info` extends ModeledCompanion[`Tls-Session-Info`] +final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with SyntheticHeader + with ScalaSessionAPI { + def renderValue[R <: Rendering](r: R): r.type = r ~~ session.toString + protected def companion = `Tls-Session-Info` /** Java API */ - def getSession(): SSLSession = session - - def lowercaseName: String = name.toRootLowerCase - def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value + def getSession: SSLSession = session } // http://tools.ietf.org/html/rfc7230#section-3.3.1 @@ -788,7 +820,8 @@ object `Transfer-Encoding` extends ModeledCompanion[`Transfer-Encoding`] { def apply(first: TransferEncoding, more: TransferEncoding*): `Transfer-Encoding` = apply(immutable.Seq(first +: more: _*)) implicit val encodingsRenderer = Renderer.defaultSeqRenderer[TransferEncoding] // cache } -final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) extends jm.headers.TransferEncoding with ModeledHeader { +final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) extends jm.headers.TransferEncoding + with RequestResponseHeader { require(encodings.nonEmpty, "encodings must not be empty") import `Transfer-Encoding`.encodingsRenderer def isChunked: Boolean = encodings.last == TransferEncodings.chunked @@ -812,7 +845,7 @@ final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) object Upgrade extends ModeledCompanion[Upgrade] { implicit val protocolsRenderer = Renderer.defaultSeqRenderer[UpgradeProtocol] } -final case class Upgrade(protocols: immutable.Seq[UpgradeProtocol]) extends ModeledHeader { +final case class Upgrade(protocols: immutable.Seq[UpgradeProtocol]) extends RequestResponseHeader { import Upgrade.protocolsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols @@ -827,7 +860,7 @@ object `User-Agent` extends ModeledCompanion[`User-Agent`] { def apply(first: ProductVersion, more: ProductVersion*): `User-Agent` = apply(immutable.Seq(first +: more: _*)) implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache } -final case class `User-Agent`(products: immutable.Seq[ProductVersion]) extends jm.headers.UserAgent with ModeledHeader { +final case class `User-Agent`(products: immutable.Seq[ProductVersion]) extends jm.headers.UserAgent with RequestHeader { require(products.nonEmpty, "products must not be empty") import `User-Agent`.productsRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ products @@ -842,7 +875,8 @@ object `WWW-Authenticate` extends ModeledCompanion[`WWW-Authenticate`] { def apply(first: HttpChallenge, more: HttpChallenge*): `WWW-Authenticate` = apply(immutable.Seq(first +: more: _*)) implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache } -final case class `WWW-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.WWWAuthenticate with ModeledHeader { +final case class `WWW-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.WWWAuthenticate + with ResponseHeader { require(challenges.nonEmpty, "challenges must not be empty") import `WWW-Authenticate`.challengesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges @@ -858,7 +892,8 @@ object `X-Forwarded-For` extends ModeledCompanion[`X-Forwarded-For`] { def apply(first: RemoteAddress, more: RemoteAddress*): `X-Forwarded-For` = apply(immutable.Seq(first +: more: _*)) implicit val addressesRenderer = Renderer.defaultSeqRenderer[RemoteAddress] // cache } -final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) extends jm.headers.XForwardedFor with ModeledHeader { +final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) extends jm.headers.XForwardedFor + with RequestHeader { require(addresses.nonEmpty, "addresses must not be empty") import `X-Forwarded-For`.addressesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ addresses diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala index ff2a9b3961..da73e5b07a 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala @@ -356,6 +356,8 @@ class ConnectionPoolSpec extends AkkaSpec(""" } case class ConnNrHeader(nr: Int) extends CustomHeader { + def renderInRequests = false + def renderInResponses = true def name = "Conn-Nr" def value = nr.toString } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala index e0e631c5c7..6e11c02852 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala @@ -108,7 +108,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll } "retrieve the EmptyHeader" in new TestSetup() { - parseAndCache("\r\n")() shouldEqual HttpHeaderParser.EmptyHeader + parseAndCache("\r\n")() shouldEqual EmptyHeader } "retrieve a cached header with an exact header name match" in new TestSetup() { diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala index e3391378ae..06cc0f6c66 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala @@ -66,11 +66,11 @@ 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"), - Age(0), + Link(Uri("http://akka.io"), LinkParams.first), Host("spray.io", 9999))) should renderTo { """POST /abc/xyz HTTP/1.1 |X-Fancy: naa - |Age: 0 + |Link: ; rel=first |Host: spray.io:9999 |User-Agent: akka-http/1.0.0 |Content-Length: 0 @@ -262,8 +262,10 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } } "render a CustomHeader header" - { - "if suppressRendering = false" in new TestSetup(None) { + "if renderInRequests = true" in new TestSetup(None) { case class MyHeader(number: Int) extends CustomHeader { + def renderInRequests = true + def renderInResponses = false def name: String = "X-My-Header" def value: String = s"No$number" } @@ -275,10 +277,10 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |""" } } - "not if suppressRendering = true" in new TestSetup(None) { + "not if renderInRequests = false" in new TestSetup(None) { case class MyInternalHeader(number: Int) extends CustomHeader { - override def suppressRendering: Boolean = true - + def renderInRequests = false + def renderInResponses = false def name: String = "X-My-Internal-Header" def value: String = s"No$number" } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala index 9ba309c58e..41c6aac340 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala @@ -414,8 +414,10 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "render a CustomHeader header" - { - "if suppressRendering = false" in new TestSetup(None) { + "if renderInResponses = true" in new TestSetup(None) { case class MyHeader(number: Int) extends CustomHeader { + def renderInRequests = false + def renderInResponses = true def name: String = "X-My-Header" def value: String = s"No$number" } @@ -428,10 +430,10 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |""" } } - "not if suppressRendering = true" in new TestSetup(None) { + "not if renderInResponses = false" in new TestSetup(None) { case class MyInternalHeader(number: Int) extends CustomHeader { - override def suppressRendering: Boolean = true - + def renderInRequests = false + def renderInResponses = false def name: String = "X-My-Internal-Header" def value: String = s"No$number" } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala index addf17f5e5..b0e09aa644 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala @@ -14,22 +14,30 @@ object ModeledCustomHeaderSpec { //#modeled-api-key-custom-header object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] { + def renderInRequests = false + def renderInResponses = false override val name = "apiKey" override def parse(value: String) = Try(new ApiTokenHeader(value)) } final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] { + def renderInRequests = false + def renderInResponses = false override val companion = ApiTokenHeader override def value: String = token } //#modeled-api-key-custom-header object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] { + def renderInRequests = false + def renderInResponses = false override val name = "different" override def parse(value: String) = if (value contains " ") Failure(new Exception("Contains illegal whitespace!")) else Success(new DifferentHeader(value)) } final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] { + def renderInRequests = false + def renderInResponses = false override val companion = DifferentHeader override def value = token } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala index 68fe36a603..8ecbc91bbd 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala @@ -47,8 +47,8 @@ private[http] object ExtractionMap { def addAll(values: Map[RequestVal[_], Any]): ExtractionMap = ExtractionMap(map ++ values) - // CustomHeader methods - override def suppressRendering: Boolean = true + def renderInRequests = false + def renderInResponses = false def name(): String = "ExtractedValues" def value(): String = "" }