1 module hunt.http.codec.http.hpack.HpackContext;
2 
3 // import hunt.http.codec.http.model;
4 
5 import hunt.http.HttpField;
6 import hunt.http.HttpHeader;
7 import hunt.http.HttpMethod;
8 import hunt.http.codec.http.model.StaticTableHttpField;
9 
10 import hunt.http.codec.http.hpack.Huffman;
11 import hunt.http.codec.http.hpack.NBitInteger;
12 
13 import hunt.Exceptions;
14 import hunt.text.Common;
15 import hunt.collection;
16 
17 import hunt.logging;
18 
19 import std.array;
20 import std.conv;
21 import std.uni;
22 import std.format;
23 
24 /**
25  * HPACK - Header Compression for HTTP/2
26  * <p>
27  * This class maintains the compression context for a single HTTP/2 connection.
28  * Specifically it holds the static and dynamic Header Field Tables and the
29  * associated sizes and limits.
30  * </p>
31  * <p>
32  * It is compliant with draft 11 of the specification
33  * </p>
34  */
35 class HpackContext {
36 
37     
38     private enum string EMPTY = "";
39     enum string[][] STATIC_TABLE =
40         [
41                 [null, null],
42         /* 1  */ [":authority", EMPTY],
43         /* 2  */ [":method", "GET"],
44         /* 3  */ [":method", "POST"],
45         /* 4  */ [":path", "/"],
46         /* 5  */ [":path", "/index.html"],
47         /* 6  */ [":scheme", "http"],
48         /* 7  */ [":scheme", "https"],
49         /* 8  */ [":status", "200"],
50         /* 9  */ [":status", "204"],
51         /* 10 */ [":status", "206"],
52         /* 11 */ [":status", "304"],
53         /* 12 */ [":status", "400"],
54         /* 13 */ [":status", "404"],
55         /* 14 */ [":status", "500"],
56         /* 15 */ ["accept-charset", EMPTY],
57         /* 16 */ ["accept-encoding", "gzip, deflate"],
58         /* 17 */ ["accept-language", EMPTY],
59         /* 18 */ ["accept-ranges", EMPTY],
60         /* 19 */ ["accept", EMPTY],
61         /* 20 */ ["access-control-allow-origin", EMPTY],
62         /* 21 */ ["age", EMPTY],
63         /* 22 */ ["allow", EMPTY],
64         /* 23 */ ["authorization", EMPTY],
65         /* 24 */ ["cache-control", EMPTY],
66         /* 25 */ ["content-disposition", EMPTY],
67         /* 26 */ ["content-encoding", EMPTY],
68         /* 27 */ ["content-language", EMPTY],
69         /* 28 */ ["content-length", EMPTY],
70         /* 29 */ ["content-location", EMPTY],
71         /* 30 */ ["content-range", EMPTY],
72         /* 31 */ ["content-type", EMPTY],
73         /* 32 */ ["cookie", EMPTY],
74         /* 33 */ ["date", EMPTY],
75         /* 34 */ ["etag", EMPTY],
76         /* 35 */ ["expect", EMPTY],
77         /* 36 */ ["expires", EMPTY],
78         /* 37 */ ["from", EMPTY],
79         /* 38 */ ["host", EMPTY],
80         /* 39 */ ["if-match", EMPTY],
81         /* 40 */ ["if-modified-since", EMPTY],
82         /* 41 */ ["if-none-match", EMPTY],
83         /* 42 */ ["if-range", EMPTY],
84         /* 43 */ ["if-unmodified-since", EMPTY],
85         /* 44 */ ["last-modified", EMPTY],
86         /* 45 */ ["link", EMPTY],
87         /* 46 */ ["location", EMPTY],
88         /* 47 */ ["max-forwards", EMPTY],
89         /* 48 */ ["proxy-authenticate", EMPTY],
90         /* 49 */ ["proxy-authorization", EMPTY],
91         /* 50 */ ["range", EMPTY],
92         /* 51 */ ["referer", EMPTY],
93         /* 52 */ ["refresh", EMPTY],
94         /* 53 */ ["retry-after", EMPTY],
95         /* 54 */ ["server", EMPTY],
96         /* 55 */ ["set-cookie", EMPTY],
97         /* 56 */ ["strict-transport-security", EMPTY],
98         /* 57 */ ["transfer-encoding", EMPTY],
99         /* 58 */ ["user-agent", EMPTY],
100         /* 59 */ ["vary", EMPTY],
101         /* 60 */ ["via", EMPTY],
102         /* 61 */ ["www-authenticate", EMPTY]
103         ];
104 
105     private __gshared static Entry[HttpField] __staticFieldMap; // = new HashMap<>();
106     private __gshared static StaticEntry[string] __staticNameMap; // = new ArrayTernaryTrie<>(true, 512);
107     private __gshared static StaticEntry[int] __staticTableByHeader; // = new StaticEntry[HttpHeader.getCount];
108     private __gshared static StaticEntry[] __staticTable; // = new StaticEntry[STATIC_TABLE.length];
109     enum int STATIC_SIZE = cast(int)STATIC_TABLE.length - 1;
110 
111     shared static this() {
112         // __staticTableByHeader = new StaticEntry[HttpHeader.getCount];
113         __staticTable = new StaticEntry[STATIC_TABLE.length];
114 
115         Set!string added = new HashSet!(string)();
116         for (int i = 1; i < STATIC_TABLE.length; i++) {
117             StaticEntry entry = null;
118 
119             string name = STATIC_TABLE[i][0];
120             string value = STATIC_TABLE[i][1];
121             // HttpHeader header = HttpHeader.CACHE[name];
122             HttpHeader header = HttpHeader.get(name);
123             if (header != HttpHeader.Null && !value.empty) {
124                     if(header == HttpHeader.C_METHOD) {
125                         HttpMethod method = HttpMethod.CACHE[value];
126                         if (method != HttpMethod.Null)
127                             entry = new StaticEntry(i, new StaticTableHttpField!(HttpMethod)(header, name, value, method));
128                     }
129                     else if(header == HttpHeader.C_SCHEME) {
130 
131                         // HttpScheme scheme = HttpScheme.CACHE.get(value);
132                         string scheme = value;
133                         if (!scheme.empty)
134                             entry = new StaticEntry(i, new StaticTableHttpField!(string)(header, name, value, scheme));
135                     }
136                     else if(header == HttpHeader.C_STATUS) {
137                         entry = new StaticEntry(i, new StaticTableHttpField!(string)(header, name, value, (value)));
138                     }
139             }
140             else
141             {
142                 // warning("name=>", name, ", length=", HttpHeader.CACHE.length);
143             }
144 
145             if (entry is null)
146                 entry = new StaticEntry(i, header == HttpHeader.Null ? new HttpField(STATIC_TABLE[i][0], value) : new HttpField(header, name, value));
147 
148             __staticTable[i] = entry;
149 
150             HttpField currentField = entry._field;
151             string fieldName = currentField.getName().toLower();
152             string fieldValue = currentField.getValue();
153             if (fieldValue !is null)
154             {
155                 // tracef("%s,  hash: %d", currentField.toString(), currentField.toHash());
156                 __staticFieldMap[currentField] = entry;
157             }
158             else
159             {
160                 warning("Empty field: ", currentField.toString());    
161             }
162 
163             if (!added.contains(fieldName)) {
164                 added.add(fieldName);
165                 __staticNameMap[fieldName] = entry;
166                 // if (__staticNameMap[fieldName] is null)
167                 //     throw new IllegalStateException("name trie too small");
168             }
169         }
170 
171         // trace(__staticNameMap);
172 
173         foreach (HttpHeader h ; HttpHeader.values()) {
174             if(h == HttpHeader.Null)
175                 continue;
176             string headerName = h.asString().toLower();
177             // tracef("headerName:%s, ordinal=%d", headerName, h.ordinal());
178 
179             StaticEntry *entry = (headerName in __staticNameMap);
180             if (entry !is null)
181                 __staticTableByHeader[h.ordinal()] = *entry;
182         }
183 
184         // trace(__staticTableByHeader);
185     }
186 
187     private int _maxDynamicTableSizeInBytes;
188     private int _dynamicTableSizeInBytes;
189     private DynamicTable _dynamicTable;
190     private Map!(HttpField, Entry) _fieldMap; // = new HashMap!(HttpField, Entry)();
191     private Map!(string, Entry) _nameMap; // = new HashMap!(string, Entry)();
192 
193     this(int maxDynamicTableSize) {
194         _fieldMap = new HashMap!(HttpField, Entry)();
195         _nameMap = new HashMap!(string, Entry)();
196 
197         _maxDynamicTableSizeInBytes = maxDynamicTableSize;
198         int guesstimateEntries = 10 + maxDynamicTableSize / (32 + 10 + 10);
199         _dynamicTable = new DynamicTable(guesstimateEntries);
200         version(HUNT_HTTP_DEBUG_MORE)
201             tracef(format("HdrTbl[%x] created max=%d", toHash(), maxDynamicTableSize));
202     }
203 
204     void resize(int newMaxDynamicTableSize) {
205         version(HUNT_HTTP_DEBUG_MORE)
206             tracef(format("HdrTbl[%x] resized max=%d->%d", toHash(), _maxDynamicTableSizeInBytes, newMaxDynamicTableSize));
207         _maxDynamicTableSizeInBytes = newMaxDynamicTableSize;
208         _dynamicTable.evict();
209     }
210 
211     Entry get(HttpField field) {
212         Entry entry = _fieldMap.get(field);
213         if (entry is null)
214         {
215             auto entryPtr = field in __staticFieldMap;
216             if(entryPtr is null) {
217                 // warningf("The field does not exist: %s, %s", field.toString(), field.toHash());
218                 version(HUNT_HTTP_DEBUG) {
219                     warning("The field does not exist: ", field.toString());
220                 }
221             } else {
222                 entry = *entryPtr;
223             }
224         }
225 
226         return entry;
227     }
228 
229     Entry get(string name) {
230         string lowerName = std.uni.toLower(name);
231 
232         StaticEntry *entry = lowerName in __staticNameMap;
233         if (entry !is null)
234             return *entry;
235         // trace("name=", name);
236 
237         // if(!_nameMap.containsKey(n))
238         //     return null;
239 
240         return _nameMap.get(lowerName);
241     }
242 
243     Entry get(int index) {
244         if (index <= STATIC_SIZE)
245             return __staticTable[index];
246 
247         return _dynamicTable.get(index);
248     }
249 
250     Entry get(HttpHeader header) {
251         version(HUNT_DEBUG) {
252             tracef("header:%s, ordinal=%d",header, header.ordinal());
253         }
254 
255         int o = header.ordinal();
256         Entry e = __staticTableByHeader.get(o, null);
257         if (e is null)
258             return get(header.asString());
259         return e;
260     }
261 
262     static Entry getStatic(HttpHeader header) {
263         return __staticTableByHeader[header.ordinal()];
264     }
265 
266     Entry add(HttpField field) {
267         Entry entry = new Entry(field);
268         int size = entry.getSize();
269         if (size > _maxDynamicTableSizeInBytes) {
270             version(HUNT_DEBUG)
271                 tracef(format("HdrTbl[%x] !added size %d>%d", toHash(), size, _maxDynamicTableSizeInBytes));
272             return null;
273         }
274         _dynamicTableSizeInBytes += size;
275         _dynamicTable.add(entry);
276         _fieldMap.put(field, entry);
277         _nameMap.put(std.uni.toLower(field.getName()), entry);
278 
279         version(HUNT_DEBUG)
280             tracef(format("HdrTbl[%x] added %s", toHash(), entry));
281         _dynamicTable.evict();
282         return entry;
283     }
284 
285     /**
286      * @return Current dynamic table size in entries
287      */
288     int size() {
289         return _dynamicTable.size();
290     }
291 
292     /**
293      * @return Current Dynamic table size in Octets
294      */
295     int getDynamicTableSize() {
296         return _dynamicTableSizeInBytes;
297     }
298 
299     /**
300      * @return Max Dynamic table size in Octets
301      */
302     int getMaxDynamicTableSize() {
303         return _maxDynamicTableSizeInBytes;
304     }
305 
306     int index(Entry entry) {
307         if (entry._slot < 0)
308             return 0;
309         if (entry.isStatic())
310             return entry._slot;
311 
312         return _dynamicTable.index(entry);
313     }
314 
315     static int staticIndex(HttpHeader header) {
316         if (header == HttpHeader.Null)
317             return 0;
318         // Entry entry = __staticNameMap[header.asString()];
319 
320         StaticEntry *entry = header.asString() in __staticNameMap;
321         if (entry is null)
322             return 0;
323         return entry._slot;
324     }
325 
326 
327     override
328     string toString() {
329         return format("HpackContext@%x{entries=%d,size=%d,max=%d}", toHash(), _dynamicTable.size(), _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes);
330     }
331 
332     private class DynamicTable {
333         Entry[] _entries;
334         int _size;
335         int _offset;
336         int _growby;
337 
338         private this(int initCapacity) {
339             _entries = new Entry[initCapacity];
340             _growby = initCapacity;
341         }
342 
343         void add(Entry entry) {
344             if (_size == _entries.length) {
345                 Entry[] entries = new Entry[_entries.length + _growby];
346                 for (int i = 0; i < _size; i++) {
347                     int slot = (_offset + i) % cast(int)_entries.length;
348                     entries[i] = _entries[slot];
349                     entries[i]._slot = i;
350                 }
351                 _entries = entries;
352                 _offset = 0;
353             }
354             int slot = (_size++ + _offset) % cast(int)_entries.length;
355             _entries[slot] = entry;
356             entry._slot = slot;
357         }
358 
359         int index(Entry entry) {
360             return STATIC_SIZE + _size - (entry._slot - _offset + cast(int)_entries.length) % cast(int)_entries.length;
361         }
362 
363         Entry get(int index) {
364             int d = index - STATIC_SIZE - 1;
365             if (d < 0 || d >= _size)
366                 return null;
367             int slot = (_offset + _size - d - 1) % cast(int)_entries.length;
368             return _entries[slot];
369         }
370 
371         int size() {
372             return _size;
373         }
374 
375         private void evict() {
376             while (_dynamicTableSizeInBytes > _maxDynamicTableSizeInBytes) {
377                 Entry entry = _entries[_offset];
378                 _entries[_offset] = null;
379                 _offset = (_offset + 1) % cast(int)_entries.length;
380                 _size--;
381                 version(HUNT_DEBUG)
382                     tracef(format("HdrTbl[%x] evict %s", toHash(), entry));
383                 _dynamicTableSizeInBytes -= entry.getSize();
384                 entry._slot = -1;
385                 _fieldMap.remove(entry.getHttpField());
386                 string lc = std.uni.toLower(entry.getHttpField().getName());
387                 if (entry == _nameMap.get(lc))
388                     _nameMap.remove(lc);
389 
390             }
391             version(HUNT_DEBUG) {
392                 tracef(format("HdrTbl[%x] entries=%d, size=%d, max=%d", toHash(), _dynamicTable.size(), 
393                     _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes));
394             }
395         }
396 
397     }
398 
399     static class Entry {
400         HttpField _field;
401         int _slot; // The index within it's array
402 
403         this() {
404             _slot = -1;
405             _field = null;
406         }
407 
408         this(HttpField field) {
409             _field = field;
410         }
411 
412         int getSize() {
413             string value = _field.getValue();
414             return 32 + cast(int)(_field.getName().length + value.length);
415         }
416 
417         HttpField getHttpField() {
418             return _field;
419         }
420 
421         bool isStatic() {
422             return false;
423         }
424 
425         byte[] getStaticHuffmanValue() {
426             return null;
427         }
428 
429         override string toString() {
430             return format("{%s,%d,%s,%x}", isStatic() ? "S" : "D", _slot, _field, toHash());
431         }
432     }
433 
434     static class StaticEntry :Entry {
435         private byte[] _huffmanValue;
436         private byte _encodedField;
437 
438         this(int index, HttpField field) {
439             super(field);
440             _slot = index;
441             string value = field.getValue();
442             if (value != null && value.length > 0) {
443                 int huffmanLen = Huffman.octetsNeeded(value);
444                 int lenLen = NBitInteger.octectsNeeded(7, huffmanLen);
445                 _huffmanValue = new byte[1 + lenLen + huffmanLen];
446                 ByteBuffer buffer = BufferUtils.toBuffer(_huffmanValue);
447 
448                 // Indicate Huffman
449                 buffer.put(cast(byte) 0x80);
450                 // Add huffman length
451                 NBitInteger.encode(buffer, 7, huffmanLen);
452                 // Encode value
453                 Huffman.encode(buffer, value);
454             } else
455                 _huffmanValue = null;
456 
457             _encodedField = cast(byte) (0x80 | index);
458         }
459 
460         override
461         bool isStatic() {
462             return true;
463         }
464 
465         override
466         byte[] getStaticHuffmanValue() {
467             return _huffmanValue;
468         }
469 
470         byte getEncodedField() {
471             return _encodedField;
472         }
473     }
474 }