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 }