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 */ 16 package com.squareup.okhttp.internal.framed; 17 18 import com.squareup.okhttp.internal.Util; 19 import java.io.IOException; 20 import java.net.Socket; 21 import java.util.ArrayList; 22 import java.util.Arrays; 23 import java.util.List; 24 import java.util.concurrent.TimeUnit; 25 import okio.Buffer; 26 import okio.BufferedSink; 27 import okio.BufferedSource; 28 import okio.Okio; 29 import okio.Source; 30 import org.junit.After; 31 import org.junit.Test; 32 33 import static com.squareup.okhttp.TestUtil.headerEntries; 34 import static com.squareup.okhttp.TestUtil.repeat; 35 import static com.squareup.okhttp.internal.framed.ErrorCode.CANCEL; 36 import static com.squareup.okhttp.internal.framed.ErrorCode.PROTOCOL_ERROR; 37 import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE; 38 import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE; 39 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_DATA; 40 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_HEADERS; 41 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_PING; 42 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_RST_STREAM; 43 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_SETTINGS; 44 import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_WINDOW_UPDATE; 45 import static org.junit.Assert.assertEquals; 46 import static org.junit.Assert.assertFalse; 47 import static org.junit.Assert.assertTrue; 48 import static org.junit.Assert.fail; 49 50 public 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) 447 .socket(socket) 448 .pushObserver(IGNORE) 449 .protocol(HTTP_2.getProtocol()) 450 .build(); 451 socket.shutdownOutput(); 452 try { 453 connection.newStream(headerEntries("a", longString), false, true); 454 fail(); 455 } catch (IOException expected) { 456 } 457 try { 458 connection.newStream(headerEntries("b", longString), false, true); 459 fail(); 460 } catch (IOException expected) { 461 } 462 } 463 464 private FramedConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings) 465 throws IOException, InterruptedException { 466 peer.setVariantAndClient(HTTP_2, client); 467 peer.sendFrame().settings(settings); 468 peer.acceptFrame(); // ACK 469 peer.acceptFrame(); // PING 470 peer.sendFrame().ping(true, 1, 0); 471 peer.play(); 472 473 // play it back 474 FramedConnection connection = connection(peer, HTTP_2); 475 476 // verify the peer received the ACK 477 MockSpdyPeer.InFrame ackFrame = peer.takeFrame(); 478 assertEquals(TYPE_SETTINGS, ackFrame.type); 479 assertEquals(0, ackFrame.streamId); 480 assertTrue(ackFrame.ack); 481 482 connection.ping().roundTripTime(); // Ensure that settings have been applied before returning. 483 return connection; 484 } 485 486 private FramedConnection connection(MockSpdyPeer peer, Variant variant) throws IOException { 487 return connectionBuilder(peer, variant).build(); 488 } 489 490 private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant) 491 throws IOException { 492 return new FramedConnection.Builder(true) 493 .socket(peer.openSocket()) 494 .pushObserver(IGNORE) 495 .protocol(variant.getProtocol()); 496 } 497 498 static final PushObserver IGNORE = new PushObserver() { 499 500 @Override public boolean onRequest(int streamId, List<Header> requestHeaders) { 501 return false; 502 } 503 504 @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) { 505 return false; 506 } 507 508 @Override public boolean onData(int streamId, BufferedSource source, int byteCount, 509 boolean last) throws IOException { 510 source.skip(byteCount); 511 return false; 512 } 513 514 @Override public void onReset(int streamId, ErrorCode errorCode) { 515 } 516 }; 517 518 private static class RecordingPushObserver implements PushObserver { 519 final List<Object> events = new ArrayList<>(); 520 521 public synchronized Object takeEvent() throws InterruptedException { 522 while (events.isEmpty()) { 523 wait(); 524 } 525 return events.remove(0); 526 } 527 528 @Override public synchronized boolean onRequest(int streamId, List<Header> requestHeaders) { 529 assertEquals(2, streamId); 530 events.add(requestHeaders); 531 notifyAll(); 532 return false; 533 } 534 535 @Override public synchronized boolean onHeaders( 536 int streamId, List<Header> responseHeaders, boolean last) { 537 assertEquals(2, streamId); 538 assertTrue(last); 539 events.add(responseHeaders); 540 notifyAll(); 541 return false; 542 } 543 544 @Override public synchronized boolean onData( 545 int streamId, BufferedSource source, int byteCount, boolean last) { 546 events.add(new AssertionError("onData")); 547 notifyAll(); 548 return false; 549 } 550 551 @Override public synchronized void onReset(int streamId, ErrorCode errorCode) { 552 events.add(new AssertionError("onReset")); 553 notifyAll(); 554 } 555 } 556 } 557