All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sttp.model.Cookie.scala Maven / Gradle / Ivy

There is a newer version: 2.0.0-RC4
Show newest version
package sttp.model

import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter

import scala.util.{Failure, Success, Try}

case class Cookie(name: String, value: String) {
  private val AllowedNameCharacters = """[a-zA-Z0-9!#$%&'*+\-.^_`|~]*""".r
  private val AllowedValueCharacters = """"?[a-zA-Z0-9!#$%&'()*+\-./:<=>?@\\[\\]^_`{|}~]*"?""".r

  require(
    AllowedNameCharacters.unapplySeq(name).isDefined,
    "Cookie name can only contain alphanumeric characters and: !#$%&'*+\\-.^_`|~"
  )

  require(
    AllowedValueCharacters.unapplySeq(name).isDefined,
    "Cookie value can only contain alphanumeric characters and: !#$%&'()*+\\-./:<=>?@[]^_`{|}~, optionally surrounded by \""
  )

  def asHeaderValue: String = s"$name=$value"
}

object Cookie {
  def parseHeaderValue(s: String): List[Cookie] = {
    s.split(";").toList.map { ss =>
      ss.split("=", 2).map(_.trim) match {
        case Array(v1)     => Cookie(v1, "")
        case Array(v1, v2) => Cookie(v1, v2)
      }
    }
  }

  def asHeaderValue(cs: List[Cookie]): String = cs.map(_.asHeaderValue).mkString("; ")
}

case class CookieValueWithMeta(
    value: String,
    expires: Option[Instant],
    maxAge: Option[Long],
    domain: Option[String],
    path: Option[String],
    secure: Boolean,
    httpOnly: Boolean
)

case class CookieWithMeta(
    name: String,
    valueWithMeta: CookieValueWithMeta
) {
  def value: String = valueWithMeta.value
  def expires: Option[Instant] = valueWithMeta.expires
  def maxAge: Option[Long] = valueWithMeta.maxAge
  def domain: Option[String] = valueWithMeta.domain
  def path: Option[String] = valueWithMeta.path
  def secure: Boolean = valueWithMeta.secure
  def httpOnly: Boolean = valueWithMeta.httpOnly

  def value(v: String): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(value = v))
  def expires(v: Option[Instant]): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(expires = v))
  def maxAge(v: Option[Long]): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(maxAge = v))
  def domain(v: Option[String]): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(domain = v))
  def path(v: Option[String]): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(path = v))
  def secure(v: Boolean): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(secure = v))
  def httpOnly(v: Boolean): CookieWithMeta = copy(valueWithMeta = valueWithMeta.copy(httpOnly = v))

  def asHeaderValue: String = {
    val components = List(
      Some(s"$name=$value"),
      expires.map(e => s"Expires=${DateTimeFormatter.RFC_1123_DATE_TIME.format(e.atZone(ZoneId.of("GMT")))}"),
      maxAge.map(a => s"Max-Age=$a"),
      domain.map(d => s"Domain=$d"),
      path.map(p => s"Path=$p"),
      if (secure) Some("Secure") else None,
      if (httpOnly) Some("HttpOnly") else None
    )

    components.flatten.mkString("; ")
  }
}

object CookieWithMeta {
  def apply(
      name: String,
      value: String,
      expires: Option[Instant] = None,
      maxAge: Option[Long] = None,
      domain: Option[String] = None,
      path: Option[String] = None,
      secure: Boolean = false,
      httpOnly: Boolean = false
  ): CookieWithMeta = CookieWithMeta(name, CookieValueWithMeta(value, expires, maxAge, domain, path, secure, httpOnly))

  // https://tools.ietf.org/html/rfc6265#section-4.1.1
  def parseHeaderValue(s: String): Either[String, CookieWithMeta] = {
    def splitkv(kv: String): (String, Option[String]) = kv.split("=", 2).map(_.trim) match {
      case Array(v1)     => (v1, None)
      case Array(v1, v2) => (v1, Some(v2))
    }

    val components = s.split(";").map(_.trim)
    val (first, other) = (components.head, components.tail)
    val (name, value) = splitkv(first)
    var result: Either[String, CookieWithMeta] = Right(CookieWithMeta(name, value.getOrElse("")))
    other.map(splitkv).map(t => (t._1.toLowerCase, t._2)).foreach {
      case (k, Some(v)) if k == "expires" =>
        Try(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(v))) match {
          case Success(expires) => result = result.right.map(_.expires(Some(expires)))
          case Failure(_)       => result = Left(s"Expires cookie attribute is not a valid RFC1123 datetime: $v")
        }
      case (k, Some(v)) if k == "max-age" =>
        Try(v.toLong) match {
          case Success(maxAge) => result = result.right.map(_.maxAge(Some(maxAge)))
          case Failure(_)      => result = Left(s"Max-Age cookie attribute is not a number: $v")
        }
      case (k, v) if k == "domain"   => result = result.right.map(_.domain(Some(v.getOrElse(""))))
      case (k, v) if k == "path"     => result = result.right.map(_.path(Some(v.getOrElse(""))))
      case (k, _) if k == "secure"   => result = result.right.map(_.secure(true))
      case (k, _) if k == "httponly" => result = result.right.map(_.httpOnly(true))
      case (k, v)                    => result = Left(s"Unknown cookie attribute: $k=${v.getOrElse("")}")
    }

    result
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy