/* * 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 com.squareup.okhttp.internal.spdy; import com.squareup.okhttp.internal.Util; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; /** * Read and write http/2 v06 frames. * http://tools.ietf.org/html/draft-ietf-httpbis-http2-06 */ final class Http20Draft06 implements Variant { private static final byte[] CONNECTION_HEADER; static { try { CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError(); } } static final int TYPE_DATA = 0x0; static final int TYPE_HEADERS = 0x1; static final int TYPE_PRIORITY = 0x2; static final int TYPE_RST_STREAM = 0x3; static final int TYPE_SETTINGS = 0x4; static final int TYPE_PUSH_PROMISE = 0x5; static final int TYPE_PING = 0x6; static final int TYPE_GOAWAY = 0x7; static final int TYPE_WINDOW_UPDATE = 0x9; static final int TYPE_CONTINUATION = 0xa; static final int FLAG_END_STREAM = 0x1; /** Used for headers, push-promise and continuation. */ static final int FLAG_END_HEADERS = 0x4; static final int FLAG_PRIORITY = 0x8; static final int FLAG_PONG = 0x1; static final int FLAG_END_FLOW_CONTROL = 0x1; @Override public FrameReader newReader(InputStream in, boolean client) { return new Reader(in, client); } @Override public FrameWriter newWriter(OutputStream out, boolean client) { return new Writer(out, client); } static final class Reader implements FrameReader { private final DataInputStream in; private final boolean client; private final Hpack.Reader hpackReader; Reader(InputStream in, boolean client) { this.in = new DataInputStream(in); this.client = client; this.hpackReader = new Hpack.Reader(this.in, client); } @Override public void readConnectionHeader() throws IOException { if (client) return; // Nothing to read; servers don't send connection headers! byte[] connectionHeader = new byte[CONNECTION_HEADER.length]; in.readFully(connectionHeader); if (!Arrays.equals(connectionHeader, CONNECTION_HEADER)) { throw ioException("Expected a connection header but was " + Arrays.toString(connectionHeader)); } } @Override public boolean nextFrame(Handler handler) throws IOException { int w1; try { w1 = in.readInt(); } catch (IOException e) { return false; // This might be a normal socket close. } int w2 = in.readInt(); int length = (w1 & 0xffff0000) >> 16; int type = (w1 & 0xff00) >> 8; int flags = w1 & 0xff; // boolean r = (w2 & 0x80000000) != 0; // Reserved. int streamId = (w2 & 0x7fffffff); switch (type) { case TYPE_DATA: readData(handler, flags, length, streamId); return true; case TYPE_HEADERS: readHeaders(handler, flags, length, streamId); return true; case TYPE_PRIORITY: readPriority(handler, flags, length, streamId); return true; case TYPE_RST_STREAM: readRstStream(handler, flags, length, streamId); return true; case TYPE_SETTINGS: readSettings(handler, flags, length, streamId); return true; case TYPE_PUSH_PROMISE: readPushPromise(handler, flags, length, streamId); return true; case TYPE_PING: readPing(handler, flags, length, streamId); return true; case TYPE_GOAWAY: readGoAway(handler, flags, length, streamId); return true; case TYPE_WINDOW_UPDATE: readWindowUpdate(handler, flags, length, streamId); return true; } throw new UnsupportedOperationException("TODO"); } private void readHeaders(Handler handler, int flags, int length, int streamId) throws IOException { if (streamId == 0) throw ioException("TYPE_HEADERS streamId == 0"); boolean inFinished = (flags & FLAG_END_STREAM) != 0; while (true) { hpackReader.readHeaders(length); if ((flags & FLAG_END_HEADERS) != 0) { hpackReader.emitReferenceSet(); List<String> namesAndValues = hpackReader.getAndReset(); int priority = -1; // TODO: priority handler.headers(false, inFinished, streamId, -1, priority, namesAndValues, HeadersMode.HTTP_20_HEADERS); return; } // Read another continuation frame. int w1 = in.readInt(); int w2 = in.readInt(); length = (w1 & 0xffff0000) >> 16; int newType = (w1 & 0xff00) >> 8; flags = w1 & 0xff; // TODO: remove in draft 8: CONTINUATION no longer sets END_STREAM inFinished = (flags & FLAG_END_STREAM) != 0; // boolean u = (w2 & 0x80000000) != 0; // Unused. int newStreamId = (w2 & 0x7fffffff); if (newType != TYPE_CONTINUATION) { throw ioException("TYPE_CONTINUATION didn't have FLAG_END_HEADERS"); } if (newStreamId != streamId) throw ioException("TYPE_CONTINUATION streamId changed"); } } private void readData(Handler handler, int flags, int length, int streamId) throws IOException { boolean inFinished = (flags & FLAG_END_STREAM) != 0; handler.data(inFinished, streamId, in, length); } private void readPriority(Handler handler, int flags, int length, int streamId) throws IOException { if (length != 4) throw ioException("TYPE_PRIORITY length: %d != 4", length); if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); int w1 = in.readInt(); // boolean r = (w1 & 0x80000000) != 0; // Reserved. int priority = (w1 & 0x7fffffff); handler.priority(streamId, priority); } private void readRstStream(Handler handler, int flags, int length, 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 = in.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 flags, int length, int streamId) throws IOException { if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length); if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); Settings settings = new Settings(); for (int i = 0; i < length; i += 8) { int w1 = in.readInt(); int value = in.readInt(); // int r = (w1 & 0xff000000) >>> 24; // Reserved. int id = w1 & 0xffffff; settings.set(id, 0, value); } handler.settings(false, settings); } private void readPushPromise(Handler handler, int flags, int length, int streamId) { // TODO: } private void readPing(Handler handler, int flags, int length, 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 = in.readInt(); int payload2 = in.readInt(); boolean reply = (flags & FLAG_PONG) != 0; handler.ping(reply, payload1, payload2); } private void readGoAway(Handler handler, int flags, int length, int streamId) throws IOException { if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); int lastStreamId = in.readInt(); int errorCodeInt = in.readInt(); int opaqueDataLength = length - 8; ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); if (errorCode == null) { throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt); } if (Util.skipByReading(in, opaqueDataLength) != opaqueDataLength) { throw new IOException("TYPE_GOAWAY opaque data was truncated"); } handler.goAway(lastStreamId, errorCode); } private void readWindowUpdate(Handler handler, int flags, int length, int streamId) throws IOException { int w1 = in.readInt(); // boolean r = (w1 & 0x80000000) != 0; // Reserved. int windowSizeIncrement = (w1 & 0x7fffffff); boolean endFlowControl = (flags & FLAG_END_FLOW_CONTROL) != 0; handler.windowUpdate(streamId, windowSizeIncrement, endFlowControl); } private static IOException ioException(String message, Object... args) throws IOException { throw new IOException(String.format(message, args)); } @Override public void close() throws IOException { in.close(); } } static final class Writer implements FrameWriter { private final DataOutputStream out; private final boolean client; private final ByteArrayOutputStream hpackBuffer; private final Hpack.Writer hpackWriter; Writer(OutputStream out, boolean client) { this.out = new DataOutputStream(out); this.client = client; this.hpackBuffer = new ByteArrayOutputStream(); this.hpackWriter = new Hpack.Writer(hpackBuffer); } @Override public synchronized void flush() throws IOException { out.flush(); } @Override public synchronized void connectionHeader() throws IOException { if (!client) return; // Nothing to write; servers don't send connection headers! out.write(CONNECTION_HEADER); } @Override public synchronized void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, int priority, int slot, List<String> nameValueBlock) throws IOException { if (inFinished) throw new UnsupportedOperationException(); headers(outFinished, streamId, priority, nameValueBlock); } @Override public synchronized void synReply(boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException { headers(outFinished, streamId, -1, nameValueBlock); } @Override public synchronized void headers(int streamId, List<String> nameValueBlock) throws IOException { headers(false, streamId, -1, nameValueBlock); } private void headers(boolean outFinished, int streamId, int priority, List<String> nameValueBlock) throws IOException { hpackBuffer.reset(); hpackWriter.writeHeaders(nameValueBlock); int type = TYPE_HEADERS; // TODO: implement CONTINUATION int length = hpackBuffer.size(); int flags = FLAG_END_HEADERS; if (outFinished) flags |= FLAG_END_STREAM; if (priority != -1) flags |= FLAG_PRIORITY; out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); if (priority != -1) out.writeInt(priority & 0x7fffffff); hpackBuffer.writeTo(out); } @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException { throw new UnsupportedOperationException("TODO"); } @Override public void data(boolean outFinished, int streamId, byte[] data) throws IOException { data(outFinished, streamId, data, 0, data.length); } @Override public synchronized void data(boolean outFinished, int streamId, byte[] data, int offset, int byteCount) throws IOException { int type = TYPE_DATA; int flags = 0; if (outFinished) flags |= FLAG_END_STREAM; out.writeInt((byteCount & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); out.write(data, offset, byteCount); } @Override public synchronized void settings(Settings settings) throws IOException { int type = TYPE_SETTINGS; int length = settings.size() * 8; int flags = 0; int streamId = 0; out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); out.writeInt(streamId & 0x7fffffff); for (int i = 0; i < Settings.COUNT; i++) { if (!settings.isSet(i)) continue; out.writeInt(i & 0xffffff); out.writeInt(settings.get(i)); } } @Override public synchronized void noop() throws IOException { throw new UnsupportedOperationException(); } @Override public synchronized void ping(boolean reply, int payload1, int payload2) throws IOException { // TODO } @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode) throws IOException { // TODO } @Override public synchronized void windowUpdate(int streamId, int deltaWindowSize) throws IOException { // TODO } @Override public void close() throws IOException { out.close(); } } }