-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathHeader.scala
167 lines (147 loc) · 8.05 KB
/
Header.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package sttp.model
import sttp.model.HeaderNames.SensitiveHeaders
import sttp.model.headers.{
AcceptEncoding,
CacheDirective,
ContentRange,
Cookie,
CookieWithMeta,
ETag,
Range,
WWWAuthenticateChallenge
}
import sttp.model.internal.Validate
import sttp.model.internal.Rfc2616.validateToken
import sttp.model.internal.Validate._
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
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): Some[(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))(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 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 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 Rfc850DatetimePattern = "dd-MMM-yyyy HH:mm:ss zzz"
private lazy val Rfc850DatetimeFormat = DateTimeFormatter.ofPattern(Rfc850DatetimePattern, Locale.US)
val Rfc850WeekDays = Set("mon", "tue", "wed", "thu", "fri", "sat", "sun")
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(i: Instant): String = DateTimeFormatter.RFC_1123_DATE_TIME.format(i.atZone(GMT))
}