1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp.internal.framed;
17
18import com.squareup.okhttp.internal.Util;
19import java.io.IOException;
20import java.net.Socket;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.List;
24import java.util.concurrent.TimeUnit;
25import okio.Buffer;
26import okio.BufferedSink;
27import okio.BufferedSource;
28import okio.Okio;
29import okio.Source;
30import org.junit.After;
31import org.junit.Test;
32
33import static com.squareup.okhttp.TestUtil.headerEntries;
34import static com.squareup.okhttp.TestUtil.repeat;
35import static com.squareup.okhttp.internal.framed.ErrorCode.CANCEL;
36import static com.squareup.okhttp.internal.framed.ErrorCode.PROTOCOL_ERROR;
37import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
38import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE;
39import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_DATA;
40import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_HEADERS;
41import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_PING;
42import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_RST_STREAM;
43import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_SETTINGS;
44import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_WINDOW_UPDATE;
45import static org.junit.Assert.assertEquals;
46import static org.junit.Assert.assertFalse;
47import static org.junit.Assert.assertTrue;
48import static org.junit.Assert.fail;
49
50public final class Http2ConnectionTest {
51  private static final Variant HTTP_2 = new Http2();
52  private final MockSpdyPeer peer = new MockSpdyPeer();
53
54  @After public void tearDown() throws Exception {
55    peer.close();
56  }
57
58  @Test public void serverPingsClientHttp2() throws Exception {
59    peer.setVariantAndClient(HTTP_2, false);
60
61    // write the mocking script
62    peer.sendFrame().ping(false, 2, 3);
63    peer.acceptFrame(); // PING
64    peer.play();
65
66    // play it back
67    connection(peer, HTTP_2);
68
69    // verify the peer received what was expected
70    MockSpdyPeer.InFrame ping = peer.takeFrame();
71    assertEquals(TYPE_PING, ping.type);
72    assertEquals(0, ping.streamId);
73    assertEquals(2, ping.payload1);
74    assertEquals(3, ping.payload2);
75    assertTrue(ping.ack);
76  }
77
78  @Test public void clientPingsServerHttp2() throws Exception {
79    peer.setVariantAndClient(HTTP_2, false);
80
81    // write the mocking script
82    peer.acceptFrame(); // PING
83    peer.sendFrame().ping(true, 1, 5);
84    peer.play();
85
86    // play it back
87    FramedConnection connection = connection(peer, HTTP_2);
88    Ping ping = connection.ping();
89    assertTrue(ping.roundTripTime() > 0);
90    assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
91
92    // verify the peer received what was expected
93    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
94    assertEquals(0, pingFrame.streamId);
95    assertEquals(1, pingFrame.payload1);
96    assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
97    assertFalse(pingFrame.ack);
98  }
99
100  @Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
101    peer.setVariantAndClient(HTTP_2, false);
102
103    Settings initial = new Settings();
104    initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 1684);
105    Settings shouldntImpactConnection = new Settings();
106    shouldntImpactConnection.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 3368);
107
108    peer.sendFrame().settings(initial);
109    peer.acceptFrame(); // ACK
110    peer.sendFrame().settings(shouldntImpactConnection);
111    peer.acceptFrame(); // ACK 2
112    peer.acceptFrame(); // HEADERS
113    peer.play();
114
115    FramedConnection connection = connection(peer, HTTP_2);
116
117    // Default is 64KiB - 1.
118    assertEquals(65535, connection.peerSettings.getInitialWindowSize(-1));
119
120    // Verify the peer received the ACK.
121    MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
122    assertEquals(TYPE_SETTINGS, ackFrame.type);
123    assertEquals(0, ackFrame.streamId);
124    assertTrue(ackFrame.ack);
125    ackFrame = peer.takeFrame();
126    assertEquals(TYPE_SETTINGS, ackFrame.type);
127    assertEquals(0, ackFrame.streamId);
128    assertTrue(ackFrame.ack);
129
130    // This stream was created *after* the connection settings were adjusted.
131    FramedStream stream = connection.newStream(headerEntries("a", "android"), false, true);
132
133    assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
134    assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
135    // New Stream is has the most recent initial window size.
136    assertEquals(3368, stream.bytesLeftInWriteWindow);
137  }
138
139  @Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
140    boolean client = false; // Peer is server, so we are client.
141    Settings settings = new Settings();
142    settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
143
144    FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
145
146    // verify the peer's settings were read and applied.
147    assertEquals(0, connection.peerSettings.getHeaderTableSize());
148    Http2.Reader frameReader = (Http2.Reader) connection.readerRunnable.frameReader;
149    assertEquals(0, frameReader.hpackReader.maxDynamicTableByteCount());
150    // TODO: when supported, check the frameWriter's compression table is unaffected.
151  }
152
153  @Test public void peerHttp2ClientDisablesPush() throws Exception {
154    boolean client = false; // Peer is client, so we are server.
155    Settings settings = new Settings();
156    settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
157
158    FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
159
160    // verify the peer's settings were read and applied.
161    assertFalse(connection.peerSettings.getEnablePush(true));
162  }
163
164  @Test public void peerIncreasesMaxFrameSize() throws Exception {
165    int newMaxFrameSize = 0x4001;
166    Settings settings = new Settings();
167    settings.set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize);
168
169    FramedConnection connection = sendHttp2SettingsAndCheckForAck(true, settings);
170
171    // verify the peer's settings were read and applied.
172    assertEquals(newMaxFrameSize, connection.peerSettings.getMaxFrameSize(-1));
173    assertEquals(newMaxFrameSize, connection.frameWriter.maxDataLength());
174  }
175
176  @Test public void receiveGoAwayHttp2() throws Exception {
177    peer.setVariantAndClient(HTTP_2, false);
178
179    // write the mocking script
180    peer.acceptFrame(); // SYN_STREAM 3
181    peer.acceptFrame(); // SYN_STREAM 5
182    peer.sendFrame().goAway(3, PROTOCOL_ERROR, Util.EMPTY_BYTE_ARRAY);
183    peer.acceptFrame(); // PING
184    peer.sendFrame().ping(true, 1, 0);
185    peer.acceptFrame(); // DATA STREAM 3
186    peer.play();
187
188    // play it back
189    FramedConnection connection = connection(peer, HTTP_2);
190    FramedStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
191    FramedStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
192    connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
193    BufferedSink sink1 = Okio.buffer(stream1.getSink());
194    BufferedSink sink2 = Okio.buffer(stream2.getSink());
195    sink1.writeUtf8("abc");
196    try {
197      sink2.writeUtf8("abc");
198      sink2.flush();
199      fail();
200    } catch (IOException expected) {
201      assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
202    }
203    sink1.writeUtf8("def");
204    sink1.close();
205    try {
206      connection.newStream(headerEntries("c", "cola"), true, true);
207      fail();
208    } catch (IOException expected) {
209      assertEquals("shutdown", expected.getMessage());
210    }
211    assertTrue(stream1.isOpen());
212    assertFalse(stream2.isOpen());
213    assertEquals(1, connection.openStreamCount());
214
215    // verify the peer received what was expected
216    MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
217    assertEquals(TYPE_HEADERS, synStream1.type);
218    MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
219    assertEquals(TYPE_HEADERS, synStream2.type);
220    MockSpdyPeer.InFrame ping = peer.takeFrame();
221    assertEquals(TYPE_PING, ping.type);
222    MockSpdyPeer.InFrame data1 = peer.takeFrame();
223    assertEquals(TYPE_DATA, data1.type);
224    assertEquals(3, data1.streamId);
225    assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
226  }
227
228  @Test public void readSendsWindowUpdateHttp2() throws Exception {
229    peer.setVariantAndClient(HTTP_2, false);
230
231    int windowSize = 100;
232    int windowUpdateThreshold = 50;
233
234    // Write the mocking script.
235    peer.acceptFrame(); // SYN_STREAM
236    peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
237    for (int i = 0; i < 3; i++) {
238      // Send frames of summing to size 50, which is windowUpdateThreshold.
239      peer.sendFrame().data(false, 3, data(24), 24);
240      peer.sendFrame().data(false, 3, data(25), 25);
241      peer.sendFrame().data(false, 3, data(1), 1);
242      peer.acceptFrame(); // connection WINDOW UPDATE
243      peer.acceptFrame(); // stream WINDOW UPDATE
244    }
245    peer.sendFrame().data(true, 3, data(0), 0);
246    peer.play();
247
248    // Play it back.
249    FramedConnection connection = connection(peer, HTTP_2);
250    connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
251    FramedStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
252    assertEquals(0, stream.unacknowledgedBytesRead);
253    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
254    Source in = stream.getSource();
255    Buffer buffer = new Buffer();
256    buffer.writeAll(in);
257    assertEquals(-1, in.read(buffer, 1));
258    assertEquals(150, buffer.size());
259
260    MockSpdyPeer.InFrame synStream = peer.takeFrame();
261    assertEquals(TYPE_HEADERS, synStream.type);
262    for (int i = 0; i < 3; i++) {
263      List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
264      for (int j = 0; j < 2; j++) {
265        MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
266        assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
267        windowUpdateStreamIds.add(windowUpdate.streamId);
268        assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
269      }
270      assertTrue(windowUpdateStreamIds.contains(0)); // connection
271      assertTrue(windowUpdateStreamIds.contains(3)); // stream
272    }
273  }
274
275  private Buffer data(int byteCount) {
276    return new Buffer().write(new byte[byteCount]);
277  }
278
279  @Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
280    peer.setVariantAndClient(HTTP_2, false);
281
282    // Write the mocking script.
283    peer.acceptFrame(); // SYN_STREAM
284    peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
285    peer.sendFrame().data(true, 3, data(0), 0);
286    peer.play();
287
288    // Play it back.
289    FramedConnection connection = connection(peer, HTTP_2);
290    FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
291    assertEquals(-1, client.getSource().read(new Buffer(), 1));
292
293    // Verify the peer received what was expected.
294    MockSpdyPeer.InFrame synStream = peer.takeFrame();
295    assertEquals(TYPE_HEADERS, synStream.type);
296    assertEquals(3, peer.frameCount());
297  }
298
299  @Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
300    peer.setVariantAndClient(HTTP_2, false);
301
302    // Write the mocking script.
303    peer.acceptFrame(); // SYN_STREAM
304    peer.acceptFrame(); // DATA
305    peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
306    peer.play();
307
308    // Play it back.
309    FramedConnection connection = connection(peer, HTTP_2);
310    FramedStream client = connection.newStream(headerEntries("b", "banana"), true, true);
311    BufferedSink out = Okio.buffer(client.getSink());
312    out.write(Util.EMPTY_BYTE_ARRAY);
313    out.flush();
314    out.close();
315
316    // Verify the peer received what was expected.
317    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
318    assertEquals(TYPE_DATA, peer.takeFrame().type);
319    assertEquals(3, peer.frameCount());
320  }
321
322  @Test public void maxFrameSizeHonored() throws Exception {
323    peer.setVariantAndClient(HTTP_2, false);
324
325    byte[] buff = new byte[peer.maxOutboundDataLength() + 1];
326    Arrays.fill(buff, (byte) '*');
327
328    // write the mocking script
329    peer.acceptFrame(); // SYN_STREAM
330    peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
331    peer.acceptFrame(); // DATA
332    peer.acceptFrame(); // DATA
333    peer.play();
334
335    // play it back
336    FramedConnection connection = connection(peer, HTTP_2);
337    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
338    BufferedSink out = Okio.buffer(stream.getSink());
339    out.write(buff);
340    out.flush();
341    out.close();
342
343    MockSpdyPeer.InFrame synStream = peer.takeFrame();
344    assertEquals(TYPE_HEADERS, synStream.type);
345    MockSpdyPeer.InFrame data = peer.takeFrame();
346    assertEquals(peer.maxOutboundDataLength(), data.data.length);
347    data = peer.takeFrame();
348    assertEquals(1, data.data.length);
349  }
350
351  @Test public void pushPromiseStream() throws Exception {
352    peer.setVariantAndClient(HTTP_2, false);
353
354    // write the mocking script
355    peer.acceptFrame(); // SYN_STREAM
356    peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
357    final List<Header> expectedRequestHeaders = Arrays.asList(
358        new Header(Header.TARGET_METHOD, "GET"),
359        new Header(Header.TARGET_SCHEME, "https"),
360        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
361        new Header(Header.TARGET_PATH, "/cached")
362    );
363    peer.sendFrame().pushPromise(3, 2, expectedRequestHeaders);
364    final List<Header> expectedResponseHeaders = Arrays.asList(
365        new Header(Header.RESPONSE_STATUS, "200")
366    );
367    peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
368    peer.sendFrame().data(true, 3, data(0), 0);
369    peer.play();
370
371    RecordingPushObserver observer = new RecordingPushObserver();
372
373    // play it back
374    FramedConnection connection = connectionBuilder(peer, HTTP_2)
375        .pushObserver(observer).build();
376    FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
377    assertEquals(-1, client.getSource().read(new Buffer(), 1));
378
379    // verify the peer received what was expected
380    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
381
382    assertEquals(expectedRequestHeaders, observer.takeEvent());
383    assertEquals(expectedResponseHeaders, observer.takeEvent());
384  }
385
386  @Test public void doublePushPromise() throws Exception {
387    peer.setVariantAndClient(HTTP_2, false);
388
389    // write the mocking script
390    peer.sendFrame().pushPromise(3, 2, headerEntries("a", "android"));
391    peer.acceptFrame(); // SYN_REPLY
392    peer.sendFrame().pushPromise(3, 2, headerEntries("b", "banana"));
393    peer.acceptFrame(); // RST_STREAM
394    peer.play();
395
396    // play it back
397    FramedConnection connection = connectionBuilder(peer, HTTP_2).build();
398    connection.newStream(headerEntries("b", "banana"), false, true);
399
400    // verify the peer received what was expected
401    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
402    assertEquals(PROTOCOL_ERROR, peer.takeFrame().errorCode);
403  }
404
405  @Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
406    peer.setVariantAndClient(HTTP_2, false);
407
408    // write the mocking script
409    peer.sendFrame().pushPromise(3, 2, Arrays.asList(
410        new Header(Header.TARGET_METHOD, "GET"),
411        new Header(Header.TARGET_SCHEME, "https"),
412        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
413        new Header(Header.TARGET_PATH, "/cached")
414    ));
415    peer.sendFrame().synReply(true, 2, Arrays.asList(
416        new Header(Header.RESPONSE_STATUS, "200")
417    ));
418    peer.acceptFrame(); // RST_STREAM
419    peer.play();
420
421    // play it back
422    connectionBuilder(peer, HTTP_2)
423        .pushObserver(PushObserver.CANCEL).build();
424
425    // verify the peer received what was expected
426    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
427    assertEquals(TYPE_RST_STREAM, rstStream.type);
428    assertEquals(2, rstStream.streamId);
429    assertEquals(CANCEL, rstStream.errorCode);
430  }
431
432  /**
433   * When writing a set of headers fails due to an {@code IOException}, make sure the writer is left
434   * in a consistent state so the next writer also gets an {@code IOException} also instead of
435   * something worse (like an {@link IllegalStateException}.
436   *
437   * <p>See https://github.com/square/okhttp/issues/1651
438   */
439  @Test public void socketExceptionWhileWritingHeaders() throws Exception {
440    peer.setVariantAndClient(HTTP_2, false);
441    peer.acceptFrame(); // SYN_STREAM.
442    peer.play();
443
444    String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
445    Socket socket = peer.openSocket();
446    FramedConnection connection = new FramedConnection.Builder(true, socket)
447        .pushObserver(IGNORE)
448        .protocol(HTTP_2.getProtocol())
449        .build();
450    socket.shutdownOutput();
451    try {
452      connection.newStream(headerEntries("a", longString), false, true);
453      fail();
454    } catch (IOException expected) {
455    }
456    try {
457      connection.newStream(headerEntries("b", longString), false, true);
458      fail();
459    } catch (IOException expected) {
460    }
461  }
462
463  private FramedConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
464      throws IOException, InterruptedException {
465    peer.setVariantAndClient(HTTP_2, client);
466    peer.sendFrame().settings(settings);
467    peer.acceptFrame(); // ACK
468    peer.acceptFrame(); // PING
469    peer.sendFrame().ping(true, 1, 0);
470    peer.play();
471
472    // play it back
473    FramedConnection connection = connection(peer, HTTP_2);
474
475    // verify the peer received the ACK
476    MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
477    assertEquals(TYPE_SETTINGS, ackFrame.type);
478    assertEquals(0, ackFrame.streamId);
479    assertTrue(ackFrame.ack);
480
481    connection.ping().roundTripTime(); // Ensure that settings have been applied before returning.
482    return connection;
483  }
484
485  private FramedConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
486    return connectionBuilder(peer, variant).build();
487  }
488
489  private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
490      throws IOException {
491    return new FramedConnection.Builder(true, peer.openSocket())
492        .pushObserver(IGNORE)
493        .protocol(variant.getProtocol());
494  }
495
496  static final PushObserver IGNORE = new PushObserver() {
497
498    @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
499      return false;
500    }
501
502    @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
503      return false;
504    }
505
506    @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
507        boolean last) throws IOException {
508      source.skip(byteCount);
509      return false;
510    }
511
512    @Override public void onReset(int streamId, ErrorCode errorCode) {
513    }
514  };
515
516  private static class RecordingPushObserver implements PushObserver {
517    final List<Object> events = new ArrayList<>();
518
519    public synchronized Object takeEvent() throws InterruptedException {
520      while (events.isEmpty()) {
521        wait();
522      }
523      return events.remove(0);
524    }
525
526    @Override public synchronized boolean onRequest(int streamId, List<Header> requestHeaders) {
527      assertEquals(2, streamId);
528      events.add(requestHeaders);
529      notifyAll();
530      return false;
531    }
532
533    @Override public synchronized boolean onHeaders(
534        int streamId, List<Header> responseHeaders, boolean last) {
535      assertEquals(2, streamId);
536      assertTrue(last);
537      events.add(responseHeaders);
538      notifyAll();
539      return false;
540    }
541
542    @Override public synchronized boolean onData(
543        int streamId, BufferedSource source, int byteCount, boolean last) {
544      events.add(new AssertionError("onData"));
545      notifyAll();
546      return false;
547    }
548
549    @Override public synchronized void onReset(int streamId, ErrorCode errorCode) {
550      events.add(new AssertionError("onReset"));
551      notifyAll();
552    }
553  }
554}
555