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 }