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