1/* 2 * Copyright (C) 2013 Square, Inc. 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.spdy; 17 18import com.squareup.okhttp.internal.Util; 19import java.io.IOException; 20import java.util.Arrays; 21import java.util.List; 22import okio.BufferedSource; 23import okio.ByteString; 24import okio.OkBuffer; 25import org.junit.Test; 26 27import static com.squareup.okhttp.internal.Util.headerEntries; 28import static org.junit.Assert.assertEquals; 29import static org.junit.Assert.assertFalse; 30import static org.junit.Assert.assertTrue; 31import static org.junit.Assert.fail; 32 33public class Http20Draft09Test { 34 static final int expectedStreamId = 15; 35 36 @Test public void unknownFrameTypeIgnored() throws IOException { 37 OkBuffer frame = new OkBuffer(); 38 39 frame.writeShort(4); // has a 4-byte field 40 frame.writeByte(99); // type 99 41 frame.writeByte(0); // no flags 42 frame.writeInt(expectedStreamId); 43 frame.writeInt(111111111); // custom data 44 45 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 46 47 // Consume the unknown frame. 48 fr.nextFrame(new BaseTestHandler()); 49 } 50 51 @Test public void onlyOneLiteralHeadersFrame() throws IOException { 52 final List<Header> sentHeaders = headerEntries("name", "value"); 53 54 OkBuffer frame = new OkBuffer(); 55 56 // Write the headers frame, specifying no more frames are expected. 57 { 58 OkBuffer headerBytes = literalHeaders(sentHeaders); 59 frame.writeShort((int) headerBytes.size()); 60 frame.writeByte(Http20Draft09.TYPE_HEADERS); 61 frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM); 62 frame.writeInt(expectedStreamId & 0x7fffffff); 63 frame.write(headerBytes, headerBytes.size()); 64 } 65 66 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 67 68 // Consume the headers frame. 69 fr.nextFrame(new BaseTestHandler() { 70 71 @Override 72 public void headers(boolean outFinished, boolean inFinished, int streamId, 73 int associatedStreamId, int priority, List<Header> headerBlock, 74 HeadersMode headersMode) { 75 assertFalse(outFinished); 76 assertTrue(inFinished); 77 assertEquals(expectedStreamId, streamId); 78 assertEquals(-1, associatedStreamId); 79 assertEquals(-1, priority); 80 assertEquals(sentHeaders, headerBlock); 81 assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); 82 } 83 }); 84 } 85 86 @Test public void headersWithPriority() throws IOException { 87 OkBuffer frame = new OkBuffer(); 88 89 final List<Header> sentHeaders = headerEntries("name", "value"); 90 91 { // Write the headers frame, specifying priority flag and value. 92 OkBuffer headerBytes = literalHeaders(sentHeaders); 93 frame.writeShort((int) (headerBytes.size() + 4)); 94 frame.writeByte(Http20Draft09.TYPE_HEADERS); 95 frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_PRIORITY); 96 frame.writeInt(expectedStreamId & 0x7fffffff); 97 frame.writeInt(0); // Highest priority is 0. 98 frame.write(headerBytes, headerBytes.size()); 99 } 100 101 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 102 103 // Consume the headers frame. 104 fr.nextFrame(new BaseTestHandler() { 105 106 @Override 107 public void headers(boolean outFinished, boolean inFinished, int streamId, 108 int associatedStreamId, int priority, List<Header> nameValueBlock, 109 HeadersMode headersMode) { 110 assertFalse(outFinished); 111 assertFalse(inFinished); 112 assertEquals(expectedStreamId, streamId); 113 assertEquals(-1, associatedStreamId); 114 assertEquals(0, priority); 115 assertEquals(sentHeaders, nameValueBlock); 116 assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); 117 } 118 }); 119 } 120 121 /** Headers are compressed, then framed. */ 122 @Test public void headersFrameThenContinuation() throws IOException { 123 124 OkBuffer frame = new OkBuffer(); 125 126 // Decoding the first header will cross frame boundaries. 127 OkBuffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")); 128 { // Write the first headers frame. 129 frame.writeShort((int) (headerBlock.size() / 2)); 130 frame.writeByte(Http20Draft09.TYPE_HEADERS); 131 frame.writeByte(0); // no flags 132 frame.writeInt(expectedStreamId & 0x7fffffff); 133 frame.write(headerBlock, headerBlock.size() / 2); 134 } 135 136 { // Write the continuation frame, specifying no more frames are expected. 137 frame.writeShort((int) headerBlock.size()); 138 frame.writeByte(Http20Draft09.TYPE_CONTINUATION); 139 frame.writeByte(Http20Draft09.FLAG_END_HEADERS); 140 frame.writeInt(expectedStreamId & 0x7fffffff); 141 frame.write(headerBlock, headerBlock.size()); 142 } 143 144 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 145 146 // Reading the above frames should result in a concatenated headerBlock. 147 fr.nextFrame(new BaseTestHandler() { 148 149 @Override 150 public void headers(boolean outFinished, boolean inFinished, int streamId, 151 int associatedStreamId, int priority, List<Header> headerBlock, 152 HeadersMode headersMode) { 153 assertFalse(outFinished); 154 assertFalse(inFinished); 155 assertEquals(expectedStreamId, streamId); 156 assertEquals(-1, associatedStreamId); 157 assertEquals(-1, priority); 158 assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock); 159 assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode); 160 } 161 }); 162 } 163 164 @Test public void pushPromise() throws IOException { 165 OkBuffer frame = new OkBuffer(); 166 167 final int expectedPromisedStreamId = 11; 168 169 final List<Header> pushPromise = Arrays.asList( 170 new Header(Header.TARGET_METHOD, "GET"), 171 new Header(Header.TARGET_SCHEME, "https"), 172 new Header(Header.TARGET_AUTHORITY, "squareup.com"), 173 new Header(Header.TARGET_PATH, "/") 174 ); 175 176 { // Write the push promise frame, specifying the associated stream ID. 177 OkBuffer headerBytes = literalHeaders(pushPromise); 178 frame.writeShort((int) (headerBytes.size() + 4)); 179 frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE); 180 frame.writeByte(Http20Draft09.FLAG_END_PUSH_PROMISE); 181 frame.writeInt(expectedStreamId & 0x7fffffff); 182 frame.writeInt(expectedPromisedStreamId & 0x7fffffff); 183 frame.write(headerBytes, headerBytes.size()); 184 } 185 186 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 187 188 // Consume the headers frame. 189 fr.nextFrame(new BaseTestHandler() { 190 @Override 191 public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) { 192 assertEquals(expectedStreamId, streamId); 193 assertEquals(expectedPromisedStreamId, promisedStreamId); 194 assertEquals(pushPromise, headerBlock); 195 } 196 }); 197 } 198 199 /** Headers are compressed, then framed. */ 200 @Test public void pushPromiseThenContinuation() throws IOException { 201 OkBuffer frame = new OkBuffer(); 202 203 final int expectedPromisedStreamId = 11; 204 205 final List<Header> pushPromise = Arrays.asList( 206 new Header(Header.TARGET_METHOD, "GET"), 207 new Header(Header.TARGET_SCHEME, "https"), 208 new Header(Header.TARGET_AUTHORITY, "squareup.com"), 209 new Header(Header.TARGET_PATH, "/") 210 ); 211 212 // Decoding the first header will cross frame boundaries. 213 OkBuffer headerBlock = literalHeaders(pushPromise); 214 int firstFrameLength = (int) (headerBlock.size() - 1); 215 { // Write the first headers frame. 216 frame.writeShort(firstFrameLength + 4); 217 frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE); 218 frame.writeByte(0); // no flags 219 frame.writeInt(expectedStreamId & 0x7fffffff); 220 frame.writeInt(expectedPromisedStreamId & 0x7fffffff); 221 frame.write(headerBlock, firstFrameLength); 222 } 223 224 { // Write the continuation frame, specifying no more frames are expected. 225 frame.writeShort(1); 226 frame.writeByte(Http20Draft09.TYPE_CONTINUATION); 227 frame.writeByte(Http20Draft09.FLAG_END_HEADERS); 228 frame.writeInt(expectedStreamId & 0x7fffffff); 229 frame.write(headerBlock, 1); 230 } 231 232 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 233 234 // Reading the above frames should result in a concatenated headerBlock. 235 fr.nextFrame(new BaseTestHandler() { 236 @Override 237 public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) { 238 assertEquals(expectedStreamId, streamId); 239 assertEquals(expectedPromisedStreamId, promisedStreamId); 240 assertEquals(pushPromise, headerBlock); 241 } 242 }); 243 } 244 245 @Test public void readRstStreamFrame() throws IOException { 246 OkBuffer frame = new OkBuffer(); 247 248 frame.writeShort(4); 249 frame.writeByte(Http20Draft09.TYPE_RST_STREAM); 250 frame.writeByte(0); // No flags 251 frame.writeInt(expectedStreamId & 0x7fffffff); 252 frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode); 253 254 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 255 256 // Consume the reset frame. 257 fr.nextFrame(new BaseTestHandler() { 258 @Override public void rstStream(int streamId, ErrorCode errorCode) { 259 assertEquals(expectedStreamId, streamId); 260 assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode); 261 } 262 }); 263 } 264 265 @Test public void readSettingsFrame() throws IOException { 266 OkBuffer frame = new OkBuffer(); 267 268 final int reducedTableSizeBytes = 16; 269 270 frame.writeShort(16); // 2 settings * 4 bytes for the code and 4 for the value. 271 frame.writeByte(Http20Draft09.TYPE_SETTINGS); 272 frame.writeByte(0); // No flags 273 frame.writeInt(0 & 0x7fffffff); // Settings are always on the connection stream 0. 274 frame.writeInt(Settings.HEADER_TABLE_SIZE & 0xffffff); 275 frame.writeInt(reducedTableSizeBytes); 276 frame.writeInt(Settings.ENABLE_PUSH & 0xffffff); 277 frame.writeInt(0); 278 279 final Http20Draft09.Reader fr = new Http20Draft09.Reader(frame, 4096, false); 280 281 // Consume the settings frame. 282 fr.nextFrame(new BaseTestHandler() { 283 @Override public void settings(boolean clearPrevious, Settings settings) { 284 assertFalse(clearPrevious); // No clearPrevious in http/2. 285 assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize()); 286 assertEquals(false, settings.getEnablePush(true)); 287 } 288 }); 289 } 290 291 @Test public void pingRoundTrip() throws IOException { 292 OkBuffer frame = new OkBuffer(); 293 294 final int expectedPayload1 = 7; 295 final int expectedPayload2 = 8; 296 297 // Compose the expected PING frame. 298 frame.writeShort(8); // length 299 frame.writeByte(Http20Draft09.TYPE_PING); 300 frame.writeByte(Http20Draft09.FLAG_ACK); 301 frame.writeInt(0); // connection-level 302 frame.writeInt(expectedPayload1); 303 frame.writeInt(expectedPayload2); 304 305 // Check writer sends the same bytes. 306 assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2)); 307 308 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 309 310 fr.nextFrame(new BaseTestHandler() { // Consume the ping frame. 311 @Override public void ping(boolean ack, int payload1, int payload2) { 312 assertTrue(ack); 313 assertEquals(expectedPayload1, payload1); 314 assertEquals(expectedPayload2, payload2); 315 } 316 }); 317 } 318 319 @Test public void maxLengthDataFrame() throws IOException { 320 OkBuffer frame = new OkBuffer(); 321 322 final byte[] expectedData = new byte[16383]; 323 Arrays.fill(expectedData, (byte) 2); 324 325 // Write the data frame. 326 frame.writeShort(expectedData.length); 327 frame.writeByte(Http20Draft09.TYPE_DATA); 328 frame.writeByte(0); // no flags 329 frame.writeInt(expectedStreamId & 0x7fffffff); 330 frame.write(expectedData); 331 332 // Check writer sends the same bytes. 333 assertEquals(frame, sendDataFrame(new OkBuffer().write(expectedData))); 334 335 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 336 337 fr.nextFrame(new BaseTestHandler() { 338 @Override public void data( 339 boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { 340 assertFalse(inFinished); 341 assertEquals(expectedStreamId, streamId); 342 assertEquals(16383, length); 343 ByteString data = source.readByteString(length); 344 for (byte b : data.toByteArray()){ 345 assertEquals(2, b); 346 } 347 } 348 }); 349 } 350 351 @Test public void tooLargeDataFrame() throws IOException { 352 try { 353 sendDataFrame(new OkBuffer().write(new byte[0x1000000])); 354 fail(); 355 } catch (IllegalArgumentException e) { 356 assertEquals("FRAME_SIZE_ERROR length > 16383: 16777216", e.getMessage()); 357 } 358 } 359 360 @Test public void windowUpdateRoundTrip() throws IOException { 361 OkBuffer frame = new OkBuffer(); 362 363 final long expectedWindowSizeIncrement = 0x7fffffff; 364 365 // Compose the expected window update frame. 366 frame.writeShort(4); // length 367 frame.writeByte(Http20Draft09.TYPE_WINDOW_UPDATE); 368 frame.writeByte(0); // No flags. 369 frame.writeInt(expectedStreamId); 370 frame.writeInt((int) expectedWindowSizeIncrement); 371 372 // Check writer sends the same bytes. 373 assertEquals(frame, windowUpdate(expectedWindowSizeIncrement)); 374 375 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 376 377 fr.nextFrame(new BaseTestHandler() { // Consume the window update frame. 378 @Override public void windowUpdate(int streamId, long windowSizeIncrement) { 379 assertEquals(expectedStreamId, streamId); 380 assertEquals(expectedWindowSizeIncrement, windowSizeIncrement); 381 } 382 }); 383 } 384 385 @Test public void badWindowSizeIncrement() throws IOException { 386 try { 387 windowUpdate(0); 388 fail(); 389 } catch (IllegalArgumentException e) { 390 assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0", 391 e.getMessage()); 392 } 393 try { 394 windowUpdate(0x80000000L); 395 fail(); 396 } catch (IllegalArgumentException e) { 397 assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648", 398 e.getMessage()); 399 } 400 } 401 402 @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException { 403 OkBuffer frame = new OkBuffer(); 404 405 final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; 406 407 // Compose the expected GOAWAY frame without debug data. 408 frame.writeShort(8); // Without debug data there's only 2 32-bit fields. 409 frame.writeByte(Http20Draft09.TYPE_GOAWAY); 410 frame.writeByte(0); // no flags. 411 frame.writeInt(0); // connection-scope 412 frame.writeInt(expectedStreamId); // last good stream. 413 frame.writeInt(expectedError.httpCode); 414 415 // Check writer sends the same bytes. 416 assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY)); 417 418 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 419 420 fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. 421 @Override public void goAway( 422 int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { 423 assertEquals(expectedStreamId, lastGoodStreamId); 424 assertEquals(expectedError, errorCode); 425 assertEquals(0, debugData.size()); 426 } 427 }); 428 } 429 430 @Test public void goAwayWithDebugDataRoundTrip() throws IOException { 431 OkBuffer frame = new OkBuffer(); 432 433 final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR; 434 final ByteString expectedData = ByteString.encodeUtf8("abcdefgh"); 435 436 // Compose the expected GOAWAY frame without debug data. 437 frame.writeShort(8 + expectedData.size()); 438 frame.writeByte(Http20Draft09.TYPE_GOAWAY); 439 frame.writeByte(0); // no flags. 440 frame.writeInt(0); // connection-scope 441 frame.writeInt(0); // never read any stream! 442 frame.writeInt(expectedError.httpCode); 443 frame.write(expectedData.toByteArray()); 444 445 // Check writer sends the same bytes. 446 assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray())); 447 448 FrameReader fr = new Http20Draft09.Reader(frame, 4096, false); 449 450 fr.nextFrame(new BaseTestHandler() { // Consume the go away frame. 451 @Override public void goAway( 452 int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { 453 assertEquals(0, lastGoodStreamId); 454 assertEquals(expectedError, errorCode); 455 assertEquals(expectedData, debugData); 456 } 457 }); 458 } 459 460 @Test public void frameSizeError() throws IOException { 461 Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true); 462 463 try { 464 writer.frameHeader(16384, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, 0); 465 fail(); 466 } catch (IllegalArgumentException e) { 467 assertEquals("FRAME_SIZE_ERROR length > 16383: 16384", e.getMessage()); 468 } 469 } 470 471 @Test public void streamIdHasReservedBit() throws IOException { 472 Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true); 473 474 try { 475 int streamId = 3; 476 streamId |= 1L << 31; // set reserved bit 477 writer.frameHeader(16383, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, streamId); 478 fail(); 479 } catch (IllegalArgumentException e) { 480 assertEquals("reserved bit set: -2147483645", e.getMessage()); 481 } 482 } 483 484 private OkBuffer literalHeaders(List<Header> sentHeaders) throws IOException { 485 OkBuffer out = new OkBuffer(); 486 new HpackDraft05.Writer(out).writeHeaders(sentHeaders); 487 return out; 488 } 489 490 private OkBuffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException { 491 OkBuffer out = new OkBuffer(); 492 new Http20Draft09.Writer(out, true).ping(ack, payload1, payload2); 493 return out; 494 } 495 496 private OkBuffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) 497 throws IOException { 498 OkBuffer out = new OkBuffer(); 499 new Http20Draft09.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData); 500 return out; 501 } 502 503 private OkBuffer sendDataFrame(OkBuffer data) throws IOException { 504 OkBuffer out = new OkBuffer(); 505 new Http20Draft09.Writer(out, true).dataFrame(expectedStreamId, Http20Draft09.FLAG_NONE, data, 506 (int) data.size()); 507 return out; 508 } 509 510 private OkBuffer windowUpdate(long windowSizeIncrement) throws IOException { 511 OkBuffer out = new OkBuffer(); 512 new Http20Draft09.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement); 513 return out; 514 } 515} 516