hpack.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /**
  2. * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  3. * SPDX-License-Identifier: Apache-2.0.
  4. */
  5. #include <aws/http/private/hpack.h>
  6. /* #TODO test empty strings */
  7. /* #TODO remove all OOM error handling in HTTP/2 & HPACK. make functions void if possible */
  8. /* RFC-7540 6.5.2 */
  9. const size_t s_hpack_dynamic_table_initial_size = 4096;
  10. const size_t s_hpack_dynamic_table_initial_elements = 512;
  11. /* TODO: shouldn't be a hardcoded max_size, it should be driven by SETTINGS_HEADER_TABLE_SIZE */
  12. const size_t s_hpack_dynamic_table_max_size = 16 * 1024 * 1024;
  13. /* Used for growing the dynamic table buffer when it fills up */
  14. const float s_hpack_dynamic_table_buffer_growth_rate = 1.5F;
  15. struct aws_http_header s_static_header_table[] = {
  16. #define HEADER(_index, _name) \
  17. [_index] = { \
  18. .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(_name), \
  19. },
  20. #define HEADER_WITH_VALUE(_index, _name, _value) \
  21. [_index] = { \
  22. .name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(_name), \
  23. .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(_value), \
  24. },
  25. #include <aws/http/private/hpack_header_static_table.def>
  26. #undef HEADER
  27. #undef HEADER_WITH_VALUE
  28. };
  29. static const size_t s_static_header_table_size = AWS_ARRAY_SIZE(s_static_header_table);
  30. struct aws_byte_cursor s_static_header_table_name_only[] = {
  31. #define HEADER(_index, _name) [_index] = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(_name),
  32. #define HEADER_WITH_VALUE(_index, _name, _value) HEADER(_index, _name)
  33. #include <aws/http/private/hpack_header_static_table.def>
  34. #undef HEADER
  35. #undef HEADER_WITH_VALUE
  36. };
  37. /* aws_http_header * -> size_t */
  38. static struct aws_hash_table s_static_header_reverse_lookup;
  39. /* aws_byte_cursor * -> size_t */
  40. static struct aws_hash_table s_static_header_reverse_lookup_name_only;
  41. static uint64_t s_header_hash(const void *key) {
  42. const struct aws_http_header *header = key;
  43. return aws_hash_combine(aws_hash_byte_cursor_ptr(&header->name), aws_hash_byte_cursor_ptr(&header->value));
  44. }
  45. static bool s_header_eq(const void *a, const void *b) {
  46. const struct aws_http_header *left = a;
  47. const struct aws_http_header *right = b;
  48. if (!aws_byte_cursor_eq(&left->name, &right->name)) {
  49. return false;
  50. }
  51. /* If the header stored in the table doesn't have a value, then it's a match */
  52. return aws_byte_cursor_eq(&left->value, &right->value);
  53. }
  54. void aws_hpack_static_table_init(struct aws_allocator *allocator) {
  55. int result = aws_hash_table_init(
  56. &s_static_header_reverse_lookup,
  57. allocator,
  58. s_static_header_table_size - 1,
  59. s_header_hash,
  60. s_header_eq,
  61. NULL,
  62. NULL);
  63. AWS_FATAL_ASSERT(AWS_OP_SUCCESS == result);
  64. result = aws_hash_table_init(
  65. &s_static_header_reverse_lookup_name_only,
  66. allocator,
  67. s_static_header_table_size - 1,
  68. aws_hash_byte_cursor_ptr,
  69. (aws_hash_callback_eq_fn *)aws_byte_cursor_eq,
  70. NULL,
  71. NULL);
  72. AWS_FATAL_ASSERT(AWS_OP_SUCCESS == result);
  73. /* Process in reverse so that name_only prefers lower indices */
  74. for (size_t i = s_static_header_table_size - 1; i > 0; --i) {
  75. /* the tables are created as 1-based indexing */
  76. result = aws_hash_table_put(&s_static_header_reverse_lookup, &s_static_header_table[i], (void *)i, NULL);
  77. AWS_FATAL_ASSERT(AWS_OP_SUCCESS == result);
  78. result = aws_hash_table_put(
  79. &s_static_header_reverse_lookup_name_only, &s_static_header_table_name_only[i], (void *)(i), NULL);
  80. AWS_FATAL_ASSERT(AWS_OP_SUCCESS == result);
  81. }
  82. }
  83. void aws_hpack_static_table_clean_up() {
  84. aws_hash_table_clean_up(&s_static_header_reverse_lookup);
  85. aws_hash_table_clean_up(&s_static_header_reverse_lookup_name_only);
  86. }
  87. #define HPACK_LOGF(level, hpack, text, ...) \
  88. AWS_LOGF_##level((hpack)->log_subject, "id=%p [HPACK]: " text, (hpack)->log_id, __VA_ARGS__)
  89. #define HPACK_LOG(level, hpack, text) HPACK_LOGF(level, hpack, "%s", text)
  90. void aws_hpack_context_init(
  91. struct aws_hpack_context *context,
  92. struct aws_allocator *allocator,
  93. enum aws_http_log_subject log_subject,
  94. const void *log_id) {
  95. AWS_ZERO_STRUCT(*context);
  96. context->allocator = allocator;
  97. context->log_subject = log_subject;
  98. context->log_id = log_id;
  99. /* Initialize dynamic table */
  100. context->dynamic_table.max_size = s_hpack_dynamic_table_initial_size;
  101. context->dynamic_table.buffer_capacity = s_hpack_dynamic_table_initial_elements;
  102. context->dynamic_table.buffer =
  103. aws_mem_calloc(allocator, context->dynamic_table.buffer_capacity, sizeof(struct aws_http_header));
  104. aws_hash_table_init(
  105. &context->dynamic_table.reverse_lookup,
  106. allocator,
  107. s_hpack_dynamic_table_initial_elements,
  108. s_header_hash,
  109. s_header_eq,
  110. NULL,
  111. NULL);
  112. aws_hash_table_init(
  113. &context->dynamic_table.reverse_lookup_name_only,
  114. allocator,
  115. s_hpack_dynamic_table_initial_elements,
  116. aws_hash_byte_cursor_ptr,
  117. (aws_hash_callback_eq_fn *)aws_byte_cursor_eq,
  118. NULL,
  119. NULL);
  120. }
  121. static struct aws_http_header *s_dynamic_table_get(const struct aws_hpack_context *context, size_t index);
  122. static void s_clean_up_dynamic_table_buffer(struct aws_hpack_context *context) {
  123. while (context->dynamic_table.num_elements > 0) {
  124. struct aws_http_header *back = s_dynamic_table_get(context, context->dynamic_table.num_elements - 1);
  125. context->dynamic_table.num_elements -= 1;
  126. /* clean-up the memory we allocate for it */
  127. aws_mem_release(context->allocator, back->name.ptr);
  128. }
  129. aws_mem_release(context->allocator, context->dynamic_table.buffer);
  130. }
  131. void aws_hpack_context_clean_up(struct aws_hpack_context *context) {
  132. if (context->dynamic_table.buffer) {
  133. s_clean_up_dynamic_table_buffer(context);
  134. }
  135. aws_hash_table_clean_up(&context->dynamic_table.reverse_lookup);
  136. aws_hash_table_clean_up(&context->dynamic_table.reverse_lookup_name_only);
  137. AWS_ZERO_STRUCT(*context);
  138. }
  139. size_t aws_hpack_get_header_size(const struct aws_http_header *header) {
  140. return header->name.len + header->value.len + 32;
  141. }
  142. size_t aws_hpack_get_dynamic_table_num_elements(const struct aws_hpack_context *context) {
  143. return context->dynamic_table.num_elements;
  144. }
  145. size_t aws_hpack_get_dynamic_table_max_size(const struct aws_hpack_context *context) {
  146. return context->dynamic_table.max_size;
  147. }
  148. /*
  149. * Gets the header from the dynamic table.
  150. * NOTE: This function only bounds checks on the buffer size, not the number of elements.
  151. */
  152. static struct aws_http_header *s_dynamic_table_get(const struct aws_hpack_context *context, size_t index) {
  153. AWS_ASSERT(index < context->dynamic_table.buffer_capacity);
  154. return &context->dynamic_table
  155. .buffer[(context->dynamic_table.index_0 + index) % context->dynamic_table.buffer_capacity];
  156. }
  157. const struct aws_http_header *aws_hpack_get_header(const struct aws_hpack_context *context, size_t index) {
  158. if (index == 0 || index >= s_static_header_table_size + context->dynamic_table.num_elements) {
  159. aws_raise_error(AWS_ERROR_INVALID_INDEX);
  160. return NULL;
  161. }
  162. /* Check static table */
  163. if (index < s_static_header_table_size) {
  164. return &s_static_header_table[index];
  165. }
  166. /* Check dynamic table */
  167. return s_dynamic_table_get(context, index - s_static_header_table_size);
  168. }
  169. /* TODO: remove `bool search_value`, this option has no reason to exist */
  170. size_t aws_hpack_find_index(
  171. const struct aws_hpack_context *context,
  172. const struct aws_http_header *header,
  173. bool search_value,
  174. bool *found_value) {
  175. *found_value = false;
  176. struct aws_hash_element *elem = NULL;
  177. if (search_value) {
  178. /* Check name-and-value first in static table */
  179. aws_hash_table_find(&s_static_header_reverse_lookup, header, &elem);
  180. if (elem) {
  181. /* TODO: Maybe always set found_value to true? Who cares that the value is empty if they matched? */
  182. /* If an element was found, check if it has a value */
  183. *found_value = ((const struct aws_http_header *)elem->key)->value.len;
  184. return (size_t)elem->value;
  185. }
  186. /* Check name-and-value in dynamic table */
  187. aws_hash_table_find(&context->dynamic_table.reverse_lookup, header, &elem);
  188. if (elem) {
  189. /* TODO: Maybe always set found_value to true? Who cares that the value is empty if they matched? */
  190. *found_value = ((const struct aws_http_header *)elem->key)->value.len;
  191. goto trans_index_from_dynamic_table;
  192. }
  193. }
  194. /* Check the name-only table. Note, even if we search for value, when we fail in searching for name-and-value, we
  195. * should also check the name only table */
  196. aws_hash_table_find(&s_static_header_reverse_lookup_name_only, &header->name, &elem);
  197. if (elem) {
  198. return (size_t)elem->value;
  199. }
  200. aws_hash_table_find(&context->dynamic_table.reverse_lookup_name_only, &header->name, &elem);
  201. if (elem) {
  202. goto trans_index_from_dynamic_table;
  203. }
  204. return 0;
  205. trans_index_from_dynamic_table:
  206. AWS_ASSERT(elem);
  207. size_t index;
  208. const size_t absolute_index = (size_t)elem->value;
  209. if (absolute_index >= context->dynamic_table.index_0) {
  210. index = absolute_index - context->dynamic_table.index_0;
  211. } else {
  212. index = (context->dynamic_table.buffer_capacity - context->dynamic_table.index_0) + absolute_index;
  213. }
  214. /* Need to add the static table size to re-base indicies */
  215. index += s_static_header_table_size;
  216. return index;
  217. }
  218. /* Remove elements from the dynamic table until it fits in max_size bytes */
  219. static int s_dynamic_table_shrink(struct aws_hpack_context *context, size_t max_size) {
  220. while (context->dynamic_table.size > max_size && context->dynamic_table.num_elements > 0) {
  221. struct aws_http_header *back = s_dynamic_table_get(context, context->dynamic_table.num_elements - 1);
  222. /* "Remove" the header from the table */
  223. context->dynamic_table.size -= aws_hpack_get_header_size(back);
  224. context->dynamic_table.num_elements -= 1;
  225. /* Remove old header from hash tables */
  226. if (aws_hash_table_remove(&context->dynamic_table.reverse_lookup, back, NULL, NULL)) {
  227. HPACK_LOG(ERROR, context, "Failed to remove header from the reverse lookup table");
  228. goto error;
  229. }
  230. /* If the name-only lookup is pointing to the element we're removing, it needs to go.
  231. * If not, it's pointing to a younger, sexier element. */
  232. struct aws_hash_element *elem = NULL;
  233. aws_hash_table_find(&context->dynamic_table.reverse_lookup_name_only, &back->name, &elem);
  234. if (elem && elem->key == back) {
  235. if (aws_hash_table_remove_element(&context->dynamic_table.reverse_lookup_name_only, elem)) {
  236. HPACK_LOG(ERROR, context, "Failed to remove header from the reverse lookup (name-only) table");
  237. goto error;
  238. }
  239. }
  240. /* clean up the memory we allocated to hold the name and value string*/
  241. aws_mem_release(context->allocator, back->name.ptr);
  242. }
  243. return AWS_OP_SUCCESS;
  244. error:
  245. return AWS_OP_ERR;
  246. }
  247. /*
  248. * Resizes the dynamic table storage buffer to new_max_elements.
  249. * Useful when inserting over capacity, or when downsizing.
  250. * Do shrink first, if you want to remove elements, or memory leak will happen.
  251. */
  252. static int s_dynamic_table_resize_buffer(struct aws_hpack_context *context, size_t new_max_elements) {
  253. /* Clear the old hash tables */
  254. aws_hash_table_clear(&context->dynamic_table.reverse_lookup);
  255. aws_hash_table_clear(&context->dynamic_table.reverse_lookup_name_only);
  256. struct aws_http_header *new_buffer = NULL;
  257. if (AWS_UNLIKELY(new_max_elements == 0)) {
  258. /* If new buffer is of size 0, don't both initializing, just clean up the old one. */
  259. goto cleanup_old_buffer;
  260. }
  261. /* Allocate the new buffer */
  262. new_buffer = aws_mem_calloc(context->allocator, new_max_elements, sizeof(struct aws_http_header));
  263. if (!new_buffer) {
  264. return AWS_OP_ERR;
  265. }
  266. /* Don't bother copying data if old buffer was of size 0 */
  267. if (AWS_UNLIKELY(context->dynamic_table.num_elements == 0)) {
  268. goto reset_dyn_table_state;
  269. }
  270. /*
  271. * Take a buffer that looks like this:
  272. *
  273. * Index 0
  274. * ^
  275. * +---------------------------+
  276. * | Below Block | Above Block |
  277. * +---------------------------+
  278. * And make it look like this:
  279. *
  280. * Index 0
  281. * ^
  282. * +-------------+-------------+
  283. * | Above Block | Below Block |
  284. * +-------------+-------------+
  285. */
  286. /* Copy as much the above block as possible */
  287. size_t above_block_size = context->dynamic_table.buffer_capacity - context->dynamic_table.index_0;
  288. if (above_block_size > new_max_elements) {
  289. above_block_size = new_max_elements;
  290. }
  291. memcpy(
  292. new_buffer,
  293. context->dynamic_table.buffer + context->dynamic_table.index_0,
  294. above_block_size * sizeof(struct aws_http_header));
  295. /* Copy as much of below block as possible */
  296. const size_t free_blocks_available = new_max_elements - above_block_size;
  297. const size_t old_blocks_to_copy = context->dynamic_table.buffer_capacity - above_block_size;
  298. const size_t below_block_size = aws_min_size(free_blocks_available, old_blocks_to_copy);
  299. if (below_block_size) {
  300. memcpy(
  301. new_buffer + above_block_size,
  302. context->dynamic_table.buffer,
  303. below_block_size * sizeof(struct aws_http_header));
  304. }
  305. /* Free the old memory */
  306. cleanup_old_buffer:
  307. aws_mem_release(context->allocator, context->dynamic_table.buffer);
  308. /* Reset state */
  309. reset_dyn_table_state:
  310. if (context->dynamic_table.num_elements > new_max_elements) {
  311. context->dynamic_table.num_elements = new_max_elements;
  312. }
  313. context->dynamic_table.buffer_capacity = new_max_elements;
  314. context->dynamic_table.index_0 = 0;
  315. context->dynamic_table.buffer = new_buffer;
  316. /* Re-insert all of the reverse lookup elements */
  317. for (size_t i = 0; i < context->dynamic_table.num_elements; ++i) {
  318. if (aws_hash_table_put(
  319. &context->dynamic_table.reverse_lookup, &context->dynamic_table.buffer[i], (void *)i, NULL)) {
  320. return AWS_OP_ERR;
  321. }
  322. if (aws_hash_table_put(
  323. &context->dynamic_table.reverse_lookup_name_only,
  324. &context->dynamic_table.buffer[i].name,
  325. (void *)i,
  326. NULL)) {
  327. return AWS_OP_ERR;
  328. }
  329. }
  330. return AWS_OP_SUCCESS;
  331. }
  332. int aws_hpack_insert_header(struct aws_hpack_context *context, const struct aws_http_header *header) {
  333. /* Don't move forward if no elements allowed in the dynamic table */
  334. if (AWS_UNLIKELY(context->dynamic_table.max_size == 0)) {
  335. return AWS_OP_SUCCESS;
  336. }
  337. const size_t header_size = aws_hpack_get_header_size(header);
  338. /* If for whatever reason this new header is bigger than the total table size, burn everything to the ground. */
  339. if (AWS_UNLIKELY(header_size > context->dynamic_table.max_size)) {
  340. /* #TODO handle this. It's not an error. It should simply result in an empty table RFC-7541 4.4 */
  341. goto error;
  342. }
  343. /* Rotate out headers until there's room for the new header (this function will return immediately if nothing needs
  344. * to be evicted) */
  345. if (s_dynamic_table_shrink(context, context->dynamic_table.max_size - header_size)) {
  346. goto error;
  347. }
  348. /* If we're out of space in the buffer, grow it */
  349. if (context->dynamic_table.num_elements == context->dynamic_table.buffer_capacity) {
  350. /* If the buffer is currently of 0 size, reset it back to its initial size */
  351. const size_t new_size =
  352. context->dynamic_table.buffer_capacity
  353. ? (size_t)(context->dynamic_table.buffer_capacity * s_hpack_dynamic_table_buffer_growth_rate)
  354. : s_hpack_dynamic_table_initial_elements;
  355. if (s_dynamic_table_resize_buffer(context, new_size)) {
  356. goto error;
  357. }
  358. }
  359. /* Decrement index 0, wrapping if necessary */
  360. if (context->dynamic_table.index_0 == 0) {
  361. context->dynamic_table.index_0 = context->dynamic_table.buffer_capacity - 1;
  362. } else {
  363. context->dynamic_table.index_0--;
  364. }
  365. /* Increment num_elements */
  366. context->dynamic_table.num_elements++;
  367. /* Increment the size */
  368. context->dynamic_table.size += header_size;
  369. /* Put the header at the "front" of the table */
  370. struct aws_http_header *table_header = s_dynamic_table_get(context, 0);
  371. /* TODO:: We can optimize this with ring buffer. */
  372. /* allocate memory for the name and value, which will be deallocated whenever the entry is evicted from the table or
  373. * the table is cleaned up. We keep the pointer in the name pointer of each entry */
  374. const size_t buf_memory_size = header->name.len + header->value.len;
  375. if (buf_memory_size) {
  376. uint8_t *buf_memory = aws_mem_acquire(context->allocator, buf_memory_size);
  377. if (!buf_memory) {
  378. return AWS_OP_ERR;
  379. }
  380. struct aws_byte_buf buf = aws_byte_buf_from_empty_array(buf_memory, buf_memory_size);
  381. /* Copy header, then backup strings into our own allocation */
  382. *table_header = *header;
  383. aws_byte_buf_append_and_update(&buf, &table_header->name);
  384. aws_byte_buf_append_and_update(&buf, &table_header->value);
  385. } else {
  386. /* if buf_memory_size is 0, no memory needed, we will insert the empty header into dynamic table */
  387. *table_header = *header;
  388. table_header->name.ptr = NULL;
  389. table_header->value.ptr = NULL;
  390. }
  391. /* Write the new header to the look up tables */
  392. if (aws_hash_table_put(
  393. &context->dynamic_table.reverse_lookup, table_header, (void *)context->dynamic_table.index_0, NULL)) {
  394. goto error;
  395. }
  396. /* Note that we can just blindly put here, we want to overwrite any older entry so it isn't accidentally removed. */
  397. if (aws_hash_table_put(
  398. &context->dynamic_table.reverse_lookup_name_only,
  399. &table_header->name,
  400. (void *)context->dynamic_table.index_0,
  401. NULL)) {
  402. goto error;
  403. }
  404. return AWS_OP_SUCCESS;
  405. error:
  406. /* Do not attempt to handle the error, if something goes wrong, close the connection */
  407. return AWS_OP_ERR;
  408. }
  409. int aws_hpack_resize_dynamic_table(struct aws_hpack_context *context, size_t new_max_size) {
  410. /* Nothing to see here! */
  411. if (new_max_size == context->dynamic_table.max_size) {
  412. return AWS_OP_SUCCESS;
  413. }
  414. if (new_max_size > s_hpack_dynamic_table_max_size) {
  415. HPACK_LOGF(
  416. ERROR,
  417. context,
  418. "New dynamic table max size %zu is greater than the supported max size (%zu)",
  419. new_max_size,
  420. s_hpack_dynamic_table_max_size);
  421. aws_raise_error(AWS_ERROR_OVERFLOW_DETECTED);
  422. goto error;
  423. }
  424. /* If downsizing, remove elements until we're within the new size constraints */
  425. if (s_dynamic_table_shrink(context, new_max_size)) {
  426. goto error;
  427. }
  428. /* Resize the buffer to the current size */
  429. if (s_dynamic_table_resize_buffer(context, context->dynamic_table.num_elements)) {
  430. goto error;
  431. }
  432. /* Update the max size */
  433. context->dynamic_table.max_size = new_max_size;
  434. return AWS_OP_SUCCESS;
  435. error:
  436. return AWS_OP_ERR;
  437. }