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