zio.http.Header.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package zio.http
import java.net.URI
import java.nio.charset.{Charset, UnsupportedCharsetException}
import java.time.ZonedDateTime
import java.util.Base64
import java.util.concurrent.ConcurrentHashMap
import scala.annotation.tailrec
import scala.collection.mutable
import scala.util.control.NonFatal
import scala.util.matching.Regex
import scala.util.{Either, Failure, Success, Try}
import zio.Config.Secret
import zio._
import zio.http.codec.RichTextCodec
import zio.http.internal.DateEncoding
sealed trait Header {
type Self <: Header
def self: Self
def headerType: Header.HeaderType.Typed[Self]
def headerName: String = headerType.name
def renderedValue: String = headerType.render(self)
private[http] def headerNameAsCharSequence: CharSequence = headerName
private[http] def renderedValueAsCharSequence: CharSequence = renderedValue
def untyped: Header.Custom = Header.Custom(headerName, renderedValue)
}
object Header {
sealed trait HeaderType {
type HeaderValue <: Header
def name: String
def parse(value: String): Either[String, HeaderValue]
def render(value: HeaderValue): String
}
object HeaderType {
type Typed[HV] = HeaderType { type HeaderValue = HV }
}
final case class Custom(customName: CharSequence, value: CharSequence) extends Header {
override type Self = Custom
override def self: Self = this
override def headerType: HeaderType.Typed[Custom] = new Header.HeaderType {
override type HeaderValue = Custom
override def name: String = self.customName.toString
override def parse(value: String): Either[String, HeaderValue] = Right(Custom(self.customName, value))
override def render(value: HeaderValue): String = value.value.toString
}
private[http] override def headerNameAsCharSequence: CharSequence = customName
private[http] override def renderedValueAsCharSequence: CharSequence = value
override def hashCode(): Int = {
var h = 0
val kLength = customName.length()
var i = 0
while (i < kLength) {
h = 17 * h + customName.charAt(i)
i = i + 1
}
i = 0
val vLength = value.length()
while (i < vLength) {
h = 17 * h + value.charAt(i)
i = i + 1
}
h
}
override def equals(that: Any): Boolean = {
that match {
case Custom(k, v) =>
def eqs(l: CharSequence, r: CharSequence): Boolean = {
if (l.length() != r.length()) false
else {
var i = 0
var equal = true
while (i < l.length()) {
if (l.charAt(i) != r.charAt(i)) {
equal = false
i = l.length()
}
i = i + 1
}
equal
}
}
eqs(self.customName, k) && eqs(self.value, v)
case _ => false
}
}
override def toString: String = (customName, value).toString()
}
final case class Accept(mimeTypes: NonEmptyChunk[Accept.MediaTypeWithQFactor]) extends Header {
override type Self = Accept
override def self: Self = this
override def headerType: HeaderType.Typed[Accept] = Accept
}
object Accept extends HeaderType {
override type HeaderValue = Accept
override def name: String = "accept"
/**
* The Accept header value one or more MIME types optionally weighed with
* quality factor.
*/
final case class MediaTypeWithQFactor(mediaType: MediaType, qFactor: Option[Double])
object MediaTypeWithQFactor {
implicit val ordering: Ordering[MediaTypeWithQFactor] =
Ordering.by[MediaTypeWithQFactor, Double](_.qFactor.getOrElse(1.0)).reverse
}
def apply(mediaType: MediaType, qFactor: Option[Double]): Accept =
Accept(NonEmptyChunk(MediaTypeWithQFactor(mediaType, qFactor)))
def apply(first: MediaType, rest: MediaType*): Accept =
Accept(NonEmptyChunk(first, rest: _*).map(MediaTypeWithQFactor(_, None)))
def apply(first: MediaTypeWithQFactor, rest: MediaTypeWithQFactor*): Accept =
Accept(NonEmptyChunk(first, rest: _*))
def parse(value: String): Either[String, Accept] = {
val acceptHeaderValues =
Chunk
.fromArray(
value
.split(",")
.map(_.trim)
.map { subValue =>
MediaType
.forContentType(subValue)
.map(mt => MediaTypeWithQFactor(mt, extractQFactor(mt)))
.getOrElse {
MediaType
.parseCustomMediaType(subValue)
.map(mt => MediaTypeWithQFactor(mt, extractQFactor(mt)))
.orNull
}
},
)
val valid = acceptHeaderValues.filter(_ != null)
if (valid.size != acceptHeaderValues.size) Left("Invalid Accept header")
else NonEmptyChunk.fromChunk(acceptHeaderValues).toRight("Invalid Accept header").map(Accept(_))
}
def render(header: Accept): String =
header.mimeTypes.map { case MediaTypeWithQFactor(mime, maybeQFactor) =>
s"${mime.fullType}${maybeQFactor.map(qFactor => s";q=$qFactor").getOrElse("")}"
}.mkString(", ")
private def extractQFactor(mediaType: MediaType): Option[Double] =
mediaType.parameters.get("q").flatMap(qFactor => Try(qFactor.toDouble).toOption)
}
/**
* Represents an AcceptEncoding header value.
*/
sealed trait AcceptEncoding extends Header {
override type Self = AcceptEncoding
override def self: Self = this
override def headerType: HeaderType.Typed[AcceptEncoding] = AcceptEncoding
val raw: String
}
object AcceptEncoding extends HeaderType {
override type HeaderValue = AcceptEncoding
override def name: String = "accept-encoding"
/**
* A compression format that uses the Brotli algorithm.
*/
final case class Br(weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = "br"
}
/**
* A compression format that uses the Lempel-Ziv-Welch (LZW) algorithm.
*/
final case class Compress(weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = "compress"
}
/**
* A compression format that uses the zlib structure with the deflate
* compression algorithm.
*/
final case class Deflate(weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = "deflate"
}
/**
* A compression format that uses the Lempel-Ziv coding (LZ77) with a 32-bit
* CRC.
*/
final case class GZip(weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = "gzip"
}
/**
* Indicates the identity function (that is, without modification or
* compression). This value is always considered as acceptable, even if
* omitted.
*/
final case class Identity(weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = "identity"
}
/**
* Maintains a chunk of AcceptEncoding values.
*/
final case class Multiple(encodings: NonEmptyChunk[AcceptEncoding]) extends AcceptEncoding {
override val raw: String = encodings.map(_.raw).mkString(",")
}
/**
* Matches any content encoding not already listed in the header. This is
* the default value if the header is not present.
*/
final case class NoPreference(weight: Option[Double]) extends AcceptEncoding {
override val raw: String = "*"
}
/**
* Represent an unknown encoding
*/
final case class Unknown(value: String, weight: Option[Double] = None) extends AcceptEncoding {
override val raw: String = value
}
private def identifyEncodingFull(raw: String): AcceptEncoding = {
val index = raw.indexOf(";q=")
if (index == -1)
identifyEncoding(raw)
else {
identifyEncoding(raw.substring(0, index), weight = Try(raw.substring(index + 3).toDouble).toOption)
}
}
private def identifyEncoding(raw: String, weight: Option[Double] = None): AcceptEncoding = {
raw.trim match {
case "br" => Br(weight)
case "compress" => Compress(weight)
case "deflate" => Deflate(weight)
case "gzip" => GZip(weight)
case "identity" => Identity(weight)
case "*" => NoPreference(weight)
case other => Unknown(other, weight)
}
}
def apply(first: AcceptEncoding, rest: AcceptEncoding*): AcceptEncoding =
Multiple(NonEmptyChunk(first, rest: _*))
def parse(value: String): Either[String, AcceptEncoding] = {
val index = value.indexOf(",")
@tailrec def loop(
value: String,
index: Int,
acc: Chunk[AcceptEncoding],
): Chunk[AcceptEncoding] = {
if (index == -1) {
acc :+ identifyEncodingFull(value)
} else {
val valueChunk = value.substring(0, index)
val remaining = value.substring(index + 1)
val nextIndex = remaining.indexOf(",")
loop(
remaining,
nextIndex,
acc :+ identifyEncodingFull(valueChunk),
)
}
}
if (index == -1)
Right(identifyEncodingFull(value))
else
NonEmptyChunk.fromChunk(loop(value, index, Chunk.empty[AcceptEncoding])) match {
case Some(value) => Right(Multiple(value))
case None => Left(s"Invalid accept encoding ($value)")
}
}
def render(encoding: AcceptEncoding): String =
encoding match {
case b @ Br(weight) => weight.fold(b.raw)(value => s"${b.raw};q=$value")
case c @ Compress(weight) => weight.fold(c.raw)(value => s"${c.raw};q=$value")
case d @ Deflate(weight) => weight.fold(d.raw)(value => s"${d.raw};q=$value")
case g @ GZip(weight) => weight.fold(g.raw)(value => s"${g.raw};q=$value")
case i @ Identity(weight) => weight.fold(i.raw)(value => s"${i.raw};q=$value")
case Multiple(encodings) => encodings.map(render).mkString(",")
case n @ NoPreference(weight) => weight.fold(n.raw)(value => s"${n.raw};q=$value")
case Unknown(u, weight) => weight.fold(u)(value => s"$u;q=$value")
}
}
/**
* The Accept-Language request HTTP header indicates the natural language and
* locale that the client prefers.
*/
sealed trait AcceptLanguage extends Header {
override type Self = AcceptLanguage
override def self: Self = this
override def headerType: HeaderType.Typed[AcceptLanguage] = AcceptLanguage
}
object AcceptLanguage extends HeaderType {
override type HeaderValue = AcceptLanguage
override def name: String = "accept-language"
case class Single(language: String, weight: Option[Double]) extends AcceptLanguage
case class Multiple(languages: NonEmptyChunk[AcceptLanguage]) extends AcceptLanguage
case object Any extends AcceptLanguage
def parse(value: String): Either[String, AcceptLanguage] = {
@tailrec def loop(index: Int, value: String, acc: Chunk[AcceptLanguage]): Chunk[AcceptLanguage] = {
if (index == -1) acc :+ parseAcceptedLanguage(value.trim)
else {
val valueChunk = value.substring(0, index)
val valueRemaining = value.substring(index + 1)
val newIndex = valueRemaining.indexOf(',')
loop(
newIndex,
valueRemaining,
acc :+ parseAcceptedLanguage(valueChunk.trim),
)
}
}
if (validCharacters.findFirstIn(value).isEmpty) Left("Accept-Language contains invalid characters")
else if (value.isEmpty) Left("Accept-Language cannot be empty")
else if (value == "*") Right(AcceptLanguage.Any)
else
NonEmptyChunk.fromChunk(loop(value.indexOf(','), value, Chunk.empty)) match {
case Some(value) => Right(Multiple(value))
case None => Left("Accept-Language cannot be empty")
}
}
def render(acceptLanguage: AcceptLanguage): String =
acceptLanguage match {
case Single(language, weight) =>
val weightString = weight match {
case Some(w) => s";q=$w"
case None => ""
}
s"$language$weightString"
case Multiple(languages) => languages.map(render).mkString(",")
case Any => "*"
}
/**
* Allowed characters in the header are 0-9, A-Z, a-z, space or *,-.;=
*/
private val validCharacters: Regex = "^[0-9a-zA-Z *,\\-.;=]+$".r
private def parseAcceptedLanguage(value: String): AcceptLanguage = {
val weightIndex = value.indexOf(";q=")
if (weightIndex != -1) {
val language = value.substring(0, weightIndex)
val weight = value.substring(weightIndex + 3)
Single(
language,
Try(weight.toDouble).toOption
.filter(w => w >= 0.0 && w <= 1.0),
)
} else Single(value, None)
}
}
/**
* The Accept-Patch response HTTP header advertises which media-type the
* server is able to understand in a PATCH request.
*/
final case class AcceptPatch(mediaTypes: NonEmptyChunk[MediaType]) extends Header {
override type Self = AcceptPatch
override def self: Self = this
override def headerType: HeaderType.Typed[AcceptPatch] = AcceptPatch
}
object AcceptPatch extends HeaderType {
override type HeaderValue = AcceptPatch
override def name: String = "accept-patch"
def parse(value: String): Either[String, AcceptPatch] =
if (value.nonEmpty) {
val parsedMediaTypes = Chunk
.fromArray(
value
.split(",")
.map(mediaTypeStr =>
MediaType
.forContentType(mediaTypeStr)
.getOrElse(
MediaType
.parseCustomMediaType(mediaTypeStr)
.orNull,
),
),
)
.filter(_ != null)
NonEmptyChunk.fromChunk(parsedMediaTypes) match {
case Some(value) => Right(AcceptPatch(value))
case None => Left("Invalid Accept-Patch header")
}
} else Left("Accept-Patch header cannot be empty")
def render(acceptPatch: AcceptPatch): String =
acceptPatch.mediaTypes.map(_.fullType).mkString(",")
}
/**
* The Accept-Ranges HTTP response header is a marker used by the server to
* advertise its support for partial requests from the client for file
* downloads. The value of this field indicates the unit that can be used to
* define a range. By default the RFC 7233 specification supports only 2
* possible values.
*/
sealed trait AcceptRanges extends Header {
override type Self = AcceptRanges
override def self: Self = this
override def headerType: HeaderType.Typed[AcceptRanges] = AcceptRanges
val encodedName: String
}
object AcceptRanges extends HeaderType {
override type HeaderValue = AcceptRanges
override def name: String = "accept-ranges"
case object Bytes extends AcceptRanges {
override val encodedName = "bytes"
}
case object None extends AcceptRanges {
override val encodedName = "none"
}
def parse(value: String): Either[String, AcceptRanges] =
value match {
case Bytes.encodedName => Right(Bytes)
case None.encodedName => Right(None)
case _ => Left("Invalid Accept-Ranges header")
}
def render(acceptRangers: AcceptRanges): String =
acceptRangers.encodedName
}
sealed trait AccessControlAllowCredentials extends Header {
override type Self = AccessControlAllowCredentials
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlAllowCredentials] = AccessControlAllowCredentials
}
object AccessControlAllowCredentials extends HeaderType {
override type HeaderValue = AccessControlAllowCredentials
override def name: String = "access-control-allow-credentials"
/**
* The Access-Control-Allow-Credentials header is sent in response to a
* preflight request which includes the Access-Control-Request-Headers to
* indicate whether or not the actual request can be made using credentials.
*/
case object Allow extends AccessControlAllowCredentials
/**
* The Access-Control-Allow-Credentials header is not sent in response to a
* preflight request.
*/
case object DoNotAllow extends AccessControlAllowCredentials
def allow(value: Boolean): AccessControlAllowCredentials =
value match {
case true => Allow
case false => DoNotAllow
}
def parse(value: String): Either[String, AccessControlAllowCredentials] =
Right {
value match {
case "true" => Allow
case "false" => DoNotAllow
case _ => DoNotAllow
}
}
def render(
accessControlAllowCredentials: AccessControlAllowCredentials,
): String =
accessControlAllowCredentials match {
case Allow => "true"
case DoNotAllow => "false"
}
}
sealed trait AccessControlAllowHeaders extends Header {
override type Self = AccessControlAllowHeaders
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlAllowHeaders] = AccessControlAllowHeaders
}
/**
* The Access-Control-Allow-Headers response header is used in response to a
* preflight request which includes the Access-Control-Request-Headers to
* indicate which HTTP headers can be used during the actual request.
*/
object AccessControlAllowHeaders extends HeaderType {
override type HeaderValue = AccessControlAllowHeaders
override def name: String = "access-control-allow-headers"
final case class Some(values: NonEmptyChunk[String]) extends AccessControlAllowHeaders
case object All extends AccessControlAllowHeaders
case object None extends AccessControlAllowHeaders
def apply(headers: String*) =
NonEmptyChunk.fromIterableOption(headers) match {
case scala.Some(value) => Some(value)
case scala.None => None
}
def parse(value: String): Either[String, AccessControlAllowHeaders] =
Right {
value match {
case "" => None
case "*" => All
case headerNames =>
NonEmptyChunk.fromChunk(
Chunk.fromArray(
headerNames
.split(",")
.map(_.trim),
),
) match {
case scala.Some(value) => Some(value)
case scala.None => None
}
}
}
def render(accessControlAllowHeaders: AccessControlAllowHeaders): String =
accessControlAllowHeaders match {
case Some(value) => value.mkString(", ")
case All => "*"
case None => ""
}
}
sealed trait AccessControlAllowMethods extends Header {
override type Self = AccessControlAllowMethods
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlAllowMethods] = AccessControlAllowMethods
def contains(method: Method): Boolean =
this match {
case AccessControlAllowMethods.All => true
case AccessControlAllowMethods.Some(methods) => methods.contains(method)
case AccessControlAllowMethods.None => false
}
}
object AccessControlAllowMethods extends HeaderType {
override type HeaderValue = AccessControlAllowMethods
override def name: String = "access-control-allow-methods"
final case class Some(methods: NonEmptyChunk[Method]) extends AccessControlAllowMethods
case object All extends AccessControlAllowMethods
case object None extends AccessControlAllowMethods
def apply(methods: Method*): AccessControlAllowMethods =
NonEmptyChunk.fromIterableOption(methods) match {
case scala.Some(value) => Some(value)
case scala.None => None
}
def parse(value: String): Either[String, AccessControlAllowMethods] = {
Right {
value match {
case "" => None
case "*" => All
case methodNames =>
NonEmptyChunk.fromChunk(
Chunk.fromArray(
methodNames
.split(",")
.map(_.trim)
.map(Method.fromString),
),
) match {
case scala.Some(value) => Some(value)
case scala.None => None
}
}
}
}
def render(accessControlAllowMethods: AccessControlAllowMethods): String =
accessControlAllowMethods match {
case Some(methods) => methods.map(_.toString()).mkString(", ")
case All => "*"
case None => ""
}
}
/**
* The Access-Control-Allow-Origin response header indicates whether the
* response can be shared with requesting code from the given origin.
*
* For requests without credentials, the literal value "*" can be specified as
* a wildcard; the value tells browsers to allow requesting code from any
* origin to access the resource. Attempting to use the wildcard with
* credentials results in an error.
*
* Specifies an origin. Only a single origin can be specified. If the
* server supports clients from multiple origins, it must return the origin
* for the specific client making the request.
*
* null Specifies the origin "null".
*/
sealed trait AccessControlAllowOrigin extends Header {
override type Self = AccessControlAllowOrigin
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlAllowOrigin] = AccessControlAllowOrigin
}
object AccessControlAllowOrigin extends HeaderType {
override type HeaderValue = AccessControlAllowOrigin
override def name: String = "access-control-allow-origin"
final case class Specific(origin: Origin) extends AccessControlAllowOrigin
case object All extends AccessControlAllowOrigin
def apply(scheme: String, host: String, port: Option[Int] = None): AccessControlAllowOrigin =
Specific(Origin(scheme, host, port))
def parse(value: String): Either[String, AccessControlAllowOrigin] = {
if (value == "*") {
Right(AccessControlAllowOrigin.All)
} else {
Origin.parse(value).map { origin =>
AccessControlAllowOrigin.Specific(origin)
}
}
}
def render(accessControlAllowOrigin: AccessControlAllowOrigin): String =
accessControlAllowOrigin match {
case Specific(origin) => Origin.render(origin)
case All => "*"
}
}
/**
* The Access-Control-Expose-Headers response header allows a server to
* indicate which response headers should be made available to scripts running
* in the browser, in response to a cross-origin request.
*/
sealed trait AccessControlExposeHeaders extends Header {
override type Self = AccessControlExposeHeaders
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlExposeHeaders] = AccessControlExposeHeaders
}
object AccessControlExposeHeaders extends HeaderType {
override type HeaderValue = AccessControlExposeHeaders
override def name: String = "access-control-expose-headers"
final case class Some(values: NonEmptyChunk[CharSequence]) extends AccessControlExposeHeaders
case object All extends AccessControlExposeHeaders
case object None extends AccessControlExposeHeaders
def parse(value: String): Either[String, AccessControlExposeHeaders] = {
Right {
value match {
case "" => None
case "*" => All
case headerNames =>
NonEmptyChunk.fromChunk(
Chunk.fromArray(
headerNames
.split(",")
.map(_.trim),
),
) match {
case scala.Some(value) => Some(value)
case scala.None => None
}
}
}
}
def render(accessControlExposeHeaders: AccessControlExposeHeaders): String =
accessControlExposeHeaders match {
case Some(value) => value.mkString(", ")
case All => "*"
case None => ""
}
}
/**
* The Access-Control-Max-Age response header indicates how long the results
* of a preflight request (that is the information contained in the
* Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can
* be cached.
*
* Maximum number of seconds the results can be cached, as an unsigned
* non-negative integer. Firefox caps this at 24 hours (86400 seconds).
* Chromium (prior to v76) caps at 10 minutes (600 seconds). Chromium
* (starting in v76) caps at 2 hours (7200 seconds). The default value is 5
* seconds.
*/
final case class AccessControlMaxAge(duration: Duration) extends Header {
override type Self = AccessControlMaxAge
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlMaxAge] = AccessControlMaxAge
}
object AccessControlMaxAge extends HeaderType {
override type HeaderValue = AccessControlMaxAge
override def name: String = "access-control-max-age"
def parse(seconds: String): Either[String, AccessControlMaxAge] =
Try(seconds.toLong).toOption.flatMap { long =>
if (long > -1) Some(AccessControlMaxAge(long.seconds))
else None
}.toRight("Invalid Access-Control-Max-Age header value")
def render(accessControlMaxAge: AccessControlMaxAge): String = {
accessControlMaxAge.duration.getSeconds.toString
}
}
final case class AccessControlRequestHeaders(values: NonEmptyChunk[String]) extends Header {
override type Self = AccessControlRequestHeaders
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlRequestHeaders] = AccessControlRequestHeaders
}
/**
* The Access-Control-Request-Headers request header is used by browsers when
* issuing a preflight request to let the server know which HTTP headers the
* client might send when the actual request is made (such as with
* setRequestHeader()). The complementary server-side header of
* Access-Control-Allow-Headers will answer this browser-side header.
*/
object AccessControlRequestHeaders extends HeaderType {
override type HeaderValue = AccessControlRequestHeaders
override def name: String = "access-control-request-headers"
def parse(values: String): Either[String, AccessControlRequestHeaders] = {
NonEmptyChunk.fromChunk(Chunk.fromArray(values.trim().split(",")).filter(_.nonEmpty)) match {
case None => Left("AccessControlRequestHeaders cannot be empty")
case Some(xs) => Right(AccessControlRequestHeaders(xs))
}
}
def render(headers: AccessControlRequestHeaders): String =
headers.values.mkString(",")
}
final case class AccessControlRequestMethod(method: Method) extends Header {
override type Self = AccessControlRequestMethod
override def self: Self = this
override def headerType: HeaderType.Typed[AccessControlRequestMethod] = AccessControlRequestMethod
}
object AccessControlRequestMethod extends HeaderType {
override type HeaderValue = AccessControlRequestMethod
override def name: String = "access-control-request-method"
def parse(value: String): Either[String, AccessControlRequestMethod] = {
val method = Method.fromString(value)
if (method == Method.CUSTOM(value)) Left(s"Invalid Access-Control-Request-Method")
else Right(AccessControlRequestMethod(method))
}
def render(requestMethod: AccessControlRequestMethod): String =
requestMethod.method.name
}
/**
* Age header value.
*/
final case class Age(duration: Duration) extends Header {
override type Self = Age
override def self: Self = this
override def headerType: HeaderType.Typed[Age] = Age
}
object Age extends HeaderType {
override type HeaderValue = Age
override def name: String = "age"
def parse(value: String): Either[String, Age] =
Try(value.trim.toInt) match {
case Failure(_) => Left(s"Invalid Age")
case Success(value) if value > 0 => Right(Age(value.seconds))
case Success(_) => Left(s"Negative Age")
}
def render(age: Age): String =
age.duration.getSeconds.toString
}
/**
* The Allow header must be sent if the server responds with a 405 Method Not
* Allowed status code to indicate which request methods can be used.
*/
final case class Allow(methods: NonEmptyChunk[Method]) extends Header {
override type Self = Allow
override def self: Self = this
override def headerType: HeaderType.Typed[Allow] = Allow
}
object Allow extends HeaderType {
override type HeaderValue = Allow
override def name: String = "allow"
val OPTIONS: Allow = Allow(NonEmptyChunk.single(Method.OPTIONS))
val GET: Allow = Allow(NonEmptyChunk.single(Method.GET))
val HEAD: Allow = Allow(NonEmptyChunk.single(Method.HEAD))
val POST: Allow = Allow(NonEmptyChunk.single(Method.POST))
val PUT: Allow = Allow(NonEmptyChunk.single(Method.PUT))
val PATCH: Allow = Allow(NonEmptyChunk.single(Method.PATCH))
val DELETE: Allow = Allow(NonEmptyChunk.single(Method.DELETE))
val TRACE: Allow = Allow(NonEmptyChunk.single(Method.TRACE))
val CONNECT: Allow = Allow(NonEmptyChunk.single(Method.CONNECT))
def parse(value: String): Either[String, Allow] = {
@tailrec def loop(index: Int, value: String, acc: Chunk[Method]): Either[String, Chunk[Method]] = {
if (value.isEmpty) Left("Invalid Allow header: empty value")
else if (index == -1) {
Method.fromString(value.trim) match {
case Method.CUSTOM(name) => Left(s"Invalid Allow method: $name")
case method: Method => Right(acc :+ method)
}
} else {
val valueChunk = value.substring(0, index)
val valueRemaining = value.substring(index + 1)
val newIndex = valueRemaining.indexOf(',')
Method.fromString(valueChunk.trim) match {
case Method.CUSTOM(name) =>
Left(s"Invalid Allow method: $name")
case method: Method =>
loop(
newIndex,
valueRemaining,
acc :+ method,
)
}
}
}
loop(value.indexOf(','), value, Chunk.empty).flatMap { methods =>
NonEmptyChunk.fromChunk(methods) match {
case Some(methods) => Right(Allow(methods))
case None => Left("Invalid Allow header: empty value")
}
}
}
def render(allow: Allow): String =
allow.methods.map(_.name).mkString(", ")
}
sealed trait AuthenticationScheme {
val name: String
}
object AuthenticationScheme {
case object Basic extends AuthenticationScheme {
override val name: String = "Basic"
}
case object Bearer extends AuthenticationScheme {
override val name: String = "Bearer"
}
case object Digest extends AuthenticationScheme {
override val name: String = "Digest"
}
case object HOBA extends AuthenticationScheme {
override val name: String = "HOBA"
}
case object Mutual extends AuthenticationScheme {
override val name: String = "Mutual"
}
case object Negotiate extends AuthenticationScheme {
override val name: String = "Negotiate"
}
case object OAuth extends AuthenticationScheme {
override val name: String = "OAuth"
}
case object Scram extends AuthenticationScheme {
override val name: String = "SCRAM"
}
case object ScramSha1 extends AuthenticationScheme {
override val name: String = "SCRAM-SHA-1"
}
case object ScramSha256 extends AuthenticationScheme {
override val name: String = "SCRAM-SHA-256"
}
case object Vapid extends AuthenticationScheme {
override val name: String = "vapid"
}
case object `AWS4-HMAC-SHA256` extends AuthenticationScheme {
override val name: String = "AWS4-HMAC-SHA256"
}
def parse(name: String): Either[String, AuthenticationScheme] = {
name.trim.toUpperCase match {
case "BASIC" => Right(Basic)
case "BEARER" => Right(Bearer)
case "DIGEST" => Right(Digest)
case "HOBA" => Right(HOBA)
case "MUTUAL" => Right(Mutual)
case "NEGOTIATE" => Right(Negotiate)
case "OAUTH" => Right(OAuth)
case "SCRAM" => Right(Scram)
case "SCRAM-SHA-1" => Right(ScramSha1)
case "SCRAM-SHA-256" => Right(ScramSha256)
case "VAPID" => Right(Vapid)
case "AWS4-HMAC-SHA256" => Right(`AWS4-HMAC-SHA256`)
case name: String => Left(s"Unsupported authentication scheme: $name")
}
}
def render(authenticationScheme: AuthenticationScheme): String =
authenticationScheme.name
}
/**
* Authorization header value.
*
* The Authorization header value contains one of the auth schemes
*/
sealed trait Authorization extends Header {
override type Self = Authorization
override def self: Self = this
override def headerType: HeaderType.Typed[Authorization] = Authorization
}
object Authorization extends HeaderType {
override type HeaderValue = Authorization
override def name: String = "authorization"
final case class Basic(username: String, password: Secret) extends Authorization
object Basic {
def apply(username: String, password: String): Basic = new Basic(username, Secret(password))
}
final case class Digest(
response: String,
username: String,
realm: String,
uri: URI,
opaque: String,
algorithm: String,
qop: String,
cnonce: String,
nonce: String,
nc: Int,
userhash: Boolean,
) extends Authorization
final case class Bearer(token: Secret) extends Authorization
object Bearer {
def apply(token: String): Bearer = Bearer(Secret(token))
}
final case class Unparsed(authScheme: String, authParameters: Secret) extends Authorization
object Unparsed {
def apply(authScheme: String, authParameters: String): Unparsed = Unparsed(authScheme, Secret(authParameters))
}
def parse(value: String): Either[String, Authorization] = {
val parts = value.split(" ").filter(_.nonEmpty)
val nParts = parts.length
if (nParts == 1) {
Right(Unparsed("", parts(0)))
} else if (nParts >= 2) {
parts(0).toLowerCase match {
case "basic" => parseBasic(parts(1))
case "digest" => parseDigest(parts.tail.mkString(" "))
case "bearer" => Right(Bearer(parts(1)))
case _ => Right(Unparsed(parts(0), parts.tail.mkString(" ")))
}
} else Left(s"Invalid Authorization header value: $value")
}
def render(header: Authorization): String = header match {
case Basic(username, password) =>
s"Basic ${Base64.getEncoder.encodeToString((s"$username:" ++: password.value).map(_.toByte).toArray)}"
case Digest(response, username, realm, uri, opaque, algo, qop, cnonce, nonce, nc, userhash) =>
s"""Digest response="$response",username="$username",realm="$realm",uri=${uri.toString},opaque="$opaque",algorithm=$algo,""" +
s"""qop=$qop,cnonce="$cnonce",nonce="$nonce",nc=$nc,userhash=${userhash.toString}"""
case Bearer(token) => s"Bearer ${token.value.asString}"
case Unparsed(scheme, params) => s"$scheme ${params.value.asString}".strip()
}
private def parseBasic(value: String): Either[String, Authorization] = {
try {
val partsOfBasic = new String(Base64.getDecoder.decode(value)).split(":")
if (partsOfBasic.length == 2) {
Right(Basic(partsOfBasic(0), Secret(partsOfBasic(1))))
} else {
Left("Basic Authorization header value is not in the format username:password")
}
} catch {
case _: IllegalArgumentException =>
Left("Basic Authorization header value is not a valid base64 encoded string")
}
}
private final val quotationMarkChar = "\""
private final val commaChar = ","
private final val equalsChar = '='
// https://datatracker.ietf.org/doc/html/rfc7616
private def parseDigest(value: String): Either[String, Authorization] =
try {
def parseDigestKey(index: Int): (String, Int) = {
val equalsIndex = value.indexOf(equalsChar, index)
val currentKey = value.substring(index, equalsIndex).toLowerCase.trim
(currentKey, equalsIndex + 1)
}
def parseDigestValue(index: Int): (String, Int) = {
val endChar = if (value(index) == '"') quotationMarkChar else commaChar
val maybeEndCharIndex = value.indexOf(endChar, index + 1)
val endCharIndex = if (maybeEndCharIndex == -1) value.length else maybeEndCharIndex
val currentValue = value.substring(index, endCharIndex).stripPrefix(quotationMarkChar)
val newIndex = if (endChar == commaChar) endCharIndex + 1 else endCharIndex + 2
(currentValue, newIndex)
}
@tailrec
def go(index: Int = 0, paramsAcc: Map[String, String] = Map.empty): Map[String, String] = if (
index < value.length
) {
val (key, tmpIndex) = parseDigestKey(index)
val (value, newIndex) = parseDigestValue(tmpIndex)
go(newIndex, paramsAcc + (key -> value))
} else paramsAcc
val params = go()
val maybeDigest = for {
response <- params.get("response")
userhash <- params.get("userhash").flatMap(v => Try(v.toBoolean).toOption).orElse(Some(false))
username = params.get("username")
usernameStar = params.get("username*")
usernameFinal <-
if (username.isDefined && usernameStar.isEmpty) {
username
} else if (username.isEmpty && usernameStar.isDefined && !userhash) {
usernameStar
} else {
None
}
realm <- params.get("realm")
uri <- params.get("uri").flatMap(v => Try(new URI(v)).toOption)
opaque <- params.get("opaque")
algo <- params.get("algorithm")
qop <- params.get("qop")
cnonce <- params.get("cnonce")
nonce <- params.get("nonce")
nc <- params.get("nc").flatMap(v => Try(v.toInt).toOption)
} yield Digest(response, usernameFinal, realm, uri, opaque, algo, qop, cnonce, nonce, nc, userhash)
maybeDigest
.toRight("Digest Authorization header value is not in the correct format")
} catch {
case _: IndexOutOfBoundsException =>
Left("Digest Authorization header value is not in the correct format")
}
}
/**
* CacheControl header value.
*/
sealed trait CacheControl extends Header {
override type Self = CacheControl
override def self: Self = this
override def headerType: HeaderType.Typed[CacheControl] = CacheControl
val raw: String
}
object CacheControl extends HeaderType {
override type HeaderValue = CacheControl
override def name: String = "cache-control"
/**
* The immutable response directive indicates that the response will not be
* updated while it's fresh
*/
case object Immutable extends CacheControl {
override val raw: String = "immutable"
}
/**
* The max-age=N response directive indicates that the response remains
* fresh until N seconds after the response is generated.
*
* The max-age=N request directive indicates that the client allows a stored
* response that is generated on the origin server within N seconds
*/
final case class MaxAge(freshForSeconds: Int) extends CacheControl {
override val raw: String = "max-age"
}
/**
* The max-stale=N request directive indicates that the client allows a
* stored response that is stale within N seconds.
*/
final case class MaxStale(staleWithinSeconds: Int) extends CacheControl {
override val raw: String = "max-stale"
}
/**
* The min-fresh=N request directive indicates that the client allows a
* stored response that is fresh for at least N seconds.
*/
final case class MinFresh(freshAtLeastSeconds: Int) extends CacheControl {
override val raw: String = "min-fresh"
}
/**
* The must-revalidate response directive indicates that the response can be
* stored in caches and can be reused while fresh. If the response becomes
* stale, it must be validated with the origin server before reuse.
*/
case object MustRevalidate extends CacheControl {
override val raw: String = "must-revalidate"
}
/**
* The must-understand response directive indicates that a cache should
* store the response only if it understands the requirements for caching
* based on status code.
*/
case object MustUnderstand extends CacheControl {
override val raw: String = "must-understand"
}
/**
* Maintains a chunk of CacheControl values.
*/
final case class Multiple(values: NonEmptyChunk[CacheControl]) extends CacheControl {
override val raw: String = values.map(_.raw).mkString(",")
}
/**
* The no-cache response directive indicates that the response can be stored
* in caches, but the response must be validated with the origin server
* before each reuse.
*
* The no-cache request directive asks caches to validate the response with
* the origin server before reuse.
*/
case object NoCache extends CacheControl {
override val raw: String = "no-cache"
}
/**
* The no-store response directive indicates that any caches of any kind
* (private or shared) should not store this response.
*
* The no-store request directive allows a client to request that caches
* refrain from storing the request and corresponding response — even if the
* origin server's response could be stored.
*/
case object NoStore extends CacheControl {
override val raw: String = "no-store"
}
/**
* The no-transform indicates that any intermediary (regardless of whether
* it implements a cache) shouldn't transform the response/request contents.
*/
case object NoTransform extends CacheControl {
override val raw: String = "no-transform"
}
/**
* The client indicates that cache should obtain an already-cached response.
* If a cache has stored a response, it's reused.
*/
case object OnlyIfCached extends CacheControl {
override val raw: String = "only-if-cached"
}
/**
* The private response directive indicates that the response can be stored
* only in a private cache
*/
case object Private extends CacheControl {
override val raw: String = "private"
}
/**
* The proxy-revalidate response directive is the equivalent of
* must-revalidate, but specifically for shared caches only.
*/
case object ProxyRevalidate extends CacheControl {
override val raw: String = "proxy-revalidate"
}
/**
* The public response directive indicates that the response can be stored
* in a shared cache.
*/
case object Public extends CacheControl {
override val raw: String = "public"
}
/**
* The s-maxage response directive also indicates how long the response is
* fresh for (similar to max-age) — but it is specific to shared caches, and
* they will ignore max-age when it is present.
*/
final case class SMaxAge(freshForSeconds: Int) extends CacheControl {
override val raw: String = "s-maxage"
}
/**
* The stale-if-error response directive indicates that the cache can reuse
* a stale response when an origin server responds with an error (500, 502,
* 503, or 504).
*/
final case class StaleIfError(seconds: Int) extends CacheControl {
override val raw: String = "stale-if-error"
}
/**
* The stale-while-revalidate response directive indicates that the cache
* could reuse a stale response while it revalidates it to a cache.
*/
final case class StaleWhileRevalidate(seconds: Int) extends CacheControl {
override val raw: String = "stale-while-revalidate"
}
def parse(value: String): Either[String, CacheControl] = {
val index = value.indexOf(",")
@tailrec def loop(value: String, index: Int, acc: Chunk[CacheControl]): Either[String, Chunk[CacheControl]] = {
if (index == -1) {
identifyCacheControl(value) match {
case Left(value) => Left(value)
case Right(cacheControl) => Right(acc :+ cacheControl)
}
} else {
val valueChunk = value.substring(0, index)
val remaining = value.substring(index + 1)
val nextIndex = remaining.indexOf(",")
identifyCacheControl(valueChunk) match {
case Left(error) => Left(error)
case Right(cacheControl) =>
loop(
remaining,
nextIndex,
acc :+ cacheControl,
)
}
}
}
if (index == -1)
identifyCacheControl(value)
else
loop(value, index, Chunk.empty[CacheControl]).flatMap { cacheControls =>
NonEmptyChunk.fromChunk(cacheControls) match {
case None => Left("Cache-Control header must contain at least one value")
case Some(value) => Right(Multiple(value))
}
}
}
def render(value: CacheControl): String = {
value match {
case Immutable => Immutable.raw
case m @ MaxAge(freshForSeconds) => s"${m.raw}=$freshForSeconds"
case m @ MaxStale(staleWithinSeconds) => s"${m.raw}=$staleWithinSeconds"
case m @ MinFresh(freshAtLeastSeconds) => s"${m.raw}=$freshAtLeastSeconds"
case MustRevalidate => MustRevalidate.raw
case MustUnderstand => MustUnderstand.raw
case Multiple(values) => values.map(render).mkString(",")
case NoCache => NoCache.raw
case NoStore => NoStore.raw
case NoTransform => NoTransform.raw
case OnlyIfCached => OnlyIfCached.raw
case Private => Private.raw
case ProxyRevalidate => ProxyRevalidate.raw
case Public => Public.raw
case s @ SMaxAge(freshForSeconds) => s"${s.raw}=$freshForSeconds"
case s @ StaleIfError(seconds) => s"${s.raw}=$seconds"
case s @ StaleWhileRevalidate(seconds) => s"${s.raw}=$seconds"
}
}
private def identifyCacheControl(value: String): Either[String, CacheControl] = {
val index = value.indexOf("=")
if (index == -1)
identifyCacheControlValue(value)
else
identifyCacheControlValue(value.substring(0, index), Try(value.substring(index + 1).toInt).toOption)
}
private def identifyCacheControlValue(value: String, seconds: Option[Int] = None): Either[String, CacheControl] = {
val trimmedValue = value.trim()
trimmedValue match {
case "max-age" => Right(MaxAge(seconds.getOrElse(0)))
case "max-stale" => Right(MaxStale(seconds.getOrElse(0)))
case "min-fresh" => Right(MinFresh(seconds.getOrElse(0)))
case "s-maxage" => Right(SMaxAge(seconds.getOrElse(0)))
case NoCache.raw => Right(NoCache)
case NoStore.raw => Right(NoStore)
case NoTransform.raw => Right(NoTransform)
case OnlyIfCached.raw => Right(OnlyIfCached)
case MustRevalidate.raw => Right(MustRevalidate)
case ProxyRevalidate.raw => Right(ProxyRevalidate)
case MustUnderstand.raw => Right(MustUnderstand)
case Private.raw => Right(Private)
case Public.raw => Right(Public)
case Immutable.raw => Right(Immutable)
case "stale-while-revalidate" => Right(StaleWhileRevalidate(seconds.getOrElse(0)))
case "stale-if-error" => Right(StaleIfError(seconds.getOrElse(0)))
case _ => Left(s"Unknown cache control value: $trimmedValue")
}
}
}
final case class ClearSiteData(directives: NonEmptyChunk[ClearSiteDataDirective]) extends Header {
override type Self = ClearSiteData
override def self: Self = this
override def headerType: HeaderType.Typed[ClearSiteData] = ClearSiteData
}
object ClearSiteData extends HeaderType {
override type HeaderValue = ClearSiteData
override def name: String = "clear-site-data"
def parse(value: String): Either[String, ClearSiteData] = {
val values = value.split(",").map(_.trim)
val directives = values.flatMap { directive =>
directive match {
case """"cache"""" => Some(ClearSiteDataDirective.Cache)
case """"clientHints"""" => Some(ClearSiteDataDirective.ClientHints)
case """"cookies"""" => Some(ClearSiteDataDirective.Cookies)
case """"storage"""" => Some(ClearSiteDataDirective.Storage)
case """"executionContexts"""" => Some(ClearSiteDataDirective.ExecutionContexts)
case """"*"""" => Some(ClearSiteDataDirective.All)
case _ => None
}
}
if (values.exists(x => !x.headOption.contains('"') || !x.lastOption.contains('"')))
Left("Invalid Clear-Site-Data header")
else {
NonEmptyChunk.fromIterableOption(directives) match {
case Some(directives) => Right(ClearSiteData(directives))
case None => Left("Invalid Clear-Site-Data header")
}
}
}
def render(clearSiteData: ClearSiteData): String =
clearSiteData.directives.map {
case ClearSiteDataDirective.Cache => """"cache""""
case ClearSiteDataDirective.ClientHints => """"clientHints""""
case ClearSiteDataDirective.Cookies => """"cookies""""
case ClearSiteDataDirective.Storage => """"storage""""
case ClearSiteDataDirective.ExecutionContexts => """"executionContexts""""
case ClearSiteDataDirective.All => """"*""""
}.mkString(", ")
}
sealed trait ClearSiteDataDirective
object ClearSiteDataDirective {
case object Cache extends ClearSiteDataDirective
case object ClientHints extends ClearSiteDataDirective
case object Cookies extends ClearSiteDataDirective
case object Storage extends ClearSiteDataDirective
case object ExecutionContexts extends ClearSiteDataDirective
case object All extends ClearSiteDataDirective
}
/**
* Connection header value.
*/
sealed trait Connection extends Header {
override type Self = Connection
override def self: Self = this
override def headerType: HeaderType.Typed[Connection] = Connection
val value: String
}
object Connection extends HeaderType {
override type HeaderValue = Connection
override def name: String = "connection"
/**
* This directive indicates that either the client or the server would like
* to close the connection. This is the default on HTTP/1.0 requests.
*/
case object Close extends Connection {
override val value: String = "close"
}
/**
* Any comma-separated list of HTTP headers [Usually keep-alive only]
* indicates that the client would like to keep the connection open. Keeping
* a connection open is the default on HTTP/1.1 requests. The list of
* headers are the name of the header to be removed by the first
* non-transparent proxy or cache in-between: these headers define the
* connection between the emitter and the first entity, not the destination
* node.
*/
case object KeepAlive extends Connection {
override val value: String = "keep-alive"
}
def parse(connection: String): Either[String, Connection] = {
connection.trim.toLowerCase() match {
case Close.value => Right(Close)
case KeepAlive.value => Right(KeepAlive)
case _ => Left("Invalid Connection")
}
}
def render(connection: Connection): String = connection.value
}
final case class ContentBase(uri: URI) extends Header {
override type Self = ContentBase
override def self: Self = this
override def headerType: HeaderType.Typed[ContentBase] = ContentBase
}
object ContentBase extends HeaderType {
override type HeaderValue = ContentBase
override def name: String = "content-base"
def parse(s: String): Either[String, ContentBase] =
Try(ContentBase(new java.net.URI(s))).toEither.left.map(_ => "Invalid Content-Base header")
def render(cb: ContentBase): String =
cb.uri.toString
def uri(uri: URI): ContentBase = ContentBase(uri)
}
sealed trait ContentDisposition extends Header {
override type Self = ContentDisposition
override def self: Self = this
override def headerType: HeaderType.Typed[ContentDisposition] = ContentDisposition
}
object ContentDisposition extends HeaderType {
override type HeaderValue = ContentDisposition
override def name: String = "content-disposition"
final case class Attachment(filename: Option[String]) extends ContentDisposition
final case class Inline(filename: Option[String]) extends ContentDisposition
final case class FormField(name: String, filename: Option[String]) extends ContentDisposition
private val AttachmentRegex = """attachment; filename="(.*)"""".r
private val InlineRegex = """inline; filename="(.*)"""".r
private val FormDataRegex = """form-data; name="(.*)"; filename="(.*)"""".r
private val FormDataNoFileNameRegex = """form-data; name="(.*)"""".r
def parse(contentDisposition: String): Either[String, ContentDisposition] = {
if (contentDisposition.startsWith("attachment")) {
Right(contentDisposition match {
case AttachmentRegex(filename) => Attachment(Some(filename))
case _ => Attachment(None)
})
} else if (contentDisposition.startsWith("inline")) {
Right(contentDisposition match {
case InlineRegex(filename) => Inline(Some(filename))
case _ => Inline(None)
})
} else if (contentDisposition.startsWith("form-data")) {
contentDisposition match {
case FormDataRegex(name, filename) => Right(FormField(name, Some(filename)))
case FormDataNoFileNameRegex(name) => Right(FormField(name, None))
case _ => Left("Invalid form-data content disposition")
}
} else {
Left("Invalid content disposition")
}
}
def render(contentDisposition: ContentDisposition): String = {
contentDisposition match {
case Attachment(filename) => s"attachment; ${filename.map("filename=" + _).getOrElse("")}"
case Inline(filename) => s"inline; ${filename.map("filename=" + _).getOrElse("")}"
case FormField(name, filename) => s"form-data; name=$name; ${filename.map("filename=" + _).getOrElse("")}"
}
}
val inline: ContentDisposition = Inline(None)
val attachment: ContentDisposition = Attachment(None)
def inline(filename: String): ContentDisposition = Inline(Some(filename))
def attachment(filename: String): ContentDisposition = Attachment(Some(filename))
def formData(name: String): ContentDisposition = FormField(name, None)
def formData(name: String, filename: String): ContentDisposition = FormField(name, Some(filename))
}
sealed trait ContentEncoding extends Header {
override type Self = ContentEncoding
override def self: Self = this
override def headerType: HeaderType.Typed[ContentEncoding] = ContentEncoding
val encoding: String
}
object ContentEncoding extends HeaderType {
override type HeaderValue = ContentEncoding
override def name: String = "content-encoding"
/**
* A format using the Brotli algorithm.
*/
case object Br extends ContentEncoding {
override val encoding: String = "br"
}
/**
* A format using the Lempel-Ziv-Welch (LZW) algorithm. The value name was
* taken from the UNIX compress program, which implemented this algorithm.
* Like the compress program, which has disappeared from most UNIX
* distributions, this content-encoding is not used by many browsers today,
* partly because of a patent issue (it expired in 2003).
*/
case object Compress extends ContentEncoding {
override val encoding: String = "compress"
}
/**
* Using the zlib structure (defined in RFC 1950) with the deflate
* compression algorithm (defined in RFC 1951).
*/
case object Deflate extends ContentEncoding {
override val encoding: String = "deflate"
}
/**
* A format using the Lempel-Ziv coding (LZ77), with a 32-bit CRC. This is
* the original format of the UNIX gzip program. The HTTP/1.1 standard also
* recommends that the servers supporting this content-encoding should
* recognize x-gzip as an alias, for compatibility purposes.
*/
case object GZip extends ContentEncoding {
override val encoding: String = "gzip"
}
/**
* Maintains a list of ContentEncoding values.
*/
final case class Multiple(encodings: NonEmptyChunk[ContentEncoding]) extends ContentEncoding {
override val encoding: String = encodings.map(_.encoding).mkString(",")
}
private def findEncoding(value: String): Option[ContentEncoding] = {
value.trim match {
case "br" => Some(Br)
case "compress" => Some(Compress)
case "deflate" => Some(Deflate)
case "gzip" => Some(GZip)
case _ => None
}
}
/**
* @param value
* of string , seperated for multiple values
* @return
* ContentEncoding
*
* Note: This implementation ignores the invalid string that might occur in
* MultipleEncodings case.
*/
def parse(value: String): Either[String, ContentEncoding] = {
val encodings = Chunk.fromArray(value.split(",").map(findEncoding)).flatten
NonEmptyChunk.fromChunk(encodings) match {
case Some(value) =>
if (value.size == 1) Right(value.head)
else Right(Multiple(value))
case None => Left("Empty ContentEncoding")
}
}
def render(value: ContentEncoding): String = value.encoding
}
sealed trait ContentLanguage extends Header {
override type Self = ContentLanguage
override def self: Self = this
override def headerType: HeaderType.Typed[ContentLanguage] = ContentLanguage
}
object ContentLanguage extends HeaderType {
override type HeaderValue = ContentLanguage
override def name: String = "content-language"
case object Arabic extends ContentLanguage
case object Bulgarian extends ContentLanguage
case object Catalan extends ContentLanguage
case object Chinese extends ContentLanguage
case object Croatian extends ContentLanguage
case object Czech extends ContentLanguage
case object Danish extends ContentLanguage
case object Dutch extends ContentLanguage
case object English extends ContentLanguage
case object Estonian extends ContentLanguage
case object Finnish extends ContentLanguage
case object French extends ContentLanguage
case object German extends ContentLanguage
case object Greek extends ContentLanguage
case object Hebrew extends ContentLanguage
case object Hindi extends ContentLanguage
case object Hungarian extends ContentLanguage
case object Icelandic extends ContentLanguage
case object Indonesian extends ContentLanguage
case object Italian extends ContentLanguage
case object Japanese extends ContentLanguage
case object Korean extends ContentLanguage
case object Latvian extends ContentLanguage
case object Lithuanian extends ContentLanguage
case object Norwegian extends ContentLanguage
case object Polish extends ContentLanguage
case object Portuguese extends ContentLanguage
case object Romanian extends ContentLanguage
case object Russian extends ContentLanguage
case object Serbian extends ContentLanguage
case object Slovak extends ContentLanguage
case object Slovenian extends ContentLanguage
case object Spanish extends ContentLanguage
case object Swedish extends ContentLanguage
case object Thai extends ContentLanguage
case object Turkish extends ContentLanguage
case object Ukrainian extends ContentLanguage
case object Vietnamese extends ContentLanguage
def parse(value: String): Either[String, ContentLanguage] =
value.toLowerCase.take(2) match {
case "ar" => Right(Arabic)
case "bg" => Right(Bulgarian)
case "ca" => Right(Catalan)
case "zh" => Right(Chinese)
case "hr" => Right(Croatian)
case "cs" => Right(Czech)
case "da" => Right(Danish)
case "nl" => Right(Dutch)
case "en" => Right(English)
case "et" => Right(Estonian)
case "fi" => Right(Finnish)
case "fr" => Right(French)
case "de" => Right(German)
case "el" => Right(Greek)
case "he" => Right(Hebrew)
case "hi" => Right(Hindi)
case "hu" => Right(Hungarian)
case "is" => Right(Icelandic)
case "id" => Right(Indonesian)
case "it" => Right(Italian)
case "ja" => Right(Japanese)
case "ko" => Right(Korean)
case "lv" => Right(Latvian)
case "lt" => Right(Lithuanian)
case "nb" => Right(Norwegian)
case "pl" => Right(Polish)
case "pt" => Right(Portuguese)
case "ro" => Right(Romanian)
case "ru" => Right(Russian)
case "sr" => Right(Serbian)
case "sk" => Right(Slovak)
case "sl" => Right(Slovenian)
case "es" => Right(Spanish)
case "sv" => Right(Swedish)
case "th" => Right(Thai)
case "tr" => Right(Turkish)
case "uk" => Right(Ukrainian)
case "vi" => Right(Vietnamese)
case _ => Left(s"Invalid ContentLanguage: $value")
}
def render(contentLanguage: ContentLanguage): String =
contentLanguage match {
case Arabic => "ar"
case Bulgarian => "bg"
case Catalan => "ca"
case Chinese => "zh"
case Croatian => "hr"
case Czech => "cs"
case Danish => "da"
case Dutch => "nl"
case English => "en"
case Estonian => "et"
case Finnish => "fi"
case French => "fr"
case German => "de"
case Greek => "el"
case Hebrew => "he"
case Hindi => "hi"
case Hungarian => "hu"
case Icelandic => "is"
case Indonesian => "id"
case Italian => "it"
case Japanese => "ja"
case Korean => "ko"
case Latvian => "lv"
case Lithuanian => "lt"
case Norwegian => "no"
case Polish => "pl"
case Portuguese => "pt"
case Romanian => "ro"
case Russian => "ru"
case Serbian => "sr"
case Slovak => "sk"
case Slovenian => "sl"
case Spanish => "es"
case Swedish => "sv"
case Thai => "th"
case Turkish => "tr"
case Ukrainian => "uk"
case Vietnamese => "vi"
}
}
/**
* The Content-Length header indicates the size of the message body, in bytes,
* sent to the recipient.
*/
final case class ContentLength(length: Long) extends Header {
override type Self = ContentLength
override def self: Self = this
override def headerType: HeaderType.Typed[ContentLength] = ContentLength
}
object ContentLength extends HeaderType {
override type HeaderValue = ContentLength
override def name: String = "content-length"
def parse(value: String): Either[String, ContentLength] =
Try(value.trim.toLong) match {
case Failure(_) => Left("Invalid Content-Length header")
case Success(value) => fromLong(value)
}
def render(contentLength: ContentLength): String =
contentLength.length.toString
private def fromLong(value: Long): Either[String, ContentLength] =
if (value >= 0)
Right(ContentLength(value))
else
Left("Invalid Content-Length header")
}
final case class ContentLocation(value: URI) extends Header {
override type Self = ContentLocation
override def self: Self = this
override def headerType: HeaderType.Typed[ContentLocation] = ContentLocation
}
object ContentLocation extends HeaderType {
override type HeaderValue = ContentLocation
override def name: String = "content-location"
def parse(value: String): Either[String, ContentLocation] =
Try(ContentLocation(new URI(value))).toEither.left.map(_ => "Invalid Content-Location header")
def render(contentLocation: ContentLocation): String =
contentLocation.value.toString
}
final case class ContentMd5(value: String) extends Header {
override type Self = ContentMd5
override def self: Self = this
override def headerType: HeaderType.Typed[ContentMd5] = ContentMd5
}
object ContentMd5 extends HeaderType {
override type HeaderValue = ContentMd5
override def name: String = "content-md5"
private val MD5Regex = """[A-Fa-f0-9]{32}""".r
def parse(value: String): Either[String, ContentMd5] =
value match {
case MD5Regex() => Right(ContentMd5(value))
case _ => Left("Invalid Content-MD5 header")
}
def render(contentMd5: ContentMd5): String =
contentMd5.value
}
sealed trait ContentRange extends Header {
override type Self = ContentRange
override def self: Self = this
override def headerType: HeaderType.Typed[ContentRange] = ContentRange
def start: Option[Int]
def end: Option[Int]
def total: Option[Int]
def unit: String
}
object ContentRange extends HeaderType {
override type HeaderValue = ContentRange
override def name: String = "content-range"
final case class EndTotal(unit: String, s: Int, e: Int, t: Int) extends ContentRange {
def start: Option[Int] = Some(s)
def end: Option[Int] = Some(e)
def total: Option[Int] = Some(t)
}
final case class StartEnd(unit: String, s: Int, e: Int) extends ContentRange {
def start: Option[Int] = Some(s)
def end: Option[Int] = Some(e)
def total: Option[Int] = None
}
final case class RangeTotal(unit: String, t: Int) extends ContentRange {
def start: Option[Int] = None
def end: Option[Int] = None
def total: Option[Int] = Some(t)
}
private val contentRangeStartEndTotalRegex = """(\w+) (\d+)-(\d+)/(\d+)""".r
private val contentRangeStartEndRegex = """(\w+) (\d+)-(\d+)/*""".r
private val contentRangeTotalRegex = """(\w+) */(\d+)""".r
def parse(s: String): Either[String, ContentRange] =
s match {
case contentRangeStartEndTotalRegex(unit, start, end, total) =>
Right(EndTotal(unit, start.toInt, end.toInt, total.toInt))
case contentRangeStartEndRegex(unit, start, end) =>
Right(StartEnd(unit, start.toInt, end.toInt))
case contentRangeTotalRegex(unit, total) =>
Right(RangeTotal(unit, total.toInt))
case _ =>
Left("Invalid content range")
}
def render(c: ContentRange): String =
c match {
case EndTotal(unit, start, end, total) =>
s"$unit $start-$end/$total"
case StartEnd(unit, start, end) =>
s"$unit $start-$end/*"
case RangeTotal(unit, total) =>
s"$unit */$total"
}
}
// scalafmt: { maxColumn = 180 }
sealed trait ContentSecurityPolicy extends Header {
override type Self = ContentSecurityPolicy
override def self: Self = this
override def headerType: HeaderType.Typed[ContentSecurityPolicy] = ContentSecurityPolicy
}
// TODO: Should we make deprecated types deprecated in code?
object ContentSecurityPolicy extends HeaderType {
override type HeaderValue = ContentSecurityPolicy
override def name: String = "content-security-policy"
final case class SourcePolicy(srcType: SourcePolicyType, src: Source) extends ContentSecurityPolicy
case object BlockAllMixedContent extends ContentSecurityPolicy
// TODO: Deprecated and only Safari supports this and it is non-standard. Should we remove it?
final case class PluginTypes(value: String) extends ContentSecurityPolicy
// TODO: no modern browser supports this. Should we remove it?
final case class Referrer(referrer: ReferrerPolicy) extends ContentSecurityPolicy
final case class ReportTo(groupName: String) extends ContentSecurityPolicy
final case class ReportUri(uri: URI) extends ContentSecurityPolicy
final case class RequireSriFor(requirement: RequireSriForValue) extends ContentSecurityPolicy
final case class Sandbox(value: SandboxValue) extends ContentSecurityPolicy
final case class TrustedTypes(value: TrustedTypesValue) extends ContentSecurityPolicy
case object UpgradeInsecureRequests extends ContentSecurityPolicy
sealed trait SourcePolicyType
object SourcePolicyType {
case object `base-uri` extends SourcePolicyType
case object `child-src` extends SourcePolicyType
case object `connect-src` extends SourcePolicyType
case object `default-src` extends SourcePolicyType
case object `font-src` extends SourcePolicyType
case object `form-action` extends SourcePolicyType
case object `frame-ancestors` extends SourcePolicyType
case object `frame-src` extends SourcePolicyType
case object `img-src` extends SourcePolicyType
case object `manifest-src` extends SourcePolicyType
case object `media-src` extends SourcePolicyType
case object `object-src` extends SourcePolicyType
case object `prefetch-src` extends SourcePolicyType
case object `script-src` extends SourcePolicyType
case object `script-src-attr` extends SourcePolicyType
case object `script-src-elem` extends SourcePolicyType
case object `style-src` extends SourcePolicyType
case object `style-src-attr` extends SourcePolicyType
case object `style-src-elem` extends SourcePolicyType
case object `upgrade-insecure-requests` extends SourcePolicyType
case object `worker-src` extends SourcePolicyType
def parse(s: String): Option[SourcePolicyType] = s match {
case "base-uri" => Some(`base-uri`)
case "child-src" => Some(`child-src`)
case "connect-src" => Some(`connect-src`)
case "default-src" => Some(`default-src`)
case "font-src" => Some(`font-src`)
case "form-action" => Some(`form-action`)
case "frame-ancestors" => Some(`frame-ancestors`)
case "frame-src" => Some(`frame-src`)
case "img-src" => Some(`img-src`)
case "manifest-src" => Some(`manifest-src`)
case "media-src" => Some(`media-src`)
case "object-src" => Some(`object-src`)
case "prefetch-src" => Some(`prefetch-src`)
case "script-src" => Some(`script-src`)
case "script-src-attr" => Some(`script-src-attr`)
case "script-src-elem" => Some(`script-src-elem`)
case "style-src" => Some(`style-src`)
case "style-src-attr" => Some(`style-src-attr`)
case "style-src-elem" => Some(`style-src-elem`)
case "upgrade-insecure-requests" => Some(`upgrade-insecure-requests`)
case "worker-src" => Some(`worker-src`)
case _ => None
}
def render(policyType: SourcePolicyType) =
policyType match {
case `base-uri` => "base-uri"
case `child-src` => "child-src"
case `connect-src` => "connect-src"
case `default-src` => "default-src"
case `font-src` => "font-src"
case `form-action` => "form-action"
case `frame-ancestors` => "frame-ancestors"
case `frame-src` => "frame-src"
case `img-src` => "img-src"
case `manifest-src` => "manifest-src"
case `media-src` => "media-src"
case `object-src` => "object-src"
case `prefetch-src` => "prefetch-src"
case `script-src` => "script-src"
case `script-src-attr` => "script-src-attr"
case `script-src-elem` => "script-src-elem"
case `style-src` => "style-src"
case `style-src-attr` => "style-src-attr"
case `style-src-elem` => "style-src-elem"
case `upgrade-insecure-requests` => "upgrade-insecure-requests"
case `worker-src` => "worker-src"
}
}
sealed trait Source {
self =>
def &&(other: Source): Source =
if (other == Source.none) self else Source.Sequence(self, other)
}
object Source {
case object none extends Source {
override def &&(other: Source): Source = other
}
final case class Host(uri: URI) extends Source
final case class Scheme(scheme: String) extends Source
case object Self extends Source
case object UnsafeEval extends Source
case object WasmUnsafeEval extends Source
case object UnsafeHashes extends Source
case object UnsafeInline extends Source
final case class Nonce(value: String) extends Source
final case class Hash(algorithm: HashAlgorithm, value: String) extends Source
case object StrictDynamic extends Source
case object ReportSample extends Source
final case class Sequence(left: Source, right: Source) extends Source
sealed trait HashAlgorithm
object HashAlgorithm {
case object Sha256 extends HashAlgorithm
case object Sha384 extends HashAlgorithm
case object Sha512 extends HashAlgorithm
def parse(s: String): Option[HashAlgorithm] = s match {
case "sha256" => Some(Sha256)
case "sha384" => Some(Sha384)
case "sha512" => Some(Sha512)
case _ => None
}
}
private val NonceRegex = "'nonce-(.*)'".r
private val Sha256Regex = "'sha256-(.*)'".r
private val Sha384Regex = "'sha384-(.*)'".r
private val Sha512Regex = "'sha512-(.*)'".r
def parse(s: String): Option[Source] = s match {
case "'none'" => Some(none)
case "'self'" => Some(Self)
case "'unsafe-eval'" => Some(UnsafeEval)
case "'wasm-eval'" => Some(WasmUnsafeEval)
case "'unsafe-hashes'" => Some(UnsafeHashes)
case "'unsafe-inline'" => Some(UnsafeInline)
case "'strict-dynamic'" => Some(StrictDynamic)
case "'report-sample'" => Some(ReportSample)
case NonceRegex(nonce) => Some(Nonce(nonce))
case Sha256Regex(hash) => Some(Hash(HashAlgorithm.Sha256, hash))
case Sha384Regex(hash) => Some(Hash(HashAlgorithm.Sha384, hash))
case Sha512Regex(hash) => Some(Hash(HashAlgorithm.Sha512, hash))
case s => Try(URI.create(s)).map(Host(_)).toOption
}
def render(source: Source): String = source match {
case Source.none => "'none'"
case Self => "'self'"
case UnsafeEval => "'unsafe-eval'"
case WasmUnsafeEval => "'wasm-eval'"
case UnsafeHashes => "'unsafe-hashes'"
case UnsafeInline => "'unsafe-inline'"
case StrictDynamic => "'strict-dynamic'"
case ReportSample => "'report-sample'"
case Nonce(nonce) => s"'nonce-$nonce'"
case Hash(algorithm, hash) => s"'$algorithm-$hash'"
case Sequence(left, right) => s"${render(left)} ${render(right)}"
case Host(uri) => uri.toString
case Scheme(scheme) => s"$scheme:"
}
def host(uri: URI): Source = Host(uri)
def scheme(scheme: String): Source = Scheme(scheme)
def nonce(value: String): Source = Nonce(value)
def hash(algorithm: HashAlgorithm, value: String): Source = Hash(algorithm, value)
}
sealed trait SandboxValue {
self =>
def &&(other: SandboxValue): SandboxValue =
if (other == SandboxValue.Empty) self else SandboxValue.Sequence(self, other)
}
object SandboxValue {
case object Empty extends SandboxValue {
override def &&(other: SandboxValue): SandboxValue = other
}
case object AllowForms extends SandboxValue
case object AllowSameOrigin extends SandboxValue
case object AllowScripts extends SandboxValue
case object AllowPopups extends SandboxValue
case object AllowModals extends SandboxValue
case object AllowOrientationLock extends SandboxValue
case object AllowPointerLock extends SandboxValue
case object AllowPresentation extends SandboxValue
case object AllowPopupsToEscapeSandbox extends SandboxValue
case object AllowTopNavigation extends SandboxValue
final case class Sequence(left: SandboxValue, right: SandboxValue) extends SandboxValue
def parse(value: String): Option[SandboxValue] = {
def parseOne: String => Option[SandboxValue] = {
case "allow-forms" => Some(AllowForms)
case "allow-same-origin" => Some(AllowSameOrigin)
case "allow-scripts" => Some(AllowScripts)
case "allow-popups" => Some(AllowPopups)
case "allow-modals" => Some(AllowModals)
case "allow-orientation-lock" => Some(AllowOrientationLock)
case "allow-pointer-lock" => Some(AllowPointerLock)
case "allow-presentation" => Some(AllowPresentation)
case "allow-popups-to-escape-sandbox" => Some(AllowPopupsToEscapeSandbox)
case "allow-top-navigation" => Some(AllowTopNavigation)
case _ => None
}
value match {
case "" => Some(Empty)
case s =>
Chunk.fromArray(s.split(" ")).foldLeft(Option(Empty): Option[SandboxValue]) {
case (Some(acc), v) => parseOne(v).map(acc && _)
case (None, _) => None
}
}
}
def render(value: SandboxValue): String = {
def toStringOne: SandboxValue => String = {
case AllowForms => "allow-forms"
case AllowSameOrigin => "allow-same-origin"
case AllowScripts => "allow-scripts"
case AllowPopups => "allow-popups"
case AllowModals => "allow-modals"
case AllowOrientationLock => "allow-orientation-lock"
case AllowPointerLock => "allow-pointer-lock"
case AllowPresentation => "allow-presentation"
case AllowPopupsToEscapeSandbox => "allow-popups-to-escape-sandbox"
case AllowTopNavigation => "allow-top-navigation"
case Empty => ""
case Sequence(left, right) => toStringOne(left) + " " + toStringOne(right)
}
toStringOne(value)
}
}
sealed trait TrustedTypesValue extends scala.Product with Serializable {
self =>
def &&(other: TrustedTypesValue): TrustedTypesValue =
if (other == TrustedTypesValue.none) self else TrustedTypesValue.Sequence(self, other)
}
object TrustedTypesValue {
case object none extends TrustedTypesValue {
override def &&(other: TrustedTypesValue): TrustedTypesValue = other
}
final case class PolicyName(value: String) extends TrustedTypesValue
case object `allow-duplicates` extends TrustedTypesValue
case object Wildcard extends TrustedTypesValue
final case class Sequence(left: TrustedTypesValue, right: TrustedTypesValue) extends TrustedTypesValue
private val PolicyNameRegex = """\*|[a-zA-Z0-9-#=_/@.%]+|'allow-duplicates'|'none'""".r
def parse(value: String): Option[TrustedTypesValue] = {
val allValues = PolicyNameRegex.findAllIn(value).toList
if (allValues.isEmpty) None
else {
Some {
allValues.map {
case "*" => TrustedTypesValue.Wildcard
case "'none'" => TrustedTypesValue.none
case "'allow-duplicates'" => TrustedTypesValue.`allow-duplicates`
case policyName => TrustedTypesValue.PolicyName(policyName)
}.reduce(_ && _)
}
}
}
def fromTrustedTypesValue(value: TrustedTypesValue): String =
value match {
case TrustedTypesValue.none => "'none'"
case TrustedTypesValue.Wildcard => "*"
case TrustedTypesValue.`allow-duplicates` => "'allow-duplicates'"
case TrustedTypesValue.PolicyName(policyName) => policyName
case TrustedTypesValue.Sequence(left, right) =>
fromTrustedTypesValue(left) + " " + fromTrustedTypesValue(right)
}
}
sealed trait ReferrerPolicy extends scala.Product with Serializable
object ReferrerPolicy {
case object `no-referrer` extends ReferrerPolicy
case object `none-when-downgrade` extends ReferrerPolicy
case object `origin` extends ReferrerPolicy
case object `origin-when-cross-origin` extends ReferrerPolicy
case object `unsafe-url` extends ReferrerPolicy
def parse(referrer: String): Option[ReferrerPolicy] =
referrer match {
case "no-referrer" => Some(`no-referrer`)
case "none-when-downgrade" => Some(`none-when-downgrade`)
case "origin" => Some(`origin`)
case "origin-when-cross-origin" => Some(`origin-when-cross-origin`)
case "unsafe-url" => Some(`unsafe-url`)
case _ => None
}
def render(referrer: ReferrerPolicy): String = referrer.productPrefix
}
sealed trait RequireSriForValue extends scala.Product with Serializable
object RequireSriForValue {
case object Script extends RequireSriForValue
case object Style extends RequireSriForValue
case object ScriptStyle extends RequireSriForValue
def parse(value: String): Option[RequireSriForValue] =
value match {
case "script" => Some(Script)
case "style" => Some(Style)
case "script style" => Some(ScriptStyle)
case _ => None
}
def fromRequireSriForValue(value: RequireSriForValue): String =
value match {
case Script => "script"
case Style => "style"
case ScriptStyle => "script style"
}
}
def defaultSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`default-src`, src.foldLeft[Source](Source.none)(_ && _))
def scriptSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`script-src`, src.foldLeft[Source](Source.none)(_ && _))
def styleSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`style-src`, src.foldLeft[Source](Source.none)(_ && _))
def imgSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`img-src`, src.foldLeft[Source](Source.none)(_ && _))
def mediaSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`media-src`, src.foldLeft[Source](Source.none)(_ && _))
def frameSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`frame-src`, src.foldLeft[Source](Source.none)(_ && _))
def fontSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`font-src`, src.foldLeft[Source](Source.none)(_ && _))
def connectSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`connect-src`, src.foldLeft[Source](Source.none)(_ && _))
def objectSrc(src: Source*): SourcePolicy =
SourcePolicy(SourcePolicyType.`object-src`, src.foldLeft[Source](Source.none)(_ && _))
private val PluginTypesRegex = "plugin-types (.*)".r
private val ReferrerRegex = "referrer (.*)".r
private val ReportToRegex = "report-to (.*)".r
private val ReportUriRegex = "report-uri (.*)".r
private val RequireSriRegex = "require-sri-for (.*)".r
private val TrustedTypesRegex = "trusted-types (.*)".r
private val SandboxRegex = "sandbox (.*)".r
private val PolicyRegex = "([a-z-]+) (.*)".r
def parse(value: String): Either[String, ContentSecurityPolicy] =
value match {
case "block-all-mixed-content" => Right(ContentSecurityPolicy.BlockAllMixedContent)
case PluginTypesRegex(types) => Right(ContentSecurityPolicy.PluginTypes(types))
case ReferrerRegex(referrer) => ReferrerPolicy.parse(referrer).map(ContentSecurityPolicy.Referrer(_)).toRight("Invalid referrer policy")
case ReportToRegex(group) => Right(ContentSecurityPolicy.ReportTo(group))
case ReportUriRegex(uri) => Try(new URI(uri)).map(ContentSecurityPolicy.ReportUri(_)).toEither.left.map(_ => "Invalid report-uri")
case RequireSriRegex(value) => RequireSriForValue.parse(value).map(ContentSecurityPolicy.RequireSriFor(_)).toRight("Invalid require-sri-for value")
case TrustedTypesRegex(value) => TrustedTypesValue.parse(value).map(ContentSecurityPolicy.TrustedTypes(_)).toRight("Invalid trusted-types value")
case SandboxRegex(sandbox) => SandboxValue.parse(sandbox).map(ContentSecurityPolicy.Sandbox(_)).toRight("Invalid sandbox value")
case "upgrade-insecure-requests" => Right(ContentSecurityPolicy.UpgradeInsecureRequests)
case PolicyRegex(policyType, policy) => ContentSecurityPolicy.fromTypeAndPolicy(policyType, policy)
case _ => Left("Invalid Content-Security-Policy")
}
def render(csp: ContentSecurityPolicy): String =
csp match {
case ContentSecurityPolicy.BlockAllMixedContent => "block-all-mixed-content"
case ContentSecurityPolicy.PluginTypes(types) => s"plugin-types $types"
case ContentSecurityPolicy.Referrer(referrer) => s"referrer ${ReferrerPolicy.render(referrer)}"
case ContentSecurityPolicy.ReportTo(reportTo) => s"report-to $reportTo"
case ContentSecurityPolicy.ReportUri(uri) => s"report-uri $uri"
case ContentSecurityPolicy.RequireSriFor(value) => s"require-sri-for ${RequireSriForValue.fromRequireSriForValue(value)}"
case ContentSecurityPolicy.TrustedTypes(value) => s"trusted-types ${TrustedTypesValue.fromTrustedTypesValue(value)}"
case ContentSecurityPolicy.Sandbox(value) => s"sandbox ${SandboxValue.render(value)}"
case ContentSecurityPolicy.UpgradeInsecureRequests => "upgrade-insecure-requests"
case SourcePolicy(policyType, policy) => s"${SourcePolicyType.render(policyType)} ${Source.render(policy)}"
}
def fromTypeAndPolicy(policyType: String, policy: String): Either[String, ContentSecurityPolicy] =
SourcePolicyType
.parse(policyType)
.flatMap(policyType => Source.parse(policy).map(SourcePolicy(policyType, _)))
.toRight("Invalid Content-Security-Policy")
}
sealed trait ContentTransferEncoding extends Header {
override type Self = ContentTransferEncoding
override def self: Self = this
override def headerType: HeaderType.Typed[ContentTransferEncoding] = ContentTransferEncoding
}
object ContentTransferEncoding extends HeaderType {
override type HeaderValue = ContentTransferEncoding
override def name: String = "content-transfer-encoding"
case object SevenBit extends ContentTransferEncoding
case object EightBit extends ContentTransferEncoding
case object Binary extends ContentTransferEncoding
case object QuotedPrintable extends ContentTransferEncoding
case object Base64 extends ContentTransferEncoding
final case class XToken(token: String) extends ContentTransferEncoding
private val XRegex = "x-(.*)".r
def parse(s: String): Either[String, ContentTransferEncoding] =
s.toLowerCase match {
case "7bit" => Right(SevenBit)
case "8bit" => Right(EightBit)
case "binary" => Right(Binary)
case "quoted-printable" => Right(QuotedPrintable)
case "base64" => Right(Base64)
case XRegex(token) => Right(XToken(token))
case _ => Left("Invalid Content-Transfer-Encoding header")
}
def render(contentTransferEncoding: ContentTransferEncoding): String =
contentTransferEncoding match {
case SevenBit => "7bit"
case EightBit => "8bit"
case Binary => "binary"
case QuotedPrintable => "quoted-printable"
case Base64 => "base64"
case XToken(token) => s"x-$token"
}
}
final case class ContentType(mediaType: MediaType, boundary: Option[Boundary] = None, charset: Option[Charset] = None) extends Header {
override type Self = ContentType
override def self: Self = this
override def headerType: HeaderType.Typed[ContentType] = ContentType
override lazy val renderedValue: String = ContentType.render(this)
}
object ContentType extends HeaderType {
private final val CacheInitialSize = 64
private final val CacheMaxSize = 8192
private val cache: ConcurrentHashMap[String, Either[String, ContentType]] =
new ConcurrentHashMap[String, Either[String, ContentType]](CacheInitialSize)
override type HeaderValue = ContentType
override def name: String = "content-type"
def parse(s: String): Either[String, ContentType] = {
// Guard against malicious registering of invalid content types
if (cache.size >= CacheMaxSize) cache.clear()
cache.computeIfAbsent(s, parseFn)
}
def render(contentType: ContentType): String = codec.encode(contentType).toOption.get
private val codec: RichTextCodec[ContentType] = {
// char `.` according to BNF not allowed as `token`, but here tolerated
val token = RichTextCodec.charsNot(' ', '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=')
val tokenString = token.repeat.string
val tokenStringQuoted = RichTextCodec.charsNot(' ', '"').repeat.string
val type1 = RichTextCodec.string.collectOrFail("unsupported main type") {
case value if MediaType.mainTypeMap.contains(value) => value
}
val type1x = (RichTextCodec.literalCI("x-") ~ tokenString).transform[String](in => s"${in._1}${in._2}")(in => ("x-", s"${in.substring(2)}"))
val codecType1 = (type1 | type1x).transform[String](_.merge) {
case x if x.startsWith("x-") => Right(x)
case x => Left(x)
}
val forwardSlash = RichTextCodec.char('/').const('/')
val codecType = (codecType1 <~ forwardSlash) ~ tokenString
val quote = RichTextCodec.char('"')
val valueQuoted = quote ~ tokenStringQuoted ~ quote
val value = valueQuoted | tokenString
val unitChunk = Chunk.single(())
val whitespaces = RichTextCodec.whitespaceChar.repeat.transform[Char](_ => ' ')(_ => unitChunk).const(' ')
val param = ((
RichTextCodec.char(';').const(';') ~>
whitespaces ~>
tokenString <~
RichTextCodec.char('=').const('=')
) ~ value)
.transformOrFailLeft[ContentType.Parameter](in => ContentType.Parameter.fromCodec(in))(in => in.toCodec)
val params = param.repeat
(codecType ~ params).transform[ContentType] { case (mainType, subType, params) =>
ContentType(
MediaType.forContentType(s"$mainType/$subType").get,
params.collectFirst { case p if p.key == ContentType.Parameter.Boundary.name => zio.http.Boundary(p.value) },
params.collectFirst { case p if p.key == ContentType.Parameter.Charset.name => java.nio.charset.Charset.forName(p.value) },
)
}(in =>
(
in.mediaType.mainType,
in.mediaType.subType, {
val charset = in.charset.map(in => Parameter.Charset(Parameter.Payload(Parameter.Charset.name, in, false)))
val boundary = in.boundary.map(in => Parameter.Boundary(Parameter.Payload(Parameter.Boundary.name, in, false)))
(charset, boundary) match {
case (Some(c), Some(b)) => Chunk(c, b)
case (Some(c), _) => Chunk.single(c)
case (_, Some(b)) => Chunk.single(b)
case _ => Chunk.empty
}
},
),
)
}
private val parseFn: java.util.function.Function[String, Either[String, ContentType]] =
codec.decode(_)
private[Header] sealed trait Parameter {
self =>
def isQuoted: Boolean = self match {
case Parameter.Boundary(payload) => payload.isQuoted
case Parameter.Charset(payload) => payload.isQuoted
case Parameter.Value(payload) => payload.isQuoted
}
def key: String = self match {
case Parameter.Value(payload) => payload.key
case Parameter.Charset(payload) => payload.key
case Parameter.Boundary(payload) => payload.key
}
def value: String = self match {
case Parameter.Value(payload) => payload.value
case Parameter.Boundary(payload) => payload.value.id
case Parameter.Charset(payload) => payload.value.toString.toLowerCase
}
def toCodec: (String, Either[(Char, String, Char), String]) =
(self.key, if (self.isQuoted) Left(('"', self.value, '"')) else Right(self.value))
}
private[Header] object Parameter {
case class Payload[A](key: String, value: A, isQuoted: Boolean)
case class Boundary(payload: Payload[zio.http.Boundary]) extends Parameter
object Boundary {
val name = "boundary"
}
case class Charset(payload: Payload[java.nio.charset.Charset]) extends Parameter
object Charset {
val name = "charset"
}
case class Value(payload: Payload[String]) extends Parameter
def make(key: String, value: String, isQuoted: Boolean): Either[String, Parameter] = {
if (key == Parameter.Charset.name) {
try Right(Parameter.Charset(Payload(key, java.nio.charset.Charset.forName(value), isQuoted)))
catch {
case _: UnsupportedCharsetException =>
Left(s"Invalid charset in Content-Type header: $value")
}
} else if (key == Parameter.Boundary.name) Right(Parameter.Boundary(Payload(key, zio.http.Boundary(value), isQuoted)))
else Right(Parameter.Value(Payload(key, value, isQuoted)))
}
def fromCodec(parse: (String, Either[(Char, String, Char), String])): Either[String, Parameter] =
Parameter.make(parse._1, parse._2.fold(a => a._2, identity), parse._2.isLeft)
}
}
final case class Date(value: ZonedDateTime) extends Header {
override type Self = Date
override def self: Self = this
override def headerType: HeaderType.Typed[Date] = Date
}
/**
* The Date general HTTP header contains the date and time at which the
* message originated.
*/
object Date extends HeaderType {
override type HeaderValue = Date
override def name: String = "date"
def parse(value: String): Either[String, Date] =
DateEncoding.default.decodeDate(value).toRight("Invalid Date header").map(Date(_))
def render(date: Date): String =
DateEncoding.default.encodeDate(date.value)
}
sealed trait DNT extends Header {
override type Self = DNT
override def self: Self = this
override def headerType: HeaderType.Typed[DNT] = DNT
}
object DNT extends HeaderType {
override type HeaderValue = DNT
override def name: String = "dnt"
case object TrackingAllowed extends DNT
case object TrackingNotAllowed extends DNT
case object NotSpecified extends DNT
def parse(value: String): Either[String, DNT] = {
value match {
case "null" => Right(NotSpecified)
case "1" => Right(TrackingNotAllowed)
case "0" => Right(TrackingAllowed)
case _ => Left("Invalid DNT header")
}
}
def render(dnt: DNT): String =
dnt match {
case NotSpecified => "null"
case TrackingAllowed => "0"
case TrackingNotAllowed => "1"
}
}
sealed trait ETag extends Header {
override type Self = ETag
override def self: Self = this
override def headerType: HeaderType.Typed[ETag] = ETag
}
object ETag extends HeaderType {
override type HeaderValue = ETag
override def name: String = "etag"
final case class Strong(validator: String) extends ETag
final case class Weak(validator: String) extends ETag
def parse(value: String): Either[String, ETag] = {
value match {
case str if str.startsWith("w/\"") && str.endsWith("\"") => Right(Weak(str.drop(3).dropRight(1)))
case str if str.startsWith("W/\"") && str.endsWith("\"") => Right(Weak(str.drop(3).dropRight(1)))
case str if str.startsWith("\"") && str.endsWith("\"") => Right(Strong(str.drop(1).dropRight(1)))
case _ => Left("Invalid ETag header")
}
}
def render(eTag: ETag): String = {
eTag match {
case Weak(value) => s"""W/"$value""""
case Strong(value) => s""""$value""""
}
}
}
/**
* The Expect HTTP request header indicates expectations that need to be met
* by the server to handle the request successfully. There is only one defined
* expectation: 100-continue
*/
sealed trait Expect extends Header {
override type Self = Expect
override def self: Self = this
override def headerType: HeaderType.Typed[Expect] = Expect
val value: String
}
object Expect extends HeaderType {
override type HeaderValue = Expect
override def name: String = "expect"
case object `100-continue` extends Expect {
val value = "100-continue"
}
def parse(value: String): Either[String, Expect] =
value match {
case `100-continue`.value => Right(`100-continue`)
case _ => Left("Invalid Expect header")
}
def render(expect: Expect): String =
expect.value
}
final case class Expires(value: ZonedDateTime) extends Header {
override type Self = Expires
override def self: Self = this
override def headerType: HeaderType.Typed[Expires] = Expires
}
/**
* The Expires HTTP header contains the date/time after which the response is
* considered expired.
*
* Invalid expiration dates with value 0 represent a date in the past and mean
* that the resource is already expired.
*
* Expires:
*
* Date: , :: GMT
*
* Example:
*
* Expires: Wed, 21 Oct 2015 07:28:00 GMT
*/
object Expires extends HeaderType {
override type HeaderValue = Expires
override def name: String = "expires"
def parse(date: String): Either[String, Expires] =
DateEncoding.default.decodeDate(date).toRight("Invalid Expires header").map(Expires(_))
def render(expires: Expires): String =
DateEncoding.default.encodeDate(expires.value)
}
final case class Forwarded(by: Option[String] = None, forValues: List[String] = Nil, host: Option[String] = None, proto: Option[String] = None) extends Header {
override type Self = Forwarded
override def self: Self = this
override def headerType: HeaderType.Typed[Forwarded] = Forwarded
}
object Forwarded extends HeaderType {
override type HeaderValue = Forwarded
override def name: String = "forwarded"
def parse(forwarded: String): Either[String, Forwarded] = {
val parts = forwarded.split(";")
val by = parts.collectFirst { case s if s.startsWith("by=") => s.drop(3) }.map(_.trim)
val forValue = parts.collectFirst { case s if s.startsWith("for=") => s.split(',') }.map(_.map(_.trim.drop(4).trim).toList).getOrElse(Nil)
val host = parts.collectFirst { case s if s.startsWith("host=") => s.drop(5) }.map(_.trim)
val proto = parts.collectFirst { case s if s.startsWith("proto=") => s.drop(6) }.map(_.trim)
Right(Forwarded(by, forValue, host, proto))
}
def render(forwarded: Forwarded): String =
s"${forwarded.by}; ${forwarded.forValues.map(v => s"for=$v").mkString(",")}; ${forwarded.host}; ${forwarded.proto}"
}
/** From header value. */
final case class From(email: String) extends Header {
override type Self = From
override def self: Self = this
override def headerType: HeaderType.Typed[From] = From
}
object From extends HeaderType {
override type HeaderValue = From
override def name: String = "from"
// Regex that does veery loose validation of email.
private val emailRegex = "([^ ]+@[^ ]+[.][^ ]+)".r
def parse(fromHeader: String): Either[String, From] =
fromHeader match {
case emailRegex(_) => Right(From(fromHeader))
case _ => Left("Invalid From header")
}
def render(from: From): String =
from.email
}
final case class Host(hostAddress: String, port: Option[Int] = None) extends Header {
override type Self = Host
override def self: Self = this
override def headerType: HeaderType.Typed[Host] = Host
}
object Host extends HeaderType {
override type HeaderValue = Host
override def name: String = "host"
def apply(hostAddress: String, port: Int): Host = Host(hostAddress, Some(port))
def parse(value: String): Either[String, Host] = {
Chunk.fromArray(value.split(":")) match {
case Chunk(host, portS) =>
Try(portS.toInt).map(port => Host(host, Some(port))).toEither.left.map(_ => "Invalid Host header")
case Chunk(host) if host.nonEmpty =>
Right(Host(host))
case _ =>
Left("Invalid Host header")
}
}
def render(host: Host): String =
host match {
case Host(address, None) => address
case Host(address, Some(port)) => s"$address:$port"
}
}
sealed trait IfMatch extends Header {
override type Self = IfMatch
override def self: Self = this
override def headerType: HeaderType.Typed[IfMatch] = IfMatch
}
object IfMatch extends HeaderType {
override type HeaderValue = IfMatch
override def name: String = "if-match"
case object Any extends IfMatch
final case class ETags(etags: NonEmptyChunk[String]) extends IfMatch
def parse(value: String): Either[String, IfMatch] = {
val etags = Chunk.fromArray(value.split(",").map(_.trim)).filter(_.nonEmpty)
etags match {
case Chunk("*") => Right(Any)
case _ =>
NonEmptyChunk.fromChunk(etags) match {
case Some(value) => Right(ETags(value))
case scala.None => Left("Invalid If-Match header")
}
}
}
def render(ifMatch: IfMatch): String = ifMatch match {
case Any => "*"
case ETags(etags) => etags.mkString(",")
}
}
final case class IfModifiedSince(value: ZonedDateTime) extends Header {
override type Self = IfModifiedSince
override def self: Self = this
override def headerType: HeaderType.Typed[IfModifiedSince] = IfModifiedSince
}
object IfModifiedSince extends HeaderType {
override type HeaderValue = IfModifiedSince
override def name: String = "if-modified-since"
def parse(value: String): Either[String, IfModifiedSince] =
DateEncoding.default.decodeDate(value).toRight("Invalid If-Modified-Since header").map(IfModifiedSince(_))
def render(ifModifiedSince: IfModifiedSince): String =
DateEncoding.default.encodeDate(ifModifiedSince.value)
}
sealed trait IfNoneMatch extends Header {
override type Self = IfNoneMatch
override def self: Self = this
override def headerType: HeaderType.Typed[IfNoneMatch] = IfNoneMatch
}
object IfNoneMatch extends HeaderType {
override type HeaderValue = IfNoneMatch
override def name: String = "if-none-match"
case object Any extends IfNoneMatch
final case class ETags(etags: NonEmptyChunk[String]) extends IfNoneMatch
def parse(value: String): Either[String, IfNoneMatch] = {
val etags = Chunk.fromArray(value.split(",").map(_.trim)).filter(_.nonEmpty)
etags match {
case Chunk("*") => Right(Any)
case _ =>
NonEmptyChunk.fromChunk(etags) match {
case Some(value) => Right(ETags(value))
case scala.None => Left("Invalid If-None-Match header")
}
}
}
def render(ifMatch: IfNoneMatch): String = ifMatch match {
case Any => "*"
case ETags(etags) => etags.mkString(",")
}
}
/**
* The If-Range HTTP request header makes a range request conditional.
* Possible values:
* - , :: GMT
* - a string of ASCII characters placed between double quotes (Like
* "675af34563dc-tr34"). A weak entity tag (one prefixed by W/) must not
* be used in this header.
*/
sealed trait IfRange extends Header {
override type Self = IfRange
override def self: Self = this
override def headerType: HeaderType.Typed[IfRange] = IfRange
}
object IfRange extends HeaderType {
override type HeaderValue = IfRange
override def name: String = "if-range"
final case class ETag(value: String) extends IfRange
final case class DateTime(value: ZonedDateTime) extends IfRange
def parse(value: String): Either[String, IfRange] =
value match {
case value if value.startsWith("\"") && value.endsWith("\"") =>
Right(ETag(value.drop(1).dropRight(1)))
case dateTime =>
DateEncoding.default.decodeDate(dateTime).toRight("Invalid If-Range header").map(DateTime(_))
}
def render(ifRange: IfRange): String =
ifRange match {
case DateTime(value) => DateEncoding.default.encodeDate(value)
case ETag(value) => s""""$value""""
}
}
final case class IfUnmodifiedSince(value: ZonedDateTime) extends Header {
override type Self = IfUnmodifiedSince
override def self: Self = this
override def headerType: HeaderType.Typed[IfUnmodifiedSince] = IfUnmodifiedSince
}
/**
* If-Unmodified-Since request header makes the request for the resource
* conditional: the server will send the requested resource or accept it in
* the case of a POST or another non-safe method only if the resource has not
* been modified after the date specified by this HTTP header.
*/
object IfUnmodifiedSince extends HeaderType {
override type HeaderValue = IfUnmodifiedSince
override def name: String = "if-unmodified-since"
def parse(value: String): Either[String, IfUnmodifiedSince] =
DateEncoding.default.decodeDate(value).toRight("Invalid If-Unmodified-Since header").map(IfUnmodifiedSince(_))
def render(ifModifiedSince: IfUnmodifiedSince): String =
DateEncoding.default.encodeDate(ifModifiedSince.value)
}
final case class LastModified(value: ZonedDateTime) extends Header {
override type Self = LastModified
override def self: Self = this
override def headerType: HeaderType.Typed[LastModified] = LastModified
}
object LastModified extends HeaderType {
override type HeaderValue = LastModified
override def name: String = "last-modified"
def parse(value: String): Either[String, LastModified] =
DateEncoding.default.decodeDate(value).toRight("Invalid Last-Modified header").map(LastModified(_))
def render(lastModified: LastModified): String =
DateEncoding.default.encodeDate(lastModified.value)
}
final case class Link(uri: URL, params: Map[String, String]) extends Header {
override type Self = Link
override def self: Self = this
override def headerType: HeaderType.Typed[Link] = Link
}
object Link extends HeaderType {
override type HeaderValue = Link
override def name: String = "link"
def parse(value: String): Either[String, Link] = {
val parts = value.split(";").map(_.trim).filter(_.nonEmpty)
if (parts.length < 2) Left("Invalid Link header")
else if (!parts(0).startsWith("<") || !parts(0).endsWith(">")) Left("Invalid Link header")
else {
val uri = parts(0).substring(1, parts(0).length - 1)
URL.decode(uri) match {
case Left(_) => Left("Invalid Link header")
case Right(url) =>
val params = parts.drop(1).map { part =>
val keyValue = part.split("=").map(_.trim).filter(_.nonEmpty)
if (keyValue.length != 2) Left("Invalid Link header")
else {
val (key, value) = (keyValue(0), keyValue(1))
val unquoted =
if (value.startsWith("\"") && value.endsWith("\"")) value.substring(1, value.length - 1)
else value
Right(key -> unquoted)
}
}
val paramsMap = params.foldLeft[Either[String, Map[String, String]]](Right(Map.empty)) {
case (Left(error), _) => Left(error)
case (Right(map), Right((key, value))) =>
if (map.contains(key)) Left("Invalid Link header")
else Right(map + (key -> value))
case _ => Left("Invalid Link header")
}
paramsMap.map(Link(url, _))
}
}
}
def render(link: Link): String = {
val params = link.params.map { case (key, value) => s"""$key="$value"""" }.mkString("; ")
s"""<${link.uri.encode}>; $params"""
}
}
/**
* Location header value.
*/
final case class Location(url: URL) extends Header {
override type Self = Location
override def self: Self = this
override def headerType: HeaderType.Typed[Location] = Location
}
object Location extends HeaderType {
override type HeaderValue = Location
override def name: String = "location"
def parse(value: String): Either[String, Location] = {
if (value == "") Left("Invalid Location header (empty)")
else
URL
.decode(value)
.left
.map(error => s"Invalid Location header: $error")
.map(url => Location(url))
}
def render(urlLocation: Location): String =
urlLocation.url.encode
}
/**
* Max-Forwards header value
*/
final case class MaxForwards(value: Int) extends Header {
override type Self = MaxForwards
override def self: Self = this
override def headerType: HeaderType.Typed[MaxForwards] = MaxForwards
}
object MaxForwards extends HeaderType {
override type HeaderValue = MaxForwards
override def name: String = "max-forwards"
def parse(value: String): Either[String, MaxForwards] = {
Try(value.toInt) match {
case Success(value) if value >= 0L => Right(MaxForwards(value))
case _ => Left("Invalid Max-Forwards header")
}
}
def render(maxForwards: MaxForwards): String =
maxForwards.value.toString
}
/** Origin header value. */
sealed trait Origin extends Header {
override type Self = Origin
override def self: Self = this
override def headerType: HeaderType.Typed[Origin] = Origin
}
object Origin extends HeaderType {
override type HeaderValue = Origin
override def name: String = "origin"
/** The Origin header value is privacy sensitive or is an opaque origin. */
case object Null extends Origin
/** The Origin header value contains scheme, host and maybe port. */
final case class Value(scheme: String, host: String, port: Option[Int] = None) extends Origin
def apply(scheme: String, host: String, port: Option[Int] = None): Origin =
Value(scheme, host, port)
def parse(value: String): Either[String, Origin] =
if (value == "null") Right(Null)
else
URL.decode(value) match {
case Left(_) => Left("Invalid Origin header")
case Right(url) if url.host.isEmpty || url.scheme.isEmpty => Left("Invalid Origin header")
case Right(url) => Right(Value(url.scheme.get.encode, url.host.get, url.portIfNotDefault))
}
def render(origin: Origin): String = {
origin match {
case Null => "null"
case Value(scheme, host, maybePort) =>
maybePort match {
case Some(port) => s"$scheme://$host:$port"
case None => s"$scheme://$host"
}
}
}
}
/** Pragma header value. */
sealed trait Pragma extends Header {
override type Self = Pragma
override def self: Self = this
override def headerType: HeaderType.Typed[Pragma] = Pragma
}
object Pragma extends HeaderType {
override type HeaderValue = Pragma
override def name: String = "pragma"
/** Pragma no-cache value. */
case object NoCache extends Pragma
/** Invalid pragma value. */
def parse(value: String): Either[String, Pragma] =
value.toLowerCase match {
case "no-cache" => Right(NoCache)
case _ => Left("Invalid Pragma header")
}
def render(pragma: Pragma): String =
pragma match {
case NoCache => "no-cache"
}
}
/**
* The HTTP Proxy-Authenticate response header defines the authentication
* method that should be used to gain access to a resource behind a proxy
* server. It authenticates the request to the proxy server, allowing it to
* transmit the request further.
*
* @param scheme
* Authentication type
* @param realm
* A description of the protected area, the realm. If no realm is specified,
* clients often display a formatted host name instead.
*/
final case class ProxyAuthenticate(scheme: AuthenticationScheme, realm: Option[String]) extends Header {
override type Self = ProxyAuthenticate
override def self: Self = this
override def headerType: HeaderType.Typed[ProxyAuthenticate] = ProxyAuthenticate
}
object ProxyAuthenticate extends HeaderType {
override type HeaderValue = ProxyAuthenticate
override def name: String = "proxy-authenticate"
def parse(value: String): Either[String, ProxyAuthenticate] = {
val parts = value.split(" realm=").map(_.trim).filter(_.nonEmpty)
parts match {
case Array(authScheme, realm) => toProxyAuthenticate(authScheme, Some(realm))
case Array(authScheme) => toProxyAuthenticate(authScheme, None)
case _ => Left("Invalid Proxy-Authenticate header")
}
}
def render(proxyAuthenticate: ProxyAuthenticate): String = proxyAuthenticate match {
case ProxyAuthenticate(scheme, Some(realm)) => s"${scheme.name} realm=$realm"
case ProxyAuthenticate(scheme, None) => s"${scheme.name}"
}
private def toProxyAuthenticate(authScheme: String, realm: Option[String]): Either[String, ProxyAuthenticate] =
AuthenticationScheme.parse(authScheme).map(ProxyAuthenticate(_, realm))
}
/**
* Proxy-Authorization:
*
* - AuthenticationScheme
*
* - The resulting string is base64 encoded
*
* Example
*
* Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
*/
final case class ProxyAuthorization(authenticationScheme: AuthenticationScheme, credential: String) extends Header {
override type Self = ProxyAuthorization
override def self: Self = this
override def headerType: HeaderType.Typed[ProxyAuthorization] = ProxyAuthorization
}
/**
* The HTTP Proxy-Authorization request header contains the credentials to
* authenticate a user agent to a proxy server, usually after the server has
* responded with a 407 Proxy Authentication Required status and the
* Proxy-Authenticate header.
*/
object ProxyAuthorization extends HeaderType {
override type HeaderValue = ProxyAuthorization
override def name: String = "proxy-authorization"
def parse(value: String): Either[String, ProxyAuthorization] = {
value.split("\\s+") match {
case Array(authorization, credential) if authorization.nonEmpty && credential.nonEmpty =>
AuthenticationScheme.parse(authorization).map { authenticationScheme =>
ProxyAuthorization(authenticationScheme, credential)
}
case _ => Left("Invalid Proxy-Authorization header")
}
}
def render(proxyAuthorization: ProxyAuthorization): String =
s"${proxyAuthorization.authenticationScheme.name} ${proxyAuthorization.credential}"
}
sealed trait Range extends Header {
override type Self = Range
override def self: Self = this
override def headerType: HeaderType.Typed[Range] = Range
}
object Range extends HeaderType {
override type HeaderValue = Range
override def name: String = "range"
final case class Single(unit: String, start: Long, end: Option[Long]) extends Range
final case class Multiple(unit: String, ranges: List[(Long, Option[Long])]) extends Range
final case class Suffix(unit: String, value: Long) extends Range
final case class Prefix(unit: String, value: Long) extends Range
def parse(value: String): Either[String, Range] = {
val parts = value.split("=")
if (parts.length != 2) Left("Invalid Range header")
else {
Try {
val unit = parts(0)
val range = parts(1)
if (range.contains(",")) {
val ranges = range.split(",").map(_.trim).toList
val parsedRanges = ranges.map { r =>
if (r.contains("-")) {
val startEnd = r.split("-")
if (startEnd.length != 2) (startEnd(0).toLong, None)
else {
val start = startEnd(0).toLong
val end = startEnd(1).toLong
(start, Some(end))
}
} else (0L, None)
}
Multiple(unit, parsedRanges)
} else if (range.contains("-")) {
val startEnd = range.split("-")
if (startEnd.length != 2)
Single(unit, startEnd(0).toLong, None)
else {
if (startEnd(0).isEmpty)
Suffix(unit, startEnd(1).toLong)
else if (startEnd(1).isEmpty)
Prefix(unit, startEnd(0).toLong)
else
Single(unit, startEnd(0).toLong, Some(startEnd(1).toLong))
}
} else {
Suffix(unit, range.toLong)
}
}.toEither.left.map(_ => "Invalid Range header")
}
}
def render(range: Range): String = range match {
case Single(unit, start, end) => s"$unit=$start-${end.getOrElse("")}"
case Multiple(unit, ranges) =>
s"$unit=${ranges.map { case (start, end) => s"$start-${end.getOrElse("")}" }.mkString(",")}"
case Suffix(unit, suffixLength) => s"$unit=-$suffixLength"
case Prefix(unit, prefixLength) => s"$unit=$prefixLength-"
}
}
/**
* The Referer HTTP request header contains the absolute or partial address
* from which a resource has been requested. The Referer header allows a
* server to identify referring pages that people are visiting from or where
* requested resources are being used. This data can be used for analytics,
* logging, optimized caching, and more.
*
* When you click a link, the Referer contains the address of the page that
* includes the link. When you make resource requests to another domain, the
* Referer contains the address of the page that uses the requested resource.
*
* The Referer header can contain an origin, path, and querystring, and may
* not contain URL fragments (i.e. #section) or username:password information.
* The request's referrer policy defines the data that can be included. See
* Referrer-Policy for more information and examples.
*/
final case class Referer(url: URL) extends Header {
override type Self = Referer
override def self: Self = this
override def headerType: HeaderType.Typed[Referer] = Referer
}
object Referer extends HeaderType {
override type HeaderValue = Referer
override def name: String = "referer"
def parse(value: String): Either[String, Referer] = {
URL.decode(value) match {
case Left(_) => Left("Invalid Referer header")
case Right(url) if url.host.isEmpty || url.scheme.isEmpty => Left("Invalid Referer header")
case Right(url) => Right(Referer(url))
}
}
def render(referer: Referer): String =
if (referer.url.kind == URL.Location.Relative) "" else referer.url.encode
}
final case class Cookie(value: NonEmptyChunk[zio.http.Cookie.Request]) extends Header {
override type Self = Cookie
override def self: Self = this
override def headerType: HeaderType.Typed[Cookie] = Cookie
}
/**
* The Cookie HTTP request header contains stored HTTP cookies associated with
* the server.
*/
object Cookie extends HeaderType {
override type HeaderValue = Cookie
override def name: String = "cookie"
def parse(value: String): Either[String, Cookie] =
zio.http.Cookie.Request.decode(value) match {
case Left(value) => Left(s"Invalid Cookie header: ${value.getMessage}")
case Right(value) =>
NonEmptyChunk.fromChunk(value) match {
case Some(value) => Right(Cookie(value))
case None => Left("Invalid Cookie header")
}
}
def render(cookie: Cookie): String =
cookie.value.map(_.encode.getOrElse("")).mkString("; ")
}
final case class SetCookie(value: zio.http.Cookie.Response) extends Header {
override type Self = SetCookie
override def self: Self = this
override def headerType: HeaderType.Typed[SetCookie] = SetCookie
}
object SetCookie extends HeaderType {
override type HeaderValue = SetCookie
override def name: String = "set-cookie"
def parse(value: String): Either[String, SetCookie] =
zio.http.Cookie.Response.decode(value) match {
case Left(value) => Left(s"Invalid Cookie header: ${value.getMessage}")
case Right(value) => Right(SetCookie(value))
}
def render(cookie: SetCookie): String =
cookie.value.encode.getOrElse("")
}
sealed trait RetryAfter extends Header {
override type Self = RetryAfter
override def self: Self = this
override def headerType: HeaderType.Typed[RetryAfter] = RetryAfter
}
/**
* The RetryAfter HTTP header contains the date/time after which to retry
*
* RetryAfter:
*
* Date: , :: GMT
*
* Example:
*
* Expires: Wed, 21 Oct 2015 07:28:00 GMT
*
* Or RetryAfter the delay seconds.
*/
object RetryAfter extends HeaderType {
override type HeaderValue = RetryAfter
override def name: String = "retry-after"
final case class ByDate(date: ZonedDateTime) extends RetryAfter
final case class ByDuration(delay: Duration) extends RetryAfter
def parse(dateOrSeconds: String): Either[String, RetryAfter] =
Try(dateOrSeconds.toLong) match {
case Failure(_) =>
DateEncoding.default.decodeDate(dateOrSeconds) match {
case Some(value) => Right(ByDate(value))
case None => Left("Invalid RetryAfter")
}
case Success(value) =>
if (value >= 0)
Right(ByDuration(value.seconds))
else
Left("Invalid RetryAfter")
}
def render(retryAfter: RetryAfter): String =
retryAfter match {
case ByDate(date) => DateEncoding.default.encodeDate(date)
case ByDuration(duration) =>
duration.getSeconds.toString
}
}
final case class SecWebSocketAccept(hashedKey: String) extends Header {
override type Self = SecWebSocketAccept
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketAccept] = SecWebSocketAccept
}
/**
* The Sec-WebSocket-Accept header is used in the websocket opening handshake.
* It would appear in the response headers. That is, this is header is sent
* from server to client to inform that server is willing to initiate a
* websocket connection.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Accept
*/
object SecWebSocketAccept extends HeaderType {
override type HeaderValue = SecWebSocketAccept
override def name: String = "sec-websocket-accept"
def parse(value: String): Either[String, SecWebSocketAccept] =
if (value.trim.isEmpty) Left("Invalid Sec-WebSocket-Accept header")
else Right(SecWebSocketAccept(value))
def render(secWebSocketAccept: SecWebSocketAccept): String =
secWebSocketAccept.hashedKey
}
sealed trait SecWebSocketExtensions extends Header {
override type Self = SecWebSocketExtensions
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketExtensions] = SecWebSocketExtensions
}
/**
* The Sec-WebSocket-Extensions header is used in the WebSocket handshake. It
* is initially sent from the client to the server, and then subsequently sent
* from the server to the client, to agree on a set of protocol-level
* extensions to use during the connection.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Extensions
*/
object SecWebSocketExtensions extends HeaderType {
override type HeaderValue = SecWebSocketExtensions
override def name: String = "sec-websocket-extensions"
// Sec-WebSocket-Extensions: foo, bar; baz=2
// Sec-WebSocket-Extensions: deflate-stream
// Sec-WebSocket-Extensions: mux; max-channels=4; flow-control,
// deflate-stream
// Sec-WebSocket-Extensions: private-extension
// Sec-WebSocket-Extensions = extension-list
// extension-list = 1#extension
// extension = extension-token *( ";" extension-param )
// extension-token = registered-token
// registered-token = token
// extension-param = token [ "=" (token | quoted-string) ]
// ;When using the quoted-string syntax variant, the value
// ;after quoted-string unescaping MUST conform to the
// ;'token' ABNF.
sealed trait Extension
object Extension {
final case class Parameter(name: String, value: String) extends Extension
final case class TokenParam(name: String) extends Extension
}
final case class Token(extension: Chunk[Extension]) extends SecWebSocketExtensions
final case class Extensions(extensions: Chunk[Token]) extends SecWebSocketExtensions
def parse(value: String): Either[String, SecWebSocketExtensions] =
if (value.trim().isEmpty) Left("Invalid Sec-WebSocket-Extensions header")
else {
val extensions: Array[Token] = value
.split(",")
.map(_.trim)
.flatMap { extension =>
val parts = extension.split(";").map(_.trim)
val tokens =
if (parts.length == 1) Array[Extension](Extension.TokenParam(parts(0)))
else {
val params: Array[Extension] = parts.map { part =>
val value = part.split("=")
val name = value(0)
if (value.length == 1) Extension.TokenParam(name)
else Extension.Parameter(name, value(1))
}
params
}
Array(Token(Chunk.fromArray(tokens)))
}
Right(Extensions(Chunk.fromArray(extensions)))
}
def render(secWebSocketExtensions: SecWebSocketExtensions): String =
secWebSocketExtensions match {
case Extensions(extensions) =>
extensions
.map(_.extension)
.map(extension => renderParams(extension))
.mkString(", ")
case Token(extensions: Chunk[Extension]) => renderParams(extensions)
}
private def renderParams(extensions: Chunk[Extension]): String = {
extensions.map {
case Extension.TokenParam(value) => value
case Extension.Parameter(name, value) => s"$name=$value"
}.mkString("; ")
}
}
final case class SecWebSocketKey(base64EncodedKey: String) extends Header {
override type Self = SecWebSocketKey
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketKey] = SecWebSocketKey
}
/**
* The Sec-WebSocket-Key header is used in the WebSocket handshake. It is sent
* from the client to the server to provide part of the information used by
* the server to prove that it received a valid WebSocket handshake. This
* helps ensure that the server does not accept connections from non-WebSocket
* clients (e.g. HTTP clients) that are being abused to send data to
* unsuspecting WebSocket servers.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Key
*/
object SecWebSocketKey extends HeaderType {
override type HeaderValue = SecWebSocketKey
override def name: String = "sec-websocket-key"
def parse(key: String): Either[String, SecWebSocketKey] = {
try {
val decodedKey = java.util.Base64.getDecoder.decode(key)
if (decodedKey.length == 16) Right(SecWebSocketKey(key))
else Left("Invalid Sec-WebSocket-Key header")
} catch {
case NonFatal(_) => Left("Invalid Sec-WebSocket-Key header")
}
}
def render(secWebSocketKey: SecWebSocketKey): String =
secWebSocketKey.base64EncodedKey
}
final case class SecWebSocketLocation(url: URL) extends Header {
override type Self = SecWebSocketLocation
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketLocation] = SecWebSocketLocation
}
object SecWebSocketLocation extends HeaderType {
override type HeaderValue = SecWebSocketLocation
override def name: String = "sec-websocket-location"
def parse(value: String): Either[String, SecWebSocketLocation] = {
if (value.trim == "") Left("Invalid Sec-WebSocket-Location header: empty value")
else
URL
.decode(value)
.left
.map(_ => "Invalid Sec-WebSocket-Location header: invalid URL")
.map(url => SecWebSocketLocation(url))
}
def render(secWebSocketLocation: SecWebSocketLocation): String =
secWebSocketLocation.url.encode
}
final case class SecWebSocketOrigin(url: URL) extends Header {
override type Self = SecWebSocketOrigin
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketOrigin] = SecWebSocketOrigin
}
/**
* The Sec-WebSocket-Origin header is used to protect against unauthorized
* cross-origin use of a WebSocket server by scripts using the |WebSocket| API
* in a Web browser. The server is informed of the script origin generating
* the WebSocket connection request.
*/
object SecWebSocketOrigin extends HeaderType {
override type HeaderValue = SecWebSocketOrigin
override def name: String = "sec-websocket-origin"
def parse(value: String): Either[String, SecWebSocketOrigin] = {
if (value.trim == "") Left("Invalid Sec-WebSocket-Origin header: empty value")
else
URL
.decode(value)
.left
.map(_ => "Invalid Sec-WebSocket-Origin header: invalid URL")
.map(url => SecWebSocketOrigin(url))
}
def render(secWebSocketOrigin: SecWebSocketOrigin): String = {
secWebSocketOrigin.url.encode
}
}
final case class SecWebSocketProtocol(subProtocols: NonEmptyChunk[String]) extends Header {
override type Self = SecWebSocketProtocol
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketProtocol] = SecWebSocketProtocol
}
/**
* The Sec-WebSocket-Protocol header field is used in the WebSocket opening
* handshake. It is sent from the client to the server and back from the
* server to the client to confirm the subprotocol of the connection. This
* enables scripts to both select a subprotocol and be sure that the server
* agreed to serve that subprotocol.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Protocol
*/
object SecWebSocketProtocol extends HeaderType {
override type HeaderValue = SecWebSocketProtocol
override def name: String = "sec-websocket-protocol"
def parse(subProtocols: String): Either[String, SecWebSocketProtocol] = {
NonEmptyChunk.fromChunk(Chunk.fromArray(subProtocols.split(",")).map(_.trim).filter(_.nonEmpty)) match {
case Some(value) => Right(SecWebSocketProtocol(value))
case None => Left("Invalid Sec-WebSocket-Protocol header")
}
}
def render(secWebSocketProtocol: SecWebSocketProtocol): String =
secWebSocketProtocol.subProtocols.mkString(", ")
}
final case class SecWebSocketVersion(version: Int) extends Header {
override type Self = SecWebSocketVersion
override def self: Self = this
override def headerType: HeaderType.Typed[SecWebSocketVersion] = SecWebSocketVersion
}
/**
* The Sec-WebSocket-Version header field is used in the WebSocket opening
* handshake. It is sent from the client to the server to indicate the
* protocol version of the connection.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Version
*/
object SecWebSocketVersion extends HeaderType {
override type HeaderValue = SecWebSocketVersion
override def name: String = "sec-websocket-version"
// https://www.iana.org/assignments/websocket/websocket.xml#version-number
def parse(version: String): Either[String, SecWebSocketVersion] =
try {
val v = version.toInt
if (v >= 0 && v <= 13) Right(SecWebSocketVersion(v))
else Left("Invalid Sec-WebSocket-Version header")
} catch {
case NonFatal(_) => Left("Invalid Sec-WebSocket-Version header")
}
def render(secWebSocketVersion: SecWebSocketVersion): String =
secWebSocketVersion.version.toString
}
/**
* Server header value.
*/
final case class Server(name: String) extends Header {
override type Self = Server
override def self: Self = this
override def headerType: HeaderType.Typed[Server] = Server
}
object Server extends HeaderType {
override type HeaderValue = Server
override def name: String = "server"
def parse(value: String): Either[String, Server] = {
val trimmedValue = value.trim
if (trimmedValue.isEmpty)
Left("Invalid Server header: empty value")
else Right(Server(trimmedValue))
}
def render(server: Server): String =
server.name
}
sealed trait Te extends Header {
override type Self = Te
override def self: Self = this
override def headerType: HeaderType.Typed[Te] = Te
def raw: String
}
object Te extends HeaderType {
override type HeaderValue = Te
override def name: String = "te"
/**
* A compression format that uses the Lempel-Ziv-Welch (LZW) algorithm.
*/
final case class Compress(weight: Option[Double]) extends Te {
override def raw: String = "compress"
}
/**
* A compression format that uses the zlib structure with the deflate
* compression algorithm.
*/
final case class Deflate(weight: Option[Double]) extends Te {
override def raw: String = "deflate"
}
/**
* A compression format that uses the Lempel-Ziv coding (LZ77) with a 32-bit
* CRC.
*/
final case class GZip(weight: Option[Double]) extends Te {
override def raw: String = "gzip"
}
/**
* Indicates the identity function (that is, without modification or
* compression). This value is always considered as acceptable, even if
* omitted.
*/
case object Trailers extends Te {
override def raw: String = "trailers"
}
/**
* Maintains a chunk of AcceptEncoding values.
*/
final case class Multiple(encodings: NonEmptyChunk[Te]) extends Te {
override def raw: String = encodings.mkString(",")
}
private def identifyTeFull(raw: String): Option[Te] = {
val index = raw.indexOf(";q=")
if (index == -1)
identifyTe(raw)
else {
identifyTe(raw.substring(0, index), Try(raw.substring(index + 3).toDouble).toOption)
}
}
private def identifyTe(raw: String, weight: Option[Double] = None): Option[Te] = {
raw.trim match {
case "compress" => Some(Compress(weight))
case "deflate" => Some(Deflate(weight))
case "gzip" => Some(GZip(weight))
case "trailers" => Some(Trailers)
case _ => None
}
}
def parse(value: String): Either[String, Te] = {
val index = value.indexOf(",")
@tailrec def loop(value: String, index: Int, acc: Chunk[Te]): Either[String, Chunk[Te]] = {
if (index == -1) {
identifyTeFull(value) match {
case Some(te) => Right(acc :+ te)
case None => Left("Invalid TE header")
}
} else {
val valueChunk = value.substring(0, index)
val remaining = value.substring(index + 1)
val nextIndex = remaining.indexOf(",")
identifyTeFull(valueChunk) match {
case Some(te) => loop(remaining, nextIndex, acc :+ te)
case None => Left("Invalid TE header")
}
}
}
if (index == -1)
identifyTeFull(value).toRight("Invalid TE header")
else
loop(value, index, Chunk.empty[Te]).flatMap { encodings =>
NonEmptyChunk.fromChunk(encodings).toRight("Invalid TE header").map(Multiple(_))
}
}
def render(encoding: Te): String = encoding match {
case c @ Compress(weight) => weight.fold(c.raw)(value => s"${c.raw};q=$value")
case d @ Deflate(weight) => weight.fold(d.raw)(value => s"${d.raw};q=$value")
case g @ GZip(weight) => weight.fold(g.raw)(value => s"${g.raw};q=$value")
case Multiple(encodings) => encodings.map(render).mkString(", ")
case Trailers => Trailers.raw
}
}
/** Trailer header value. */
final case class Trailer(header: String) extends Header {
override type Self = Trailer
override def self: Self = this
override def headerType: HeaderType.Typed[Trailer] = Trailer
}
object Trailer extends HeaderType {
override type HeaderValue = Trailer
override def name: String = "trailer"
private val headerRegex = "([a-z-_]*)".r
def parse(value: String): Either[String, Trailer] =
value.toLowerCase match {
case headerRegex(value) => Right(Trailer(value))
case _ => Left("Invalid Trailer header")
}
def render(trailer: Trailer): String =
trailer.header
}
sealed trait TransferEncoding extends Header {
override type Self = TransferEncoding
override def self: Self = this
override def headerType: HeaderType.Typed[TransferEncoding] = TransferEncoding
val encoding: String
}
object TransferEncoding extends HeaderType {
override type HeaderValue = TransferEncoding
override def name: String = "transfer-encoding"
/**
* Data is sent in a series of chunks.
*/
case object Chunked extends TransferEncoding {
override val encoding: String = "chunked"
}
/**
* A format using the Lempel-Ziv-Welch (LZW) algorithm. The value name was
* taken from the UNIX compress program, which implemented this algorithm.
* Like the compress program, which has disappeared from most UNIX
* distributions, this content-encoding is not used by many browsers today,
* partly because of a patent issue (it expired in 2003).
*/
case object Compress extends TransferEncoding {
override val encoding: String = "compress"
}
/**
* Using the zlib structure (defined in RFC 1950) with the deflate
* compression algorithm (defined in RFC 1951).
*/
case object Deflate extends TransferEncoding {
override val encoding: String = "deflate"
}
/**
* A format using the Lempel-Ziv coding (LZ77), with a 32-bit CRC. This is
* the original format of the UNIX gzip program. The HTTP/1.1 standard also
* recommends that the servers supporting this content-encoding should
* recognize x-gzip as an alias, for compatibility purposes.
*/
case object GZip extends TransferEncoding {
override val encoding: String = "gzip"
}
/**
* Maintains a list of TransferEncoding values.
*/
final case class Multiple(encodings: NonEmptyChunk[TransferEncoding]) extends TransferEncoding {
override val encoding: String = encodings.map(_.encoding).mkString(",")
}
private def findEncoding(value: String): Option[TransferEncoding] = {
value.trim match {
case "chunked" => Some(Chunked)
case "compress" => Some(Compress)
case "deflate" => Some(Deflate)
case "gzip" => Some(GZip)
case _ => None
}
}
/**
* @param value
* of string , separated for multiple values
* @return
* TransferEncoding
*
* Note: This implementation ignores the invalid string that might occur in
* MultipleEncodings case.
*/
def parse(value: String): Either[String, TransferEncoding] = {
val encodings = Chunk.fromArray(value.split(",")).map(findEncoding).flatten
NonEmptyChunk.fromChunk(encodings) match {
case Some(value) =>
if (value.size == 1) Right(value.head)
else Right(Multiple(value))
case None => Left("Empty TransferEncoding")
}
}
def render(value: TransferEncoding): String = value.encoding
}
sealed trait Upgrade extends Header {
override type Self = Upgrade
override def self: Self = this
override def headerType: HeaderType.Typed[Upgrade] = Upgrade
}
object Upgrade extends HeaderType {
override type HeaderValue = Upgrade
override def name: String = "upgrade"
final case class Multiple(protocols: NonEmptyChunk[Protocol]) extends Upgrade
final case class Protocol(protocol: String, version: String) extends Upgrade
def parse(value: String): Either[String, Upgrade] = {
NonEmptyChunk.fromChunk(Chunk.fromArray(value.split(",")).map(parseProtocol)) match {
case None => Left("Invalid Upgrade header")
case Some(value) =>
if (value.size == 1) value.head
else
value.tail
.foldLeft(value.head.map(NonEmptyChunk.single(_))) {
case (Right(acc), Right(value)) => Right(acc :+ value)
case (Left(error), _) => Left(error)
case (_, Left(value)) => Left(value)
}
.map(Multiple(_))
}
}
def render(upgrade: Upgrade): String =
upgrade match {
case Multiple(protocols) => protocols.map(render).mkString(", ")
case Protocol(protocol, version) => s"$protocol/$version"
}
private def parseProtocol(value: String): Either[String, Protocol] =
Chunk.fromArray(value.split("/")).map(_.trim) match {
case Chunk(protocol, version) => Right(Protocol(protocol, version))
case _ => Left("Invalid Upgrade header")
}
}
final case class UpgradeInsecureRequests() extends Header {
override type Self = UpgradeInsecureRequests
override def self: Self = this
override def headerType: HeaderType.Typed[UpgradeInsecureRequests] = UpgradeInsecureRequests
}
/**
* The HTTP Upgrade-Insecure-Requests request header sends a signal to the
* server expressing the client's preference for an encrypted and
* authenticated response.
*/
object UpgradeInsecureRequests extends HeaderType {
override type HeaderValue = UpgradeInsecureRequests
override def name: String = "upgrade-insecure-requests"
def parse(value: String): Either[String, UpgradeInsecureRequests] =
if (value.trim == "1") Right(UpgradeInsecureRequests())
else Left("Invalid Upgrade-Insecure-Requests header")
def render(upgradeInsecureRequests: UpgradeInsecureRequests): String =
"1"
}
final case class UserAgent(
product: UserAgent.ProductOrComment.Product,
parts: List[UserAgent.ProductOrComment] = Nil,
) extends Header {
override type Self = UserAgent
override def self: Self = this
override def headerType: HeaderType.Typed[UserAgent] = UserAgent
}
/**
* The "User-Agent" header field contains information about the user agent
* originating the request, which is often used by servers to help identify
* the scope of reported interoperability problems, to work around or tailor
* responses to avoid particular user agent limitations, and for analytics
* regarding browser or operating system use
*/
object UserAgent extends HeaderType {
override type HeaderValue = UserAgent
override def name: String = "user-agent"
sealed trait ProductOrComment
object ProductOrComment {
final case class Product(name: String, version: Option[String]) extends ProductOrComment
final case class Comment(comment: String) extends ProductOrComment
}
private val tokenPattern = "[!#$%&'*+\\-.^_`|~\\w]"
private val commentPattern = "\\((.*?)\\)"
private val productPattern = s"($tokenPattern+)(?:/($tokenPattern+))?"
private val productRegex = s"(?i)$productPattern\\s*?(.*)".r
private val commentRegex = s"(?i)$commentPattern".r
private val productOrCommentRegex = s"(?i)(?:(?:$productPattern)|(?:$commentPattern))".r
private def hasBalancedParentheses(s: String): Boolean = {
val (balance, isValid) = s.foldLeft((0, true)) {
case ((balance, true), '(') => (balance + 1, true)
case ((balance, true), ')') =>
if (balance > 0) (balance - 1, true)
else (balance, false)
case (state, _) => state
}
isValid && balance == 0
}
def parse(userAgent: String): Either[String, UserAgent] = {
userAgent match {
case productRegex(name, version, tail) if hasBalancedParentheses(tail) =>
val product = ProductOrComment.Product(name, Option(version))
val parts = productOrCommentRegex.findAllIn(tail).toList.collect {
case productRegex(name, version, _) => ProductOrComment.Product(name, Option(version))
case commentRegex(comment) => ProductOrComment.Comment(comment)
}
Right(UserAgent(product, parts))
case _ =>
Left("Invalid User-Agent header")
}
}
def render(userAgent: UserAgent): String =
(userAgent.product :: userAgent.parts).map(fromProductOrComment).mkString(" ")
private def fromProductOrComment(value: ProductOrComment): String = value match {
case ProductOrComment.Product(name, version) =>
s"""$name${version.map("/" + _).getOrElse("")}"""
case ProductOrComment.Comment(comment) =>
s"($comment)"
}
}
/** Vary header value. */
sealed trait Vary extends Header {
override type Self = Vary
override def self: Self = this
override def headerType: HeaderType.Typed[Vary] = Vary
}
object Vary extends HeaderType {
override type HeaderValue = Vary
override def name: String = "vary"
case class Headers(headers: NonEmptyChunk[String]) extends Vary
case object Star extends Vary
def apply(first: String, rest: String*): Vary = Headers(NonEmptyChunk(first, rest: _*))
def parse(value: String): Either[String, Vary] = {
Chunk.fromArray(value.toLowerCase().split("[, ]+")) match {
case Chunk("*") => Right(Star)
case chunk if value.nonEmpty =>
NonEmptyChunk.fromChunk(chunk) match {
case Some(chunk) => Right(Headers(chunk.map(_.trim)))
case None => Left("Invalid Vary header")
}
case _ => Left("Invalid Vary header")
}
}
def render(vary: Vary): String = {
vary match {
case Star => "*"
case Headers(list) => list.mkString(", ")
}
}
}
sealed trait Via extends Header {
override type Self = Via
override def self: Self = this
override def headerType: HeaderType.Typed[Via] = Via
}
/**
* The Via general header is added by proxies, both forward and reverse, and
* can appear in the request or response headers. It is used for tracking
* message forwards, avoiding request loops, and identifying the protocol
* capabilities of senders along the request/response chain
*/
object Via extends HeaderType {
override type HeaderValue = Via
override def name: String = "via"
sealed trait ReceivedProtocol
object ReceivedProtocol {
final case class Version(version: String) extends ReceivedProtocol
final case class ProtocolVersion(protocol: String, version: String) extends ReceivedProtocol
}
final case class Detailed(receivedProtocol: ReceivedProtocol, receivedBy: String, comment: Option[String]) extends Via
final case class Multiple(values: NonEmptyChunk[Via]) extends Via
def parse(values: String): Either[String, Via] = {
val viaValues = Chunk.fromArray(values.split(",")).map(_.trim).map { value =>
Chunk.fromArray(value.split(" ")) match {
case Chunk(receivedProtocol, receivedBy) =>
toReceivedProtocol(receivedProtocol).map { rp =>
Detailed(rp, receivedBy, None)
}
case Chunk(receivedProtocol, receivedBy, comment) =>
toReceivedProtocol(receivedProtocol).map { rp =>
Detailed(rp, receivedBy, Some(comment))
}
case _ =>
Left("Invalid Via header")
}
}
NonEmptyChunk.fromChunk(viaValues) match {
case None => Left("Invalid Via header")
case Some(value) =>
if (value.size == 1) value.head
else
value.tail
.foldLeft(value.head.map(NonEmptyChunk.single(_))) {
case (Right(acc), Right(value)) => Right(acc :+ value)
case (Left(error), _) => Left(error)
case (_, Left(value)) => Left(value)
}
.map(Multiple(_))
}
}
def render(via: Via): String =
via match {
case Multiple(values) =>
values.map(render).mkString(", ")
case Detailed(receivedProtocol, receivedBy, comment) =>
s"${fromReceivedProtocol(receivedProtocol)} $receivedBy ${comment.getOrElse("")}"
}
private def fromReceivedProtocol(receivedProtocol: ReceivedProtocol): String =
receivedProtocol match {
case ReceivedProtocol.Version(version) => version
case ReceivedProtocol.ProtocolVersion(protocol, version) => s"$protocol/$version"
}
private def toReceivedProtocol(value: String): Either[String, ReceivedProtocol] = {
value.split("/").toList match {
case version :: Nil => Right(ReceivedProtocol.Version(version))
case protocol :: version :: Nil => Right(ReceivedProtocol.ProtocolVersion(protocol, version))
case _ => Left("Invalid received protocol")
}
}
}
sealed trait WWWAuthenticate extends Header {
override type Self = WWWAuthenticate
override def self: Self = this
override def headerType: HeaderType.Typed[WWWAuthenticate] = WWWAuthenticate
}
object WWWAuthenticate extends HeaderType {
override type HeaderValue = WWWAuthenticate
override def name: String = "www-authenticate"
final case class Basic(realm: Option[String] = None, charset: String = "UTF-8") extends WWWAuthenticate
final case class Bearer(
realm: String,
scope: Option[String] = None,
error: Option[String] = None,
errorDescription: Option[String] = None,
) extends WWWAuthenticate
final case class Digest(
realm: Option[String],
domain: Option[String] = None,
nonce: Option[String] = None,
opaque: Option[String] = None,
stale: Option[Boolean] = None,
algorithm: Option[String] = None,
qop: Option[String] = None,
charset: Option[String] = None,
userhash: Option[Boolean] = None,
) extends WWWAuthenticate
final case class HOBA(realm: Option[String], challenge: String, maxAge: Int) extends WWWAuthenticate
final case class Mutual(realm: String, error: Option[String] = None, errorDescription: Option[String] = None) extends WWWAuthenticate
final case class Negotiate(authData: Option[String] = None) extends WWWAuthenticate
final case class SCRAM(
realm: String,
sid: String,
data: String,
) extends WWWAuthenticate
final case class `AWS4-HMAC-SHA256`(
realm: String,
credentials: Option[String] = None,
signedHeaders: String,
signature: String,
) extends WWWAuthenticate
final case class Unknown(scheme: String, realm: String, params: Map[String, String]) extends WWWAuthenticate
private val challengeRegEx = """(\w+) (.*)""".r
private val auth = """(\w+)=(?:"([^"]+)"|([^,]+))""".r
private val nonQuotedValues = Set("max_age", "stale", "userhash", "algorithm", "charset")
def parse(value: String): Either[String, WWWAuthenticate] =
Try {
val challengeRegEx(scheme, challenge) = value: @unchecked
val params = auth
.findAllMatchIn(challenge)
.map { m =>
val key = m.group(1)
val value = Option(m.group(2)).getOrElse(m.group(3))
key -> value
}
.toMap
AuthenticationScheme.parse(scheme).map {
case AuthenticationScheme.Basic =>
Basic(params.get("realm"), params.getOrElse("charset", "UTF-8"))
case AuthenticationScheme.Bearer =>
Bearer(
realm = params("realm"),
scope = params.get("scope"),
error = params.get("error"),
errorDescription = params.get("error_description"),
)
case AuthenticationScheme.Digest =>
Digest(
realm = params.get("realm"),
domain = params.get("domain"),
nonce = params.get("nonce"),
opaque = params.get("opaque"),
stale = params.get("stale").map(_.toBoolean),
algorithm = params.get("algorithm"),
qop = params.get("qop"),
charset = params.get("charset"),
userhash = params.get("userhash").map(_.toBoolean),
)
case AuthenticationScheme.HOBA =>
HOBA(
realm = params.get("realm"),
challenge = params("challenge"),
maxAge = params("max_age").toInt,
)
case AuthenticationScheme.Mutual =>
Mutual(
realm = params("realm"),
error = params.get("error"),
errorDescription = params.get("error_description"),
)
case AuthenticationScheme.Negotiate =>
Negotiate(Some(challenge))
case AuthenticationScheme.Scram =>
SCRAM(
realm = params("realm"),
sid = params("sid"),
data = params("data"),
)
case AuthenticationScheme.`AWS4-HMAC-SHA256` =>
`AWS4-HMAC-SHA256`(
realm = params("realm"),
credentials = params.get("credentials"),
signedHeaders = params("signedHeaders"),
signature = params("signature"),
)
case _ =>
Unknown(scheme, params("realm"), params)
}
}.toEither.left.map(_ => s"Invalid WWW-Authenticate header").flatMap {
case Right(value) => Right(value)
case Left(value) => Left(value)
}
def render(wwwAuthenticate: WWWAuthenticate): String = {
val (scheme, params) = wwwAuthenticate match {
case Basic(realm, charset) =>
"Basic" -> mutable.LinkedHashMap("realm" -> realm.getOrElse(""), charset -> charset)
case Bearer(realm, scope, error, errorDescription) =>
"Bearer" -> mutable.LinkedHashMap(
"realm" -> realm,
"scope" -> scope.getOrElse(""),
"error" -> error.getOrElse(""),
"error_description" -> errorDescription.getOrElse(""),
)
case Digest(realm, domain, nonce, opaque, stale, algorithm, qop, charset, userhash) =>
"Digest" -> mutable.LinkedHashMap(
"realm" -> realm.getOrElse(""),
"domain" -> domain.getOrElse(""),
"nonce" -> nonce.getOrElse(""),
"opaque" -> opaque.getOrElse(""),
"stale" -> stale.getOrElse(false).toString,
"algorithm" -> algorithm.getOrElse(""),
"qop" -> qop.getOrElse(""),
"charset" -> charset.getOrElse(""),
"userhash" -> userhash.getOrElse(false).toString,
)
case HOBA(realm, challenge, maxAge) =>
"HOBA" -> mutable.LinkedHashMap(
"realm" -> realm.getOrElse(""),
"challenge" -> challenge,
"max_age" -> maxAge.toString,
)
case Mutual(realm, error, errorDescription) =>
"Mutual" -> mutable.LinkedHashMap(
"realm" -> realm,
"error" -> error.getOrElse(""),
"error_description" -> errorDescription.getOrElse(""),
)
case Negotiate(authData) =>
"Negotiate" -> mutable.LinkedHashMap(
"" -> authData.getOrElse(""),
)
case SCRAM(realm, sid, data) =>
"SCRAM" -> mutable.LinkedHashMap(
"realm" -> realm,
"sid" -> sid,
"data" -> data,
)
case `AWS4-HMAC-SHA256`(realm, credentials, signedHeaders, signature) =>
"AWS4-HMAC-SHA256" -> mutable.LinkedHashMap(
"realm" -> realm,
"credentials" -> credentials.getOrElse(""),
"signedHeaders" -> signedHeaders,
"signature" -> signature,
)
case Unknown(scheme, _, params) =>
scheme -> params
}
scheme + params.filter { case (_, v) => v.nonEmpty }.map { case (k, v) =>
if (k.isEmpty) s"$v" else s"$k=${formatValue(k, v)}"
}
.mkString(" ", ", ", "")
}
private def formatValue(key: String, value: String): String = {
if (nonQuotedValues.contains(key)) value else "\"" + value + "\""
}
}
sealed trait XFrameOptions extends Header {
override type Self = XFrameOptions
override def self: Self = this
override def headerType: HeaderType.Typed[XFrameOptions] = XFrameOptions
}
object XFrameOptions extends HeaderType {
override type HeaderValue = XFrameOptions
override def name: String = "x-frame-options"
case object Deny extends XFrameOptions
case object SameOrigin extends XFrameOptions
def parse(value: String): Either[String, XFrameOptions] = {
value.trim.toUpperCase match {
case "DENY" => Right(Deny)
case "SAMEORIGIN" => Right(SameOrigin)
case _ => Left("Invalid X-Frame-Options header")
}
}
def render(xFrameOptions: XFrameOptions): String =
xFrameOptions match {
case Deny => "DENY"
case SameOrigin => "SAMEORIGIN"
}
}
final case class XRequestedWith(value: String) extends Header {
override type Self = XRequestedWith
override def self: Self = this
override def headerType: HeaderType.Typed[XRequestedWith] = XRequestedWith
}
object XRequestedWith extends HeaderType {
override type HeaderValue = XRequestedWith
override def name: String = "x-requested-with"
def parse(value: String): Either[String, XRequestedWith] =
Right(XRequestedWith(value))
def render(xRequestedWith: XRequestedWith): String =
xRequestedWith.value
}
}