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