All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.okhttp3.internal.http2.Http2Reader.kt Maven / Gradle / Ivy

There is a newer version: 5.0.0-alpha.14
Show newest version
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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.Closeable
import java.io.EOFException
import java.io.IOException
import java.util.logging.Level.FINE
import java.util.logging.Logger
import okhttp3.internal.and
import okhttp3.internal.format
import okhttp3.internal.http2.Http2.CONNECTION_PREFACE
import okhttp3.internal.http2.Http2.FLAG_ACK
import okhttp3.internal.http2.Http2.FLAG_COMPRESSED
import okhttp3.internal.http2.Http2.FLAG_END_HEADERS
import okhttp3.internal.http2.Http2.FLAG_END_STREAM
import okhttp3.internal.http2.Http2.FLAG_PADDED
import okhttp3.internal.http2.Http2.FLAG_PRIORITY
import okhttp3.internal.http2.Http2.INITIAL_MAX_FRAME_SIZE
import okhttp3.internal.http2.Http2.TYPE_CONTINUATION
import okhttp3.internal.http2.Http2.TYPE_DATA
import okhttp3.internal.http2.Http2.TYPE_GOAWAY
import okhttp3.internal.http2.Http2.TYPE_HEADERS
import okhttp3.internal.http2.Http2.TYPE_PING
import okhttp3.internal.http2.Http2.TYPE_PRIORITY
import okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE
import okhttp3.internal.http2.Http2.TYPE_RST_STREAM
import okhttp3.internal.http2.Http2.TYPE_SETTINGS
import okhttp3.internal.http2.Http2.TYPE_WINDOW_UPDATE
import okhttp3.internal.http2.Http2.formattedType
import okhttp3.internal.http2.Http2.frameLog
import okhttp3.internal.http2.Http2.frameLogWindowUpdate
import okhttp3.internal.readMedium
import okio.Buffer
import okio.BufferedSource
import okio.ByteString
import okio.Source
import okio.Timeout

/**
 * Reads HTTP/2 transport frames.
 *
 * This implementation assumes we do not send an increased [frame][Settings.getMaxFrameSize] to the
 * peer. Hence, we expect all frames to have a max length of [Http2.INITIAL_MAX_FRAME_SIZE].
 */
class Http2Reader(
  /** Creates a frame reader with max header table size of 4096. */
  private val source: BufferedSource,
  private val client: Boolean,
) : Closeable {
  private val continuation: ContinuationSource = ContinuationSource(this.source)
  private val hpackReader: Hpack.Reader =
    Hpack.Reader(
      source = continuation,
      headerTableSizeSetting = 4096,
    )

  @Throws(IOException::class)
  fun readConnectionPreface(handler: Handler) {
    if (client) {
      // The client reads the initial SETTINGS frame.
      if (!nextFrame(true, handler)) {
        throw IOException("Required SETTINGS preface not received")
      }
    } else {
      // The server reads the CONNECTION_PREFACE byte string.
      val connectionPreface = source.readByteString(CONNECTION_PREFACE.size.toLong())
      if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION ${connectionPreface.hex()}"))
      if (CONNECTION_PREFACE != connectionPreface) {
        throw IOException("Expected a connection header but was ${connectionPreface.utf8()}")
      }
    }
  }

  @Throws(IOException::class)
  fun nextFrame(
    requireSettings: Boolean,
    handler: Handler,
  ): Boolean {
    try {
      source.require(9) // Frame header size.
    } catch (e: EOFException) {
      return false // This might be a normal socket close.
    }

    //  0                   1                   2                   3
    //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    // |                 Length (24)                   |
    // +---------------+---------------+---------------+
    // |   Type (8)    |   Flags (8)   |
    // +-+-+-----------+---------------+-------------------------------+
    // |R|                 Stream Identifier (31)                      |
    // +=+=============================================================+
    // |                   Frame Payload (0...)                      ...
    // +---------------------------------------------------------------+
    val length = source.readMedium()
    if (length > INITIAL_MAX_FRAME_SIZE) {
      throw IOException("FRAME_SIZE_ERROR: $length")
    }
    val type = source.readByte() and 0xff
    val flags = source.readByte() and 0xff
    val streamId = source.readInt() and 0x7fffffff // Ignore reserved bit.
    if (type != TYPE_WINDOW_UPDATE && logger.isLoggable(FINE)) {
      logger.fine(frameLog(true, streamId, length, type, flags))
    }

    if (requireSettings && type != TYPE_SETTINGS) {
      throw IOException("Expected a SETTINGS frame but was ${formattedType(type)}")
    }

    when (type) {
      TYPE_DATA -> readData(handler, length, flags, streamId)
      TYPE_HEADERS -> readHeaders(handler, length, flags, streamId)
      TYPE_PRIORITY -> readPriority(handler, length, flags, streamId)
      TYPE_RST_STREAM -> readRstStream(handler, length, flags, streamId)
      TYPE_SETTINGS -> readSettings(handler, length, flags, streamId)
      TYPE_PUSH_PROMISE -> readPushPromise(handler, length, flags, streamId)
      TYPE_PING -> readPing(handler, length, flags, streamId)
      TYPE_GOAWAY -> readGoAway(handler, length, flags, streamId)
      TYPE_WINDOW_UPDATE -> readWindowUpdate(handler, length, flags, streamId)
      else -> source.skip(length.toLong()) // Implementations MUST discard frames of unknown types.
    }

    return true
  }

  @Throws(IOException::class)
  private fun readHeaders(
    handler: Handler,
    length: Int,
    flags: Int,
    streamId: Int,
  ) {
    if (streamId == 0) throw IOException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0")

    val endStream = (flags and FLAG_END_STREAM) != 0
    val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0

    var headerBlockLength = length
    if (flags and FLAG_PRIORITY != 0) {
      readPriority(handler, streamId)
      headerBlockLength -= 5 // account for above read.
    }
    headerBlockLength = lengthWithoutPadding(headerBlockLength, flags, padding)
    val headerBlock = readHeaderBlock(headerBlockLength, padding, flags, streamId)

    handler.headers(endStream, streamId, -1, headerBlock)
  }

  @Throws(IOException::class)
  private fun readHeaderBlock(
    length: Int,
    padding: Int,
    flags: Int,
    streamId: Int,
  ): List
{ continuation.left = length continuation.length = continuation.left continuation.padding = padding continuation.flags = flags continuation.streamId = streamId // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.1.2.5 hpackReader.readHeaders() return hpackReader.getAndResetHeaderList() } @Throws(IOException::class) private fun readData( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId == 0) throw IOException("PROTOCOL_ERROR: TYPE_DATA streamId == 0") // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED val inFinished = flags and FLAG_END_STREAM != 0 val gzipped = flags and FLAG_COMPRESSED != 0 if (gzipped) { throw IOException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA") } val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0 val dataLength = lengthWithoutPadding(length, flags, padding) handler.data(inFinished, streamId, source, dataLength) source.skip(padding.toLong()) } @Throws(IOException::class) private fun readPriority( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 5) throw IOException("TYPE_PRIORITY length: $length != 5") if (streamId == 0) throw IOException("TYPE_PRIORITY streamId == 0") readPriority(handler, streamId) } @Throws(IOException::class) private fun readPriority( handler: Handler, streamId: Int, ) { val w1 = source.readInt() val exclusive = w1 and 0x80000000.toInt() != 0 val streamDependency = w1 and 0x7fffffff val weight = (source.readByte() and 0xff) + 1 handler.priority(streamId, streamDependency, weight, exclusive) } @Throws(IOException::class) private fun readRstStream( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 4) throw IOException("TYPE_RST_STREAM length: $length != 4") if (streamId == 0) throw IOException("TYPE_RST_STREAM streamId == 0") val errorCodeInt = source.readInt() val errorCode = ErrorCode.fromHttp2(errorCodeInt) ?: throw IOException( "TYPE_RST_STREAM unexpected error code: $errorCodeInt", ) handler.rstStream(streamId, errorCode) } @Throws(IOException::class) private fun readSettings( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId != 0) throw IOException("TYPE_SETTINGS streamId != 0") if (flags and FLAG_ACK != 0) { if (length != 0) throw IOException("FRAME_SIZE_ERROR ack frame should be empty!") handler.ackSettings() return } if (length % 6 != 0) throw IOException("TYPE_SETTINGS length % 6 != 0: $length") val settings = Settings() for (i in 0 until length step 6) { var id = source.readShort() and 0xffff val value = source.readInt() when (id) { // SETTINGS_HEADER_TABLE_SIZE 1 -> { } // SETTINGS_ENABLE_PUSH 2 -> { if (value != 0 && value != 1) { throw IOException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1") } } // SETTINGS_MAX_CONCURRENT_STREAMS 3 -> id = 4 // Renumbered in draft 10. // SETTINGS_INITIAL_WINDOW_SIZE 4 -> { id = 7 // Renumbered in draft 10. if (value < 0) { throw IOException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1") } } // SETTINGS_MAX_FRAME_SIZE 5 -> { if (value < INITIAL_MAX_FRAME_SIZE || value > 16777215) { throw IOException("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: $value") } } // SETTINGS_MAX_HEADER_LIST_SIZE 6 -> { // Advisory only, so ignored. } // Must ignore setting with unknown id. else -> { } } settings[id] = value } handler.settings(false, settings) } @Throws(IOException::class) private fun readPushPromise( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId == 0) { throw IOException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0") } val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0 val promisedStreamId = source.readInt() and 0x7fffffff val headerBlockLength = lengthWithoutPadding(length - 4, flags, padding) // - 4 for readInt(). val headerBlock = readHeaderBlock(headerBlockLength, padding, flags, streamId) handler.pushPromise(streamId, promisedStreamId, headerBlock) } @Throws(IOException::class) private fun readPing( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 8) throw IOException("TYPE_PING length != 8: $length") if (streamId != 0) throw IOException("TYPE_PING streamId != 0") val payload1 = source.readInt() val payload2 = source.readInt() val ack = flags and FLAG_ACK != 0 handler.ping(ack, payload1, payload2) } @Throws(IOException::class) private fun readGoAway( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length < 8) throw IOException("TYPE_GOAWAY length < 8: $length") if (streamId != 0) throw IOException("TYPE_GOAWAY streamId != 0") val lastStreamId = source.readInt() val errorCodeInt = source.readInt() val opaqueDataLength = length - 8 val errorCode = ErrorCode.fromHttp2(errorCodeInt) ?: throw IOException( "TYPE_GOAWAY unexpected error code: $errorCodeInt", ) var debugData = ByteString.EMPTY if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. debugData = source.readByteString(opaqueDataLength.toLong()) } handler.goAway(lastStreamId, errorCode, debugData) } /** Unlike other `readXxx()` functions, this one must log the frame before returning. */ @Throws(IOException::class) private fun readWindowUpdate( handler: Handler, length: Int, flags: Int, streamId: Int, ) { val increment: Long try { if (length != 4) throw IOException("TYPE_WINDOW_UPDATE length !=4: $length") increment = source.readInt() and 0x7fffffffL if (increment == 0L) throw IOException("windowSizeIncrement was 0") } catch (e: Exception) { logger.fine(frameLog(true, streamId, length, TYPE_WINDOW_UPDATE, flags)) throw e } if (logger.isLoggable(FINE)) { logger.fine( frameLogWindowUpdate( inbound = true, streamId = streamId, length = length, windowSizeIncrement = increment, ), ) } handler.windowUpdate(streamId, increment) } @Throws(IOException::class) override fun close() { source.close() } /** * Decompression of the header block occurs above the framing layer. This class lazily reads * continuation frames as they are needed by [Hpack.Reader.readHeaders]. */ internal class ContinuationSource( private val source: BufferedSource, ) : Source { var length: Int = 0 var flags: Int = 0 var streamId: Int = 0 var left: Int = 0 var padding: Int = 0 @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { while (left == 0) { source.skip(padding.toLong()) padding = 0 if (flags and FLAG_END_HEADERS != 0) return -1L readContinuationHeader() // TODO: test case for empty continuation header? } val read = source.read(sink, minOf(byteCount, left.toLong())) if (read == -1L) return -1L left -= read.toInt() return read } override fun timeout(): Timeout = source.timeout() @Throws(IOException::class) override fun close() { } @Throws(IOException::class) private fun readContinuationHeader() { val previousStreamId = streamId left = source.readMedium() length = left val type = source.readByte() and 0xff flags = source.readByte() and 0xff if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags)) streamId = source.readInt() and 0x7fffffff if (type != TYPE_CONTINUATION) throw IOException("$type != TYPE_CONTINUATION") if (streamId != previousStreamId) throw IOException("TYPE_CONTINUATION streamId changed") } } interface Handler { @Throws(IOException::class) fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) /** * Create or update incoming headers, creating the corresponding streams if necessary. Frames * that trigger this are HEADERS and PUSH_PROMISE. * * @param inFinished true if the sender will not send further frames. * @param streamId the stream owning these headers. * @param associatedStreamId the stream that triggered the sender to create this stream. */ fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) fun rstStream( streamId: Int, errorCode: ErrorCode, ) fun settings( clearPrevious: Boolean, settings: Settings, ) /** HTTP/2 only. */ fun ackSettings() /** * Read a connection-level ping from the peer. `ack` indicates this is a reply. The data * in `payload1` and `payload2` opaque binary, and there are no rules on the content. */ fun ping( ack: Boolean, payload1: Int, payload2: Int, ) /** * The peer tells us to stop creating streams. It is safe to replay streams with * `ID > lastGoodStreamId` on a new connection. In- flight streams with * `ID <= lastGoodStreamId` can only be replayed on a new connection if they are idempotent. * * @param lastGoodStreamId the last stream ID the peer processed before sending this message. If * [lastGoodStreamId] is zero, the peer processed no frames. * @param errorCode reason for closing the connection. * @param debugData only valid for HTTP/2; opaque debug data to send. */ fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) /** * Notifies that an additional `windowSizeIncrement` bytes can be sent on `streamId`, or the * connection if `streamId` is zero. */ fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) /** * Called when reading a headers or priority frame. This may be used to change the stream's * weight from the default (16) to a new value. * * @param streamId stream which has a priority change. * @param streamDependency the stream ID this stream is dependent on. * @param weight relative proportion of priority in `[1..256]`. * @param exclusive inserts this stream ID as the sole child of `streamDependency`. */ fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ) /** * HTTP/2 only. Receive a push promise header block. * * A push promise contains all the headers that pertain to a server-initiated request, and a * `promisedStreamId` to which response frames will be delivered. Push promise frames are sent * as a part of the response to `streamId`. * * @param streamId client-initiated stream ID. Must be an odd number. * @param promisedStreamId server-initiated stream ID. Must be an even number. * @param requestHeaders minimally includes `:method`, `:scheme`, `:authority`, and `:path`. */ @Throws(IOException::class) fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) /** * HTTP/2 only. Expresses that resources for the connection or a client- initiated stream are * available from a different network location or protocol configuration. * * See [alt-svc][alt_svc]. * * [alt_svc]: http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-01 * * @param streamId when a client-initiated stream ID (odd number), the origin of this alternate * service is the origin of the stream. When zero, the origin is specified in the `origin` * parameter. * @param origin when present, the [origin](http://tools.ietf.org/html/rfc6454) is typically * represented as a combination of scheme, host and port. When empty, the origin is that of * the `streamId`. * @param protocol an ALPN protocol, such as `h2`. * @param host an IP address or hostname. * @param port the IP port associated with the service. * @param maxAge time in seconds that this alternative is considered fresh. */ fun alternateService( streamId: Int, origin: String, protocol: ByteString, host: String, port: Int, maxAge: Long, ) } companion object { val logger: Logger = Logger.getLogger(Http2::class.java.name) @Throws(IOException::class) fun lengthWithoutPadding( length: Int, flags: Int, padding: Int, ): Int { var result = length if (flags and FLAG_PADDED != 0) result-- // Account for reading the padding length. if (padding > result) { throw IOException("PROTOCOL_ERROR padding $padding > remaining length $result") } result -= padding return result } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy