1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-24 04:02:07 +03:00

Adjust hpack impl to be compliant with draft 3 examples, and enable http2 in MWS.

This commit is contained in:
Adrian Cole
2013-12-08 16:27:33 -08:00
parent a6d63eb816
commit 4db7288561
3 changed files with 240 additions and 115 deletions

View File

@@ -70,8 +70,7 @@ import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
*/
public final class MockWebServer {
private static final byte[] NPN_PROTOCOLS = {
// TODO: support HTTP/2.0.
// 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', '2', '.', '0',
17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', '2', '.', '0',
6, 's', 'p', 'd', 'y', '/', '3',
8, 'h', 't', 't', 'p', '/', '1', '.', '1'
};

View File

@@ -13,77 +13,97 @@ import java.util.List;
* http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03
*/
final class Hpack {
static class HeaderEntry {
private final String name;
private final String value;
HeaderEntry(String name, String value) {
this.name = name;
this.value = value;
}
// TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
int length() {
return 32 + name.length() + value.length();
}
}
static final int PREFIX_5_BITS = 0x1f;
static final int PREFIX_6_BITS = 0x3f;
static final int PREFIX_7_BITS = 0x7f;
static final int PREFIX_8_BITS = 0xff;
static final List<String> INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList(
":scheme", "http",
":scheme", "https",
":host", "",
":path", "/",
":method", "GET",
"accept", "",
"accept-charset", "",
"accept-encoding", "",
"accept-language", "",
"cookie", "",
"if-modified-since", "",
"user-agent", "",
"referer", "",
"authorization", "",
"allow", "",
"cache-control", "",
"connection", "",
"content-length", "",
"content-type", "",
"date", "",
"expect", "",
"from", "",
"if-match", "",
"if-none-match", "",
"if-range", "",
"if-unmodified-since", "",
"max-forwards", "",
"proxy-authorization", "",
"range", "",
"via", ""
static final List<HeaderEntry> INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList(
new HeaderEntry(":scheme", "http"),
new HeaderEntry(":scheme", "https"),
new HeaderEntry(":host", ""),
new HeaderEntry(":path", "/"),
new HeaderEntry(":method", "GET"),
new HeaderEntry("accept", ""),
new HeaderEntry("accept-charset", ""),
new HeaderEntry("accept-encoding", ""),
new HeaderEntry("accept-language", ""),
new HeaderEntry("cookie", ""),
new HeaderEntry("if-modified-since", ""),
new HeaderEntry("user-agent", ""),
new HeaderEntry("referer", ""),
new HeaderEntry("authorization", ""),
new HeaderEntry("allow", ""),
new HeaderEntry("cache-control", ""),
new HeaderEntry("connection", ""),
new HeaderEntry("content-length", ""),
new HeaderEntry("content-type", ""),
new HeaderEntry("date", ""),
new HeaderEntry("expect", ""),
new HeaderEntry("from", ""),
new HeaderEntry("if-match", ""),
new HeaderEntry("if-none-match", ""),
new HeaderEntry("if-range", ""),
new HeaderEntry("if-unmodified-since", ""),
new HeaderEntry("max-forwards", ""),
new HeaderEntry("proxy-authorization", ""),
new HeaderEntry("range", ""),
new HeaderEntry("via", "")
);
static final List<String> INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList(
":status", "200",
"age", "",
"cache-control", "",
"content-length", "",
"content-type", "",
"date", "",
"etag", "",
"expires", "",
"last-modified", "",
"server", "",
"set-cookie", "",
"vary", "",
"via", "",
"access-control-allow-origin", "",
"accept-ranges", "",
"allow", "",
"connection", "",
"content-disposition", "",
"content-encoding", "",
"content-language", "",
"content-location", "",
"content-range", "",
"link", "",
"location", "",
"proxy-authenticate", "",
"refresh", "",
"retry-after", "",
"strict-transport-security", "",
"transfer-encoding", "",
"www-authenticate", ""
static final List<HeaderEntry> INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList(
new HeaderEntry(":status", "200"),
new HeaderEntry("age", ""),
new HeaderEntry("cache-control", ""),
new HeaderEntry("content-length", ""),
new HeaderEntry("content-type", ""),
new HeaderEntry("date", ""),
new HeaderEntry("etag", ""),
new HeaderEntry("expires", ""),
new HeaderEntry("last-modified", ""),
new HeaderEntry("server", ""),
new HeaderEntry("set-cookie", ""),
new HeaderEntry("vary", ""),
new HeaderEntry("via", ""),
new HeaderEntry("access-control-allow-origin", ""),
new HeaderEntry("accept-ranges", ""),
new HeaderEntry("allow", ""),
new HeaderEntry("connection", ""),
new HeaderEntry("content-disposition", ""),
new HeaderEntry("content-encoding", ""),
new HeaderEntry("content-language", ""),
new HeaderEntry("content-location", ""),
new HeaderEntry("content-range", ""),
new HeaderEntry("link", ""),
new HeaderEntry("location", ""),
new HeaderEntry("proxy-authenticate", ""),
new HeaderEntry("refresh", ""),
new HeaderEntry("retry-after", ""),
new HeaderEntry("strict-transport-security", ""),
new HeaderEntry("transfer-encoding", ""),
new HeaderEntry("www-authenticate", "")
);
// Update these when initial tables change to sum of each entry length.
static final int INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH = 1262;
static final int INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH = 1304;
private Hpack() {
}
@@ -92,16 +112,20 @@ final class Hpack {
private final DataInputStream in;
private final BitSet referenceSet = new BitSet();
private final List<String> headerTable;
private final List<HeaderEntry> headerTable;
private final List<String> emittedHeaders = new ArrayList<String>();
private long bufferSize = 4096;
private long bufferSize = 0;
private long bytesLeft = 0;
Reader(DataInputStream in, boolean client) {
this.in = in;
this.headerTable = new ArrayList<String>(client
? INITIAL_CLIENT_TO_SERVER_HEADER_TABLE
: INITIAL_SERVER_TO_CLIENT_HEADER_TABLE);
if (client) { // we are reading from the server
this.headerTable = new ArrayList<HeaderEntry>(INITIAL_SERVER_TO_CLIENT_HEADER_TABLE);
this.bufferSize = INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH;
} else {
this.headerTable = new ArrayList<HeaderEntry>(INITIAL_CLIENT_TO_SERVER_HEADER_TABLE);
this.bufferSize = INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH;
}
}
/**
@@ -161,8 +185,6 @@ final class Hpack {
referenceSet.clear(index);
} else {
referenceSet.set(index);
emittedHeaders.add(getName(index));
emittedHeaders.add(getValue(index));
}
}
@@ -184,23 +206,17 @@ final class Hpack {
private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
throws IOException {
int index = headerTable.size();
String name = getName(nameIndex);
String value = readString();
appendToHeaderTable(name, value);
emittedHeaders.add(name);
emittedHeaders.add(value);
referenceSet.set(index);
int index = headerTable.size(); // append to tail
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
int index = headerTable.size();
String name = readString();
String value = readString();
appendToHeaderTable(name, value);
emittedHeaders.add(name);
emittedHeaders.add(value);
referenceSet.set(index);
int index = headerTable.size(); // append to tail
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex)
@@ -208,43 +224,39 @@ final class Hpack {
int index = readInt(readByte(), PREFIX_8_BITS);
String name = getName(nameIndex);
String value = readString();
replaceInHeaderTable(index, name, value);
emittedHeaders.add(name);
emittedHeaders.add(value);
referenceSet.set(index);
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithSubstitutionIndexingNewName() throws IOException {
String name = readString();
int index = readInt(readByte(), PREFIX_8_BITS);
String value = readString();
replaceInHeaderTable(index, name, value);
emittedHeaders.add(name);
emittedHeaders.add(value);
referenceSet.set(index);
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private String getName(int index) {
return headerTable.get(index * 2);
return headerTable.get(index).name;
}
private String getValue(int index) {
return headerTable.get(index * 2 + 1);
return headerTable.get(index).value;
}
private void appendToHeaderTable(String name, String value) {
insertIntoHeaderTable(headerTable.size() * 2, name, value);
}
private void insertIntoHeaderTable(int index, HeaderEntry entry) {
int delta = entry.length();
if (index != headerTable.size()) {
delta -= headerTable.get(index).length();
}
private void replaceInHeaderTable(int index, String name, String value) {
remove(index);
insertIntoHeaderTable(index, name, value);
}
private void insertIntoHeaderTable(int index, String name, String value) {
// TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
int delta = 32 + name.length() + value.length();
// if the new or replacement header is too big, drop all entries.
if (delta > maxBufferSize) {
headerTable.clear();
bufferSize = 0;
// emit the large header to the callback.
emittedHeaders.add(entry.name);
emittedHeaders.add(entry.value);
return;
}
// Prune headers to the required length.
while (bufferSize + delta > maxBufferSize) {
@@ -252,22 +264,21 @@ final class Hpack {
index--;
}
if (delta > maxBufferSize) {
return; // New values won't fit in the buffer; skip 'em.
if (index < 0) { // we pruned it, so insert at beginning
index = 0;
headerTable.add(index, entry);
} else if (index == headerTable.size()) { // append to the end
headerTable.add(index, entry);
} else { // replace value at same position
headerTable.set(index, entry);
}
if (index == 0) index = 0;
headerTable.add(index * 2, name);
headerTable.add(index * 2 + 1, value);
bufferSize += delta;
referenceSet.set(index);
}
private void remove(int index) {
String name = headerTable.remove(index * 2);
String value = headerTable.remove(index * 2); // No +1 because it's shifted by remove() above.
// TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
bufferSize -= (32 + name.length() + value.length());
bufferSize -= headerTable.remove(index).length();
}
private int readByte() throws IOException {

View File

@@ -28,9 +28,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class Http20Draft06Test {
static final int expectedStreamId = 15;
@Test public void onlyOneLiteralHeadersFrame() throws IOException {
final int expectedStreamId = 15;
final List<String> sentHeaders = Arrays.asList("name", "value");
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -67,7 +67,6 @@ public class Http20Draft06Test {
}
@Test public void headersFrameThenContinuation() throws IOException {
final int expectedStreamId = 15;
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
@@ -112,6 +111,122 @@ public class Http20Draft06Test {
});
}
/**
* HPACK has a max header table size, which can be smaller than the max header message.
* Ensure the larger header content is not lost.
*/
@Test public void tooLargeToHPackIsStillEmitted() throws IOException {
char[] tooLarge = new char[4096];
Arrays.fill(tooLarge, 'a');
final List<String> sentHeaders = Arrays.asList("foo", new String(tooLarge));
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
writeOnlyHeadersFrame(literalHeaders(sentHeaders), dataOut);
FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false);
// Consume the large header set.
fr.nextFrame(new BaseTestHandler() {
@Override
public void headers(boolean outFinished, boolean inFinished, int streamId,
int associatedStreamId, int priority, List<String> nameValueBlock,
HeadersMode headersMode) {
assertEquals(sentHeaders, nameValueBlock);
}
});
}
@Test public void usingDraft06Examples() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
writeOnlyHeadersFrame(firstHeaderSetBytes(), dataOut);
writeOnlyHeadersFrame(secondHeaderSetBytes(), dataOut);
FrameReader fr = new Http20Draft06.Reader(new ByteArrayInputStream(out.toByteArray()), false);
// Consume the first header set.
fr.nextFrame(new BaseTestHandler() {
@Override
public void headers(boolean outFinished, boolean inFinished, int streamId,
int associatedStreamId, int priority, List<String> nameValueBlock,
HeadersMode headersMode) {
assertEquals(Arrays.asList(":path", "/my-example/index.html", "user-agent", "my-user-agent",
"mynewheader", "first"), nameValueBlock);
}
});
// Consume the second header set.
fr.nextFrame(new BaseTestHandler() {
@Override
public void headers(boolean outFinished, boolean inFinished, int streamId,
int associatedStreamId, int priority, List<String> nameValueBlock,
HeadersMode headersMode) {
assertEquals(Arrays.asList(
":path", "/my-example/resources/script.js",
"user-agent", "my-user-agent",
"mynewheader", "second"
), nameValueBlock);
}
});
}
// Deviates from draft only to fix doc bugs noted in https://github.com/igrigorik/http-2 specs.
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.1
static byte[] firstHeaderSetBytes() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(0x44); // literal header with incremental indexing, name index = 3
out.write(0x16); // header value string length = 22
out.write("/my-example/index.html".getBytes(), 0, 22);
out.write(0x4C); // literal header with incremental indexing, name index = 11
out.write(0x0D); // header value string length = 13
out.write("my-user-agent".getBytes(), 0, 13);
out.write(0x40); // literal header with incremental indexing, new name
out.write(0x0B); // header name string length = 11
out.write("mynewheader".getBytes(), 0, 11);
out.write(0x05); // header value string length = 5
out.write("first".getBytes(), 0, 5);
return out.toByteArray();
}
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-C.2
static byte[] secondHeaderSetBytes() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(0x9e); // indexed header, index = 30: removal from reference set
out.write(0xa0); // indexed header, index = 32: removal from reference set
out.write(0x04); // literal header, substitution indexing, name index = 3
out.write(0x1e); // replaced entry index = 30
out.write(0x1f); // header value string length = 31
out.write("/my-example/resources/script.js".getBytes(), 0, 31);
out.write(0x5f);
out.write(0x02); // literal header, incremental indexing, name index = 32
out.write(0x06); // header value string length = 6
out.write("second".getBytes(), 0, 6);
return out.toByteArray();
}
static void writeOnlyHeadersFrame(byte[] headersSet, DataOutputStream dataOut)
throws IOException {
dataOut.writeShort(headersSet.length);
dataOut.write(Http20Draft06.TYPE_HEADERS);
dataOut.write(Http20Draft06.FLAG_END_HEADERS | Http20Draft06.FLAG_END_STREAM);
dataOut.writeInt(expectedStreamId & 0x7fffffff); // stream 15 with reserved bit set
dataOut.write(headersSet);
}
static byte[] literalHeaders(List<String> sentHeaders) throws IOException {
ByteArrayOutputStream headerBytes = new ByteArrayOutputStream();
new Hpack.Writer(new DataOutputStream(headerBytes)).writeHeaders(sentHeaders);