/*
 * 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.IOException;
import java.util.List;
import java.util.logging.Logger;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
import okio.Source;
import okio.Timeout;

import static java.util.logging.Level.FINE;
import static okhttp3.internal.Util.format;
import static okhttp3.internal.http2.Http2.CONNECTION_PREFACE;
import static okhttp3.internal.http2.Http2.FLAG_ACK;
import static okhttp3.internal.http2.Http2.FLAG_COMPRESSED;
import static okhttp3.internal.http2.Http2.FLAG_END_HEADERS;
import static okhttp3.internal.http2.Http2.FLAG_END_STREAM;
import static okhttp3.internal.http2.Http2.FLAG_PADDED;
import static okhttp3.internal.http2.Http2.FLAG_PRIORITY;
import static okhttp3.internal.http2.Http2.INITIAL_MAX_FRAME_SIZE;
import static okhttp3.internal.http2.Http2.TYPE_CONTINUATION;
import static okhttp3.internal.http2.Http2.TYPE_DATA;
import static okhttp3.internal.http2.Http2.TYPE_GOAWAY;
import static okhttp3.internal.http2.Http2.TYPE_HEADERS;
import static okhttp3.internal.http2.Http2.TYPE_PING;
import static okhttp3.internal.http2.Http2.TYPE_PRIORITY;
import static okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE;
import static okhttp3.internal.http2.Http2.TYPE_RST_STREAM;
import static okhttp3.internal.http2.Http2.TYPE_SETTINGS;
import static okhttp3.internal.http2.Http2.TYPE_WINDOW_UPDATE;
import static okhttp3.internal.http2.Http2.frameLog;
import static okhttp3.internal.http2.Http2.ioException;
import static okio.ByteString.EMPTY;

Reads HTTP/2 transport frames.

This implementation assumes we do not send an increased frame size setting to the peer. Hence, we expect all frames to have a max length of Http2.INITIAL_MAX_FRAME_SIZE.

/** * Reads HTTP/2 transport frames. * * <p>This implementation assumes we do not send an increased {@link Settings#getMaxFrameSize frame * size setting} to the peer. Hence, we expect all frames to have a max length of {@link * Http2#INITIAL_MAX_FRAME_SIZE}. */
final class Http2Reader implements Closeable { static final Logger logger = Logger.getLogger(Http2.class.getName()); private final BufferedSource source; private final ContinuationSource continuation; private final boolean client; // Visible for testing. final Hpack.Reader hpackReader;
Creates a frame reader with max header table size of 4096.
/** Creates a frame reader with max header table size of 4096. */
Http2Reader(BufferedSource source, boolean client) { this.source = source; this.client = client; this.continuation = new ContinuationSource(this.source); this.hpackReader = new Hpack.Reader(4096, continuation); } public void readConnectionPreface(Handler handler) throws IOException { 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. ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size()); if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION %s", connectionPreface.hex())); if (!CONNECTION_PREFACE.equals(connectionPreface)) { throw ioException("Expected a connection header but was %s", connectionPreface.utf8()); } } } public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException { try { source.require(9); // Frame header size } catch (IOException e) { 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...) ... // +---------------------------------------------------------------+ int length = readMedium(source); if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) { throw ioException("FRAME_SIZE_ERROR: %s", length); } byte type = (byte) (source.readByte() & 0xff); if (requireSettings && type != TYPE_SETTINGS) { throw ioException("Expected a SETTINGS frame but was %s", type); } byte flags = (byte) (source.readByte() & 0xff); int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit. if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags)); switch (type) { case TYPE_DATA: readData(handler, length, flags, streamId); break; case TYPE_HEADERS: readHeaders(handler, length, flags, streamId); break; case TYPE_PRIORITY: readPriority(handler, length, flags, streamId); break; case TYPE_RST_STREAM: readRstStream(handler, length, flags, streamId); break; case TYPE_SETTINGS: readSettings(handler, length, flags, streamId); break; case TYPE_PUSH_PROMISE: readPushPromise(handler, length, flags, streamId); break; case TYPE_PING: readPing(handler, length, flags, streamId); break; case TYPE_GOAWAY: readGoAway(handler, length, flags, streamId); break; case TYPE_WINDOW_UPDATE: readWindowUpdate(handler, length, flags, streamId); break; default: // Implementations MUST discard frames that have unknown or unsupported types. source.skip(length); } return true; } private void readHeaders(Handler handler, int length, byte flags, int streamId) throws IOException { if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); boolean endStream = (flags & FLAG_END_STREAM) != 0; short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; if ((flags & FLAG_PRIORITY) != 0) { readPriority(handler, streamId); length -= 5; // account for above read. } length = lengthWithoutPadding(length, flags, padding); List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId); handler.headers(endStream, streamId, -1, headerBlock); } private List<Header> readHeaderBlock(int length, short padding, byte flags, int streamId) throws IOException { continuation.length = continuation.left = length; 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(); } private void readData(Handler handler, int length, byte flags, int streamId) throws IOException { if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_DATA streamId == 0"); // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED boolean inFinished = (flags & FLAG_END_STREAM) != 0; boolean gzipped = (flags & FLAG_COMPRESSED) != 0; if (gzipped) { throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); } short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; length = lengthWithoutPadding(length, flags, padding); handler.data(inFinished, streamId, source, length); source.skip(padding); } private void readPriority(Handler handler, int length, byte flags, int streamId) throws IOException { if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length); if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); readPriority(handler, streamId); } private void readPriority(Handler handler, int streamId) throws IOException { int w1 = source.readInt(); boolean exclusive = (w1 & 0x80000000) != 0; int streamDependency = (w1 & 0x7fffffff); int weight = (source.readByte() & 0xff) + 1; handler.priority(streamId, streamDependency, weight, exclusive); } private void readRstStream(Handler handler, int length, byte flags, int streamId) throws IOException { if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length); if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0"); int errorCodeInt = source.readInt(); ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); if (errorCode == null) { throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt); } handler.rstStream(streamId, errorCode); } private void readSettings(Handler handler, int length, byte flags, int streamId) throws IOException { if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); if ((flags & 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: %s", length); Settings settings = new Settings(); for (int i = 0; i < length; i += 6) { int id = source.readShort() & 0xFFFF; int value = source.readInt(); switch (id) { case 1: // SETTINGS_HEADER_TABLE_SIZE break; case 2: // SETTINGS_ENABLE_PUSH if (value != 0 && value != 1) { throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); } break; case 3: // SETTINGS_MAX_CONCURRENT_STREAMS id = 4; // Renumbered in draft 10. break; case 4: // SETTINGS_INITIAL_WINDOW_SIZE id = 7; // Renumbered in draft 10. if (value < 0) { throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); } break; case 5: // SETTINGS_MAX_FRAME_SIZE if (value < INITIAL_MAX_FRAME_SIZE || value > 16777215) { throw ioException("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: %s", value); } break; case 6: // SETTINGS_MAX_HEADER_LIST_SIZE break; // Advisory only, so ignored. default: break; // Must ignore setting with unknown id. } settings.set(id, value); } handler.settings(false, settings); } private void readPushPromise(Handler handler, int length, byte flags, int streamId) throws IOException { if (streamId == 0) { throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0"); } short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; int promisedStreamId = source.readInt() & 0x7fffffff; length -= 4; // account for above read. length = lengthWithoutPadding(length, flags, padding); List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId); handler.pushPromise(streamId, promisedStreamId, headerBlock); } private void readPing(Handler handler, int length, byte flags, int streamId) throws IOException { if (length != 8) throw ioException("TYPE_PING length != 8: %s", length); if (streamId != 0) throw ioException("TYPE_PING streamId != 0"); int payload1 = source.readInt(); int payload2 = source.readInt(); boolean ack = (flags & FLAG_ACK) != 0; handler.ping(ack, payload1, payload2); } private void readGoAway(Handler handler, int length, byte flags, int streamId) throws IOException { if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0"); int lastStreamId = source.readInt(); int errorCodeInt = source.readInt(); int opaqueDataLength = length - 8; ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); if (errorCode == null) { throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); } ByteString debugData = EMPTY; if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. debugData = source.readByteString(opaqueDataLength); } handler.goAway(lastStreamId, errorCode, debugData); } private void readWindowUpdate(Handler handler, int length, byte flags, int streamId) throws IOException { if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length); long increment = (source.readInt() & 0x7fffffffL); if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); handler.windowUpdate(streamId, increment); } @Override public void close() throws IOException { source.close(); }
Decompression of the header block occurs above the framing layer. This class lazily reads continuation frames as they are needed by Reader.readHeaders().
/** * Decompression of the header block occurs above the framing layer. This class lazily reads * continuation frames as they are needed by {@link Hpack.Reader#readHeaders()}. */
static final class ContinuationSource implements Source { private final BufferedSource source; int length; byte flags; int streamId; int left; short padding; ContinuationSource(BufferedSource source) { this.source = source; } @Override public long read(Buffer sink, long byteCount) throws IOException { while (left == 0) { source.skip(padding); padding = 0; if ((flags & FLAG_END_HEADERS) != 0) return -1; readContinuationHeader(); // TODO: test case for empty continuation header? } long read = source.read(sink, Math.min(byteCount, left)); if (read == -1) return -1; left -= read; return read; } @Override public Timeout timeout() { return source.timeout(); } @Override public void close() throws IOException { } private void readContinuationHeader() throws IOException { int previousStreamId = streamId; length = left = readMedium(source); byte type = (byte) (source.readByte() & 0xff); flags = (byte) (source.readByte() & 0xff); if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags)); streamId = (source.readInt() & 0x7fffffff); if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type); if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed"); } } static int readMedium(BufferedSource source) throws IOException { return (source.readByte() & 0xff) << 16 | (source.readByte() & 0xff) << 8 | (source.readByte() & 0xff); } static int lengthWithoutPadding(int length, byte flags, short padding) throws IOException { if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length. if (padding > length) { throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); } return (short) (length - padding); } interface Handler { void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException;
Create or update incoming headers, creating the corresponding streams if necessary. Frames that trigger this are HEADERS and PUSH_PROMISE.
Params:
  • inFinished – true if the sender will not send further frames.
  • streamId – the stream owning these headers.
  • associatedStreamId – the stream that triggered the sender to create this stream.
/** * 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. */
void headers(boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock); void rstStream(int streamId, ErrorCode errorCode); void settings(boolean clearPrevious, Settings settings);
HTTP/2 only.
/** HTTP/2 only. */
void 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.
/** * Read a connection-level ping from the peer. {@code ack} indicates this is a reply. The data * in {@code payload1} and {@code payload2} opaque binary, and there are no rules on the * content. */
void ping(boolean ack, int payload1, int payload2);
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.
Params:
  • lastGoodStreamId – the last stream ID the peer processed before sending this message. If lastGoodStreamId is zero, the peer processed no frames.
  • errorCode – reason for closing the connection.
  • debugData – only valid for HTTP/2; opaque debug data to send.
/** * The peer tells us to stop creating streams. It is safe to replay streams with {@code ID > * lastGoodStreamId} on a new connection. In- flight streams with {@code 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 * {@code 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. */
void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData);
Notifies that an additional windowSizeIncrement bytes can be sent on streamId, or the connection if streamId is zero.
/** * Notifies that an additional {@code windowSizeIncrement} bytes can be sent on {@code * streamId}, or the connection if {@code streamId} is zero. */
void windowUpdate(int streamId, long windowSizeIncrement);
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.
Params:
  • streamId – stream which has a priority change.
  • streamDependency – the stream ID this stream is dependent on.
  • weight – relative proportion of priority in [1..256].
  • exclusive – inserts this stream ID as the sole child of streamDependency.
/** * 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 {@code streamDependency}. */
void priority(int streamId, int streamDependency, int weight, boolean exclusive);
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.

Params:
  • streamId – client-initiated stream ID. Must be an odd number.
  • promisedStreamId – server-initiated stream ID. Must be an even number.
  • requestHeaders – minimally includes :method, :scheme, :authority, and (@code :path}.
/** * HTTP/2 only. Receive a push promise header block. <p> A push promise contains all the headers * that pertain to a server-initiated request, and a {@code promisedStreamId} to which response * frames will be delivered. Push promise frames are sent as a part of the response to {@code * 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 {@code :method}, {@code :scheme}, {@code * :authority}, and (@code :path}. */
void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) throws IOException;
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

Params:
  • 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.
  • origin – when present, the origin is typically represented as a combination of scheme, host and port. When empty, the origin is that of the streamId.
  • protocol – an ALPN protocol, such as h2.
  • host – an IP address or hostname.
  • port – the IP port associated with the service.
  • maxAge – time in seconds that this alternative is considered fresh.
/** * HTTP/2 only. Expresses that resources for the connection or a client- initiated stream are * available from a different network location or protocol configuration. * * <p>See <a href="http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-01">alt-svc</a> * * @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 {@code origin} * parameter. * @param origin when present, the <a href="http://tools.ietf.org/html/rfc6454">origin</a> is * typically represented as a combination of scheme, host and port. When empty, the origin is * that of the {@code streamId}. * @param protocol an ALPN protocol, such as {@code 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. */
void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge); } }