1 package com.squareup.okhttp.internal.spdy; 2 3 import com.squareup.okhttp.internal.BitArray; 4 import java.io.IOException; 5 import java.util.ArrayList; 6 import java.util.Arrays; 7 import java.util.Collections; 8 import java.util.LinkedHashMap; 9 import java.util.List; 10 import java.util.Map; 11 import okio.BufferedSource; 12 import okio.ByteString; 13 import okio.OkBuffer; 14 import okio.Okio; 15 import okio.Source; 16 17 /** 18 * Read and write HPACK v05. 19 * 20 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05 21 * 22 * This implementation uses an array for the header table with a bitset for 23 * references. Dynamic entries are added to the array, starting in the last 24 * position moving forward. When the array fills, it is doubled. 25 */ 26 final class HpackDraft05 { 27 private static final int PREFIX_6_BITS = 0x3f; 28 private static final int PREFIX_7_BITS = 0x7f; 29 30 private static final Header[] STATIC_HEADER_TABLE = new Header[] { 31 new Header(Header.TARGET_AUTHORITY, ""), 32 new Header(Header.TARGET_METHOD, "GET"), 33 new Header(Header.TARGET_METHOD, "POST"), 34 new Header(Header.TARGET_PATH, "/"), 35 new Header(Header.TARGET_PATH, "/index.html"), 36 new Header(Header.TARGET_SCHEME, "http"), 37 new Header(Header.TARGET_SCHEME, "https"), 38 new Header(Header.RESPONSE_STATUS, "200"), 39 new Header(Header.RESPONSE_STATUS, "500"), 40 new Header(Header.RESPONSE_STATUS, "404"), 41 new Header(Header.RESPONSE_STATUS, "403"), 42 new Header(Header.RESPONSE_STATUS, "400"), 43 new Header(Header.RESPONSE_STATUS, "401"), 44 new Header("accept-charset", ""), 45 new Header("accept-encoding", ""), 46 new Header("accept-language", ""), 47 new Header("accept-ranges", ""), 48 new Header("accept", ""), 49 new Header("access-control-allow-origin", ""), 50 new Header("age", ""), 51 new Header("allow", ""), 52 new Header("authorization", ""), 53 new Header("cache-control", ""), 54 new Header("content-disposition", ""), 55 new Header("content-encoding", ""), 56 new Header("content-language", ""), 57 new Header("content-length", ""), 58 new Header("content-location", ""), 59 new Header("content-range", ""), 60 new Header("content-type", ""), 61 new Header("cookie", ""), 62 new Header("date", ""), 63 new Header("etag", ""), 64 new Header("expect", ""), 65 new Header("expires", ""), 66 new Header("from", ""), 67 new Header("host", ""), 68 new Header("if-match", ""), 69 new Header("if-modified-since", ""), 70 new Header("if-none-match", ""), 71 new Header("if-range", ""), 72 new Header("if-unmodified-since", ""), 73 new Header("last-modified", ""), 74 new Header("link", ""), 75 new Header("location", ""), 76 new Header("max-forwards", ""), 77 new Header("proxy-authenticate", ""), 78 new Header("proxy-authorization", ""), 79 new Header("range", ""), 80 new Header("referer", ""), 81 new Header("refresh", ""), 82 new Header("retry-after", ""), 83 new Header("server", ""), 84 new Header("set-cookie", ""), 85 new Header("strict-transport-security", ""), 86 new Header("transfer-encoding", ""), 87 new Header("user-agent", ""), 88 new Header("vary", ""), 89 new Header("via", ""), 90 new Header("www-authenticate", "") 91 }; 92 93 private HpackDraft05() { 94 } 95 96 // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2 97 static final class Reader { 98 private final Huffman.Codec huffmanCodec; 99 100 private final List<Header> emittedHeaders = new ArrayList<Header>(); 101 private final BufferedSource source; 102 private int maxHeaderTableByteCount; 103 104 // Visible for testing. 105 Header[] headerTable = new Header[8]; 106 // Array is populated back to front, so new entries always have lowest index. 107 int nextHeaderIndex = headerTable.length - 1; 108 int headerCount = 0; 109 110 /** 111 * Set bit positions indicate {@code headerTable[pos]} should be emitted. 112 */ 113 // Using a BitArray as it has left-shift operator. 114 BitArray referencedHeaders = new BitArray.FixedCapacity(); 115 116 /** 117 * Set bit positions indicate {@code headerTable[pos]} was already emitted. 118 */ 119 BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); 120 int headerTableByteCount = 0; 121 122 Reader(boolean client, int maxHeaderTableByteCount, Source source) { 123 this.huffmanCodec = client ? Huffman.Codec.RESPONSE : Huffman.Codec.REQUEST; 124 this.maxHeaderTableByteCount = maxHeaderTableByteCount; 125 this.source = Okio.buffer(source); 126 } 127 128 int maxHeaderTableByteCount() { 129 return maxHeaderTableByteCount; 130 } 131 132 /** 133 * Called by the reader when the peer sent a new header table size setting. 134 * <p> 135 * Evicts entries or clears the table as needed. 136 */ 137 void maxHeaderTableByteCount(int newMaxHeaderTableByteCount) { 138 this.maxHeaderTableByteCount = newMaxHeaderTableByteCount; 139 if (maxHeaderTableByteCount < headerTableByteCount) { 140 if (maxHeaderTableByteCount == 0) { 141 clearHeaderTable(); 142 } else { 143 evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount); 144 } 145 } 146 } 147 148 private void clearHeaderTable() { 149 clearReferenceSet(); 150 Arrays.fill(headerTable, null); 151 nextHeaderIndex = headerTable.length - 1; 152 headerCount = 0; 153 headerTableByteCount = 0; 154 } 155 156 /** Returns the count of entries evicted. */ 157 private int evictToRecoverBytes(int bytesToRecover) { 158 int entriesToEvict = 0; 159 if (bytesToRecover > 0) { 160 // determine how many headers need to be evicted. 161 for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) { 162 bytesToRecover -= headerTable[j].hpackSize; 163 headerTableByteCount -= headerTable[j].hpackSize; 164 headerCount--; 165 entriesToEvict++; 166 } 167 referencedHeaders.shiftLeft(entriesToEvict); 168 emittedReferencedHeaders.shiftLeft(entriesToEvict); 169 System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable, 170 nextHeaderIndex + 1 + entriesToEvict, headerCount); 171 nextHeaderIndex += entriesToEvict; 172 } 173 return entriesToEvict; 174 } 175 176 /** 177 * Read {@code byteCount} bytes of headers from the source stream into the 178 * set of emitted headers. 179 */ 180 void readHeaders() throws IOException { 181 while (!source.exhausted()) { 182 int b = source.readByte() & 0xff; 183 if (b == 0x80) { // 10000000 184 clearReferenceSet(); 185 } else if ((b & 0x80) == 0x80) { // 1NNNNNNN 186 int index = readInt(b, PREFIX_7_BITS); 187 readIndexedHeader(index - 1); 188 } else { // 0NNNNNNN 189 if (b == 0x40) { // 01000000 190 readLiteralHeaderWithoutIndexingNewName(); 191 } else if ((b & 0x40) == 0x40) { // 01NNNNNN 192 int index = readInt(b, PREFIX_6_BITS); 193 readLiteralHeaderWithoutIndexingIndexedName(index - 1); 194 } else if (b == 0) { // 00000000 195 readLiteralHeaderWithIncrementalIndexingNewName(); 196 } else if ((b & 0xc0) == 0) { // 00NNNNNN 197 int index = readInt(b, PREFIX_6_BITS); 198 readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); 199 } else { 200 // TODO: we should throw something that we can coerce to a PROTOCOL_ERROR 201 throw new AssertionError("unhandled byte: " + Integer.toBinaryString(b)); 202 } 203 } 204 } 205 } 206 207 private void clearReferenceSet() { 208 referencedHeaders.clear(); 209 emittedReferencedHeaders.clear(); 210 } 211 212 void emitReferenceSet() { 213 for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) { 214 if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) { 215 emittedHeaders.add(headerTable[i]); 216 } 217 } 218 } 219 220 /** 221 * Returns all headers emitted since they were last cleared, then clears the 222 * emitted headers. 223 */ 224 List<Header> getAndReset() { 225 List<Header> result = new ArrayList<Header>(emittedHeaders); 226 emittedHeaders.clear(); 227 emittedReferencedHeaders.clear(); 228 return result; 229 } 230 231 private void readIndexedHeader(int index) { 232 if (isStaticHeader(index)) { 233 Header staticEntry = STATIC_HEADER_TABLE[index - headerCount]; 234 if (maxHeaderTableByteCount == 0) { 235 emittedHeaders.add(staticEntry); 236 } else { 237 insertIntoHeaderTable(-1, staticEntry); 238 } 239 } else { 240 int headerTableIndex = headerTableIndex(index); 241 if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately. 242 emittedHeaders.add(headerTable[headerTableIndex]); 243 emittedReferencedHeaders.set(headerTableIndex); 244 } 245 referencedHeaders.toggle(headerTableIndex); 246 } 247 } 248 249 // referencedHeaders is relative to nextHeaderIndex + 1. 250 private int headerTableIndex(int index) { 251 return nextHeaderIndex + 1 + index; 252 } 253 254 private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { 255 ByteString name = getName(index); 256 ByteString value = readByteString(false); 257 emittedHeaders.add(new Header(name, value)); 258 } 259 260 private void readLiteralHeaderWithoutIndexingNewName() throws IOException { 261 ByteString name = readByteString(true); 262 ByteString value = readByteString(false); 263 emittedHeaders.add(new Header(name, value)); 264 } 265 266 private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) 267 throws IOException { 268 ByteString name = getName(nameIndex); 269 ByteString value = readByteString(false); 270 insertIntoHeaderTable(-1, new Header(name, value)); 271 } 272 273 private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { 274 ByteString name = readByteString(true); 275 ByteString value = readByteString(false); 276 insertIntoHeaderTable(-1, new Header(name, value)); 277 } 278 279 private ByteString getName(int index) { 280 if (isStaticHeader(index)) { 281 return STATIC_HEADER_TABLE[index - headerCount].name; 282 } else { 283 return headerTable[headerTableIndex(index)].name; 284 } 285 } 286 287 private boolean isStaticHeader(int index) { 288 return index >= headerCount; 289 } 290 291 /** index == -1 when new. */ 292 private void insertIntoHeaderTable(int index, Header entry) { 293 int delta = entry.hpackSize; 294 if (index != -1) { // Index -1 == new header. 295 delta -= headerTable[headerTableIndex(index)].hpackSize; 296 } 297 298 // if the new or replacement header is too big, drop all entries. 299 if (delta > maxHeaderTableByteCount) { 300 clearHeaderTable(); 301 // emit the large header to the callback. 302 emittedHeaders.add(entry); 303 return; 304 } 305 306 // Evict headers to the required length. 307 int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount; 308 int entriesEvicted = evictToRecoverBytes(bytesToRecover); 309 310 if (index == -1) { // Adding a value to the header table. 311 if (headerCount + 1 > headerTable.length) { // Need to grow the header table. 312 Header[] doubled = new Header[headerTable.length * 2]; 313 System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length); 314 if (doubled.length == 64) { 315 referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity(); 316 emittedReferencedHeaders = 317 ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity(); 318 } 319 referencedHeaders.shiftLeft(headerTable.length); 320 emittedReferencedHeaders.shiftLeft(headerTable.length); 321 nextHeaderIndex = headerTable.length - 1; 322 headerTable = doubled; 323 } 324 index = nextHeaderIndex--; 325 referencedHeaders.set(index); 326 headerTable[index] = entry; 327 headerCount++; 328 } else { // Replace value at same position. 329 index += headerTableIndex(index) + entriesEvicted; 330 referencedHeaders.set(index); 331 headerTable[index] = entry; 332 } 333 headerTableByteCount += delta; 334 } 335 336 private int readByte() throws IOException { 337 return source.readByte() & 0xff; 338 } 339 340 int readInt(int firstByte, int prefixMask) throws IOException { 341 int prefix = firstByte & prefixMask; 342 if (prefix < prefixMask) { 343 return prefix; // This was a single byte value. 344 } 345 346 // This is a multibyte value. Read 7 bits at a time. 347 int result = prefixMask; 348 int shift = 0; 349 while (true) { 350 int b = readByte(); 351 if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255]. 352 result += (b & 0x7f) << shift; 353 shift += 7; 354 } else { 355 result += b << shift; // Last byte. 356 break; 357 } 358 } 359 return result; 360 } 361 362 /** 363 * Reads a potentially Huffman encoded string byte string. When 364 * {@code asciiLowercase} is true, bytes will be converted to lowercase. 365 */ 366 ByteString readByteString(boolean asciiLowercase) throws IOException { 367 int firstByte = readByte(); 368 boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN 369 int length = readInt(firstByte, PREFIX_7_BITS); 370 371 ByteString byteString = source.readByteString(length); 372 373 if (huffmanDecode) { 374 byteString = huffmanCodec.decode(byteString); // TODO: streaming Huffman! 375 } 376 377 if (asciiLowercase) { 378 byteString = byteString.toAsciiLowercase(); 379 } 380 381 return byteString; 382 } 383 } 384 385 private static final Map<ByteString, Integer> NAME_TO_FIRST_INDEX = nameToFirstIndex(); 386 387 private static Map<ByteString, Integer> nameToFirstIndex() { 388 Map<ByteString, Integer> result = 389 new LinkedHashMap<ByteString, Integer>(STATIC_HEADER_TABLE.length); 390 for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { 391 if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { 392 result.put(STATIC_HEADER_TABLE[i].name, i); 393 } 394 } 395 return Collections.unmodifiableMap(result); 396 } 397 398 static final class Writer { 399 private final OkBuffer out; 400 401 Writer(OkBuffer out) { 402 this.out = out; 403 } 404 405 void writeHeaders(List<Header> headerBlock) throws IOException { 406 // TODO: implement index tracking 407 for (int i = 0, size = headerBlock.size(); i < size; i++) { 408 ByteString name = headerBlock.get(i).name; 409 Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); 410 if (staticIndex != null) { 411 // Literal Header Field without Indexing - Indexed Name. 412 writeInt(staticIndex + 1, PREFIX_6_BITS, 0x40); 413 writeByteString(headerBlock.get(i).value); 414 } else { 415 out.writeByte(0x40); // Literal Header without Indexing - New Name. 416 writeByteString(name); 417 writeByteString(headerBlock.get(i).value); 418 } 419 } 420 } 421 422 // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.1 423 void writeInt(int value, int prefixMask, int bits) throws IOException { 424 // Write the raw value for a single byte value. 425 if (value < prefixMask) { 426 out.writeByte(bits | value); 427 return; 428 } 429 430 // Write the mask to start a multibyte value. 431 out.writeByte(bits | prefixMask); 432 value -= prefixMask; 433 434 // Write 7 bits at a time 'til we're done. 435 while (value >= 0x80) { 436 int b = value & 0x7f; 437 out.writeByte(b | 0x80); 438 value >>>= 7; 439 } 440 out.writeByte(value); 441 } 442 443 void writeByteString(ByteString data) throws IOException { 444 writeInt(data.size(), PREFIX_7_BITS, 0); 445 out.write(data); 446 } 447 } 448 } 449