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