http: case-insensitive header table search 99/42199/4
authorMatus Fabian <[email protected]>
Fri, 17 Jan 2025 19:46:30 +0000 (14:46 -0500)
committerFlorin Coras <[email protected]>
Wed, 22 Jan 2025 19:07:24 +0000 (19:07 +0000)
header names are case-insensitive, see RFC9110 section 5.1

Type: improvement

Change-Id: Ic5ef5615397537ba48fc56b18c882fa838c54751
Signed-off-by: Matus Fabian <[email protected]>
src/plugins/hs_apps/http_cli.c
src/plugins/hs_apps/proxy.c
src/plugins/http/http.h
src/plugins/http/http_plugin.rst
src/plugins/http/test/http_test.c

index 89eb49c..d670ae6 100644 (file)
@@ -419,14 +419,14 @@ hcs_ts_rx_callback (session_t *ts)
                          msg.data.headers_len, hs->req_headers.buf);
       ASSERT (rv == msg.data.headers_len);
       http_build_header_table (&hs->req_headers, msg);
-      const http_header_t *accept = http_get_header (
+      const http_token_t *accept_value = http_get_header (
        &hs->req_headers, http_header_name_token (HTTP_HEADER_ACCEPT));
-      if (accept)
+      if (accept_value)
        {
-         HCS_DBG ("client accept: %U", format_http_bytes, accept->value.base,
-                  accept->value.len);
+         HCS_DBG ("client accept: %U", format_http_bytes, accept_value->base,
+                  accept_value->len);
          /* just for testing purpose, we don't care about precedence */
-         if (http_token_contains (accept->value.base, accept->value.len,
+         if (http_token_contains (accept_value->base, accept_value->len,
                                   http_token_lit ("text/plain")))
            args.plain_text = 1;
        }
index f3b1fdc..7407bcb 100644 (file)
@@ -592,13 +592,12 @@ proxy_http_connect (session_t *s, vnet_connect_args_t *a)
                          msg.data.headers_len, req_headers.buf);
       ASSERT (rv == msg.data.headers_len);
       http_build_header_table (&req_headers, msg);
-      const http_header_t *capsule_protocol = http_get_header (
+      const http_token_t *capsule_protocol = http_get_header (
        &req_headers, http_header_name_token (HTTP_HEADER_CAPSULE_PROTOCOL));
       if (capsule_protocol)
        {
          PROXY_DBG ("Capsule-Protocol header present");
-         if (!http_token_is (capsule_protocol->value.base,
-                             capsule_protocol->value.len,
+         if (!http_token_is (capsule_protocol->base, capsule_protocol->len,
                              http_token_lit (HTTP_BOOLEAN_TRUE)))
            {
              PROXY_DBG ("Capsule-Protocol invalid value");
index 008f663..637452e 100644 (file)
@@ -861,7 +861,7 @@ typedef struct
 
 typedef struct
 {
-  http_header_t *headers;
+  http_token_t *values;
   uword *value_by_name;
   u8 *buf;
   char **concatenated_values;
@@ -869,7 +869,7 @@ typedef struct
 
 #define HTTP_HEADER_TABLE_NULL                                                \
   {                                                                           \
-    .headers = 0, .value_by_name = 0, .buf = 0, .concatenated_values = 0,     \
+    .values = 0, .value_by_name = 0, .buf = 0, .concatenated_values = 0,      \
   }
 
 always_inline u8
@@ -882,17 +882,52 @@ http_token_is (const char *actual, uword actual_len, const char *expected,
   return memcmp (actual, expected, expected_len) == 0 ? 1 : 0;
 }
 
+/* Based on searching for a value in a given range from Hacker's Delight */
+always_inline uword
+http_tolower_word (uword x)
+{
+#if uword_bits == 64
+  uword all_bytes = 0x0101010101010101;
+#else
+  uword all_bytes = 0x01010101;
+#endif
+  uword d, y;
+  d = (x | (0x80 * all_bytes)) - (0x41 * all_bytes);
+  d = ~((x | (0x7F * all_bytes)) ^ d);
+  y = (d & (0x7F * all_bytes)) + (0x66 * all_bytes);
+  y = y | d;
+  y = y | (0x7F * all_bytes);
+  y = ~y;
+  y = (y >> 2) & (0x20 * all_bytes);
+  return (x | y);
+}
+
 always_inline u8
 http_token_is_case (const char *actual, uword actual_len, const char *expected,
                    uword expected_len)
 {
-  uword i;
+  uword i, last_a = 0, last_e = 0;
+  uword *a, *e;
   ASSERT (actual != 0);
   if (actual_len != expected_len)
     return 0;
-  for (i = 0; i < expected_len; i++)
+
+  i = expected_len;
+  a = (uword *) actual;
+  e = (uword *) expected;
+  while (i >= sizeof (uword))
     {
-      if (tolower (actual[i]) != tolower (expected[i]))
+      if (http_tolower_word (*a) != http_tolower_word (*e))
+       return 0;
+      a++;
+      e++;
+      i -= sizeof (uword);
+    }
+  if (i > 0)
+    {
+      clib_memcpy_fast (&last_a, a, i);
+      clib_memcpy_fast (&last_e, e, i);
+      if (http_tolower_word (last_a) != http_tolower_word (last_e))
        return 0;
     }
   return 1;
@@ -927,7 +962,7 @@ http_reset_header_table (http_header_table_t *ht)
   for (i = 0; i < vec_len (ht->concatenated_values); i++)
     vec_free (ht->concatenated_values[i]);
   vec_reset_length (ht->concatenated_values);
-  vec_reset_length (ht->headers);
+  vec_reset_length (ht->values);
   vec_reset_length (ht->buf);
   hash_free (ht->value_by_name);
 }
@@ -955,7 +990,7 @@ http_free_header_table (http_header_table_t *ht)
   for (i = 0; i < vec_len (ht->concatenated_values); i++)
     vec_free (ht->concatenated_values[i]);
   vec_free (ht->concatenated_values);
-  vec_free (ht->headers);
+  vec_free (ht->values);
   vec_free (ht->buf);
   hash_free (ht->value_by_name);
 }
@@ -964,7 +999,37 @@ static uword
 _http_ht_hash_key_sum (hash_t *h, uword key)
 {
   http_token_t *name = uword_to_pointer (key, http_token_t *);
-  return hash_memory (name->base, name->len, 0);
+  uword last[3] = {};
+  uwordu *q = (uword *) name->base;
+  u64 a, b, c, n;
+
+  a = b = (uword_bits == 64) ? 0x9e3779b97f4a7c13LL : 0x9e3779b9;
+  c = 0;
+  n = name->len;
+
+  while (n >= 3 * sizeof (uword))
+    {
+      a += http_tolower_word (q[0]);
+      b += http_tolower_word (q[1]);
+      c += http_tolower_word (q[2]);
+      hash_mix (a, b, c);
+      n -= 3 * sizeof (uword);
+      q += 3;
+    }
+
+  c += name->len;
+
+  if (n > 0)
+    {
+      clib_memcpy_fast (&last, q, n);
+      a += http_tolower_word (last[0]);
+      b += http_tolower_word (last[1]);
+      c += http_tolower_word (last[2]);
+    }
+
+  hash_mix (a, b, c);
+
+  return c;
 }
 
 static uword
@@ -973,7 +1038,22 @@ _http_ht_hash_key_equal (hash_t *h, uword key1, uword key2)
   http_token_t *name1 = uword_to_pointer (key1, http_token_t *);
   http_token_t *name2 = uword_to_pointer (key2, http_token_t *);
   return name1 && name2 &&
-        http_token_is (name1->base, name1->len, name2->base, name2->len);
+        http_token_is_case (name1->base, name1->len, name2->base, name2->len);
+}
+
+static u8 *
+_http_ht_format_pair (u8 *s, va_list *args)
+{
+  http_header_table_t *ht = va_arg (*args, http_header_table_t *);
+  void *CLIB_UNUSED (*v) = va_arg (*args, void *);
+  hash_pair_t *p = va_arg (*args, hash_pair_t *);
+  http_token_t *name = uword_to_pointer (p->key, http_token_t *);
+  http_token_t *value = vec_elt_at_index (ht->values, p->value[0]);
+
+  s = format (s, "%U: %U", format_http_bytes, name->base, name->len,
+             format_http_bytes, value->base, value->len);
+
+  return s;
 }
 
 /**
@@ -988,16 +1068,15 @@ _http_ht_hash_key_equal (hash_t *h, uword key1, uword key2)
 always_inline void
 http_build_header_table (http_header_table_t *ht, http_msg_t msg)
 {
-  http_token_t name;
-  http_header_t *header;
+  http_token_t name, *value;
   http_field_line_t *field_lines, *field_line;
   uword *p;
 
   ASSERT (ht);
   field_lines = uword_to_pointer (msg.data.headers_ctx, http_field_line_t *);
-  ht->value_by_name =
-    hash_create2 (0, 0, sizeof (uword), _http_ht_hash_key_sum,
-                 _http_ht_hash_key_equal, 0, 0);
+  ht->value_by_name = hash_create2 (
+    0, sizeof (http_token_t), sizeof (uword), _http_ht_hash_key_sum,
+    _http_ht_hash_key_equal, _http_ht_format_pair, ht);
 
   vec_foreach (field_line, field_lines)
     {
@@ -1008,27 +1087,25 @@ http_build_header_table (http_header_table_t *ht, http_msg_t msg)
       if (p)
        {
          char *new_value = 0;
-         header = vec_elt_at_index (ht->headers, p[0]);
-         u32 new_len = header->value.len + field_line->value_len + 2;
+         value = vec_elt_at_index (ht->values, p[0]);
+         u32 new_len = value->len + field_line->value_len + 2;
          vec_validate (new_value, new_len - 1);
-         clib_memcpy (new_value, header->value.base, header->value.len);
-         new_value[header->value.len] = ',';
-         new_value[header->value.len + 1] = ' ';
-         clib_memcpy (new_value + header->value.len + 2,
+         clib_memcpy (new_value, value->base, value->len);
+         new_value[value->len] = ',';
+         new_value[value->len + 1] = ' ';
+         clib_memcpy (new_value + value->len + 2,
                       ht->buf + field_line->value_offset,
                       field_line->value_len);
          vec_add1 (ht->concatenated_values, new_value);
-         header->value.base = new_value;
-         header->value.len = new_len;
+         value->base = new_value;
+         value->len = new_len;
          continue;
        }
       /* or create new record */
-      vec_add2 (ht->headers, header, 1);
-      header->name.base = name.base;
-      header->name.len = name.len;
-      header->value.base = (char *) (ht->buf + field_line->value_offset);
-      header->value.len = field_line->value_len;
-      hash_set_mem (ht->value_by_name, &header->name, header - ht->headers);
+      vec_add2 (ht->values, value, 1);
+      value->base = (char *) (ht->buf + field_line->value_offset);
+      value->len = field_line->value_len;
+      hash_set_mem_alloc (&ht->value_by_name, &name, value - ht->values);
     }
 }
 
@@ -1038,21 +1115,21 @@ http_build_header_table (http_header_table_t *ht, http_msg_t msg)
  * @param header_table Header table to search.
  * @param name Header name to match.
  *
- * @return Header in case of success, @c 0 otherwise.
+ * @return Header value in case of success, @c 0 otherwise.
  */
-always_inline const http_header_t *
+always_inline const http_token_t *
 http_get_header (http_header_table_t *header_table, const char *name,
                 uword name_len)
 {
   uword *p;
-  http_header_t *header;
+  http_token_t *value;
   http_token_t name_token = { (char *) name, name_len };
 
   p = hash_get_mem (header_table->value_by_name, &name_token);
   if (p)
     {
-      header = vec_elt_at_index (header_table->headers, p[0]);
-      return header;
+      value = vec_elt_at_index (header_table->values, p[0]);
+      return value;
     }
 
   return 0;
index 90ffb19..55c5afc 100644 (file)
@@ -139,11 +139,14 @@ Following example shows how to parse headers:
       /* build header table */
       http_build_header_table (&ht, msg);
       /* get Accept header */
-      const http_header_t *accept = http_get_header (&ht,
+      const http_token_t *accept_value = http_get_header (&ht,
         http_header_name_token (HTTP_HEADER_ACCEPT));
       if (accept_value)
         {
-          /* do something interesting */
+          if (http_token_contains (accept_value->base, accept_value->len, http_token_lit ("text/plain")))
+            {
+              /* do something interesting */
+            }
         }
       /* free header table */
       http_free_header_table (&ht);
@@ -187,7 +190,7 @@ Modified example above:
       /* build header table */
       http_build_header_table (&ctx->ht, msg);
       /* get Accept header */
-      const http_header_t *accept = http_get_header (&ctx->ht,
+      const http_token_t *accept_value = http_get_header (&ctx->ht,
         http_header_name_token (HTTP_HEADER_ACCEPT));
       if (accept_value)
         {
@@ -495,7 +498,7 @@ Following example shows how to parse headers:
       /* build header table */
       http_build_header_table (&ht, msg);
       /* get Content-Type header */
-      const http_header_t *content_type = http_get_header (&ht,
+      const http_token_t *content_type = http_get_header (&ht,
         http_header_name_token (HTTP_HEADER_CONTENT_TYPE));
       if (content_type)
         {
index 7cf36f8..bfaa285 100644 (file)
@@ -5,6 +5,7 @@
 #include <vnet/plugin/plugin.h>
 #include <vpp/app/version.h>
 #include <http/http.h>
+#include <http/http_header_names.h>
 
 #define HTTP_TEST_I(_cond, _comment, _args...)                                \
   ({                                                                          \
@@ -350,6 +351,188 @@ http_test_udp_payload_datagram (vlib_main_t *vm)
   return 0;
 }
 
+static int
+http_test_http_token_is_case (vlib_main_t *vm)
+{
+  static const char eq_1[] = "content-length";
+  static const char eq_2[] = "CONtENT-lenGth";
+  static const char eq_3[] = "caPsulE-ProtOcol";
+  static const char eq_4[] = "ACCESS-CONTROL-REQUEST-METHOD";
+  static const char ne_1[] = "content_length";
+  static const char ne_2[] = "content-lengXh";
+  static const char ne_3[] = "coNtent-lengXh";
+  static const char ne_4[] = "content-len";
+  static const char ne_5[] = "comtent-length";
+  static const char ne_6[] = "content-lengtR";
+  u8 rv;
+
+  rv = http_token_is_case (
+    eq_1, strlen (eq_1), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 1), "'%s' and '%s' are equal", eq_1,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    eq_2, strlen (eq_2), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 1), "'%s' and '%s' are equal", eq_2,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv =
+    http_token_is_case (eq_3, strlen (eq_3),
+                       http_header_name_token (HTTP_HEADER_CAPSULE_PROTOCOL));
+  HTTP_TEST ((rv == 1), "'%s' and '%s' are equal", eq_3,
+            http_header_name_str (HTTP_HEADER_CAPSULE_PROTOCOL))
+
+  rv = http_token_is_case (
+    eq_4, strlen (eq_4),
+    http_header_name_token (HTTP_HEADER_ACCESS_CONTROL_REQUEST_METHOD));
+  HTTP_TEST ((rv == 1), "'%s' and '%s' are equal", eq_4,
+            http_header_name_str (HTTP_HEADER_ACCESS_CONTROL_REQUEST_METHOD))
+
+  rv = http_token_is_case (
+    ne_1, strlen (ne_1), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_1,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    ne_2, strlen (ne_2), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_2,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    ne_3, strlen (ne_3), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_3,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    ne_4, strlen (ne_4), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_4,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    ne_5, strlen (ne_5), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_5,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  rv = http_token_is_case (
+    ne_6, strlen (ne_6), http_header_name_token (HTTP_HEADER_CONTENT_LENGTH));
+  HTTP_TEST ((rv == 0), "'%s' and '%s' are not equal", ne_6,
+            http_header_name_str (HTTP_HEADER_CONTENT_LENGTH))
+
+  return 0;
+}
+
+static int
+http_test_http_header_table (vlib_main_t *vm)
+{
+  http_header_table_t ht = HTTP_HEADER_TABLE_NULL;
+  const char buf[] = "daTe: Wed, 15 Jan 2025 16:17:33 GMT"
+                    "conTent-tYpE: text/html; charset=utf-8"
+                    "STRICT-transport-security: max-age=31536000"
+                    "sAnDwich: Eggs"
+                    "CONTENT-ENCODING: GZIP"
+                    "sandwich: Spam";
+  http_msg_t msg = {};
+  http_field_line_t *headers = 0, *field_line;
+  const http_token_t *value;
+  u8 rv;
+
+  /* daTe */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 0;
+  field_line->name_len = 4;
+  field_line->value_offset = 6;
+  field_line->value_len = 29;
+  /* conTent-tYpE */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 35;
+  field_line->name_len = 12;
+  field_line->value_offset = 49;
+  field_line->value_len = 24;
+  /* STRICT-transport-security */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 73;
+  field_line->name_len = 25;
+  field_line->value_offset = 100;
+  field_line->value_len = 16;
+  /* sAnDwich */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 116;
+  field_line->name_len = 8;
+  field_line->value_offset = 126;
+  field_line->value_len = 4;
+  /* CONTENT-ENCODING */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 130;
+  field_line->name_len = 16;
+  field_line->value_offset = 148;
+  field_line->value_len = 4;
+  /* sandwich */
+  vec_add2 (headers, field_line, 1);
+  field_line->name_offset = 152;
+  field_line->name_len = 8;
+  field_line->value_offset = 162;
+  field_line->value_len = 4;
+
+  msg.data.headers_ctx = pointer_to_uword (headers);
+  msg.data.headers_len = strlen (buf);
+
+  http_init_header_table_buf (&ht, msg);
+  memcpy (ht.buf, buf, strlen (buf));
+  http_build_header_table (&ht, msg);
+
+  vlib_cli_output (vm, "%U", format_hash, ht.value_by_name, 1);
+
+  value = http_get_header (
+    &ht, http_header_name_token (HTTP_HEADER_CONTENT_ENCODING));
+  HTTP_TEST ((value != 0), "'%s' is in headers",
+            http_header_name_str (HTTP_HEADER_CONTENT_ENCODING));
+  rv = http_token_is (value->base, value->len, http_token_lit ("GZIP"));
+  HTTP_TEST ((rv = 1), "header value '%U' should be 'GZIP'", format_http_bytes,
+            value->base, value->len);
+
+  value =
+    http_get_header (&ht, http_header_name_token (HTTP_HEADER_CONTENT_TYPE));
+  HTTP_TEST ((value != 0), "'%s' is in headers",
+            http_header_name_str (HTTP_HEADER_CONTENT_TYPE));
+
+  value = http_get_header (&ht, http_header_name_token (HTTP_HEADER_DATE));
+  HTTP_TEST ((value != 0), "'%s' is in headers",
+            http_header_name_str (HTTP_HEADER_DATE));
+
+  value = http_get_header (
+    &ht, http_header_name_token (HTTP_HEADER_STRICT_TRANSPORT_SECURITY));
+  HTTP_TEST ((value != 0), "'%s' is in headers",
+            http_header_name_str (HTTP_HEADER_STRICT_TRANSPORT_SECURITY));
+
+  value = http_get_header (&ht, http_token_lit ("DATE"));
+  HTTP_TEST ((value != 0), "'DATE' is in headers");
+
+  value = http_get_header (&ht, http_token_lit ("date"));
+  HTTP_TEST ((value != 0), "'date' is in headers");
+
+  /* repeated header */
+  value = http_get_header (&ht, http_token_lit ("sandwich"));
+  HTTP_TEST ((value != 0), "'sandwich' is in headers");
+  rv = http_token_is (value->base, value->len, http_token_lit ("Eggs, Spam"));
+  HTTP_TEST ((rv = 1), "header value '%U' should be 'Eggs, Spam'",
+            format_http_bytes, value->base, value->len);
+
+  value = http_get_header (&ht, http_token_lit ("Jade"));
+  HTTP_TEST ((value == 0), "'Jade' is not in headers");
+
+  value = http_get_header (&ht, http_token_lit ("CONTENT"));
+  HTTP_TEST ((value == 0), "'CONTENT' is not in headers");
+
+  value =
+    http_get_header (&ht, http_header_name_token (HTTP_HEADER_ACCEPT_CHARSET));
+  HTTP_TEST ((value == 0), "'%s' is not in headers",
+            http_header_name_str (HTTP_HEADER_ACCEPT_CHARSET));
+
+  http_free_header_table (&ht);
+  vec_free (headers);
+  return 0;
+}
+
 static clib_error_t *
 test_http_command_fn (vlib_main_t *vm, unformat_input_t *input,
                      vlib_cli_command_t *cmd)
@@ -363,6 +546,10 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input,
        res = http_test_parse_masque_host_port (vm);
       else if (unformat (input, "udp-payload-datagram"))
        res = http_test_udp_payload_datagram (vm);
+      else if (unformat (input, "token-is-case"))
+       res = http_test_http_token_is_case (vm);
+      else if (unformat (input, "header-table"))
+       res = http_test_http_header_table (vm);
       else if (unformat (input, "all"))
        {
          if ((res = http_test_parse_authority (vm)))
@@ -371,6 +558,10 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input,
            goto done;
          if ((res = http_test_udp_payload_datagram (vm)))
            goto done;
+         if ((res = http_test_http_token_is_case (vm)))
+           goto done;
+         if ((res = http_test_http_header_table (vm)))
+           goto done;
        }
       else
        break;