Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 1 | /* |
| 2 | * HPACK header table management (RFC7541) - type definitions and prototypes |
| 3 | * |
| 4 | * Copyright (C) 2014-2017 Willy Tarreau <willy@haproxy.org> |
| 5 | * Copyright (C) 2017 HAProxy Technologies |
| 6 | * |
| 7 | * Permission is hereby granted, free of charge, to any person obtaining |
| 8 | * a copy of this software and associated documentation files (the |
| 9 | * "Software"), to deal in the Software without restriction, including |
| 10 | * without limitation the rights to use, copy, modify, merge, publish, |
| 11 | * distribute, sublicense, and/or sell copies of the Software, and to |
| 12 | * permit persons to whom the Software is furnished to do so, subject to |
| 13 | * the following conditions: |
| 14 | * |
| 15 | * The above copyright notice and this permission notice shall be |
| 16 | * included in all copies or substantial portions of the Software. |
| 17 | * |
| 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES |
| 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
| 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
| 25 | * OTHER DEALINGS IN THE SOFTWARE. |
| 26 | */ |
| 27 | #ifndef _COMMON_HPACK_TBL_H |
| 28 | #define _COMMON_HPACK_TBL_H |
| 29 | |
Willy Tarreau | a1bd1fa | 2019-03-29 17:26:33 +0100 | [diff] [blame] | 30 | #include <inttypes.h> |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 31 | #include <stdlib.h> |
| 32 | #include <common/config.h> |
| 33 | #include <common/http-hdr.h> |
| 34 | #include <common/ist.h> |
Willy Tarreau | 2bdcc70 | 2020-05-19 11:31:11 +0200 | [diff] [blame] | 35 | #include <common/memory.h> |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 36 | |
| 37 | /* Dynamic Headers Table, usable for tables up to 4GB long and values of 64kB-1. |
| 38 | * The model can be improved by using offsets relative to the table entry's end |
| 39 | * or to the end of the area, or by moving the descriptors at the end of the |
| 40 | * table and the data at the beginning. This entry is 8 bytes long, which is 1/4 |
| 41 | * of the bookkeeping planned by the HPACK spec. Thus it saves 24 bytes per |
| 42 | * header field, meaning that even with a single header, 24 extra bytes can be |
| 43 | * stored (ie one such descriptor). At 29.2 average bytes per header field as |
| 44 | * found in the hpack test case, that's slightly more than 1.5kB of space saved |
| 45 | * from a 4kB block, resulting in contiguous space almost always being |
| 46 | * available. |
| 47 | * |
| 48 | * Principle: the table is stored in a contiguous array containing both the |
| 49 | * descriptors and the contents. Descriptors are stored at the beginning of the |
| 50 | * array while contents are stored starting from the end. Most of the time there |
| 51 | * is enough room left in the table to insert a new header field, thanks to the |
| 52 | * savings on the descriptor size. Thus by inserting headers from the end it's |
| 53 | * possible to maximize the delay before a collision of DTEs and data. In order |
| 54 | * to always insert from the right, we need to keep a reference to the latest |
| 55 | * inserted element and look before it. The last inserted cell's address defines |
Ilya Shipitsin | 77e3b4a | 2020-03-10 12:06:11 +0500 | [diff] [blame] | 56 | * the lowest known address still in use, unless the area wraps in which case |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 57 | * the available space lies between the end of the tail and the beginning of the |
| 58 | * head. |
| 59 | * |
| 60 | * In order to detect collisions between data blocks and DTEs, we also maintain |
| 61 | * an index to the lowest element facing the DTE table, called "front". This one |
| 62 | * is updated each time an element is inserted before it. Once the buffer wraps, |
| 63 | * this element doesn't have to be updated anymore until it is released, in |
| 64 | * which case the buffer doesn't wrap anymore and the front element becomes the |
| 65 | * head again. |
| 66 | * |
| 67 | * Various heuristics are possible concerning the opportunity to wrap the |
| 68 | * entries to limit the risk of collisions with the DTE, but experimentation |
| 69 | * shows that thanks to the important savings made on the descriptors, the |
| 70 | * likeliness of finding a large amount of free space at the end of the area is |
| 71 | * much higher than the risk of colliding, so in the end the most naive |
| 72 | * algorithms work pretty fine. Typical ratios of 1 collision per 2000 requests |
| 73 | * have been observed. |
| 74 | * |
| 75 | * The defragmentation should be rare ; a study on live data shows on average |
| 76 | * 29.2 bytes used per header field. This plus the 32 bytes overhead fix an |
| 77 | * average of 66.9 header fields per 4kB table. This brings a 1606 bytes saving |
| 78 | * using the current storage description, ensuring that oldest headers are |
| 79 | * linearly removed by the sender before fragmentation occurs. This means that |
| 80 | * for all smaller header fields there will not be any requirement to defragment |
| 81 | * the area and most of the time it will even be possible to copy the old values |
| 82 | * directly within the buffer after creating a new entry. On average within the |
| 83 | * available space there will be enough room to store 1606/(29.2+8)=43 extra |
| 84 | * header fields without switching to another place. |
| 85 | * |
| 86 | * The table header fits in the table itself, it only takes 16 bytes, so in the |
| 87 | * worst case (1 single header) it's possible to store 4096 - 16 - 8 = 4072 |
| 88 | * data bytes, which is larger than the 4064 the protocol requires (4096 - 32). |
| 89 | */ |
| 90 | |
| 91 | /* One dynamic table entry descriptor */ |
| 92 | struct hpack_dte { |
| 93 | uint32_t addr; /* storage address, relative to the dte address */ |
| 94 | uint16_t nlen; /* header name length */ |
| 95 | uint16_t vlen; /* header value length */ |
| 96 | }; |
| 97 | |
| 98 | /* Note: the table's head plus a struct hpack_dte must be smaller than or equal to 32 |
| 99 | * bytes so that a single large header can always fit. Here that's 16 bytes for |
| 100 | * the header, plus 8 bytes per slot. |
| 101 | * Note that when <used> == 0, front, head, and wrap are undefined. |
| 102 | */ |
| 103 | struct hpack_dht { |
| 104 | uint32_t size; /* allocated table size in bytes */ |
| 105 | uint32_t total; /* sum of nlen + vlen in bytes */ |
| 106 | uint16_t front; /* slot number of the first node after the idx table */ |
| 107 | uint16_t wrap; /* number of allocated slots, wraps here */ |
| 108 | uint16_t head; /* last inserted slot number */ |
| 109 | uint16_t used; /* number of slots in use */ |
| 110 | struct hpack_dte dte[0]; /* dynamic table entries */ |
| 111 | }; |
| 112 | |
| 113 | /* supported hpack encoding/decoding errors */ |
| 114 | enum { |
| 115 | HPACK_ERR_NONE = 0, /* no error */ |
| 116 | HPACK_ERR_ALLOC_FAIL, /* memory allocation error */ |
| 117 | HPACK_ERR_UNKNOWN_OPCODE, /* invalid first byte */ |
| 118 | HPACK_ERR_TRUNCATED, /* truncated stream */ |
| 119 | HPACK_ERR_HUFFMAN, /* huffman decoding error */ |
| 120 | HPACK_ERR_INVALID_PHDR, /* invalid pseudo header field name */ |
| 121 | HPACK_ERR_MISPLACED_PHDR, /* pseudo header field after a regular header field */ |
| 122 | HPACK_ERR_DUPLICATE_PHDR, /* duplicate pseudo header field */ |
| 123 | HPACK_ERR_DHT_INSERT_FAIL, /* failed to insert into DHT */ |
| 124 | HPACK_ERR_TOO_LARGE, /* decoded request/response is too large */ |
| 125 | HPACK_ERR_MISSING_METHOD, /* :method is missing */ |
| 126 | HPACK_ERR_MISSING_SCHEME, /* :scheme is missing */ |
| 127 | HPACK_ERR_MISSING_PATH, /* :path is missing */ |
| 128 | HPACK_ERR_MISSING_AUTHORITY, /* :authority is missing with CONNECT */ |
| 129 | HPACK_ERR_SCHEME_NOT_ALLOWED, /* :scheme not allowed with CONNECT */ |
| 130 | HPACK_ERR_PATH_NOT_ALLOWED, /* :path not allowed with CONNECT */ |
Willy Tarreau | 1e7d444 | 2019-01-24 10:47:10 +0100 | [diff] [blame] | 131 | HPACK_ERR_INVALID_ARGUMENT, /* an invalid argument was passed */ |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 132 | }; |
| 133 | |
| 134 | /* static header table as in RFC7541 Appendix A. [0] unused. */ |
| 135 | #define HPACK_SHT_SIZE 62 |
| 136 | extern const struct http_hdr hpack_sht[HPACK_SHT_SIZE]; |
Willy Tarreau | 2bdcc70 | 2020-05-19 11:31:11 +0200 | [diff] [blame] | 137 | extern struct pool_head *pool_head_hpack_tbl; |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 138 | |
Willy Tarreau | 0ff9b3d | 2020-05-22 12:05:27 +0200 | [diff] [blame] | 139 | /* when built outside of haproxy, HPACK_STANDALONE must be defined, and |
| 140 | * pool_head_hpack_tbl->size must be set to the DHT size. |
| 141 | */ |
| 142 | #ifndef HPACK_STANDALONE |
| 143 | #define hpack_alloc(pool) pool_alloc(pool) |
| 144 | #define hpack_free(pool, ptr) pool_free(pool, ptr) |
| 145 | #else |
| 146 | #define hpack_alloc(pool) malloc(pool->size) |
| 147 | #define hpack_free(pool, ptr) free(ptr) |
| 148 | #endif |
| 149 | |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 150 | extern int __hpack_dht_make_room(struct hpack_dht *dht, unsigned int needed); |
| 151 | extern int hpack_dht_insert(struct hpack_dht *dht, struct ist name, struct ist value); |
| 152 | |
| 153 | /* return a pointer to the entry designated by index <idx> (starting at 1) or |
| 154 | * NULL if this index is not there. |
| 155 | */ |
| 156 | static inline const struct hpack_dte *hpack_get_dte(const struct hpack_dht *dht, uint16_t idx) |
| 157 | { |
| 158 | idx--; |
| 159 | |
| 160 | if (idx >= dht->used) |
| 161 | return NULL; |
| 162 | |
| 163 | if (idx <= dht->head) |
| 164 | idx = dht->head - idx; |
| 165 | else |
| 166 | idx = dht->head - idx + dht->wrap; |
| 167 | |
| 168 | return &dht->dte[idx]; |
| 169 | } |
| 170 | |
Willy Tarreau | d85ba4e | 2017-12-03 12:12:17 +0100 | [diff] [blame] | 171 | /* returns non-zero if <idx> is valid for table <dht> */ |
Willy Tarreau | 7f2a44d | 2018-09-17 14:07:33 +0200 | [diff] [blame] | 172 | static inline int hpack_valid_idx(const struct hpack_dht *dht, uint32_t idx) |
Willy Tarreau | d85ba4e | 2017-12-03 12:12:17 +0100 | [diff] [blame] | 173 | { |
| 174 | return idx < dht->used + HPACK_SHT_SIZE; |
| 175 | } |
| 176 | |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 177 | /* return a pointer to the header name for entry <dte>. */ |
| 178 | static inline struct ist hpack_get_name(const struct hpack_dht *dht, const struct hpack_dte *dte) |
| 179 | { |
| 180 | struct ist ret = { |
| 181 | .ptr = (void *)dht + dte->addr, |
| 182 | .len = dte->nlen, |
| 183 | }; |
| 184 | return ret; |
| 185 | } |
| 186 | |
| 187 | /* return a pointer to the header value for entry <dte>. */ |
| 188 | static inline struct ist hpack_get_value(const struct hpack_dht *dht, const struct hpack_dte *dte) |
| 189 | { |
| 190 | struct ist ret = { |
| 191 | .ptr = (void *)dht + dte->addr + dte->nlen, |
| 192 | .len = dte->vlen, |
| 193 | }; |
| 194 | return ret; |
| 195 | } |
| 196 | |
| 197 | /* takes an idx, returns the associated name */ |
Willy Tarreau | 7f2a44d | 2018-09-17 14:07:33 +0200 | [diff] [blame] | 198 | static inline struct ist hpack_idx_to_name(const struct hpack_dht *dht, uint32_t idx) |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 199 | { |
| 200 | const struct hpack_dte *dte; |
| 201 | |
| 202 | if (idx < HPACK_SHT_SIZE) |
| 203 | return hpack_sht[idx].n; |
| 204 | |
| 205 | dte = hpack_get_dte(dht, idx - HPACK_SHT_SIZE + 1); |
| 206 | if (!dte) |
| 207 | return ist("### ERR ###"); // error |
| 208 | |
| 209 | return hpack_get_name(dht, dte); |
| 210 | } |
| 211 | |
| 212 | /* takes an idx, returns the associated value */ |
Willy Tarreau | 7f2a44d | 2018-09-17 14:07:33 +0200 | [diff] [blame] | 213 | static inline struct ist hpack_idx_to_value(const struct hpack_dht *dht, uint32_t idx) |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 214 | { |
| 215 | const struct hpack_dte *dte; |
| 216 | |
| 217 | if (idx < HPACK_SHT_SIZE) |
| 218 | return hpack_sht[idx].v; |
| 219 | |
| 220 | dte = hpack_get_dte(dht, idx - HPACK_SHT_SIZE + 1); |
| 221 | if (!dte) |
| 222 | return ist("### ERR ###"); // error |
| 223 | |
| 224 | return hpack_get_value(dht, dte); |
| 225 | } |
| 226 | |
| 227 | /* Purges table dht until a header field of <needed> bytes fits according to |
| 228 | * the protocol (adding 32 bytes overhead). Returns non-zero on success, zero |
| 229 | * on failure (ie: table empty but still not sufficient). |
| 230 | */ |
| 231 | static inline int hpack_dht_make_room(struct hpack_dht *dht, unsigned int needed) |
| 232 | { |
Willy Tarreau | 6c71e46 | 2017-12-04 17:58:37 +0100 | [diff] [blame] | 233 | if (dht->used * 32 + dht->total + needed + 32 <= dht->size) |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 234 | return 1; |
Willy Tarreau | 6c71e46 | 2017-12-04 17:58:37 +0100 | [diff] [blame] | 235 | else if (!dht->used) |
| 236 | return 0; |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 237 | |
| 238 | return __hpack_dht_make_room(dht, needed); |
| 239 | } |
| 240 | |
| 241 | /* allocate a dynamic headers table of <size> bytes and return it initialized */ |
| 242 | static inline void hpack_dht_init(struct hpack_dht *dht, uint32_t size) |
| 243 | { |
| 244 | dht->size = size; |
| 245 | dht->total = 0; |
| 246 | dht->used = 0; |
| 247 | } |
| 248 | |
Willy Tarreau | 2bdcc70 | 2020-05-19 11:31:11 +0200 | [diff] [blame] | 249 | /* allocate a dynamic headers table from the pool and return it initialized */ |
| 250 | static inline struct hpack_dht *hpack_dht_alloc() |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 251 | { |
| 252 | struct hpack_dht *dht; |
| 253 | |
Willy Tarreau | 2bdcc70 | 2020-05-19 11:31:11 +0200 | [diff] [blame] | 254 | if (unlikely(!pool_head_hpack_tbl)) |
| 255 | return NULL; |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 256 | |
Willy Tarreau | 0ff9b3d | 2020-05-22 12:05:27 +0200 | [diff] [blame] | 257 | dht = hpack_alloc(pool_head_hpack_tbl); |
Willy Tarreau | 2bdcc70 | 2020-05-19 11:31:11 +0200 | [diff] [blame] | 258 | if (dht) |
| 259 | hpack_dht_init(dht, pool_head_hpack_tbl->size); |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 260 | return dht; |
| 261 | } |
| 262 | |
| 263 | /* free a dynamic headers table */ |
| 264 | static inline void hpack_dht_free(struct hpack_dht *dht) |
| 265 | { |
Willy Tarreau | 0ff9b3d | 2020-05-22 12:05:27 +0200 | [diff] [blame] | 266 | hpack_free(pool_head_hpack_tbl, dht); |
Willy Tarreau | ce04094 | 2017-05-30 18:46:58 +0200 | [diff] [blame] | 267 | } |
| 268 | |
| 269 | #endif /* _COMMON_HPACK_TBL_H */ |