okhttp3.internal.http2.Hpack.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of okhttp Show documentation
Show all versions of okhttp Show documentation
Square’s meticulous HTTP client for Java and Kotlin.
The newest version!
/*
* Copyright (C) 2013 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.internal.http2
import java.io.IOException
import java.util.Arrays
import java.util.Collections
import java.util.LinkedHashMap
import okhttp3.internal.and
import okhttp3.internal.http2.Header.Companion.RESPONSE_STATUS
import okhttp3.internal.http2.Header.Companion.TARGET_AUTHORITY
import okhttp3.internal.http2.Header.Companion.TARGET_METHOD
import okhttp3.internal.http2.Header.Companion.TARGET_PATH
import okhttp3.internal.http2.Header.Companion.TARGET_SCHEME
import okio.Buffer
import okio.BufferedSource
import okio.ByteString
import okio.Source
import okio.buffer
/**
* Read and write HPACK v10.
*
* http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12
*
* This implementation uses an array for the dynamic table and a list for indexed entries. Dynamic
* entries are added to the array, starting in the last position moving forward. When the array
* fills, it is doubled.
*/
@Suppress("NAME_SHADOWING")
object Hpack {
private const val PREFIX_4_BITS = 0x0f
private const val PREFIX_5_BITS = 0x1f
private const val PREFIX_6_BITS = 0x3f
private const val PREFIX_7_BITS = 0x7f
private const val SETTINGS_HEADER_TABLE_SIZE = 4_096
/**
* The decoder has ultimate control of the maximum size of the dynamic table but we can choose
* to use less. We'll put a cap at 16K. This is arbitrary but should be enough for most purposes.
*/
private const val SETTINGS_HEADER_TABLE_SIZE_LIMIT = 16_384
val STATIC_HEADER_TABLE = arrayOf(
Header(TARGET_AUTHORITY, ""),
Header(TARGET_METHOD, "GET"),
Header(TARGET_METHOD, "POST"),
Header(TARGET_PATH, "/"),
Header(TARGET_PATH, "/index.html"),
Header(TARGET_SCHEME, "http"),
Header(TARGET_SCHEME, "https"),
Header(RESPONSE_STATUS, "200"),
Header(RESPONSE_STATUS, "204"),
Header(RESPONSE_STATUS, "206"),
Header(RESPONSE_STATUS, "304"),
Header(RESPONSE_STATUS, "400"),
Header(RESPONSE_STATUS, "404"),
Header(RESPONSE_STATUS, "500"),
Header("accept-charset", ""),
Header("accept-encoding", "gzip, deflate"),
Header("accept-language", ""),
Header("accept-ranges", ""),
Header("accept", ""),
Header("access-control-allow-origin", ""),
Header("age", ""),
Header("allow", ""),
Header("authorization", ""),
Header("cache-control", ""),
Header("content-disposition", ""),
Header("content-encoding", ""),
Header("content-language", ""),
Header("content-length", ""),
Header("content-location", ""),
Header("content-range", ""),
Header("content-type", ""),
Header("cookie", ""),
Header("date", ""),
Header("etag", ""),
Header("expect", ""),
Header("expires", ""),
Header("from", ""),
Header("host", ""),
Header("if-match", ""),
Header("if-modified-since", ""),
Header("if-none-match", ""),
Header("if-range", ""),
Header("if-unmodified-since", ""),
Header("last-modified", ""),
Header("link", ""),
Header("location", ""),
Header("max-forwards", ""),
Header("proxy-authenticate", ""),
Header("proxy-authorization", ""),
Header("range", ""),
Header("referer", ""),
Header("refresh", ""),
Header("retry-after", ""),
Header("server", ""),
Header("set-cookie", ""),
Header("strict-transport-security", ""),
Header("transfer-encoding", ""),
Header("user-agent", ""),
Header("vary", ""),
Header("via", ""),
Header("www-authenticate", "")
)
val NAME_TO_FIRST_INDEX = nameToFirstIndex()
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-3.1
class Reader @JvmOverloads constructor(
source: Source,
private val headerTableSizeSetting: Int,
private var maxDynamicTableByteCount: Int = headerTableSizeSetting
) {
private val headerList = mutableListOf()
private val source: BufferedSource = source.buffer()
// Visible for testing.
@JvmField var dynamicTable = arrayOfNulls(8)
// Array is populated back to front, so new entries always have lowest index.
private var nextHeaderIndex = dynamicTable.size - 1
@JvmField var headerCount = 0
@JvmField var dynamicTableByteCount = 0
fun getAndResetHeaderList(): List {
val result = headerList.toList()
headerList.clear()
return result
}
fun maxDynamicTableByteCount(): Int = maxDynamicTableByteCount
private fun adjustDynamicTableByteCount() {
if (maxDynamicTableByteCount < dynamicTableByteCount) {
if (maxDynamicTableByteCount == 0) {
clearDynamicTable()
} else {
evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount)
}
}
}
private fun clearDynamicTable() {
dynamicTable.fill(null)
nextHeaderIndex = dynamicTable.size - 1
headerCount = 0
dynamicTableByteCount = 0
}
/** Returns the count of entries evicted. */
private fun evictToRecoverBytes(bytesToRecover: Int): Int {
var bytesToRecover = bytesToRecover
var entriesToEvict = 0
if (bytesToRecover > 0) {
// determine how many headers need to be evicted.
var j = dynamicTable.size - 1
while (j >= nextHeaderIndex && bytesToRecover > 0) {
val toEvict = dynamicTable[j]!!
bytesToRecover -= toEvict.hpackSize
dynamicTableByteCount -= toEvict.hpackSize
headerCount--
entriesToEvict++
j--
}
System.arraycopy(dynamicTable, nextHeaderIndex + 1, dynamicTable,
nextHeaderIndex + 1 + entriesToEvict, headerCount)
nextHeaderIndex += entriesToEvict
}
return entriesToEvict
}
/**
* Read `byteCount` bytes of headers from the source stream. This implementation does not
* propagate the never indexed flag of a header.
*/
@Throws(IOException::class)
fun readHeaders() {
while (!source.exhausted()) {
val b = source.readByte() and 0xff
when {
b == 0x80 -> {
// 10000000
throw IOException("index == 0")
}
b and 0x80 == 0x80 -> {
// 1NNNNNNN
val index = readInt(b, PREFIX_7_BITS)
readIndexedHeader(index - 1)
}
b == 0x40 -> {
// 01000000
readLiteralHeaderWithIncrementalIndexingNewName()
}
b and 0x40 == 0x40 -> {
// 01NNNNNN
val index = readInt(b, PREFIX_6_BITS)
readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1)
}
b and 0x20 == 0x20 -> {
// 001NNNNN
maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS)
if (maxDynamicTableByteCount < 0 || maxDynamicTableByteCount > headerTableSizeSetting) {
throw IOException("Invalid dynamic table size update $maxDynamicTableByteCount")
}
adjustDynamicTableByteCount()
}
b == 0x10 || b == 0 -> {
// 000?0000 - Ignore never indexed bit.
readLiteralHeaderWithoutIndexingNewName()
}
else -> {
// 000?NNNN - Ignore never indexed bit.
val index = readInt(b, PREFIX_4_BITS)
readLiteralHeaderWithoutIndexingIndexedName(index - 1)
}
}
}
}
@Throws(IOException::class)
private fun readIndexedHeader(index: Int) {
if (isStaticHeader(index)) {
val staticEntry = STATIC_HEADER_TABLE[index]
headerList.add(staticEntry)
} else {
val dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.size)
if (dynamicTableIndex < 0 || dynamicTableIndex >= dynamicTable.size) {
throw IOException("Header index too large ${index + 1}")
}
headerList += dynamicTable[dynamicTableIndex]!!
}
}
// referencedHeaders is relative to nextHeaderIndex + 1.
private fun dynamicTableIndex(index: Int): Int {
return nextHeaderIndex + 1 + index
}
@Throws(IOException::class)
private fun readLiteralHeaderWithoutIndexingIndexedName(index: Int) {
val name = getName(index)
val value = readByteString()
headerList.add(Header(name, value))
}
@Throws(IOException::class)
private fun readLiteralHeaderWithoutIndexingNewName() {
val name = checkLowercase(readByteString())
val value = readByteString()
headerList.add(Header(name, value))
}
@Throws(IOException::class)
private fun readLiteralHeaderWithIncrementalIndexingIndexedName(nameIndex: Int) {
val name = getName(nameIndex)
val value = readByteString()
insertIntoDynamicTable(-1, Header(name, value))
}
@Throws(IOException::class)
private fun readLiteralHeaderWithIncrementalIndexingNewName() {
val name = checkLowercase(readByteString())
val value = readByteString()
insertIntoDynamicTable(-1, Header(name, value))
}
@Throws(IOException::class)
private fun getName(index: Int): ByteString {
return if (isStaticHeader(index)) {
STATIC_HEADER_TABLE[index].name
} else {
val dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.size)
if (dynamicTableIndex < 0 || dynamicTableIndex >= dynamicTable.size) {
throw IOException("Header index too large ${index + 1}")
}
dynamicTable[dynamicTableIndex]!!.name
}
}
private fun isStaticHeader(index: Int): Boolean {
return index >= 0 && index <= STATIC_HEADER_TABLE.size - 1
}
/** index == -1 when new. */
private fun insertIntoDynamicTable(index: Int, entry: Header) {
var index = index
headerList.add(entry)
var delta = entry.hpackSize
if (index != -1) { // Index -1 == new header.
delta -= dynamicTable[dynamicTableIndex(index)]!!.hpackSize
}
// if the new or replacement header is too big, drop all entries.
if (delta > maxDynamicTableByteCount) {
clearDynamicTable()
return
}
// Evict headers to the required length.
val bytesToRecover = dynamicTableByteCount + delta - maxDynamicTableByteCount
val entriesEvicted = evictToRecoverBytes(bytesToRecover)
if (index == -1) { // Adding a value to the dynamic table.
if (headerCount + 1 > dynamicTable.size) { // Need to grow the dynamic table.
val doubled = arrayOfNulls(dynamicTable.size * 2)
System.arraycopy(dynamicTable, 0, doubled, dynamicTable.size, dynamicTable.size)
nextHeaderIndex = dynamicTable.size - 1
dynamicTable = doubled
}
index = nextHeaderIndex--
dynamicTable[index] = entry
headerCount++
} else { // Replace value at same position.
index += dynamicTableIndex(index) + entriesEvicted
dynamicTable[index] = entry
}
dynamicTableByteCount += delta
}
@Throws(IOException::class)
private fun readByte(): Int {
return source.readByte() and 0xff
}
@Throws(IOException::class)
fun readInt(firstByte: Int, prefixMask: Int): Int {
val prefix = firstByte and prefixMask
if (prefix < prefixMask) {
return prefix // This was a single byte value.
}
// This is a multibyte value. Read 7 bits at a time.
var result = prefixMask
var shift = 0
while (true) {
val b = readByte()
if (b and 0x80 != 0) { // Equivalent to (b >= 128) since b is in [0..255].
result += b and 0x7f shl shift
shift += 7
} else {
result += b shl shift // Last byte.
break
}
}
return result
}
/** Reads a potentially Huffman encoded byte string. */
@Throws(IOException::class)
fun readByteString(): ByteString {
val firstByte = readByte()
val huffmanDecode = firstByte and 0x80 == 0x80 // 1NNNNNNN
val length = readInt(firstByte, PREFIX_7_BITS).toLong()
return if (huffmanDecode) {
val decodeBuffer = Buffer()
Huffman.decode(source, length, decodeBuffer)
decodeBuffer.readByteString()
} else {
source.readByteString(length)
}
}
}
private fun nameToFirstIndex(): Map {
val result = LinkedHashMap(STATIC_HEADER_TABLE.size)
for (i in STATIC_HEADER_TABLE.indices) {
if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) {
result[STATIC_HEADER_TABLE[i].name] = i
}
}
return Collections.unmodifiableMap(result)
}
class Writer @JvmOverloads constructor(
@JvmField var headerTableSizeSetting: Int = SETTINGS_HEADER_TABLE_SIZE,
private val useCompression: Boolean = true,
private val out: Buffer
) {
/**
* In the scenario where the dynamic table size changes multiple times between transmission of
* header blocks, we need to keep track of the smallest value in that interval.
*/
private var smallestHeaderTableSizeSetting = Integer.MAX_VALUE
private var emitDynamicTableSizeUpdate: Boolean = false
@JvmField var maxDynamicTableByteCount: Int = headerTableSizeSetting
// Visible for testing.
@JvmField var dynamicTable = arrayOfNulls(8)
// Array is populated back to front, so new entries always have lowest index.
private var nextHeaderIndex = dynamicTable.size - 1
@JvmField var headerCount = 0
@JvmField var dynamicTableByteCount = 0
private fun clearDynamicTable() {
dynamicTable.fill(null)
nextHeaderIndex = dynamicTable.size - 1
headerCount = 0
dynamicTableByteCount = 0
}
/** Returns the count of entries evicted. */
private fun evictToRecoverBytes(bytesToRecover: Int): Int {
var bytesToRecover = bytesToRecover
var entriesToEvict = 0
if (bytesToRecover > 0) {
// determine how many headers need to be evicted.
var j = dynamicTable.size - 1
while (j >= nextHeaderIndex && bytesToRecover > 0) {
bytesToRecover -= dynamicTable[j]!!.hpackSize
dynamicTableByteCount -= dynamicTable[j]!!.hpackSize
headerCount--
entriesToEvict++
j--
}
System.arraycopy(dynamicTable, nextHeaderIndex + 1, dynamicTable,
nextHeaderIndex + 1 + entriesToEvict, headerCount)
Arrays.fill(dynamicTable, nextHeaderIndex + 1, nextHeaderIndex + 1 + entriesToEvict, null)
nextHeaderIndex += entriesToEvict
}
return entriesToEvict
}
private fun insertIntoDynamicTable(entry: Header) {
val delta = entry.hpackSize
// if the new or replacement header is too big, drop all entries.
if (delta > maxDynamicTableByteCount) {
clearDynamicTable()
return
}
// Evict headers to the required length.
val bytesToRecover = dynamicTableByteCount + delta - maxDynamicTableByteCount
evictToRecoverBytes(bytesToRecover)
if (headerCount + 1 > dynamicTable.size) { // Need to grow the dynamic table.
val doubled = arrayOfNulls(dynamicTable.size * 2)
System.arraycopy(dynamicTable, 0, doubled, dynamicTable.size, dynamicTable.size)
nextHeaderIndex = dynamicTable.size - 1
dynamicTable = doubled
}
val index = nextHeaderIndex--
dynamicTable[index] = entry
headerCount++
dynamicTableByteCount += delta
}
/** This does not use "never indexed" semantics for sensitive headers. */
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-6.2.3
@Throws(IOException::class)
fun writeHeaders(headerBlock: List) {
if (emitDynamicTableSizeUpdate) {
if (smallestHeaderTableSizeSetting < maxDynamicTableByteCount) {
// Multiple dynamic table size updates!
writeInt(smallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20)
}
emitDynamicTableSizeUpdate = false
smallestHeaderTableSizeSetting = Integer.MAX_VALUE
writeInt(maxDynamicTableByteCount, PREFIX_5_BITS, 0x20)
}
for (i in 0 until headerBlock.size) {
val header = headerBlock[i]
val name = header.name.toAsciiLowercase()
val value = header.value
var headerIndex = -1
var headerNameIndex = -1
val staticIndex = NAME_TO_FIRST_INDEX[name]
if (staticIndex != null) {
headerNameIndex = staticIndex + 1
if (headerNameIndex in 2..7) {
// Only search a subset of the static header table. Most entries have an empty value, so
// it's unnecessary to waste cycles looking at them. This check is built on the
// observation that the header entries we care about are in adjacent pairs, and we
// always know the first index of the pair.
if (STATIC_HEADER_TABLE[headerNameIndex - 1].value == value) {
headerIndex = headerNameIndex
} else if (STATIC_HEADER_TABLE[headerNameIndex].value == value) {
headerIndex = headerNameIndex + 1
}
}
}
if (headerIndex == -1) {
for (j in nextHeaderIndex + 1 until dynamicTable.size) {
if (dynamicTable[j]!!.name == name) {
if (dynamicTable[j]!!.value == value) {
headerIndex = j - nextHeaderIndex + STATIC_HEADER_TABLE.size
break
} else if (headerNameIndex == -1) {
headerNameIndex = j - nextHeaderIndex + STATIC_HEADER_TABLE.size
}
}
}
}
when {
headerIndex != -1 -> {
// Indexed Header Field.
writeInt(headerIndex, PREFIX_7_BITS, 0x80)
}
headerNameIndex == -1 -> {
// Literal Header Field with Incremental Indexing - New Name.
out.writeByte(0x40)
writeByteString(name)
writeByteString(value)
insertIntoDynamicTable(header)
}
name.startsWith(Header.PSEUDO_PREFIX) && TARGET_AUTHORITY != name -> {
// Follow Chromes lead - only include the :authority pseudo header, but exclude all other
// pseudo headers. Literal Header Field without Indexing - Indexed Name.
writeInt(headerNameIndex, PREFIX_4_BITS, 0)
writeByteString(value)
}
else -> {
// Literal Header Field with Incremental Indexing - Indexed Name.
writeInt(headerNameIndex, PREFIX_6_BITS, 0x40)
writeByteString(value)
insertIntoDynamicTable(header)
}
}
}
}
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-4.1.1
fun writeInt(value: Int, prefixMask: Int, bits: Int) {
var value = value
// Write the raw value for a single byte value.
if (value < prefixMask) {
out.writeByte(bits or value)
return
}
// Write the mask to start a multibyte value.
out.writeByte(bits or prefixMask)
value -= prefixMask
// Write 7 bits at a time 'til we're done.
while (value >= 0x80) {
val b = value and 0x7f
out.writeByte(b or 0x80)
value = value ushr 7
}
out.writeByte(value)
}
@Throws(IOException::class)
fun writeByteString(data: ByteString) {
if (useCompression && Huffman.encodedLength(data) < data.size) {
val huffmanBuffer = Buffer()
Huffman.encode(data, huffmanBuffer)
val huffmanBytes = huffmanBuffer.readByteString()
writeInt(huffmanBytes.size, PREFIX_7_BITS, 0x80)
out.write(huffmanBytes)
} else {
writeInt(data.size, PREFIX_7_BITS, 0)
out.write(data)
}
}
fun resizeHeaderTable(headerTableSizeSetting: Int) {
this.headerTableSizeSetting = headerTableSizeSetting
val effectiveHeaderTableSize = minOf(headerTableSizeSetting, SETTINGS_HEADER_TABLE_SIZE_LIMIT)
if (maxDynamicTableByteCount == effectiveHeaderTableSize) return // No change.
if (effectiveHeaderTableSize < maxDynamicTableByteCount) {
smallestHeaderTableSizeSetting =
minOf(smallestHeaderTableSizeSetting, effectiveHeaderTableSize)
}
emitDynamicTableSizeUpdate = true
maxDynamicTableByteCount = effectiveHeaderTableSize
adjustDynamicTableByteCount()
}
private fun adjustDynamicTableByteCount() {
if (maxDynamicTableByteCount < dynamicTableByteCount) {
if (maxDynamicTableByteCount == 0) {
clearDynamicTable()
} else {
evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount)
}
}
}
}
/**
* An HTTP/2 response cannot contain uppercase header characters and must be treated as
* malformed.
*/
@Throws(IOException::class)
fun checkLowercase(name: ByteString): ByteString {
for (i in 0 until name.size) {
if (name[i] in 'A'.toByte()..'Z'.toByte()) {
throw IOException("PROTOCOL_ERROR response malformed: mixed case name: ${name.utf8()}")
}
}
return name
}
}