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                 warning("The field does not exist: ", field.toString());
219             else
220                 entry = *entryPtr;
221         }
222 
223         return entry;
224     }
225 
226     Entry get(string name) {
227         string lowerName = std.uni.toLower(name);
228 
229         StaticEntry *entry = lowerName in __staticNameMap;
230         if (entry !is null)
231             return *entry;
232         // trace("name=", name);
233 
234         // if(!_nameMap.containsKey(n))
235         //     return null;
236 
237         return _nameMap.get(lowerName);
238     }
239 
240     Entry get(int index) {
241         if (index <= STATIC_SIZE)
242             return __staticTable[index];
243 
244         return _dynamicTable.get(index);
245     }
246 
247     Entry get(HttpHeader header) {
248         version(HUNT_DEBUG) {
249             tracef("header:%s, ordinal=%d",header, header.ordinal());
250         }
251 
252         int o = header.ordinal();
253         Entry e = __staticTableByHeader.get(o, null);
254         if (e is null)
255             return get(header.asString());
256         return e;
257     }
258 
259     static Entry getStatic(HttpHeader header) {
260         return __staticTableByHeader[header.ordinal()];
261     }
262 
263     Entry add(HttpField field) {
264         Entry entry = new Entry(field);
265         int size = entry.getSize();
266         if (size > _maxDynamicTableSizeInBytes) {
267             version(HUNT_DEBUG)
268                 tracef(format("HdrTbl[%x] !added size %d>%d", toHash(), size, _maxDynamicTableSizeInBytes));
269             return null;
270         }
271         _dynamicTableSizeInBytes += size;
272         _dynamicTable.add(entry);
273         _fieldMap.put(field, entry);
274         _nameMap.put(std.uni.toLower(field.getName()), entry);
275 
276         version(HUNT_DEBUG)
277             tracef(format("HdrTbl[%x] added %s", toHash(), entry));
278         _dynamicTable.evict();
279         return entry;
280     }
281 
282     /**
283      * @return Current dynamic table size in entries
284      */
285     int size() {
286         return _dynamicTable.size();
287     }
288 
289     /**
290      * @return Current Dynamic table size in Octets
291      */
292     int getDynamicTableSize() {
293         return _dynamicTableSizeInBytes;
294     }
295 
296     /**
297      * @return Max Dynamic table size in Octets
298      */
299     int getMaxDynamicTableSize() {
300         return _maxDynamicTableSizeInBytes;
301     }
302 
303     int index(Entry entry) {
304         if (entry._slot < 0)
305             return 0;
306         if (entry.isStatic())
307             return entry._slot;
308 
309         return _dynamicTable.index(entry);
310     }
311 
312     static int staticIndex(HttpHeader header) {
313         if (header == HttpHeader.Null)
314             return 0;
315         // Entry entry = __staticNameMap[header.asString()];
316 
317         StaticEntry *entry = header.asString() in __staticNameMap;
318         if (entry is null)
319             return 0;
320         return entry._slot;
321     }
322 
323 
324     override
325     string toString() {
326         return format("HpackContext@%x{entries=%d,size=%d,max=%d}", toHash(), _dynamicTable.size(), _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes);
327     }
328 
329     private class DynamicTable {
330         Entry[] _entries;
331         int _size;
332         int _offset;
333         int _growby;
334 
335         private this(int initCapacity) {
336             _entries = new Entry[initCapacity];
337             _growby = initCapacity;
338         }
339 
340         void add(Entry entry) {
341             if (_size == _entries.length) {
342                 Entry[] entries = new Entry[_entries.length + _growby];
343                 for (int i = 0; i < _size; i++) {
344                     int slot = (_offset + i) % cast(int)_entries.length;
345                     entries[i] = _entries[slot];
346                     entries[i]._slot = i;
347                 }
348                 _entries = entries;
349                 _offset = 0;
350             }
351             int slot = (_size++ + _offset) % cast(int)_entries.length;
352             _entries[slot] = entry;
353             entry._slot = slot;
354         }
355 
356         int index(Entry entry) {
357             return STATIC_SIZE + _size - (entry._slot - _offset + cast(int)_entries.length) % cast(int)_entries.length;
358         }
359 
360         Entry get(int index) {
361             int d = index - STATIC_SIZE - 1;
362             if (d < 0 || d >= _size)
363                 return null;
364             int slot = (_offset + _size - d - 1) % cast(int)_entries.length;
365             return _entries[slot];
366         }
367 
368         int size() {
369             return _size;
370         }
371 
372         private void evict() {
373             while (_dynamicTableSizeInBytes > _maxDynamicTableSizeInBytes) {
374                 Entry entry = _entries[_offset];
375                 _entries[_offset] = null;
376                 _offset = (_offset + 1) % cast(int)_entries.length;
377                 _size--;
378                 version(HUNT_DEBUG)
379                     tracef(format("HdrTbl[%x] evict %s", toHash(), entry));
380                 _dynamicTableSizeInBytes -= entry.getSize();
381                 entry._slot = -1;
382                 _fieldMap.remove(entry.getHttpField());
383                 string lc = std.uni.toLower(entry.getHttpField().getName());
384                 if (entry == _nameMap.get(lc))
385                     _nameMap.remove(lc);
386 
387             }
388             version(HUNT_DEBUG) {
389                 tracef(format("HdrTbl[%x] entries=%d, size=%d, max=%d", toHash(), _dynamicTable.size(), 
390                     _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes));
391             }
392         }
393 
394     }
395 
396     static class Entry {
397         HttpField _field;
398         int _slot; // The index within it's array
399 
400         this() {
401             _slot = -1;
402             _field = null;
403         }
404 
405         this(HttpField field) {
406             _field = field;
407         }
408 
409         int getSize() {
410             string value = _field.getValue();
411             return 32 + cast(int)(_field.getName().length + value.length);
412         }
413 
414         HttpField getHttpField() {
415             return _field;
416         }
417 
418         bool isStatic() {
419             return false;
420         }
421 
422         byte[] getStaticHuffmanValue() {
423             return null;
424         }
425 
426         override string toString() {
427             return format("{%s,%d,%s,%x}", isStatic() ? "S" : "D", _slot, _field, toHash());
428         }
429     }
430 
431     static class StaticEntry :Entry {
432         private byte[] _huffmanValue;
433         private byte _encodedField;
434 
435         this(int index, HttpField field) {
436             super(field);
437             _slot = index;
438             string value = field.getValue();
439             if (value != null && value.length > 0) {
440                 int huffmanLen = Huffman.octetsNeeded(value);
441                 int lenLen = NBitInteger.octectsNeeded(7, huffmanLen);
442                 _huffmanValue = new byte[1 + lenLen + huffmanLen];
443                 ByteBuffer buffer = BufferUtils.toBuffer(_huffmanValue);
444 
445                 // Indicate Huffman
446                 buffer.put(cast(byte) 0x80);
447                 // Add huffman length
448                 NBitInteger.encode(buffer, 7, huffmanLen);
449                 // Encode value
450                 Huffman.encode(buffer, value);
451             } else
452                 _huffmanValue = null;
453 
454             _encodedField = cast(byte) (0x80 | index);
455         }
456 
457         override
458         bool isStatic() {
459             return true;
460         }
461 
462         override
463         byte[] getStaticHuffmanValue() {
464             return _huffmanValue;
465         }
466 
467         byte getEncodedField() {
468             return _encodedField;
469         }
470     }
471 }