tables.d 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. //module vibe.http.internal.hpack.tables;
  2. module vibe.http.internal.http2.hpack.tables;
  3. import vibe.http.internal.http2.hpack.exception;
  4. import vibe.http.status;
  5. import vibe.http.common;
  6. import vibe.core.log;
  7. import vibe.core.sync;
  8. import vibe.container.ringbuffer : RingBuffer;
  9. import std.variant;
  10. import std.traits;
  11. import std.meta;
  12. import std.range;
  13. import std.algorithm.iteration;
  14. import std.math : log10;
  15. import taggedalgebraic;
  16. alias HTTP2SettingValue = uint;
  17. // 4096 octets
  18. enum DEFAULT_DYNAMIC_TABLE_SIZE = 4096;
  19. /*
  20. 2.3. Indexing Tables
  21. HPACK uses two tables for associating header fields to indexes. The
  22. static table (see Section 2.3.1) is predefined and contains common
  23. header fields (most of them with an empty value). The dynamic table
  24. (see Section 2.3.2) is dynamic and can be used by the encoder to
  25. index header fields repeated in the encoded header lists.
  26. These two tables are combined into a single address space for
  27. defining index values (see Section 2.3.3).
  28. 2.3.1. Static Table
  29. The static table consists of a predefined static list of header
  30. fields. Its entries are defined in Appendix A.
  31. 2.3.2. Dynamic Table
  32. The dynamic table consists of a list of header fields maintained in
  33. first-in, first-out order. The first and newest entry in a dynamic
  34. table is at the lowest index, and the oldest entry of a dynamic tabl
  35. is at the highest index.
  36. The dynamic table is initially empty. Entries are added as each
  37. header block is decompressed.
  38. The dynamic table is initially empty. Entries are added as each
  39. header block is decompressed.
  40. The dynamic table can contain duplicate entries (i.e., entries with
  41. the same name and same value). Therefore, duplicate entries MUST NOT
  42. be treated as an error by a decoder.
  43. The encoder decides how to update the dynamic table and as such can
  44. control how much memory is used by the dynamic table. To limit the
  45. memory requirements of the decoder, the dynamic table size is
  46. strictly bounded (see Section 4.2).
  47. The decoder updates the dynamic table during the processing of a list
  48. of header field representations (see Section 3.2).
  49. */
  50. // wraps a header field = name:value
  51. struct HTTP2HeaderTableField {
  52. private union HeaderValue {
  53. string str;
  54. string[] strarr;
  55. HTTPStatus status;
  56. HTTPMethod method;
  57. }
  58. string name;
  59. TaggedAlgebraic!HeaderValue value;
  60. bool index = true;
  61. bool neverIndex = false;
  62. // initializers
  63. static foreach(t; __traits(allMembers, HeaderValue)) {
  64. mixin("this(string n, " ~
  65. typeof(__traits(getMember, HeaderValue, t)).stringof ~
  66. " v) @safe { name = n; value = v; }");
  67. }
  68. this(R)(R t) @safe
  69. if(is(ElementType!R : string))
  70. {
  71. assert(t.length == 2, "Invalid range for HTTP2HeaderTableField initializer");
  72. this(t[0], t[1]);
  73. }
  74. }
  75. // fixed as per HPACK RFC
  76. immutable size_t STATIC_TABLE_SIZE = 61;
  77. /** static table to index most common headers
  78. * fixed size, fixed order of entries (read only)
  79. * cannot be updated while decoding a header block
  80. */
  81. static immutable HTTP2HeaderTableField[STATIC_TABLE_SIZE+1] StaticTable;
  82. shared static this() {
  83. StaticTable = [
  84. HTTP2HeaderTableField("",""), // 0 index is not allowed
  85. HTTP2HeaderTableField(":authority", ""),
  86. HTTP2HeaderTableField(":method", HTTPMethod.GET),
  87. HTTP2HeaderTableField(":method", HTTPMethod.POST),
  88. HTTP2HeaderTableField(":path", "/"),
  89. HTTP2HeaderTableField(":path", "/index.html"),
  90. HTTP2HeaderTableField(":scheme", "http"),
  91. HTTP2HeaderTableField(":scheme", "https"),
  92. HTTP2HeaderTableField(":status", HTTPStatus.ok), // 200
  93. HTTP2HeaderTableField(":status", HTTPStatus.noContent), // 204
  94. HTTP2HeaderTableField(":status", HTTPStatus.partialContent), // 206
  95. HTTP2HeaderTableField(":status", HTTPStatus.notModified), // 304
  96. HTTP2HeaderTableField(":status", HTTPStatus.badRequest), // 400
  97. HTTP2HeaderTableField(":status", HTTPStatus.notFound), // 404
  98. HTTP2HeaderTableField(":status", HTTPStatus.internalServerError), // 500
  99. HTTP2HeaderTableField("accept-charset", ""),
  100. HTTP2HeaderTableField("accept-encoding", ["gzip", "deflate"]),
  101. HTTP2HeaderTableField("accept-language", ""),
  102. HTTP2HeaderTableField("accept-ranges", ""),
  103. HTTP2HeaderTableField("accept", ""),
  104. HTTP2HeaderTableField("access-control-allow-origin", ""),
  105. HTTP2HeaderTableField("age", ""),
  106. HTTP2HeaderTableField("allow", ""),
  107. HTTP2HeaderTableField("authorization", ""),
  108. HTTP2HeaderTableField("cache-control", ""),
  109. HTTP2HeaderTableField("content-disposition", ""),
  110. HTTP2HeaderTableField("content-encoding", ""),
  111. HTTP2HeaderTableField("content-language", ""),
  112. HTTP2HeaderTableField("content-length", ""),
  113. HTTP2HeaderTableField("content-location", ""),
  114. HTTP2HeaderTableField("content-range", ""),
  115. HTTP2HeaderTableField("content-type", ""),
  116. HTTP2HeaderTableField("cookie", ""),
  117. HTTP2HeaderTableField("date", ""),
  118. HTTP2HeaderTableField("etag", ""),
  119. HTTP2HeaderTableField("expect", ""),
  120. HTTP2HeaderTableField("expires", ""),
  121. HTTP2HeaderTableField("from", ""),
  122. HTTP2HeaderTableField("host", ""),
  123. HTTP2HeaderTableField("if-match", ""),
  124. HTTP2HeaderTableField("if-modified-since", ""),
  125. HTTP2HeaderTableField("if-none-match", ""),
  126. HTTP2HeaderTableField("if-range", ""),
  127. HTTP2HeaderTableField("if-unmodified-since", ""),
  128. HTTP2HeaderTableField("last-modified", ""),
  129. HTTP2HeaderTableField("link", ""),
  130. HTTP2HeaderTableField("location", ""),
  131. HTTP2HeaderTableField("max-forwards", ""),
  132. HTTP2HeaderTableField("proxy-authenticate", ""),
  133. HTTP2HeaderTableField("proxy-authorization", ""),
  134. HTTP2HeaderTableField("range", ""),
  135. HTTP2HeaderTableField("referer", ""),
  136. HTTP2HeaderTableField("refresh", ""),
  137. HTTP2HeaderTableField("retry-after", ""),
  138. HTTP2HeaderTableField("server", ""),
  139. HTTP2HeaderTableField("set-cookie", ""),
  140. HTTP2HeaderTableField("strict-transport-security", ""),
  141. HTTP2HeaderTableField("transfer-encoding", ""),
  142. HTTP2HeaderTableField("user-agent", ""),
  143. HTTP2HeaderTableField("vary", ""),
  144. HTTP2HeaderTableField("via", ""),
  145. HTTP2HeaderTableField("www-authenticate", "")
  146. ];
  147. }
  148. private ref immutable(HTTP2HeaderTableField) getStaticTableEntry(size_t key) @safe @nogc
  149. {
  150. assert(key > 0 && key < StaticTable.length, "Invalid static table index");
  151. return StaticTable[key];
  152. }
  153. // compute size of an entry as per RFC
  154. HTTP2SettingValue computeEntrySize(HTTP2HeaderTableField f) @safe
  155. {
  156. alias k = HTTP2HeaderTableField.value.Kind;
  157. HTTP2SettingValue ret = cast(HTTP2SettingValue)f.name.length + 32;
  158. final switch (f.value.kind) {
  159. case k.str: ret += f.value.get!string.length; break;
  160. case k.strarr: ret += f.value.get!(string[]).map!(s => s.length).sum(); break;
  161. case k.status: ret += cast(size_t)log10(cast(double)f.value.get!HTTPStatus) + 1; break;
  162. case k.method: ret += httpMethodString(f.value.get!HTTPMethod).length; break;
  163. }
  164. return ret;
  165. }
  166. private struct DynamicTable {
  167. private {
  168. // default table is 4096 octs. / n. octets of an empty HTTP2HeaderTableField struct (32)
  169. RingBuffer!(HTTP2HeaderTableField, DEFAULT_DYNAMIC_TABLE_SIZE/HTTP2HeaderTableField.sizeof, false) m_table;
  170. // extra table is a circular buffer, initially empty, used when
  171. // maxsize > DEFAULT_DYNAMIC_TABLE_SIZE
  172. RingBuffer!HTTP2HeaderTableField m_extraTable;
  173. // as defined in SETTINGS_HEADER_TABLE_SIZE
  174. HTTP2SettingValue m_maxsize;
  175. // current size
  176. size_t m_size = 0;
  177. // last index (table index starts from 1)
  178. size_t m_index = 0;
  179. // extra table index (starts from 0)
  180. size_t m_extraIndex = 0;
  181. }
  182. this(HTTP2SettingValue ms) @trusted nothrow
  183. {
  184. m_maxsize = ms;
  185. if(ms > DEFAULT_DYNAMIC_TABLE_SIZE) {
  186. m_extraTable.capacity = (ms - DEFAULT_DYNAMIC_TABLE_SIZE)/HTTP2HeaderTableField.sizeof;
  187. }
  188. }
  189. @property void dispose() nothrow { m_extraTable.dispose(); }
  190. // number of elements inside dynamic table
  191. @property size_t size() @safe @nogc { return m_size; }
  192. @property size_t index() @safe @nogc { return m_index; }
  193. HTTP2HeaderTableField opIndex(size_t idx) @safe @nogc
  194. {
  195. size_t totIndex = m_index + m_extraIndex;
  196. assert(idx > 0 && idx <= totIndex, "Invalid table index");
  197. if(idx > m_index && idx < totIndex) return m_extraTable[idx-m_index];
  198. else return m_table[idx-1];
  199. }
  200. // insert at the head
  201. void insert(HTTP2HeaderTableField header) @safe
  202. {
  203. auto nsize = computeEntrySize(header);
  204. // ensure that the new entry does not exceed table capacity
  205. while(m_size + nsize > m_maxsize) {
  206. //logDebug("Maximum header table size exceeded"); // requires gc
  207. remove();
  208. }
  209. // insert
  210. if(m_size + nsize > DEFAULT_DYNAMIC_TABLE_SIZE) {
  211. m_extraTable.put(header);
  212. m_extraIndex++;
  213. } else {
  214. m_table.put(header);
  215. m_index++;
  216. }
  217. m_size += nsize;
  218. }
  219. // evict an entry
  220. void remove() @safe
  221. {
  222. enforceHPACK(!m_table.empty, "Cannot remove element from empty table");
  223. if(m_extraIndex > 0) {
  224. m_size -= computeEntrySize(m_extraTable.back);
  225. m_extraTable.removeFront();
  226. m_extraIndex--;
  227. } else {
  228. m_size -= computeEntrySize(m_table.back);
  229. m_table.removeFront();
  230. m_index--;
  231. }
  232. }
  233. /** new size should be lower than the max set one
  234. * after size is successfully changed, an ACK has to be sent
  235. * multiple changes between two header fields are possible
  236. * if multiple changes occour, only the smallest maximum size
  237. * requested has to be acknowledged
  238. */
  239. void updateSize(HTTP2SettingValue sz) @safe @nogc
  240. {
  241. m_maxsize = sz;
  242. }
  243. }
  244. unittest {
  245. // static table
  246. auto a = getStaticTableEntry(1);
  247. static assert(is(typeof(a) == immutable(HTTP2HeaderTableField)));
  248. assert(a.name == ":authority");
  249. assert(getStaticTableEntry(2).name == ":method" && getStaticTableEntry(2).value == HTTPMethod.GET);
  250. DynamicTable dt = DynamicTable(DEFAULT_DYNAMIC_TABLE_SIZE+2048);
  251. assert(dt.size == 0);
  252. assert(dt.index == 0);
  253. // dynamic table
  254. import std.algorithm.comparison : equal;
  255. auto h = HTTP2HeaderTableField("test", "testval");
  256. dt.insert(h);
  257. assert(dt.size > 0);
  258. assert(dt.index == 1);
  259. assert(dt[dt.index].name == "test");
  260. dt.remove();
  261. assert(dt.size == 0);
  262. assert(dt.index == 0);
  263. }
  264. /** provides an unified address space through operator overloading
  265. * this is the only interface that will be used for the two tables
  266. */
  267. struct IndexingTable {
  268. private {
  269. DynamicTable m_dynamic;
  270. RecursiveTaskMutex m_lock;
  271. }
  272. // requires the maximum size for the dynamic table
  273. this(HTTP2SettingValue ms) @trusted nothrow
  274. {
  275. m_dynamic = DynamicTable(ms);
  276. try m_lock = new RecursiveTaskMutex;
  277. catch (Exception e) assert(false, e.msg);
  278. }
  279. ~this()
  280. nothrow {
  281. m_dynamic.dispose();
  282. }
  283. @property size_t size() @safe @nogc { return STATIC_TABLE_SIZE + m_dynamic.index + 1; }
  284. @property bool empty() @safe @nogc { return m_dynamic.size == 0; }
  285. @property HTTP2HeaderTableField front() @safe { return this[0]; }
  286. @property void popFront() @safe
  287. {
  288. assert(!empty, "Cannot call popFront on an empty dynamic table");
  289. m_lock.performLocked!({
  290. m_dynamic.remove();
  291. });
  292. }
  293. // element retrieval
  294. HTTP2HeaderTableField opIndex(size_t idx) @safe
  295. {
  296. enforceHPACK(idx > 0 && idx < size(), "Invalid HPACK table index");
  297. if (idx < STATIC_TABLE_SIZE+1) return getStaticTableEntry(idx);
  298. else return m_dynamic[m_dynamic.index - (idx - STATIC_TABLE_SIZE) + 1];
  299. }
  300. // dollar == size
  301. // +1 to mantain consistency with the dollar operator
  302. size_t opDollar() @safe @nogc
  303. {
  304. return size();
  305. }
  306. // assignment can only be done on the dynamic table
  307. void insert(HTTP2HeaderTableField hf) @safe
  308. {
  309. m_lock.performLocked!({
  310. m_dynamic.insert(hf);
  311. });
  312. }
  313. // update max dynamic table size
  314. void updateSize(HTTP2SettingValue sz) @safe
  315. {
  316. m_lock.performLocked!({
  317. m_dynamic.updateSize(sz);
  318. });
  319. }
  320. }
  321. unittest {
  322. // indexing table
  323. IndexingTable table = IndexingTable(DEFAULT_DYNAMIC_TABLE_SIZE);
  324. assert(table[2].name == ":method" && table[2].value == HTTPMethod.GET);
  325. // assignment
  326. auto h = HTTP2HeaderTableField("test", "testval");
  327. table.insert(h);
  328. assert(table.size == STATIC_TABLE_SIZE + 2);
  329. assert(table[STATIC_TABLE_SIZE+1].name == "test");
  330. auto h2 = HTTP2HeaderTableField("test2", "testval2");
  331. table.insert(h2);
  332. assert(table.size == STATIC_TABLE_SIZE + 3);
  333. assert(table[STATIC_TABLE_SIZE+1].name == "test2");
  334. // dollar
  335. auto h3 = HTTP2HeaderTableField("test3", "testval3");
  336. table.insert(h3);
  337. assert(table.size == STATIC_TABLE_SIZE + 4);
  338. assert(table[$-1].name == "test");
  339. assert(table[$-2].name == "test2");
  340. assert(table[STATIC_TABLE_SIZE+1].name == "test3");
  341. // test removal on full table
  342. HTTP2SettingValue hts = computeEntrySize(h); // only one header
  343. IndexingTable t2 = IndexingTable(hts);
  344. t2.insert(h);
  345. t2.insert(h);
  346. assert(t2.size == STATIC_TABLE_SIZE + 2);
  347. assert(t2[STATIC_TABLE_SIZE + 1].name == "test");
  348. assert(t2[$ - 1].name == "test");
  349. auto h4 = HTTP2HeaderTableField("","");
  350. hts = computeEntrySize(h4); // entry size of an empty field is 32 octets
  351. assert(hts == 32);
  352. }