blob: 24c512d9ba6cdbb05fd532899d38dbfdc7282619 [file] [log] [blame]
/*
* 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 com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import org.junit.After;
import org.junit.Test;
import static com.squareup.okhttp.TestUtil.headerEntries;
import static com.squareup.okhttp.TestUtil.repeat;
import static com.squareup.okhttp.internal.framed.ErrorCode.CANCEL;
import static com.squareup.okhttp.internal.framed.ErrorCode.PROTOCOL_ERROR;
import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_DATA;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_HEADERS;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_PING;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_RST_STREAM;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_SETTINGS;
import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_WINDOW_UPDATE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class Http2ConnectionTest {
private static final Variant HTTP_2 = new Http2();
private final MockSpdyPeer peer = new MockSpdyPeer();
@After public void tearDown() throws Exception {
peer.close();
}
@Test public void serverPingsClientHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.sendFrame().ping(false, 2, 3);
peer.acceptFrame(); // PING
peer.play();
// play it back
connection(peer, HTTP_2);
// verify the peer received what was expected
MockSpdyPeer.InFrame ping = peer.takeFrame();
assertEquals(TYPE_PING, ping.type);
assertEquals(0, ping.streamId);
assertEquals(2, ping.payload1);
assertEquals(3, ping.payload2);
assertTrue(ping.ack);
}
@Test public void clientPingsServerHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 5);
peer.play();
// play it back
FramedConnection connection = connection(peer, HTTP_2);
Ping ping = connection.ping();
assertTrue(ping.roundTripTime() > 0);
assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
// verify the peer received what was expected
MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
assertEquals(0, pingFrame.streamId);
assertEquals(1, pingFrame.payload1);
assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
assertFalse(pingFrame.ack);
}
@Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
Settings initial = new Settings();
initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 1684);
Settings shouldntImpactConnection = new Settings();
shouldntImpactConnection.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 3368);
peer.sendFrame().settings(initial);
peer.acceptFrame(); // ACK
peer.sendFrame().settings(shouldntImpactConnection);
peer.acceptFrame(); // ACK 2
peer.acceptFrame(); // HEADERS
peer.play();
FramedConnection connection = connection(peer, HTTP_2);
// Default is 64KiB - 1.
assertEquals(65535, connection.peerSettings.getInitialWindowSize(-1));
// Verify the peer received the ACK.
MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
assertEquals(TYPE_SETTINGS, ackFrame.type);
assertEquals(0, ackFrame.streamId);
assertTrue(ackFrame.ack);
ackFrame = peer.takeFrame();
assertEquals(TYPE_SETTINGS, ackFrame.type);
assertEquals(0, ackFrame.streamId);
assertTrue(ackFrame.ack);
// This stream was created *after* the connection settings were adjusted.
FramedStream stream = connection.newStream(headerEntries("a", "android"), false, true);
assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
// New Stream is has the most recent initial window size.
assertEquals(3368, stream.bytesLeftInWriteWindow);
}
@Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
boolean client = false; // Peer is server, so we are client.
Settings settings = new Settings();
settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
// verify the peer's settings were read and applied.
assertEquals(0, connection.peerSettings.getHeaderTableSize());
Http2.Reader frameReader = (Http2.Reader) connection.readerRunnable.frameReader;
assertEquals(0, frameReader.hpackReader.maxDynamicTableByteCount());
// TODO: when supported, check the frameWriter's compression table is unaffected.
}
@Test public void peerHttp2ClientDisablesPush() throws Exception {
boolean client = false; // Peer is client, so we are server.
Settings settings = new Settings();
settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
// verify the peer's settings were read and applied.
assertFalse(connection.peerSettings.getEnablePush(true));
}
@Test public void peerIncreasesMaxFrameSize() throws Exception {
int newMaxFrameSize = 0x4001;
Settings settings = new Settings();
settings.set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize);
FramedConnection connection = sendHttp2SettingsAndCheckForAck(true, settings);
// verify the peer's settings were read and applied.
assertEquals(newMaxFrameSize, connection.peerSettings.getMaxFrameSize(-1));
assertEquals(newMaxFrameSize, connection.frameWriter.maxDataLength());
}
@Test public void receiveGoAwayHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.acceptFrame(); // SYN_STREAM 3
peer.acceptFrame(); // SYN_STREAM 5
peer.sendFrame().goAway(3, PROTOCOL_ERROR, Util.EMPTY_BYTE_ARRAY);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // DATA STREAM 3
peer.play();
// play it back
FramedConnection connection = connection(peer, HTTP_2);
FramedStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
FramedStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
BufferedSink sink1 = Okio.buffer(stream1.getSink());
BufferedSink sink2 = Okio.buffer(stream2.getSink());
sink1.writeUtf8("abc");
try {
sink2.writeUtf8("abc");
sink2.flush();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
}
sink1.writeUtf8("def");
sink1.close();
try {
connection.newStream(headerEntries("c", "cola"), true, true);
fail();
} catch (IOException expected) {
assertEquals("shutdown", expected.getMessage());
}
assertTrue(stream1.isOpen());
assertFalse(stream2.isOpen());
assertEquals(1, connection.openStreamCount());
// verify the peer received what was expected
MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream1.type);
MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream2.type);
MockSpdyPeer.InFrame ping = peer.takeFrame();
assertEquals(TYPE_PING, ping.type);
MockSpdyPeer.InFrame data1 = peer.takeFrame();
assertEquals(TYPE_DATA, data1.type);
assertEquals(3, data1.streamId);
assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
}
@Test public void readSendsWindowUpdateHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
int windowSize = 100;
int windowUpdateThreshold = 50;
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
for (int i = 0; i < 3; i++) {
// Send frames of summing to size 50, which is windowUpdateThreshold.
peer.sendFrame().data(false, 3, data(24), 24);
peer.sendFrame().data(false, 3, data(25), 25);
peer.sendFrame().data(false, 3, data(1), 1);
peer.acceptFrame(); // connection WINDOW UPDATE
peer.acceptFrame(); // stream WINDOW UPDATE
}
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
FramedConnection connection = connection(peer, HTTP_2);
connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
FramedStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
assertEquals(0, stream.unacknowledgedBytesRead);
assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
Source in = stream.getSource();
Buffer buffer = new Buffer();
buffer.writeAll(in);
assertEquals(-1, in.read(buffer, 1));
assertEquals(150, buffer.size());
MockSpdyPeer.InFrame synStream = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream.type);
for (int i = 0; i < 3; i++) {
List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
for (int j = 0; j < 2; j++) {
MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
windowUpdateStreamIds.add(windowUpdate.streamId);
assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
}
assertTrue(windowUpdateStreamIds.contains(0)); // connection
assertTrue(windowUpdateStreamIds.contains(3)); // stream
}
}
private Buffer data(int byteCount) {
return new Buffer().write(new byte[byteCount]);
}
@Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
FramedConnection connection = connection(peer, HTTP_2);
FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
assertEquals(-1, client.getSource().read(new Buffer(), 1));
// Verify the peer received what was expected.
MockSpdyPeer.InFrame synStream = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream.type);
assertEquals(3, peer.frameCount());
}
@Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // DATA
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.play();
// Play it back.
FramedConnection connection = connection(peer, HTTP_2);
FramedStream client = connection.newStream(headerEntries("b", "banana"), true, true);
BufferedSink out = Okio.buffer(client.getSink());
out.write(Util.EMPTY_BYTE_ARRAY);
out.flush();
out.close();
// Verify the peer received what was expected.
assertEquals(TYPE_HEADERS, peer.takeFrame().type);
assertEquals(TYPE_DATA, peer.takeFrame().type);
assertEquals(3, peer.frameCount());
}
@Test public void maxFrameSizeHonored() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
byte[] buff = new byte[peer.maxOutboundDataLength() + 1];
Arrays.fill(buff, (byte) '*');
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // DATA
peer.acceptFrame(); // DATA
peer.play();
// play it back
FramedConnection connection = connection(peer, HTTP_2);
FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
BufferedSink out = Okio.buffer(stream.getSink());
out.write(buff);
out.flush();
out.close();
MockSpdyPeer.InFrame synStream = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream.type);
MockSpdyPeer.InFrame data = peer.takeFrame();
assertEquals(peer.maxOutboundDataLength(), data.data.length);
data = peer.takeFrame();
assertEquals(1, data.data.length);
}
@Test public void pushPromiseStream() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
final List<Header> expectedRequestHeaders = Arrays.asList(
new Header(Header.TARGET_METHOD, "GET"),
new Header(Header.TARGET_SCHEME, "https"),
new Header(Header.TARGET_AUTHORITY, "squareup.com"),
new Header(Header.TARGET_PATH, "/cached")
);
peer.sendFrame().pushPromise(3, 2, expectedRequestHeaders);
final List<Header> expectedResponseHeaders = Arrays.asList(
new Header(Header.RESPONSE_STATUS, "200")
);
peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
RecordingPushObserver observer = new RecordingPushObserver();
// play it back
FramedConnection connection = connectionBuilder(peer, HTTP_2)
.pushObserver(observer).build();
FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
assertEquals(-1, client.getSource().read(new Buffer(), 1));
// verify the peer received what was expected
assertEquals(TYPE_HEADERS, peer.takeFrame().type);
assertEquals(expectedRequestHeaders, observer.takeEvent());
assertEquals(expectedResponseHeaders, observer.takeEvent());
}
@Test public void doublePushPromise() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.sendFrame().pushPromise(3, 2, headerEntries("a", "android"));
peer.acceptFrame(); // SYN_REPLY
peer.sendFrame().pushPromise(3, 2, headerEntries("b", "banana"));
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
FramedConnection connection = connectionBuilder(peer, HTTP_2).build();
connection.newStream(headerEntries("b", "banana"), false, true);
// verify the peer received what was expected
assertEquals(TYPE_HEADERS, peer.takeFrame().type);
assertEquals(PROTOCOL_ERROR, peer.takeFrame().errorCode);
}
@Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
// write the mocking script
peer.sendFrame().pushPromise(3, 2, Arrays.asList(
new Header(Header.TARGET_METHOD, "GET"),
new Header(Header.TARGET_SCHEME, "https"),
new Header(Header.TARGET_AUTHORITY, "squareup.com"),
new Header(Header.TARGET_PATH, "/cached")
));
peer.sendFrame().synReply(true, 2, Arrays.asList(
new Header(Header.RESPONSE_STATUS, "200")
));
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
connectionBuilder(peer, HTTP_2)
.pushObserver(PushObserver.CANCEL).build();
// verify the peer received what was expected
MockSpdyPeer.InFrame rstStream = peer.takeFrame();
assertEquals(TYPE_RST_STREAM, rstStream.type);
assertEquals(2, rstStream.streamId);
assertEquals(CANCEL, rstStream.errorCode);
}
/**
* When writing a set of headers fails due to an {@code IOException}, make sure the writer is left
* in a consistent state so the next writer also gets an {@code IOException} also instead of
* something worse (like an {@link IllegalStateException}.
*
* <p>See https://github.com/square/okhttp/issues/1651
*/
@Test public void socketExceptionWhileWritingHeaders() throws Exception {
peer.setVariantAndClient(HTTP_2, false);
peer.acceptFrame(); // SYN_STREAM.
peer.play();
String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
Socket socket = peer.openSocket();
FramedConnection connection = new FramedConnection.Builder(true, socket)
.pushObserver(IGNORE)
.protocol(HTTP_2.getProtocol())
.build();
socket.shutdownOutput();
try {
connection.newStream(headerEntries("a", longString), false, true);
fail();
} catch (IOException expected) {
}
try {
connection.newStream(headerEntries("b", longString), false, true);
fail();
} catch (IOException expected) {
}
}
private FramedConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
throws IOException, InterruptedException {
peer.setVariantAndClient(HTTP_2, client);
peer.sendFrame().settings(settings);
peer.acceptFrame(); // ACK
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
FramedConnection connection = connection(peer, HTTP_2);
// verify the peer received the ACK
MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
assertEquals(TYPE_SETTINGS, ackFrame.type);
assertEquals(0, ackFrame.streamId);
assertTrue(ackFrame.ack);
connection.ping().roundTripTime(); // Ensure that settings have been applied before returning.
return connection;
}
private FramedConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
return connectionBuilder(peer, variant).build();
}
private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
return new FramedConnection.Builder(true, peer.openSocket())
.pushObserver(IGNORE)
.protocol(variant.getProtocol());
}
static final PushObserver IGNORE = new PushObserver() {
@Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
return false;
}
@Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
return false;
}
@Override public boolean onData(int streamId, BufferedSource source, int byteCount,
boolean last) throws IOException {
source.skip(byteCount);
return false;
}
@Override public void onReset(int streamId, ErrorCode errorCode) {
}
};
private static class RecordingPushObserver implements PushObserver {
final List<Object> events = new ArrayList<>();
public synchronized Object takeEvent() throws InterruptedException {
while (events.isEmpty()) {
wait();
}
return events.remove(0);
}
@Override public synchronized boolean onRequest(int streamId, List<Header> requestHeaders) {
assertEquals(2, streamId);
events.add(requestHeaders);
notifyAll();
return false;
}
@Override public synchronized boolean onHeaders(
int streamId, List<Header> responseHeaders, boolean last) {
assertEquals(2, streamId);
assertTrue(last);
events.add(responseHeaders);
notifyAll();
return false;
}
@Override public synchronized boolean onData(
int streamId, BufferedSource source, int byteCount, boolean last) {
events.add(new AssertionError("onData"));
notifyAll();
return false;
}
@Override public synchronized void onReset(int streamId, ErrorCode errorCode) {
events.add(new AssertionError("onReset"));
notifyAll();
}
}
}