forked from square/okhttp
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Headers.kt
457 lines (411 loc) · 14.3 KB
/
Headers.kt
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 okhttp3
import java.time.Instant
import java.util.ArrayList
import java.util.Collections
import java.util.Date
import java.util.Locale
import java.util.TreeMap
import java.util.TreeSet
import okhttp3.Headers.Builder
import okhttp3.internal.format
import okhttp3.internal.http.toHttpDateOrNull
import okhttp3.internal.http.toHttpDateString
import okhttp3.internal.isSensitiveHeader
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
/**
* The header fields of a single HTTP message. Values are uninterpreted strings; use `Request` and
* `Response` for interpreted headers. This class maintains the order of the header fields within
* the HTTP message.
*
* This class tracks header values line-by-line. A field with multiple comma- separated values on
* the same line will be treated as a field with a single value by this class. It is the caller's
* responsibility to detect and split on commas if their field permits multiple values. This
* simplifies use of single-valued fields whose values routinely contain commas, such as cookies or
* dates.
*
* This class trims whitespace from values. It never returns values with leading or trailing
* whitespace.
*
* Instances of this class are immutable. Use [Builder] to create instances.
*/
@Suppress("NAME_SHADOWING")
class Headers private constructor(
private val namesAndValues: Array<String>
) : Iterable<Pair<String, String>> {
/** Returns the last value corresponding to the specified field, or null. */
operator fun get(name: String): String? = get(namesAndValues, name)
/**
* Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
* either the field is absent or cannot be parsed as a date.
*/
fun getDate(name: String): Date? = get(name)?.toHttpDateOrNull()
/**
* Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
* either the field is absent or cannot be parsed as a date.
*/
@IgnoreJRERequirement
fun getInstant(name: String): Instant? {
val value = getDate(name)
return value?.toInstant()
}
/** Returns the number of field values. */
@get:JvmName("size") val size: Int
get() = namesAndValues.size / 2
@JvmName("-deprecated_size")
@Deprecated(
message = "moved to val",
replaceWith = ReplaceWith(expression = "size"),
level = DeprecationLevel.ERROR)
fun size(): Int = size
/** Returns the field at `position`. */
fun name(index: Int): String = namesAndValues[index * 2]
/** Returns the value at `index`. */
fun value(index: Int): String = namesAndValues[index * 2 + 1]
/** Returns an immutable case-insensitive set of header names. */
fun names(): Set<String> {
val result = TreeSet(String.CASE_INSENSITIVE_ORDER)
for (i in 0 until size) {
result.add(name(i))
}
return Collections.unmodifiableSet(result)
}
/** Returns an immutable list of the header values for `name`. */
fun values(name: String): List<String> {
var result: MutableList<String>? = null
for (i in 0 until size) {
if (name.equals(name(i), ignoreCase = true)) {
if (result == null) result = ArrayList(2)
result.add(value(i))
}
}
return if (result != null) {
Collections.unmodifiableList(result)
} else {
emptyList()
}
}
/**
* Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the
* approximate size of HTTP/2 headers before they are compressed with HPACK. This value is
* intended to be used as a metric: smaller headers are more efficient to encode and transmit.
*/
fun byteCount(): Long {
// Each header name has 2 bytes of overhead for ': ' and every header value has 2 bytes of
// overhead for '\r\n'.
var result = (namesAndValues.size * 2).toLong()
for (i in 0 until namesAndValues.size) {
result += namesAndValues[i].length.toLong()
}
return result
}
override operator fun iterator(): Iterator<Pair<String, String>> {
return Array(size) { name(it) to value(it) }.iterator()
}
fun newBuilder(): Builder {
val result = Builder()
result.namesAndValues += namesAndValues
return result
}
/**
* Returns true if `other` is a `Headers` object with the same headers, with the same casing, in
* the same order. Note that two headers instances may be *semantically* equal but not equal
* according to this method. In particular, none of the following sets of headers are equal
* according to this method:
*
* 1. Original
* ```
* Content-Type: text/html
* Content-Length: 50
* ```
*
* 2. Different order
*
* ```
* Content-Length: 50
* Content-Type: text/html
* ```
*
* 3. Different case
*
* ```
* content-type: text/html
* content-length: 50
* ```
*
* 4. Different values
*
* ```
* Content-Type: text/html
* Content-Length: 050
* ```
*
* Applications that require semantically equal headers should convert them into a canonical form
* before comparing them for equality.
*/
override fun equals(other: Any?): Boolean {
return other is Headers && namesAndValues.contentEquals(other.namesAndValues)
}
override fun hashCode(): Int = namesAndValues.contentHashCode()
/**
* Returns header names and values. The names and values are separated by `: ` and each pair is
* followed by a newline character `\n`.
*
* Since OkHttp 5 this redacts these sensitive headers:
*
* * `Authorization`
* * `Cookie`
* * `Proxy-Authorization`
* * `Set-Cookie`
*
* To get all headers as a human-readable string use `toMultimap().toString()`.
*/
override fun toString(): String {
return buildString {
for (i in 0 until size) {
val name = name(i)
val value = value(i)
append(name)
append(": ")
append(if (isSensitiveHeader(name)) "██" else value)
append("\n")
}
}
}
fun toMultimap(): Map<String, List<String>> {
val result = TreeMap<String, MutableList<String>>(String.CASE_INSENSITIVE_ORDER)
for (i in 0 until size) {
val name = name(i).toLowerCase(Locale.US)
var values: MutableList<String>? = result[name]
if (values == null) {
values = ArrayList(2)
result[name] = values
}
values.add(value(i))
}
return result
}
class Builder {
internal val namesAndValues: MutableList<String> = ArrayList(20)
/**
* Add a header line without any validation. Only appropriate for headers from the remote peer
* or cache.
*/
internal fun addLenient(line: String) = apply {
val index = line.indexOf(':', 1)
when {
index != -1 -> {
addLenient(line.substring(0, index), line.substring(index + 1))
}
line[0] == ':' -> {
// Work around empty header names and header names that start with a colon (created by old
// broken SPDY versions of the response cache).
addLenient("", line.substring(1)) // Empty header name.
}
else -> {
// No header name.
addLenient("", line)
}
}
}
/** Add an header line containing a field name, a literal colon, and a value. */
fun add(line: String) = apply {
val index = line.indexOf(':')
require(index != -1) { "Unexpected header: $line" }
add(line.substring(0, index).trim(), line.substring(index + 1))
}
/**
* Add a header with the specified name and value. Does validation of header names and values.
*/
fun add(name: String, value: String) = apply {
checkName(name)
checkValue(value, name)
addLenient(name, value)
}
/**
* Add a header with the specified name and value. Does validation of header names, allowing
* non-ASCII values.
*/
fun addUnsafeNonAscii(name: String, value: String) = apply {
checkName(name)
addLenient(name, value)
}
/**
* Adds all headers from an existing collection.
*/
fun addAll(headers: Headers) = apply {
for (i in 0 until headers.size) {
addLenient(headers.name(i), headers.value(i))
}
}
/**
* Add a header with the specified name and formatted date. Does validation of header names and
* value.
*/
fun add(name: String, value: Date) = apply {
add(name, value.toHttpDateString())
}
/**
* Add a header with the specified name and formatted instant. Does validation of header names
* and value.
*/
@IgnoreJRERequirement
fun add(name: String, value: Instant) = apply {
add(name, Date(value.toEpochMilli()))
}
/**
* Set a field with the specified date. If the field is not found, it is added. If the field is
* found, the existing values are replaced.
*/
operator fun set(name: String, value: Date) = apply {
set(name, value.toHttpDateString())
}
/**
* Set a field with the specified instant. If the field is not found, it is added. If the field
* is found, the existing values are replaced.
*/
@IgnoreJRERequirement
operator fun set(name: String, value: Instant) = apply {
return set(name, Date(value.toEpochMilli()))
}
/**
* Add a field with the specified value without any validation. Only appropriate for headers
* from the remote peer or cache.
*/
internal fun addLenient(name: String, value: String) = apply {
namesAndValues.add(name)
namesAndValues.add(value.trim())
}
fun removeAll(name: String) = apply {
var i = 0
while (i < namesAndValues.size) {
if (name.equals(namesAndValues[i], ignoreCase = true)) {
namesAndValues.removeAt(i) // name
namesAndValues.removeAt(i) // value
i -= 2
}
i += 2
}
}
/**
* Set a field with the specified value. If the field is not found, it is added. If the field is
* found, the existing values are replaced.
*/
operator fun set(name: String, value: String) = apply {
checkName(name)
checkValue(value, name)
removeAll(name)
addLenient(name, value)
}
/** Equivalent to `build().get(name)`, but potentially faster. */
operator fun get(name: String): String? {
for (i in namesAndValues.size - 2 downTo 0 step 2) {
if (name.equals(namesAndValues[i], ignoreCase = true)) {
return namesAndValues[i + 1]
}
}
return null
}
fun build(): Headers = Headers(namesAndValues.toTypedArray())
}
companion object {
private fun get(namesAndValues: Array<String>, name: String): String? {
for (i in namesAndValues.size - 2 downTo 0 step 2) {
if (name.equals(namesAndValues[i], ignoreCase = true)) {
return namesAndValues[i + 1]
}
}
return null
}
/**
* Returns headers for the alternating header names and values. There must be an even number of
* arguments, and they must alternate between header names and values.
*/
@JvmStatic
@JvmName("of")
fun headersOf(vararg namesAndValues: String): Headers {
require(namesAndValues.size % 2 == 0) { "Expected alternating header names and values" }
// Make a defensive copy and clean it up.
val namesAndValues: Array<String> = namesAndValues.clone() as Array<String>
for (i in namesAndValues.indices) {
require(namesAndValues[i] != null) { "Headers cannot be null" }
namesAndValues[i] = namesAndValues[i].trim()
}
// Check for malformed headers.
for (i in namesAndValues.indices step 2) {
val name = namesAndValues[i]
val value = namesAndValues[i + 1]
checkName(name)
checkValue(value, name)
}
return Headers(namesAndValues)
}
@JvmName("-deprecated_of")
@Deprecated(
message = "function name changed",
replaceWith = ReplaceWith(expression = "headersOf(*namesAndValues)"),
level = DeprecationLevel.ERROR)
fun of(vararg namesAndValues: String): Headers {
return headersOf(*namesAndValues)
}
/** Returns headers for the header names and values in the [Map]. */
@JvmStatic
@JvmName("of")
fun Map<String, String>.toHeaders(): Headers {
// Make a defensive copy and clean it up.
val namesAndValues = arrayOfNulls<String>(size * 2)
var i = 0
for ((k, v) in this) {
val name = k.trim()
val value = v.trim()
checkName(name)
checkValue(value, name)
namesAndValues[i] = name
namesAndValues[i + 1] = value
i += 2
}
return Headers(namesAndValues as Array<String>)
}
@JvmName("-deprecated_of")
@Deprecated(
message = "function moved to extension",
replaceWith = ReplaceWith(expression = "headers.toHeaders()"),
level = DeprecationLevel.ERROR)
fun of(headers: Map<String, String>): Headers {
return headers.toHeaders()
}
private fun checkName(name: String) {
require(name.isNotEmpty()) { "name is empty" }
for (i in name.indices) {
val c = name[i]
require(c in '\u0021'..'\u007e') {
format("Unexpected char %#04x at %d in header name: %s", c.toInt(), i, name)
}
}
}
private fun checkValue(value: String, name: String) {
for (i in value.indices) {
val c = value[i]
require(c == '\t' || c in '\u0020'..'\u007e') {
format("Unexpected char %#04x at %d in %s value", c.toInt(), i, name) +
(if (isSensitiveHeader(name)) "" else ": $value")
}
}
}
}
}