http: QPACK header decoding w/o dynamic table 51/43651/5
authorMatus Fabian <[email protected]>
Tue, 2 Sep 2025 15:27:00 +0000 (11:27 -0400)
committerFlorin Coras <[email protected]>
Mon, 29 Sep 2025 16:24:59 +0000 (16:24 +0000)
Type: feature

Change-Id: Iee49328d21f4c2ab0f384de9864be6d2f65f50eb
Signed-off-by: Matus Fabian <[email protected]>
src/plugins/http/CMakeLists.txt
src/plugins/http/http3/http3.h [new file with mode: 0644]
src/plugins/http/http3/qpack.c [new file with mode: 0644]
src/plugins/http/http3/qpack.h [new file with mode: 0644]
src/plugins/http/test/http_test.c

index f5dabe2..694d4fd 100644 (file)
@@ -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 (file)
index 0000000..5557e3c
--- /dev/null
@@ -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 <vppinfra/format.h>
+#include <vppinfra/types.h>
+
+#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 (file)
index 0000000..6118c7b
--- /dev/null
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <http/http3/qpack.h>
+#include <http/http2/hpack.h>
+
+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 (file)
index 0000000..c24e933
--- /dev/null
@@ -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 <http/http3/http3.h>
+#include <http/http.h>
+
+#endif /* SRC_PLUGINS_HTTP_QPACK_H_ */
index cf04fc1..a4ecc7a 100644 (file)
@@ -8,6 +8,7 @@
 #include <http/http_header_names.h>
 #include <http/http2/hpack.h>
 #include <http/http2/frame.h>
+#include <http/http3/qpack.h>
 
 #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;