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 }