From: Matus Fabian Date: Tue, 2 Sep 2025 15:27:00 +0000 (-0400) Subject: http: QPACK header decoding w/o dynamic table X-Git-Url: https://gerrit.fd.io/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F51%2F43651%2F5;p=vpp.git http: QPACK header decoding w/o dynamic table Type: feature Change-Id: Iee49328d21f4c2ab0f384de9864be6d2f65f50eb Signed-off-by: Matus Fabian --- diff --git a/src/plugins/http/CMakeLists.txt b/src/plugins/http/CMakeLists.txt index f5dabe20729..694d4fdcea6 100644 --- a/src/plugins/http/CMakeLists.txt +++ b/src/plugins/http/CMakeLists.txt @@ -16,6 +16,7 @@ add_vpp_plugin(http http2/hpack.c http2/http2.c http2/frame.c + http3/qpack.c http.c http_buffer.c http_timer.c diff --git a/src/plugins/http/http3/http3.h b/src/plugins/http/http3/http3.h new file mode 100644 index 00000000000..5557e3c1a90 --- /dev/null +++ b/src/plugins/http/http3/http3.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#ifndef SRC_PLUGINS_HTTP_HTTP3_H_ +#define SRC_PLUGINS_HTTP_HTTP3_H_ + +#include +#include + +#define foreach_http3_errors \ + _ (NO_ERROR, "NO_ERROR", 0x0100) \ + _ (GENERAL_PROTOCOL_ERROR, "GENERAL_PROTOCOL_ERROR", 0x0101) \ + _ (INTERNAL_ERROR, "INTERNAL_ERROR", 0x0102) \ + _ (STREAM_CREATION_ERROR, "STREAM_CREATION_ERROR", 0x0103) \ + _ (CLOSED_CRITICAL_STREAM, "CLOSED_CRITICAL_STREAM", 0x0104) \ + _ (FRAME_UNEXPECTED, "FRAME_UNEXPECTED", 0x0105) \ + _ (FRAME_ERROR, "FRAME_ERROR", 0x0106) \ + _ (EXCESSIVE_LOAD, "EXCESSIVE_LOAD", 0x0107) \ + _ (ID_ERROR, "ID_ERROR", 0x0108) \ + _ (SETTINGS_ERROR, "SETTINGS_ERROR", 0x0109) \ + _ (MISSING_SETTINGS, "MISSING_SETTINGS", 0x010a) \ + _ (REQUEST_REJECTED, "REQUEST_REJECTED", 0x010b) \ + _ (REQUEST_CANCELLED, "REQUEST_CANCELLED", 0x010c) \ + _ (REQUEST_INCOMPLETE, "REQUEST_INCOMPLETE", 0x010d) \ + _ (MESSAGE_ERROR, "MESSAGE_ERROR", 0x010e) \ + _ (CONNECT_ERROR, "CONNECT_ERROR", 0x010f) \ + _ (VERSION_FALLBACK, "VERSION_FALLBACK", 0x0110) \ + _ (QPACK_DECOMPRESSION_FAILED, "QPACK_DECOMPRESSION_FAILED", 0x0200) \ + _ (QPACK_ENCODER_STREAM_ERROR, "QPACK_ENCODER_STREAM_ERROR", 0x0201) \ + _ (QPACK_DECODER_STREAM_ERROR, "QPACK_DECODER_STREAM_ERROR", 0x0202) + +typedef enum +{ +#define _(sym, str, val) HTTP3_ERROR_##sym = val, + foreach_http3_errors +#undef _ +} http3_error_t; + +static inline u8 * +format_http3_error (u8 *s, va_list *va) +{ + http3_error_t e = va_arg (*va, http3_error_t); + u8 *t = 0; + + switch (e) + { +#define _(sym, str, val) \ + case HTTP3_ERROR_##sym: \ + t = (u8 *) str; \ + break; + foreach_http3_errors +#undef _ + default : return format (s, "BUG: unknown"); + } + return format (s, "%s", t); +} + +#endif /* SRC_PLUGINS_HTTP_HTTP3_H_ */ diff --git a/src/plugins/http/http3/qpack.c b/src/plugins/http/http3/qpack.c new file mode 100644 index 00000000000..6118c7b6547 --- /dev/null +++ b/src/plugins/http/http3/qpack.c @@ -0,0 +1,285 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include +#include + +typedef struct +{ + char *name; + uword name_len; + char *value; + uword value_len; +} qpack_static_table_entry_t; + +#define name_val_token_lit(name, value) \ + (name), sizeof (name) - 1, (value), sizeof (value) - 1 + +/* RFC9204 Appendix A */ +static qpack_static_table_entry_t qpack_static_table[] = { + { name_val_token_lit (":authority", "") }, + { name_val_token_lit (":path", "/") }, + { name_val_token_lit ("age", "0") }, + { name_val_token_lit ("content-disposition", "") }, + { name_val_token_lit ("content-length", "0") }, + { name_val_token_lit ("cookie", "") }, + { name_val_token_lit ("date", "") }, + { name_val_token_lit ("etag", "") }, + { name_val_token_lit ("if-modified-since", "") }, + { name_val_token_lit ("if-none-match", "") }, + { name_val_token_lit ("last-modified", "") }, + { name_val_token_lit ("link", "") }, + { name_val_token_lit ("location", "") }, + { name_val_token_lit ("referer", "") }, + { name_val_token_lit ("set-cookie", "") }, + { name_val_token_lit (":method", "CONNECT") }, + { name_val_token_lit (":metho", "DELETE") }, + { name_val_token_lit (":method", "GET") }, + { name_val_token_lit (":method", "HEAD") }, + { name_val_token_lit (":method", "OPTIONS") }, + { name_val_token_lit (":method", "POST") }, + { name_val_token_lit (":method", "PUT") }, + { name_val_token_lit (":scheme", "http") }, + { name_val_token_lit (":scheme", "https") }, + { name_val_token_lit (":status", "103") }, + { name_val_token_lit (":status", "200") }, + { name_val_token_lit (":status", "304") }, + { name_val_token_lit (":status", "404") }, + { name_val_token_lit (":status", "503") }, + { name_val_token_lit ("accept", "*/*") }, + { name_val_token_lit ("accept", "application/dns-message") }, + { name_val_token_lit ("accept-encoding", "gzip, deflate, br") }, + { name_val_token_lit ("accept-ranges", "bytes") }, + { name_val_token_lit ("access-control-allow-headers", "cache-control") }, + { name_val_token_lit ("access-control-allow-headers", "content-type") }, + { name_val_token_lit ("access-control-allow-origin", "*") }, + { name_val_token_lit ("cache-control", "max-age=0") }, + { name_val_token_lit ("cache-control", "max-age=2592000") }, + { name_val_token_lit ("cache-control", "max-age=604800") }, + { name_val_token_lit ("cache-control", "no-cache") }, + { name_val_token_lit ("cache-control", "no-store") }, + { name_val_token_lit ("cache-control", "public, max-age=31536000") }, + { name_val_token_lit ("content-encoding ", "r") }, + { name_val_token_lit ("content-encoding", "gzip") }, + { name_val_token_lit ("content-type", "application/dns-message") }, + { name_val_token_lit ("content-type", "application/javascript") }, + { name_val_token_lit ("content-type", "application/json") }, + { name_val_token_lit ("content-type", "application/x-www-form-urlencoded") }, + { name_val_token_lit ("content-type", "image/gif") }, + { name_val_token_lit ("content-type", "image/jpeg") }, + { name_val_token_lit ("content-type", "image/png") }, + { name_val_token_lit ("content-type", "text/css") }, + { name_val_token_lit ("content-type", "text/html; charset=utf-8") }, + { name_val_token_lit ("content-type", "text/plain") }, + { name_val_token_lit ("content-type", "text/plain;charset=utf-8") }, + { name_val_token_lit ("range", "bytes=0-") }, + { name_val_token_lit ("strict-transport-security", "max-age=31536000") }, + { name_val_token_lit ("strict-transport-security", + "max-age=31536000; includesubdomains") }, + { name_val_token_lit ("strict-transport-security", + "max-age=31536000; includesubdomains; preload") }, + { name_val_token_lit ("vary", "accept-encoding") }, + { name_val_token_lit ("vary", "origin") }, + { name_val_token_lit ("x-content-type-options", "nosniff") }, + { name_val_token_lit ("x-xss-protection", "1; mode=block") }, + { name_val_token_lit (":status", "100") }, + { name_val_token_lit (":status", "204") }, + { name_val_token_lit (":status", "206") }, + { name_val_token_lit (":status", "302") }, + { name_val_token_lit (":status", "400") }, + { name_val_token_lit (":status", "403") }, + { name_val_token_lit (":status", "421") }, + { name_val_token_lit (":status", "425") }, + { name_val_token_lit (":status", "500") }, + { name_val_token_lit ("accept-language", "") }, + { name_val_token_lit ("access-control-allow-credentials", "FALSE") }, + { name_val_token_lit ("access-control-allow-credentials", "TRUE") }, + { name_val_token_lit ("access-control-allow-headers", "*") }, + { name_val_token_lit ("access-control-allow-methods", "get") }, + { name_val_token_lit ("access-control-allow-methods", + "get, post, options") }, + { name_val_token_lit ("access-control-allow-methods", "options") }, + { name_val_token_lit ("access-control-expose-headers", "content-length") }, + { name_val_token_lit ("access-control-request-headers", "content-type") }, + { name_val_token_lit ("access-control-request-method", "get") }, + { name_val_token_lit ("access-control-request-method", "post") }, + { name_val_token_lit ("alt-svc", "clear") }, + { name_val_token_lit ("authorization", "") }, + { name_val_token_lit ( + "content-security-policy", + "script-src 'none'; object-src 'none'; base-uri 'none'") }, + { name_val_token_lit ("early-data", "1") }, + { name_val_token_lit ("expect-ct", "") }, + { name_val_token_lit ("forwarded", "") }, + { name_val_token_lit ("if-range", "") }, + { name_val_token_lit ("origin", "") }, + { name_val_token_lit ("purpose", "prefetch") }, + { name_val_token_lit ("server", "") }, + { name_val_token_lit ("timing-allow-origin", "*") }, + { name_val_token_lit ("upgrade-insecure-requests", "1") }, + { name_val_token_lit ("user-agent", "") }, + { name_val_token_lit ("x-forwarded-for", "") }, + { name_val_token_lit ("x-frame-options", "deny") }, + { name_val_token_lit ("x-frame-options", "sameorigin") }, +}; + +#define QPACK_STATIC_TABLE_SIZE \ + (sizeof (qpack_static_table) / sizeof (qpack_static_table[0])) + +STATIC_ASSERT (QPACK_STATIC_TABLE_SIZE == 99, + "static table must have 99 entries"); + +static http3_error_t +qpack_get_static_table_entry (uword index, http_token_t *name, + http_token_t *value, u8 value_is_indexed) +{ + if (index >= QPACK_STATIC_TABLE_SIZE) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + + qpack_static_table_entry_t *e = &qpack_static_table[index]; + name->base = e->name; + name->len = e->name_len; + if (value_is_indexed) + { + if (e->value_len == 0) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + value->base = e->value; + value->len = e->value_len; + } + + return HTTP3_ERROR_NO_ERROR; +} + +static http3_error_t +qpack_decode_string (u8 **src, u8 *end, u8 **buf, uword *buf_len, + u8 prefix_len) +{ + u8 *p, is_huffman; + uword len; + int rv; + + ASSERT (prefix_len >= 2 && prefix_len <= 8); + if (*src == end) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + + p = *src; + /* first bit for H flag */ + is_huffman = (*p >> (prefix_len - 1)) & 0x01; + + /* length is integer with (N-1) bit prefix */ + len = hpack_decode_int (&p, end, prefix_len - 1); + if (PREDICT_FALSE (len == HPACK_INVALID_INT)) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + + /* do we have everything? */ + if (len > (end - p)) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + + if (is_huffman) + { + *src = (p + len); + rv = hpack_decode_huffman (&p, p + len, buf, buf_len); + return rv ? HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED : + HTTP3_ERROR_NO_ERROR; + } + else + { + /* enough space? */ + if (len > *buf_len) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + + clib_memcpy (*buf, p, len); + *buf_len -= len; + *buf += len; + *src = (p + len); + return HTTP3_ERROR_NO_ERROR; + } +} + +__clib_export http3_error_t +qpack_decode_header (u8 **src, u8 *end, u8 **buf, uword *buf_len, + u32 *name_len, u32 *value_len, void *decoder_ctx) +{ + u8 *p; + uword index, old_len; + http_token_t name, value; + http3_error_t rv; + + ASSERT (*src < end); + p = *src; + +#define COPY_TOKEN(_token) \ + if (_token.len > *buf_len) \ + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; \ + clib_memcpy (*buf, _token.base, _token.len); \ + *buf_len -= _token.len; \ + *buf += _token.len; + + switch (*p >> 4) + { + case 12 ... 15: + /* indexed field line, static table */ + index = hpack_decode_int (&p, end, 6); + if (index == HPACK_INVALID_INT) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + rv = qpack_get_static_table_entry (index, &name, &value, 1); + if (rv != HTTP3_ERROR_NO_ERROR) + return rv; + COPY_TOKEN (name); + *name_len = name.len; + COPY_TOKEN (value); + *value_len = value.len; + break; + case 8 ... 11: + /* TODO: indexed field line, dynamic table */ + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + case 7: + case 5: + /* literal field line with name reference, static table */ + index = hpack_decode_int (&p, end, 4); + if (index == HPACK_INVALID_INT) + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + rv = qpack_get_static_table_entry (index, &name, &value, 0); + if (rv != HTTP3_ERROR_NO_ERROR) + return rv; + COPY_TOKEN (name); + *name_len = name.len; + old_len = *buf_len; + rv = qpack_decode_string (&p, end, buf, buf_len, 8); + if (rv != HTTP3_ERROR_NO_ERROR) + return rv; + *value_len = old_len - *buf_len; + break; + case 6: + case 4: + /* TODO: literal field line with name reference, dynamic table */ + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + case 3: + case 2: + /* literal field line with literal name */ + old_len = *buf_len; + rv = qpack_decode_string (&p, end, buf, buf_len, 4); + if (rv != HTTP3_ERROR_NO_ERROR) + return rv; + *name_len = old_len - *buf_len; + old_len = *buf_len; + rv = qpack_decode_string (&p, end, buf, buf_len, 8); + if (rv != HTTP3_ERROR_NO_ERROR) + return rv; + *value_len = old_len - *buf_len; + break; + case 1: + /* TODO: indexed field line with post-base index */ + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + case 0: + /* TODO: literal field line with post-base name reference */ + return HTTP3_ERROR_QPACK_DECOMPRESSION_FAILED; + default: + ASSERT (0); + break; + } + + *src = p; + return HTTP3_ERROR_NO_ERROR; +} diff --git a/src/plugins/http/http3/qpack.h b/src/plugins/http/http3/qpack.h new file mode 100644 index 00000000000..c24e933a6b0 --- /dev/null +++ b/src/plugins/http/http3/qpack.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#ifndef SRC_PLUGINS_HTTP_QPACK_H_ +#define SRC_PLUGINS_HTTP_QPACK_H_ + +#include +#include + +#endif /* SRC_PLUGINS_HTTP_QPACK_H_ */ diff --git a/src/plugins/http/test/http_test.c b/src/plugins/http/test/http_test.c index cf04fc1a2af..a4ecc7ac13f 100644 --- a/src/plugins/http/test/http_test.c +++ b/src/plugins/http/test/http_test.c @@ -8,6 +8,7 @@ #include #include #include +#include #define HTTP_TEST_I(_cond, _comment, _args...) \ ({ \ @@ -1617,6 +1618,55 @@ http_test_h2_frame (vlib_main_t *vm) return 0; } +static int +http_test_qpack (vlib_main_t *vm) +{ + vlib_cli_output (vm, "qpack_decode_header"); + + static http3_error_t (*_qpack_decode_header) ( + u8 * *src, u8 * end, u8 * *buf, uword * buf_len, u32 * name_len, + u32 * value_len); + + _qpack_decode_header = + vlib_get_plugin_symbol ("http_plugin.so", "qpack_decode_header"); + + u8 *pos, *bp, *buf = 0, *input = 0; + uword blen; + u32 name_len, value_len; + http3_error_t rv; + +#define TEST(i, e_name, e_value) \ + vec_validate (input, sizeof (i) - 2); \ + memcpy (input, i, sizeof (i) - 1); \ + pos = input; \ + vec_validate_init_empty (buf, 63, 0); \ + bp = buf; \ + blen = vec_len (buf); \ + rv = _qpack_decode_header (&pos, vec_end (input), &bp, &blen, &name_len, \ + &value_len); \ + HTTP_TEST ((rv == HTTP3_ERROR_NO_ERROR && name_len == strlen (e_name) && \ + value_len == strlen (e_value) && \ + !memcmp (buf, e_name, name_len) && \ + !memcmp (buf + name_len, e_value, value_len) && \ + vec_len (buf) == (blen + name_len + value_len) && \ + pos == vec_end (input) && bp == buf + name_len + value_len), \ + "%U is decoded as '%U: %U'", format_hex_bytes, input, \ + vec_len (input), format_http_bytes, buf, name_len, \ + format_http_bytes, buf + name_len, value_len); \ + vec_free (input); \ + vec_free (buf); + + /* literal field line with name reference, static table */ + TEST ("\x51\x0B\x2F\x69\x6E\x64\x65\x78\x2E\x68\x74\x6D\x6C", ":path", + "/index.html"); + /* indexed field line, static table */ + TEST ("\xC1", ":path", "/"); + /* literal field line with literal name */ + TEST ("\x23\x61\x62\x63\x01\x5A", "abc", "Z"); + + return 0; +} + static clib_error_t * test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd) @@ -1638,6 +1688,8 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, res = http_test_hpack (vm); else if (unformat (input, "h2-frame")) res = http_test_h2_frame (vm); + else if (unformat (input, "qpack")) + res = http_test_qpack (vm); else if (unformat (input, "all")) { if ((res = http_test_parse_authority (vm))) @@ -1654,6 +1706,8 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, goto done; if ((res = http_test_h2_frame (vm))) goto done; + if ((res = http_test_qpack (vm))) + goto done; } else break;