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

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

The newest version!
package sttp.model

import sttp.model.HeaderNames.SensitiveHeaders
import sttp.model.headers.{
  AcceptEncoding,
  CacheDirective,
  ContentRange,
  Cookie,
  CookieWithMeta,
  ETag,
  Origin,
  Range,
  WWWAuthenticateChallenge
}
import sttp.model.internal.Validate
import sttp.model.internal.Rfc2616.validateToken
import sttp.model.internal.Rfc9110.validateFieldValue
import sttp.model.internal.Validate._

import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.Locale
import scala.util.{Failure, Success, Try}
import scala.util.hashing.MurmurHash3

/** An HTTP header. The [[name]] property is case-insensitive during equality checks.
  *
  * To compare if two headers have the same name, use the [[is]] method, which does a case-insensitive check, instead of
  * comparing the [[name]] property.
  *
  * The [[name]] and [[value]] should be already encoded (if necessary), as when serialised, they end up unmodified in
  * the header.
  */
class Header(val name: String, val value: String) {

  /** Check if the name of this header is the same as the given one. The names are compared in a case-insensitive way.
    */
  def is(otherName: String): Boolean = name.equalsIgnoreCase(otherName)

  /** @return
    *   Representation in the format: `[name]: [value]`.
    */
  override def toString: String = s"$name: $value"
  override def hashCode(): Int = MurmurHash3.mixLast(name.toLowerCase.hashCode, value.hashCode)
  override def equals(that: Any): Boolean =
    that match {
      case h: AnyRef if this.eq(h) => true
      case h: Header               => is(h.name) && (value == h.value)
      case _                       => false
    }

  /** @return
    *   Representation in the format: `[name]: [value]`. If the header is sensitive (see
    *   [[HeaderNames.SensitiveHeaders]]), the value is omitted.
    */
  def toStringSafe(sensitiveHeaders: Set[String] = SensitiveHeaders): String =
    s"$name: ${if (HeaderNames.isSensitive(name, sensitiveHeaders)) "***" else value}"
}

/** For a description of the behavior of `apply`, `safeApply` and `unsafeApply` methods, see [[sttp.model]].
  */
object Header {
  def unapply(h: Header): Option[(String, String)] = Some((h.name, h.value))

  /** @throws IllegalArgumentException
    *   If the header name contains illegal characters.
    */
  def unsafeApply(name: String, value: String): Header = safeApply(name, value).getOrThrow

  def safeApply(name: String, value: String): Either[String, Header] = {
    Validate.all(validateToken("Header name", name), validateFieldValue(value))(apply(name, value))
  }

  def apply(name: String, value: String): Header = new Header(name, value)

  //

  def accept(mediaType: MediaType, additionalMediaTypes: MediaType*): Header = accept(
    s"${(mediaType :: additionalMediaTypes.toList).map(_.noCharset).mkString(", ")}"
  )
  def accept(mediaRanges: String): Header = Header(HeaderNames.Accept, mediaRanges)
  def acceptCharset(charsetRanges: String): Header = Header(HeaderNames.AcceptCharset, charsetRanges)
  def acceptEncoding(encodingRanges: String): Header = Header(HeaderNames.AcceptEncoding, encodingRanges)
  def accessControlAllowCredentials(allow: Boolean): Header =
    Header(HeaderNames.AccessControlAllowCredentials, allow.toString)
  def accessControlAllowHeaders(headerNames: String*): Header =
    Header(HeaderNames.AccessControlAllowHeaders, headerNames.mkString(", "))
  def accessControlAllowMethods(methods: Method*): Header =
    Header(HeaderNames.AccessControlAllowMethods, methods.map(_.method).mkString(", "))
  def accessControlAllowOrigin(originRange: String): Header =
    Header(HeaderNames.AccessControlAllowOrigin, originRange)
  def accessControlExposeHeaders(headerNames: String*): Header =
    Header(HeaderNames.AccessControlExposeHeaders, headerNames.mkString(", "))
  def accessControlMaxAge(deltaSeconds: Long): Header =
    Header(HeaderNames.AccessControlMaxAge, deltaSeconds.toString)
  def accessControlRequestHeaders(headerNames: String*): Header =
    Header(HeaderNames.AccessControlRequestHeaders, headerNames.mkString(", "))
  def accessControlRequestMethod(method: Method): Header =
    Header(HeaderNames.AccessControlRequestMethod, method.toString)
  def authorization(authType: String, credentials: String): Header =
    Header(HeaderNames.Authorization, s"$authType $credentials")
  def acceptEncoding(acceptEncoding: AcceptEncoding): Header =
    Header(HeaderNames.AcceptEncoding, acceptEncoding.toString)
  def cacheControl(first: CacheDirective, other: CacheDirective*): Header = cacheControl(first +: other)
  def cacheControl(directives: Iterable[CacheDirective]): Header =
    Header(HeaderNames.CacheControl, directives.map(_.toString).mkString(", "))
  def contentLength(length: Long): Header = Header(HeaderNames.ContentLength, length.toString)
  def contentEncoding(encoding: String): Header = Header(HeaderNames.ContentEncoding, encoding)
  def contentType(mediaType: MediaType): Header = Header(HeaderNames.ContentType, mediaType.toString)
  def contentRange(contentRange: ContentRange): Header = Header(HeaderNames.ContentRange, contentRange.toString)
  def cookie(firstCookie: Cookie, otherCookies: Cookie*): Header =
    Header(HeaderNames.Cookie, (firstCookie +: otherCookies).map(_.toString).mkString("; "))
  def etag(tag: String): Header = etag(ETag(tag))
  def etag(tag: ETag): Header = Header(HeaderNames.Etag, tag.toString)
  def expires(i: Instant): Header = Header(HeaderNames.Expires, toHttpDateString(i))
  def ifNoneMatch(tags: List[ETag]): Header = Header(HeaderNames.IfNoneMatch, ETag.toString(tags))
  def ifModifiedSince(i: Instant): Header = Header(HeaderNames.IfModifiedSince, toHttpDateString(i))
  def ifUnmodifiedSince(i: Instant): Header = Header(HeaderNames.IfUnmodifiedSince, toHttpDateString(i))
  def lastModified(i: Instant): Header = Header(HeaderNames.LastModified, toHttpDateString(i))
  def location(uri: String): Header = Header(HeaderNames.Location, uri)
  def location(uri: Uri): Header = Header(HeaderNames.Location, uri.toString)
  def origin(origin: Origin): Header = Header(HeaderNames.Origin, origin.toString)
  def proxyAuthorization(authType: String, credentials: String): Header =
    Header(HeaderNames.ProxyAuthorization, s"$authType $credentials")
  def range(range: Range): Header = Header(HeaderNames.Range, range.toString)
  def setCookie(cookie: CookieWithMeta): Header = Header(HeaderNames.SetCookie, cookie.toString)
  def userAgent(userAgent: String): Header = Header(HeaderNames.UserAgent, userAgent)
  def wwwAuthenticate(challenge: WWWAuthenticateChallenge): Header =
    Header(HeaderNames.WwwAuthenticate, challenge.toString)
  def wwwAuthenticate(
      firstChallenge: WWWAuthenticateChallenge,
      otherChallenges: WWWAuthenticateChallenge*
  ): List[Header] =
    (firstChallenge :: otherChallenges.toList).map(c => Header(HeaderNames.WwwAuthenticate, c.toString))
  def vary(headerNames: String*): Header = Header(HeaderNames.Vary, headerNames.mkString(", "))
  def xForwardedFor(firstAddress: String, otherAddresses: String*): Header =
    Header(HeaderNames.XForwardedFor, (firstAddress +: otherAddresses).mkString(", "))

  // TODO: remove lazy once native supports java time
  private lazy val GMT = ZoneId.of("GMT")

  //

  private lazy val Rfc850DatetimeFormat =
    new DateTimeFormatterBuilder()
      .appendPattern("dd-MMM-")
      .appendValueReduced(ChronoField.YEAR, 2, 4, 1970)
      .appendPattern(" HH:mm:ss zzz")
      .toFormatter(Locale.US);

  val Rfc850WeekDays = Set("mon", "tue", "wed", "thu", "fri", "sat", "sun") // not private b/c of bin-compat
  private val Rfc1123WeekDays: Array[String] = Array("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
  private val Rfc1123Months: Array[String] =
    Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

  private def parseRfc850DateTime(v: String): Instant = {
    val expiresParts = v.split(", ")
    if (expiresParts.length != 2)
      throw new Exception("There must be exactly one \", \"")
    if (!Rfc850WeekDays.contains(expiresParts(0).trim.toLowerCase(Locale.ENGLISH)))
      throw new Exception("String must start with weekday name")
    Instant.from(Rfc850DatetimeFormat.parse(expiresParts(1)))
  }

  def parseHttpDate(v: String): Either[String, Instant] =
    Try(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(v))) match {
      case Success(r) => Right(r)
      case Failure(e) =>
        Try(parseRfc850DateTime(v)) match {
          case Success(r) => Right(r)
          case Failure(_) => Left(s"Invalid http date: $v (${e.getMessage})")
        }
    }
  def unsafeParseHttpDate(s: String): Instant = parseHttpDate(s).getOrThrow

  def toHttpDateString(instantTime: Instant): String = {
    val dateTime = instantTime.atZone(GMT)
    val dayOfWeek = Rfc1123WeekDays(dateTime.getDayOfWeek.getValue - 1)
    val month = Rfc1123Months(dateTime.getMonth.getValue - 1)
    val dayOfMonth = dateTime.getDayOfMonth
    val year = dateTime.getYear
    val hour = dateTime.getHour
    val minute = dateTime.getMinute
    val second = dateTime.getSecond

    f"$dayOfWeek, $dayOfMonth%02d $month $year%04d $hour%02d:$minute%02d:$second%02d GMT"
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy