1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-12 10:23:16 +03:00

Headers multiplatform (#6996)

This commit is contained in:
Yuri Schimke
2022-01-09 19:08:18 +00:00
committed by GitHub
parent 43bc338e8b
commit aca02bf13c
9 changed files with 1125 additions and 751 deletions

View File

@@ -0,0 +1,154 @@
/*
* 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 okhttp3.Headers.Builder
/**
* 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")
expect class Headers internal constructor(
namesAndValues: Array<String>
) : Iterable<Pair<String, String>> {
internal val namesAndValues: Array<String>
/** Returns the last value corresponding to the specified field, or null. */
operator fun get(name: String): String?
/** Returns the number of field values. */
val size: Int
/** Returns the field at `position`. */
fun name(index: Int): String
/** Returns the value at `index`. */
fun value(index: Int): String
/** Returns an immutable case-insensitive set of header names. */
fun names(): Set<String>
/** Returns an immutable list of the header values for `name`. */
fun values(name: String): List<String>
override operator fun iterator(): Iterator<Pair<String, String>>
fun newBuilder(): Builder
/**
* 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
/**
* 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`
*/
override fun toString(): String
class Builder internal constructor() {
internal val namesAndValues: MutableList<String>
/**
* Add a header with the specified name and value. Does validation of header names and values.
*/
fun add(name: String, value: String): Builder
/**
* Adds all headers from an existing collection.
*/
fun addAll(headers: Headers): Builder
fun removeAll(name: String): Builder
/**
* 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): Builder
/** Equivalent to `build().get(name)`, but potentially faster. */
operator fun get(name: String): String?
fun build(): Headers
}
companion object {
/**
* 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.
*/
fun headersOf(vararg namesAndValues: String): Headers
/** Returns headers for the header names and values in the [Map]. */
fun Map<String, String>.toHeaders(): Headers
}
}

View File

@@ -0,0 +1,191 @@
/*
* 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.internal
import okhttp3.Headers
internal fun Headers.commonName(index: Int): String = namesAndValues[index * 2]
internal fun Headers.commonValue(index: Int): String = namesAndValues[index * 2 + 1]
internal fun Headers.commonValues(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 result?.toList().orEmpty()
}
internal fun Headers.commonIterator(): Iterator<Pair<String, String>> {
return Array(size) { name(it) to value(it) }.iterator()
}
internal fun Headers.commonNewBuilder(): Headers.Builder {
val result = Headers.Builder()
result.namesAndValues += namesAndValues
return result
}
internal fun Headers.commonEquals(other: Any?): Boolean {
return other is Headers && namesAndValues.contentEquals(other.namesAndValues)
}
internal fun Headers.commonHashCode(): Int = namesAndValues.contentHashCode()
internal fun Headers.commonToString(): 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")
}
}
}
internal fun commonHeadersGet(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
}
internal fun Headers.Builder.commonAdd(name: String, value: String) = apply {
headersCheckName(name)
headersCheckValue(value, name)
commonAddLenient(name, value)
}
internal fun Headers.Builder.commonAddAll(headers: Headers) = apply {
for (i in 0 until headers.size) {
commonAddLenient(headers.name(i), headers.value(i))
}
}
internal fun Headers.Builder.commonAddLenient(name: String, value: String) = apply {
namesAndValues.add(name)
namesAndValues.add(value.trim())
}
internal fun Headers.Builder.commonRemoveAll(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.
*/
internal fun Headers.Builder.commonSet(name: String, value: String) = apply {
headersCheckName(name)
headersCheckValue(value, name)
removeAll(name)
commonAddLenient(name, value)
}
/** Equivalent to `build().get(name)`, but potentially faster. */
internal fun Headers.Builder.commonGet(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
}
internal fun Headers.Builder.commonBuild(): Headers = Headers(namesAndValues.toTypedArray())
internal fun headersCheckName(name: String) {
require(name.isNotEmpty()) { "name is empty" }
for (i in name.indices) {
val c = name[i]
require(c in '\u0021'..'\u007e') {
"Unexpected char 0x${c.charCode()} at $i in header name: $name"
}
}
}
internal fun headersCheckValue(value: String, name: String) {
for (i in value.indices) {
val c = value[i]
require(c == '\t' || c in '\u0020'..'\u007e') {
"Unexpected char 0x${c.charCode()} at $i in $name value" +
(if (isSensitiveHeader(name)) "" else ": $value")
}
}
}
private fun Char.charCode() = code.toString(16).let {
if (it.length < 2) {
"0$it"
} else {
it
}
}
internal fun commonHeadersOf(vararg inputNamesAndValues: String): Headers {
require(inputNamesAndValues.size % 2 == 0) { "Expected alternating header names and values" }
// Make a defensive copy and clean it up.
val namesAndValues: Array<String> = arrayOf(*inputNamesAndValues)
for (i in namesAndValues.indices) {
require(namesAndValues[i] != null) { "Headers cannot be null" }
namesAndValues[i] = inputNamesAndValues[i].trim()
}
// Check for malformed headers.
for (i in namesAndValues.indices step 2) {
val name = namesAndValues[i]
val value = namesAndValues[i + 1]
headersCheckName(name)
headersCheckValue(value, name)
}
return Headers(namesAndValues)
}
internal fun Map<String, String>.commonToHeaders(): 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()
headersCheckName(name)
headersCheckValue(value, name)
namesAndValues[i] = name
namesAndValues[i + 1] = value
i += 2
}
return Headers(namesAndValues as Array<String>)
}

View File

@@ -0,0 +1,286 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed 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 assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import kotlin.test.Test
import kotlin.test.fail
import okhttp3.Headers.Companion.headersOf
import okhttp3.Headers.Companion.toHeaders
class HeadersTest {
@Test fun ofTrims() {
val headers = headersOf("\t User-Agent \n", " \r OkHttp ")
assertThat(headers.name(0)).isEqualTo("User-Agent")
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofThrowsOddNumberOfHeaders() {
try {
headersOf("User-Agent", "OkHttp", "Content-Length")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofThrowsOnEmptyName() {
try {
headersOf("", "OkHttp")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofAcceptsEmptyValue() {
val headers = headersOf("User-Agent", "")
assertThat(headers.value(0)).isEqualTo("")
}
@Test fun ofMakesDefensiveCopy() {
val namesAndValues = arrayOf(
"User-Agent",
"OkHttp"
)
val headers = headersOf(*namesAndValues)
namesAndValues[1] = "Chrome"
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofRejectsNullChar() {
try {
headersOf("User-Agent", "Square\u0000OkHttp")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapThrowsOnEmptyName() {
try {
mapOf("" to "OkHttp").toHeaders()
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapThrowsOnBlankName() {
try {
mapOf(" " to "OkHttp").toHeaders()
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapAcceptsEmptyValue() {
val headers = mapOf("User-Agent" to "").toHeaders()
assertThat(headers.value(0)).isEqualTo("")
}
@Test fun ofMapTrimsKey() {
val headers = mapOf(" User-Agent " to "OkHttp").toHeaders()
assertThat(headers.name(0)).isEqualTo("User-Agent")
}
@Test fun ofMapTrimsValue() {
val headers = mapOf("User-Agent" to " OkHttp ").toHeaders()
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofMapMakesDefensiveCopy() {
val namesAndValues = mutableMapOf<String, String>()
namesAndValues["User-Agent"] = "OkHttp"
val headers = namesAndValues.toHeaders()
namesAndValues["User-Agent"] = "Chrome"
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofMapRejectsNullCharInName() {
try {
mapOf("User-\u0000Agent" to "OkHttp").toHeaders()
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapRejectsNullCharInValue() {
try {
mapOf("User-Agent" to "Square\u0000OkHttp").toHeaders()
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun builderRejectsUnicodeInHeaderName() {
try {
Headers.Builder().add("héader1", "value1")
fail("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun builderRejectsUnicodeInHeaderValue() {
try {
Headers.Builder().add("header1", "valué1")
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun varargFactoryRejectsUnicodeInHeaderName() {
try {
headersOf("héader1", "value1")
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun varargFactoryRejectsUnicodeInHeaderValue() {
try {
headersOf("header1", "valué1")
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun mapFactoryRejectsUnicodeInHeaderName() {
try {
mapOf("héader1" to "value1").toHeaders()
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun mapFactoryRejectsUnicodeInHeaderValue() {
try {
mapOf("header1" to "valué1").toHeaders()
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun sensitiveHeadersNotIncludedInExceptions() {
try {
Headers.Builder().add("Authorization", "valué1")
fail("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Authorization value")
}
try {
Headers.Builder().add("Cookie", "valué1")
fail("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Cookie value")
}
try {
Headers.Builder().add("Proxy-Authorization", "valué1")
fail("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Proxy-Authorization value")
}
try {
Headers.Builder().add("Set-Cookie", "valué1")
fail("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Set-Cookie value")
}
}
@Test fun headersEquals() {
val headers1 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
val headers2 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
assertThat(headers2).isEqualTo(headers1)
assertThat(headers2.hashCode()).isEqualTo(headers1.hashCode())
}
@Test fun headersNotEquals() {
val headers1 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
val headers2 = Headers.Builder()
.add("Connection", "keep-alive")
.add("Transfer-Encoding", "chunked")
.build()
assertThat(headers2).isNotEqualTo(headers1)
assertThat(headers2.hashCode()).isNotEqualTo(headers1.hashCode().toLong())
}
@Test fun headersToString() {
val headers = Headers.Builder()
.add("A", "a")
.add("B", "bb")
.build()
assertThat(headers.toString()).isEqualTo("A: a\nB: bb\n")
}
@Test fun headersToStringRedactsSensitiveHeaders() {
val headers = Headers.Builder()
.add("content-length", "99")
.add("authorization", "peanutbutter")
.add("proxy-authorization", "chocolate")
.add("cookie", "drink=coffee")
.add("set-cookie", "accessory=sugar")
.add("user-agent", "OkHttp")
.build()
assertThat(headers.toString()).isEqualTo(
"""
|content-length: 99
|authorization: ██
|proxy-authorization: ██
|cookie: ██
|set-cookie: ██
|user-agent: OkHttp
|""".trimMargin()
)
}
@Test fun headersAddAll() {
val sourceHeaders = Headers.Builder()
.add("A", "aa")
.add("a", "aa")
.add("B", "bb")
.build()
val headers = Headers.Builder()
.add("A", "a")
.addAll(sourceHeaders)
.add("C", "c")
.build()
assertThat(headers.toString()).isEqualTo("A: a\nA: aa\na: aa\nB: bb\nC: c\n")
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* Licensed 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 assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
class HeadersJsTest {
@Test
fun nameIndexesAreStrict() {
val headers = Headers.headersOf("a", "b", "c", "d")
assertThat(headers.name(-1)).isEqualTo(undefined)
assertThat(headers.name(0)).isEqualTo("a")
assertThat(headers.name(1)).isEqualTo("c")
assertThat(headers.name(2)).isEqualTo(undefined)
}
@Test
fun valueIndexesAreStrict() {
val headers = Headers.headersOf("a", "b", "c", "d")
assertThat(headers.value(-1)).isEqualTo(undefined)
assertThat(headers.value(0)).isEqualTo("b")
assertThat(headers.value(1)).isEqualTo("d")
assertThat(headers.value(2)).isEqualTo(undefined)
}
}

View File

@@ -24,35 +24,34 @@ 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.commonAdd
import okhttp3.internal.commonAddAll
import okhttp3.internal.commonAddLenient
import okhttp3.internal.commonBuild
import okhttp3.internal.commonEquals
import okhttp3.internal.commonGet
import okhttp3.internal.commonHashCode
import okhttp3.internal.commonHeadersGet
import okhttp3.internal.commonHeadersOf
import okhttp3.internal.commonIterator
import okhttp3.internal.commonName
import okhttp3.internal.commonNewBuilder
import okhttp3.internal.commonRemoveAll
import okhttp3.internal.commonSet
import okhttp3.internal.commonToHeaders
import okhttp3.internal.commonToString
import okhttp3.internal.commonValue
import okhttp3.internal.commonValues
import okhttp3.internal.headersCheckName
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>
actual class Headers internal actual constructor(
internal actual 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)
actual operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name)
/**
* Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
@@ -70,8 +69,7 @@ class Headers private constructor(
return value?.toInstant()
}
/** Returns the number of field values. */
@get:JvmName("size") val size: Int
@get:JvmName("size") actual val size: Int
get() = namesAndValues.size / 2
@JvmName("-deprecated_size")
@@ -81,14 +79,11 @@ class Headers private constructor(
level = DeprecationLevel.ERROR)
fun size(): Int = size
/** Returns the field at `position`. */
fun name(index: Int): String = namesAndValues[index * 2]
actual fun name(index: Int): String = commonName(index)
/** Returns the value at `index`. */
fun value(index: Int): String = namesAndValues[index * 2 + 1]
actual fun value(index: Int): String = commonValue(index)
/** Returns an immutable case-insensitive set of header names. */
fun names(): Set<String> {
actual fun names(): Set<String> {
val result = TreeSet(String.CASE_INSENSITIVE_ORDER)
for (i in 0 until size) {
result.add(name(i))
@@ -96,21 +91,7 @@ class Headers private constructor(
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()
}
}
actual fun values(name: String): List<String> = commonValues(name)
/**
* Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the
@@ -129,83 +110,15 @@ class Headers private constructor(
return result
}
override operator fun iterator(): Iterator<Pair<String, String>> {
return Array(size) { name(it) to value(it) }.iterator()
}
actual override operator fun iterator(): Iterator<Pair<String, String>> = commonIterator()
fun newBuilder(): Builder {
val result = Builder()
result.namesAndValues += namesAndValues
return result
}
actual fun newBuilder(): Builder = commonNewBuilder()
/**
* 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)
}
actual override fun equals(other: Any?): Boolean = commonEquals(other)
override fun hashCode(): Int = namesAndValues.contentHashCode()
override fun hashCode(): Int = commonHashCode()
/**
* 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")
}
}
}
actual override fun toString(): String = commonToString()
fun toMultimap(): Map<String, List<String>> {
val result = TreeMap<String, MutableList<String>>(String.CASE_INSENSITIVE_ORDER)
@@ -221,8 +134,8 @@ class Headers private constructor(
return result
}
class Builder {
internal val namesAndValues: MutableList<String> = ArrayList(20)
actual class Builder {
internal actual val namesAndValues: MutableList<String> = ArrayList(20)
/**
* Add a header line without any validation. Only appropriate for headers from the remote peer
@@ -253,32 +166,18 @@ class Headers private constructor(
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)
}
actual fun add(name: String, value: String) = commonAdd(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)
headersCheckName(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))
}
}
actual fun addAll(headers: Headers) = commonAddAll(headers)
/**
* Add a header with the specified name and formatted date. Does validation of header names and
@@ -318,83 +217,26 @@ class Headers private constructor(
* 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())
}
internal fun addLenient(name: String, value: String) = commonAddLenient(name, value)
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
}
}
actual fun removeAll(name: String) = commonRemoveAll(name)
/**
* 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)
}
actual operator fun set(name: String, value: String) = commonSet(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
}
actual operator fun get(name: String): String? = commonGet(name)
fun build(): Headers = Headers(namesAndValues.toTypedArray())
actual fun build(): Headers = commonBuild()
}
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.
*/
actual companion object {
@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)
}
actual fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues)
@JvmName("-deprecated_of")
@Deprecated(
@@ -405,25 +247,9 @@ class Headers private constructor(
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>)
}
actual fun Map<String, String>.toHeaders(): Headers = commonToHeaders()
@JvmName("-deprecated_of")
@Deprecated(
@@ -433,25 +259,5 @@ class Headers private constructor(
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.code, 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.code, i, name) +
(if (isSensitiveHeader(name)) "" else ": $value")
}
}
}
}
}

View File

@@ -15,466 +15,19 @@
*/
package okhttp3
import java.time.Instant
import java.util.Date
import okhttp3.Headers.Companion.headersOf
import okhttp3.Headers.Companion.toHeaders
import okhttp3.TestUtil.headerEntries
import okhttp3.internal.EMPTY_HEADERS
import okhttp3.internal.http.parseChallenges
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.http2HeadersList
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.readHttp2HeadersList
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class HeadersTest {
@Test fun readNameValueBlockDropsForbiddenHeadersHttp2() {
val headerBlock = headersOf(
":status", "200 OK",
":version", "HTTP/1.1",
"connection", "close"
)
val request = Request.Builder().url("http://square.com/").build()
val response = readHttp2HeadersList(headerBlock, Protocol.HTTP_2).request(request).build()
val headers = response.headers
assertThat(headers.size).isEqualTo(1)
assertThat(headers.name(0)).isEqualTo(":version")
assertThat(headers.value(0)).isEqualTo("HTTP/1.1")
}
@Test fun http2HeadersListDropsForbiddenHeadersHttp2() {
val request = Request.Builder()
.url("http://square.com/")
.header("Connection", "upgrade")
.header("Upgrade", "websocket")
.header("Host", "square.com")
.header("TE", "gzip")
.build()
val expected = headerEntries(
":method", "GET",
":path", "/",
":authority", "square.com",
":scheme", "http"
)
assertThat(http2HeadersList(request)).isEqualTo(expected)
}
@Test fun http2HeadersListDontDropTeIfTrailersHttp2() {
val request = Request.Builder()
.url("http://square.com/")
.header("TE", "trailers")
.build()
val expected = headerEntries(
":method", "GET",
":path", "/",
":scheme", "http",
"te", "trailers"
)
assertThat(http2HeadersList(request)).isEqualTo(expected)
}
@Test fun ofTrims() {
val headers = headersOf("\t User-Agent \n", " \r OkHttp ")
assertThat(headers.name(0)).isEqualTo("User-Agent")
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun addParsing() {
val headers = Headers.Builder()
.add("foo: bar")
.add(" foo: baz") // Name leading whitespace is trimmed.
.add("foo : bak") // Name trailing whitespace is trimmed.
.add("\tkey\t:\tvalue\t") // '\t' also counts as whitespace
.add("ping: pong ") // Value whitespace is trimmed.
.add("kit:kat") // Space after colon is not required.
.build()
assertThat(headers.values("foo")).containsExactly("bar", "baz", "bak")
assertThat(headers.values("key")).containsExactly("value")
assertThat(headers.values("ping")).containsExactly("pong")
assertThat(headers.values("kit")).containsExactly("kat")
}
@Test fun addThrowsOnEmptyName() {
try {
Headers.Builder().add(": bar")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
try {
Headers.Builder().add(" : bar")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addThrowsOnNoColon() {
try {
Headers.Builder().add("foo bar")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addThrowsOnMultiColon() {
try {
Headers.Builder().add(":status: 200 OK")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addUnsafeNonAsciiRejectsUnicodeName() {
try {
Headers.Builder()
.addUnsafeNonAscii("héader1", "value1")
.build()
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message).isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun addUnsafeNonAsciiAcceptsUnicodeValue() {
val headers = Headers.Builder()
.addUnsafeNonAscii("header1", "valué1")
.build()
assertThat(headers.toString()).isEqualTo("header1: valué1\n")
}
@Test fun ofThrowsOddNumberOfHeaders() {
try {
headersOf("User-Agent", "OkHttp", "Content-Length")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofThrowsOnEmptyName() {
try {
headersOf("", "OkHttp")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofAcceptsEmptyValue() {
val headers = headersOf("User-Agent", "")
assertThat(headers.value(0)).isEqualTo("")
}
@Test fun ofMakesDefensiveCopy() {
val namesAndValues = arrayOf(
"User-Agent",
"OkHttp"
)
val headers = headersOf(*namesAndValues)
namesAndValues[1] = "Chrome"
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofRejectsNullChar() {
try {
headersOf("User-Agent", "Square\u0000OkHttp")
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapThrowsOnNull() {
try {
(mapOf("User-Agent" to null) as Map<String, String>).toHeaders()
fail<Any>()
} catch (expected: NullPointerException) {
}
}
@Test fun ofMapThrowsOnEmptyName() {
try {
mapOf("" to "OkHttp").toHeaders()
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapThrowsOnBlankName() {
try {
mapOf(" " to "OkHttp").toHeaders()
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapAcceptsEmptyValue() {
val headers = mapOf("User-Agent" to "").toHeaders()
assertThat(headers.value(0)).isEqualTo("")
}
@Test fun ofMapTrimsKey() {
val headers = mapOf(" User-Agent " to "OkHttp").toHeaders()
assertThat(headers.name(0)).isEqualTo("User-Agent")
}
@Test fun ofMapTrimsValue() {
val headers = mapOf("User-Agent" to " OkHttp ").toHeaders()
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofMapMakesDefensiveCopy() {
val namesAndValues = mutableMapOf<String, String>()
namesAndValues["User-Agent"] = "OkHttp"
val headers = namesAndValues.toHeaders()
namesAndValues["User-Agent"] = "Chrome"
assertThat(headers.value(0)).isEqualTo("OkHttp")
}
@Test fun ofMapRejectsNullCharInName() {
try {
mapOf("User-\u0000Agent" to "OkHttp").toHeaders()
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun ofMapRejectsNullCharInValue() {
try {
mapOf("User-Agent" to "Square\u0000OkHttp").toHeaders()
fail<Any>()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun toMultimapGroupsHeaders() {
val headers = headersOf(
"cache-control", "no-cache",
"cache-control", "no-store",
"user-agent", "OkHttp"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
}
@Test fun toMultimapUsesCanonicalCase() {
val headers = headersOf(
"cache-control", "no-store",
"Cache-Control", "no-cache",
"User-Agent", "OkHttp"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
}
@Test fun toMultimapAllowsCaseInsensitiveGet() {
val headers = headersOf(
"cache-control", "no-store",
"Cache-Control", "no-cache"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["Cache-Control"]!!.size).isEqualTo(2)
}
@Test fun nameIndexesAreStrict() {
val headers = headersOf("a", "b", "c", "d")
try {
headers.name(-1)
fail<Any>()
} catch (expected: IndexOutOfBoundsException) {
}
assertThat(headers.name(0)).isEqualTo("a")
assertThat(headers.name(1)).isEqualTo("c")
try {
headers.name(2)
fail<Any>()
} catch (expected: IndexOutOfBoundsException) {
}
}
@Test fun valueIndexesAreStrict() {
val headers = headersOf("a", "b", "c", "d")
try {
headers.value(-1)
fail<Any>()
} catch (expected: IndexOutOfBoundsException) {
}
assertThat(headers.value(0)).isEqualTo("b")
assertThat(headers.value(1)).isEqualTo("d")
try {
headers.value(2)
fail<Any>()
} catch (expected: IndexOutOfBoundsException) {
}
}
@Test fun builderRejectsUnicodeInHeaderName() {
try {
Headers.Builder().add("héader1", "value1")
fail<Any>("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun builderRejectsUnicodeInHeaderValue() {
try {
Headers.Builder().add("header1", "valué1")
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun varargFactoryRejectsUnicodeInHeaderName() {
try {
headersOf("héader1", "value1")
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun varargFactoryRejectsUnicodeInHeaderValue() {
try {
headersOf("header1", "valué1")
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun mapFactoryRejectsUnicodeInHeaderName() {
try {
mapOf("héader1" to "value1").toHeaders()
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun mapFactoryRejectsUnicodeInHeaderValue() {
try {
mapOf("header1" to "valué1").toHeaders()
fail<Any>("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
}
}
@Test fun sensitiveHeadersNotIncludedInExceptions() {
try {
Headers.Builder().add("Authorization", "valué1")
fail<Any>("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Authorization value")
}
try {
Headers.Builder().add("Cookie", "valué1")
fail<Any>("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Cookie value")
}
try {
Headers.Builder().add("Proxy-Authorization", "valué1")
fail<Any>("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Proxy-Authorization value")
}
try {
Headers.Builder().add("Set-Cookie", "valué1")
fail<Any>("Should have complained about invalid name")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message)
.isEqualTo("Unexpected char 0xe9 at 4 in Set-Cookie value")
}
}
@Test fun headersEquals() {
val headers1 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
val headers2 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
assertThat(headers2).isEqualTo(headers1)
assertThat(headers2.hashCode()).isEqualTo(headers1.hashCode())
}
@Test fun headersNotEquals() {
val headers1 = Headers.Builder()
.add("Connection", "close")
.add("Transfer-Encoding", "chunked")
.build()
val headers2 = Headers.Builder()
.add("Connection", "keep-alive")
.add("Transfer-Encoding", "chunked")
.build()
assertThat(headers2).isNotEqualTo(headers1)
assertThat(headers2.hashCode()).isNotEqualTo(headers1.hashCode().toLong())
}
@Test fun headersToString() {
val headers = Headers.Builder()
.add("A", "a")
.add("B", "bb")
.build()
assertThat(headers.toString()).isEqualTo("A: a\nB: bb\n")
}
@Test fun headersToStringRedactsSensitiveHeaders() {
val headers = Headers.Builder()
.add("content-length", "99")
.add("authorization", "peanutbutter")
.add("proxy-authorization", "chocolate")
.add("cookie", "drink=coffee")
.add("set-cookie", "accessory=sugar")
.add("user-agent", "OkHttp")
.build()
assertThat(headers.toString()).isEqualTo(
"""
|content-length: 99
|authorization:
|proxy-authorization:
|cookie:
|set-cookie:
|user-agent: OkHttp
|""".trimMargin()
)
}
@Test fun headersAddAll() {
val sourceHeaders = Headers.Builder()
.add("A", "aa")
.add("a", "aa")
.add("B", "bb")
.build()
val headers = Headers.Builder()
.add("A", "a")
.addAll(sourceHeaders)
.add("C", "c")
.build()
assertThat(headers.toString()).isEqualTo("A: a\nA: aa\na: aa\nB: bb\nC: c\n")
}
class HeadersChallengesTest {
/** See https://github.com/square/okhttp/issues/2780. */
@Test fun testDigestChallengeWithStrictRfc2617Header() {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks"
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -493,7 +46,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrlask"
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -512,7 +65,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest qop=\"auth\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaksjdflk"
+ "asdf\", realm=\"myrealm\", stale=\"FALSE\""
+ "asdf\", realm=\"myrealm\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -531,7 +84,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest qop=\"auth\", underrealm=\"myrealm\", nonce=\"fjalskdflwej"
+ "rlaskdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
+ "rlaskdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -550,7 +103,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrl"
+ "askdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
+ "askdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -569,7 +122,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjfl"
+ "aksjdflkasdf\", qop=\"auth\", stale=\"FALSE\""
+ "aksjdflkasdf\", qop=\"auth\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -588,7 +141,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "DiGeSt qop=\"auth\", rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlask"
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -608,7 +161,7 @@ class HeadersTest {
val headers = Headers.Builder()
.add(
"WWW-Authenticate", "DIgEsT rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks"
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
)
.build()
val challenges = headers.parseChallenges("WWW-Authenticate")
@@ -808,7 +361,7 @@ class HeadersTest {
.build()
assertThat(headers.parseChallenges("WWW-Authenticate"))
.isEqualTo(listOf(Challenge("Other", mapOf(null to "abc==")))
)
)
}
@Test fun token68AndAuthParams() {
@@ -859,59 +412,4 @@ class HeadersTest {
Challenge("Basic", mapOf("realm" to "myotherrealm"))
)
}
@Test fun byteCount() {
assertThat(EMPTY_HEADERS.byteCount()).isEqualTo(0L)
assertThat(
Headers.Builder()
.add("abc", "def")
.build()
.byteCount()
).isEqualTo(10L)
assertThat(
Headers.Builder()
.add("abc", "def")
.add("ghi", "jkl")
.build()
.byteCount()
).isEqualTo(20L)
}
@Test fun addDate() {
val expected = Date(0L)
val headers = Headers.Builder()
.add("testDate", expected)
.build()
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
assertThat(headers.getDate("testDate")).isEqualTo(Date(0L))
}
@Test fun addInstant() {
val expected = Instant.ofEpochMilli(0L)
val headers = Headers.Builder()
.add("Test-Instant", expected)
.build()
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
}
@Test fun setDate() {
val expected = Date(1000)
val headers = Headers.Builder()
.add("testDate", Date(0L))
.set("testDate", expected)
.build()
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
assertThat(headers.getDate("testDate")).isEqualTo(expected)
}
@Test fun setInstant() {
val expected = Instant.ofEpochMilli(1000L)
val headers = Headers.Builder()
.add("Test-Instant", Instant.ofEpochMilli(0L))
.set("Test-Instant", expected)
.build()
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed 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.*
import kotlin.test.fail
import okhttp3.Headers.Companion.toHeaders
import okhttp3.internal.EMPTY_HEADERS
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class HeadersJvmTest {
@Test fun byteCount() {
assertThat(EMPTY_HEADERS.byteCount()).isEqualTo(0L)
assertThat(
Headers.Builder()
.add("abc", "def")
.build()
.byteCount()
).isEqualTo(10L)
assertThat(
Headers.Builder()
.add("abc", "def")
.add("ghi", "jkl")
.build()
.byteCount()
).isEqualTo(20L)
}
@Test fun addDate() {
val expected = Date(0L)
val headers = Headers.Builder()
.add("testDate", expected)
.build()
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
assertThat(headers.getDate("testDate")).isEqualTo(Date(0L))
}
@Test fun addInstant() {
val expected = Instant.ofEpochMilli(0L)
val headers = Headers.Builder()
.add("Test-Instant", expected)
.build()
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
}
@Test fun setDate() {
val expected = Date(1000)
val headers = Headers.Builder()
.add("testDate", Date(0L))
.set("testDate", expected)
.build()
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
assertThat(headers.getDate("testDate")).isEqualTo(expected)
}
@Test fun setInstant() {
val expected = Instant.ofEpochMilli(1000L)
val headers = Headers.Builder()
.add("Test-Instant", Instant.ofEpochMilli(0L))
.set("Test-Instant", expected)
.build()
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
}
@Test fun addParsing() {
val headers = Headers.Builder()
.add("foo: bar")
.add(" foo: baz") // Name leading whitespace is trimmed.
.add("foo : bak") // Name trailing whitespace is trimmed.
.add("\tkey\t:\tvalue\t") // '\t' also counts as whitespace
.add("ping: pong ") // Value whitespace is trimmed.
.add("kit:kat") // Space after colon is not required.
.build()
assertThat(headers.values("foo")).containsExactly("bar", "baz", "bak")
assertThat(headers.values("key")).containsExactly("value")
assertThat(headers.values("ping")).containsExactly("pong")
assertThat(headers.values("kit")).containsExactly("kat")
}
@Test fun addThrowsOnEmptyName() {
try {
Headers.Builder().add(": bar")
fail()
} catch (expected: IllegalArgumentException) {
}
try {
Headers.Builder().add(" : bar")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addThrowsOnNoColon() {
try {
Headers.Builder().add("foo bar")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addThrowsOnMultiColon() {
try {
Headers.Builder().add(":status: 200 OK")
fail()
} catch (expected: IllegalArgumentException) {
}
}
@Test fun addUnsafeNonAsciiRejectsUnicodeName() {
try {
Headers.Builder()
.addUnsafeNonAscii("héader1", "value1")
.build()
fail("Should have complained about invalid value")
} catch (expected: IllegalArgumentException) {
assertThat(expected.message).isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
}
}
@Test fun addUnsafeNonAsciiAcceptsUnicodeValue() {
val headers = Headers.Builder()
.addUnsafeNonAscii("header1", "valué1")
.build()
assertThat(headers.toString()).isEqualTo("header1: valué1\n")
}
// Fails on JS, ClassCastException: Illegal cast
@Test fun ofMapThrowsOnNull() {
try {
(mapOf("User-Agent" to null) as Map<String, String>).toHeaders()
fail()
} catch (expected: NullPointerException) {
}
}
@Test fun toMultimapGroupsHeaders() {
val headers = Headers.headersOf(
"cache-control", "no-cache",
"cache-control", "no-store",
"user-agent", "OkHttp"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
}
@Test fun toMultimapUsesCanonicalCase() {
val headers = Headers.headersOf(
"cache-control", "no-store",
"Cache-Control", "no-cache",
"User-Agent", "OkHttp"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
}
@Test fun toMultimapAllowsCaseInsensitiveGet() {
val headers = Headers.headersOf(
"cache-control", "no-store",
"Cache-Control", "no-cache"
)
val headerMap = headers.toMultimap()
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
assertThat(headerMap["Cache-Control"]!!.size).isEqualTo(2)
}
@Test fun nameIndexesAreStrict() {
val headers = Headers.headersOf("a", "b", "c", "d")
try {
headers.name(-1)
fail()
} catch (expected: IndexOutOfBoundsException) {
}
assertThat(headers.name(0)).isEqualTo("a")
assertThat(headers.name(1)).isEqualTo("c")
try {
headers.name(2)
fail()
} catch (expected: IndexOutOfBoundsException) {
}
}
@Test fun valueIndexesAreStrict() {
val headers = Headers.headersOf("a", "b", "c", "d")
try {
headers.value(-1)
fail()
} catch (expected: IndexOutOfBoundsException) {
}
assertThat(headers.value(0)).isEqualTo("b")
assertThat(headers.value(1)).isEqualTo("d")
try {
headers.value(2)
fail()
} catch (expected: IndexOutOfBoundsException) {
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed 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 okhttp3.Headers.Companion.headersOf
import okhttp3.TestUtil.headerEntries
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.http2HeadersList
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.readHttp2HeadersList
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class HeadersRequestTest {
@Test fun readNameValueBlockDropsForbiddenHeadersHttp2() {
val headerBlock = headersOf(
":status", "200 OK",
":version", "HTTP/1.1",
"connection", "close"
)
val request = Request.Builder().url("http://square.com/").build()
val response = readHttp2HeadersList(headerBlock, Protocol.HTTP_2).request(request).build()
val headers = response.headers
assertThat(headers.size).isEqualTo(1)
assertThat(headers.name(0)).isEqualTo(":version")
assertThat(headers.value(0)).isEqualTo("HTTP/1.1")
}
@Test fun http2HeadersListDropsForbiddenHeadersHttp2() {
val request = Request.Builder()
.url("http://square.com/")
.header("Connection", "upgrade")
.header("Upgrade", "websocket")
.header("Host", "square.com")
.header("TE", "gzip")
.build()
val expected = headerEntries(
":method", "GET",
":path", "/",
":authority", "square.com",
":scheme", "http"
)
assertThat(http2HeadersList(request)).isEqualTo(expected)
}
@Test fun http2HeadersListDontDropTeIfTrailersHttp2() {
val request = Request.Builder()
.url("http://square.com/")
.header("TE", "trailers")
.build()
val expected = headerEntries(
":method", "GET",
":path", "/",
":scheme", "http",
"te", "trailers"
)
assertThat(http2HeadersList(request)).isEqualTo(expected)
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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 okhttp3.internal.commonAdd
import okhttp3.internal.commonAddAll
import okhttp3.internal.commonAddLenient
import okhttp3.internal.commonBuild
import okhttp3.internal.commonEquals
import okhttp3.internal.commonGet
import okhttp3.internal.commonHashCode
import okhttp3.internal.commonHeadersGet
import okhttp3.internal.commonHeadersOf
import okhttp3.internal.commonIterator
import okhttp3.internal.commonName
import okhttp3.internal.commonNewBuilder
import okhttp3.internal.commonRemoveAll
import okhttp3.internal.commonSet
import okhttp3.internal.commonToHeaders
import okhttp3.internal.commonToString
import okhttp3.internal.commonValue
import okhttp3.internal.commonValues
@Suppress("NAME_SHADOWING")
actual class Headers internal actual constructor(
internal actual val namesAndValues: Array<String>
) : Iterable<Pair<String, String>> {
actual operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name)
actual val size: Int
get() = namesAndValues.size / 2
actual fun name(index: Int): String = commonName(index)
actual fun value(index: Int): String = commonValue(index)
actual fun names(): Set<String> {
return (0 until size step 2).map { name(it) }.distinctBy { it.lowercase() }.toSet()
}
actual fun values(name: String): List<String> = commonValues(name)
/**
* 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
}
actual override operator fun iterator(): Iterator<Pair<String, String>> = commonIterator()
actual fun newBuilder(): Builder = commonNewBuilder()
actual override fun equals(other: Any?): Boolean = commonEquals(other)
override fun hashCode(): Int = commonHashCode()
actual override fun toString(): String = commonToString()
actual class Builder {
internal actual val namesAndValues: MutableList<String> = ArrayList(20)
actual fun add(name: String, value: String) = commonAdd(name, value)
actual fun addAll(headers: Headers) = commonAddAll(headers)
actual fun removeAll(name: String) = commonRemoveAll(name)
/**
* 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.
*/
actual operator fun set(name: String, value: String) = commonSet(name, value)
/** Equivalent to `build().get(name)`, but potentially faster. */
actual operator fun get(name: String): String? = commonGet(name)
actual fun build(): Headers = commonBuild()
}
actual companion object {
actual fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues)
actual fun Map<String, String>.toHeaders(): Headers = commonToHeaders()
}
}