package com.squareup.okhttp.internal.spdy; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.List; /** * Read and write HPACK v01. * http://http2.github.io/compression-spec/compression-spec.html#rfc.status */ final class Hpack { 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<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", ""); private Hpack() { } static class Reader { private final long maxBufferSize = 4096; // TODO: needs to come from settings. private final DataInputStream in; private final BitSet referenceSet = new BitSet(); private final List<String> headerTable; private final List<String> emittedHeaders = new ArrayList<String>(); private long bufferSize = 4096; 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); } /** * Read {@code byteCount} bytes of headers from the source stream into the * set of emitted headers. */ public void readHeaders(int byteCount) throws IOException { bytesLeft += byteCount; // TODO: limit to 'byteCount' bytes? while (bytesLeft > 0) { int b = readByte(); if ((b & 0x80) != 0) { int index = readInt(b, PREFIX_7_BITS); readIndexedHeader(index); } else if (b == 0x60) { readLiteralHeaderWithoutIndexingNewName(); } else if ((b & 0xe0) == 0x60) { int index = readInt(b, PREFIX_5_BITS); readLiteralHeaderWithoutIndexingIndexedName(index - 1); } else if (b == 0x40) { readLiteralHeaderWithIncrementalIndexingNewName(); } else if ((b & 0xe0) == 0x40) { int index = readInt(b, PREFIX_5_BITS); readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); } else if (b == 0) { readLiteralHeaderWithSubstitutionIndexingNewName(); } else if ((b & 0xc0) == 0) { int index = readInt(b, PREFIX_6_BITS); readLiteralHeaderWithSubstitutionIndexingIndexedName(index - 1); } else { throw new AssertionError(); } } } public void emitReferenceSet() { for (int i = referenceSet.nextSetBit(0); i != -1; i = referenceSet.nextSetBit(i + 1)) { emittedHeaders.add(getName(i)); emittedHeaders.add(getValue(i)); } } /** * Returns all headers emitted since they were last cleared, then clears the * emitted headers. */ public List<String> getAndReset() { List<String> result = new ArrayList<String>(emittedHeaders); emittedHeaders.clear(); return result; } private void readIndexedHeader(int index) { if (referenceSet.get(index)) { referenceSet.clear(index); } else { referenceSet.set(index); emittedHeaders.add(getName(index)); emittedHeaders.add(getValue(index)); } } private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { String name = getName(index); String value = readString(); emittedHeaders.add(name); emittedHeaders.add(value); } private void readLiteralHeaderWithoutIndexingNewName() throws IOException { String name = readString(); String value = readString(); emittedHeaders.add(name); emittedHeaders.add(value); } 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); } 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); } private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex) throws IOException { 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); } 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); } private String getName(int index) { return headerTable.get(index * 2); } private String getValue(int index) { return headerTable.get(index * 2 + 1); } private void appendToHeaderTable(String name, String value) { insertIntoHeaderTable(headerTable.size() * 2, name, value); } 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(); // Prune headers to the required length. while (bufferSize + delta > maxBufferSize) { remove(0); index--; } if (delta > maxBufferSize) { return; // New values won't fit in the buffer; skip 'em. } if (index == 0) index = 0; headerTable.add(index * 2, name); headerTable.add(index * 2 + 1, value); bufferSize += delta; } 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()); } private int readByte() throws IOException { bytesLeft--; return in.readByte() & 0xff; } int readInt(int firstByte, int prefixMask) throws IOException { int prefix = firstByte & prefixMask; if (prefix < prefixMask) { return prefix; // This was a single byte value. } // This is a multibyte value. Read 7 bits at a time. int result = prefixMask; int shift = 0; while (true) { int b = readByte(); if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255]. result += (b & 0x7f) << shift; shift += 7; } else { result += b << shift; // Last byte. break; } } return result; } /** * Reads a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this method * may be used to read strings that are known to be ASCII-only. */ public String readString() throws IOException { int firstByte = readByte(); int length = readInt(firstByte, PREFIX_8_BITS); byte[] encoded = new byte[length]; bytesLeft -= length; in.readFully(encoded); return new String(encoded, "UTF-8"); } } static class Writer { private final OutputStream out; Writer(OutputStream out) { this.out = out; } public void writeHeaders(List<String> nameValueBlock) throws IOException { // TODO: implement a compression strategy. for (int i = 0, size = nameValueBlock.size(); i < size; i += 2) { out.write(0x60); // Literal Header without Indexing - New Name. writeString(nameValueBlock.get(i)); writeString(nameValueBlock.get(i + 1)); } } public void writeInt(int value, int prefixMask, int bits) throws IOException { // Write the raw value for a single byte value. if (value < prefixMask) { out.write(bits | value); return; } // Write the mask to start a multibyte value. out.write(bits | prefixMask); value -= prefixMask; // Write 7 bits at a time 'til we're done. while (value >= 0x80) { int b = value & 0x7f; out.write(b | 0x80); value >>>= 7; } out.write(value); } /** * Writes a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this * method can be used to write strings that are known to be ASCII-only. */ public void writeString(String headerName) throws IOException { byte[] bytes = headerName.getBytes("UTF-8"); writeInt(bytes.length, PREFIX_8_BITS, 0); out.write(bytes); } } }