http: connection upgrade mechanism 18/41918/8
authorMatus Fabian <[email protected]>
Thu, 21 Nov 2024 16:01:45 +0000 (17:01 +0100)
committerFlorin Coras <[email protected]>
Tue, 10 Dec 2024 05:10:41 +0000 (05:10 +0000)
Handle "Connection" and "Upgrade" headers in http transport layer which
are used to create a tunnel for some other protocol on the same
connection.

Type: improvement

Change-Id: Icf5479f36fbcc7259b157eaad957211be5ea2aae
Signed-off-by: Matus Fabian <[email protected]>
extras/hs-test/http_test.go
extras/hs-test/infra/suite_vpp_proxy.go
src/plugins/hs_apps/http_cli.c
src/plugins/hs_apps/http_client.c
src/plugins/http/http.c
src/plugins/http/http.h
src/plugins/http/http_plugin.rst

index 8127fdc..e84e67f 100644 (file)
@@ -36,7 +36,7 @@ func init() {
                HttpClientErrRespTest, HttpClientPostFormTest, HttpClientGet128kbResponseTest, HttpClientGetResponseBodyTest,
                HttpClientGetNoResponseBodyTest, HttpClientPostFileTest, HttpClientPostFilePtrTest, HttpUnitTest,
                HttpRequestLineTest, HttpClientGetTimeout, HttpStaticFileHandlerWrkTest, HttpStaticUrlHandlerWrkTest, HttpConnTimeoutTest,
-               HttpClientGetRepeat, HttpClientPostRepeat)
+               HttpClientGetRepeat, HttpClientPostRepeat, HttpIgnoreH2UpgradeTest)
        RegisterNoTopoSoloTests(HttpStaticPromTest, HttpGetTpsTest, HttpGetTpsInterruptModeTest, PromConcurrentConnectionsTest,
                PromMemLeakTest, HttpClientPostMemLeakTest, HttpInvalidClientRequestMemLeakTest, HttpPostTpsTest, HttpPostTpsInterruptModeTest,
                PromConsecutiveConnectionsTest)
@@ -1227,12 +1227,12 @@ func HttpContentLengthTest(s *NoTopoSuite) {
        validatePostInterfaceStats(s, resp)
 
        resp, err = TcpSendReceive(serverAddress+":80",
-               "POST /interface_stats.json HTTP/1.1\r\n Content-Length:  4 \r\n\r\n"+ifName)
+               "POST /interface_stats.json HTTP/1.1\r\nContent-Length:  4 \r\n\r\n"+ifName)
        s.AssertNil(err, fmt.Sprint(err))
        validatePostInterfaceStats(s, resp)
 
        resp, err = TcpSendReceive(serverAddress+":80",
-               "POST /interface_stats.json HTTP/1.1\r\n\tContent-Length:\t\t4\r\n\r\n"+ifName)
+               "POST /interface_stats.json HTTP/1.1\r\nContent-Length:\t\t4\r\n\r\n"+ifName)
        s.AssertNil(err, fmt.Sprint(err))
        validatePostInterfaceStats(s, resp)
 }
@@ -1289,13 +1289,42 @@ func HttpHeadersTest(s *NoTopoSuite) {
        serverAddress := s.VppAddr()
        vpp.Vppctl("http cli server")
 
-       resp, err := TcpSendReceive(
-               serverAddress+":80",
-               "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent:test\r\nAccept:text/xml\r\nAccept:\ttext/plain\t \r\nAccept:text/html\r\n\r\n")
+       transport := http.DefaultTransport
+       transport.(*http.Transport).Proxy = nil
+       transport.(*http.Transport).DisableKeepAlives = false
+       client := &http.Client{
+               Transport: transport,
+               Timeout:   time.Second * 30,
+       }
+
+       req, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil)
        s.AssertNil(err, fmt.Sprint(err))
-       s.AssertContains(resp, "HTTP/1.1 200 OK")
-       s.AssertContains(resp, "Content-Type: text/plain")
-       s.AssertNotContains(resp, "<html>", "html content received instead of plain text")
+       req.Header.Add("Accept", "text/xml")
+       req.Header.Add("Accept-Language", "*")
+       req.Header.Add("Accept", "text/plain")
+       req.Header.Add("Accept", "text/html")
+       resp, err := client.Do(req)
+       s.AssertNil(err, fmt.Sprint(err))
+       defer resp.Body.Close()
+       s.Log(DumpHttpResp(resp, true))
+       s.AssertHttpStatus(resp, 200)
+       s.AssertHttpHeaderWithValue(resp, "Content-Type", "text/plain")
+       data, err := io.ReadAll(resp.Body)
+       s.AssertNil(err, fmt.Sprint(err))
+       s.AssertNotContains(string(data), "<html>", "html content received instead of plain text")
+
+       req2, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil)
+       s.AssertNil(err, fmt.Sprint(err))
+       req2.Header.Add("Accept", "text/html")
+       resp2, err := client.Do(req2)
+       s.AssertNil(err, fmt.Sprint(err))
+       defer resp2.Body.Close()
+       s.Log(DumpHttpResp(resp2, true))
+       s.AssertHttpStatus(resp2, 200)
+       s.AssertHttpHeaderWithValue(resp2, "Content-Type", "text/html")
+       data2, err := io.ReadAll(resp2.Body)
+       s.AssertNil(err, fmt.Sprint(err))
+       s.AssertContains(string(data2), "<html>", "html content not received")
 }
 
 func HttpInvalidHeadersTest(s *NoTopoSuite) {
@@ -1382,3 +1411,28 @@ func HttpConnTimeoutTest(s *NoTopoSuite) {
        _, err = conn.Read(reply)
        s.AssertMatchError(err, io.EOF, "connection not closed by server")
 }
+
+func HttpIgnoreH2UpgradeTest(s *NoTopoSuite) {
+       vpp := s.GetContainerByName("vpp").VppInstance
+       serverAddress := s.VppAddr()
+       s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers"))
+
+       transport := http.DefaultTransport
+       transport.(*http.Transport).Proxy = nil
+       transport.(*http.Transport).DisableKeepAlives = false
+       client := &http.Client{
+               Transport: transport,
+               Timeout:   time.Second * 30,
+       }
+
+       req, err := http.NewRequest("GET", "http://"+serverAddress+":80/version.json", nil)
+       s.AssertNil(err, fmt.Sprint(err))
+       req.Header.Add("Connection", "Upgrade")
+       req.Header.Add("Upgrade", "HTTP/2.0")
+       resp, err := client.Do(req)
+       s.AssertNil(err, fmt.Sprint(err))
+       defer resp.Body.Close()
+       s.Log(DumpHttpResp(resp, true))
+       s.AssertHttpStatus(resp, 200)
+       s.AssertHttpHeaderNotPresent(resp, "Upgrade")
+}
index d696109..57308df 100644 (file)
@@ -150,6 +150,7 @@ func (s *VppProxySuite) CurlDownloadResourceViaTunnel(uri string, proxyUri strin
        s.AssertContains(writeOut, "GET response code: 200")
        s.AssertNotContains(log, "bytes remaining to read")
        s.AssertNotContains(log, "Operation timed out")
+       s.AssertNotContains(log, "Upgrade:")
 }
 
 func (s *VppProxySuite) CurlUploadResourceViaTunnel(uri, proxyUri, file string) {
@@ -158,6 +159,7 @@ func (s *VppProxySuite) CurlUploadResourceViaTunnel(uri, proxyUri, file string)
        s.AssertContains(writeOut, "CONNECT response code: 200")
        s.AssertContains(writeOut, "PUT response code: 201")
        s.AssertNotContains(log, "Operation timed out")
+       s.AssertNotContains(log, "Upgrade:")
 }
 
 var _ = Describe("VppProxySuite", Ordered, ContinueOnFailure, func() {
index dfa90f9..3ca86d2 100644 (file)
@@ -51,6 +51,7 @@ typedef struct
   u8 *tx_buf;
   u32 tx_offset;
   u32 vpp_session_index;
+  http_header_table_t req_headers;
   http_header_t *resp_headers;
 } hcs_session_t;
 
@@ -177,7 +178,7 @@ start_send_data (hcs_session_t *hs, http_status_code_t status)
   if (vec_len (hs->resp_headers))
     {
       headers_buf = http_serialize_headers (hs->resp_headers);
-      vec_free (hs->resp_headers);
+      vec_reset_length (hs->resp_headers);
       msg.data.headers_offset = 0;
       msg.data.headers_len = vec_len (headers_buf);
     }
@@ -370,7 +371,8 @@ hcs_ts_rx_callback (session_t *ts)
 
   hs = hcs_session_get (ts->thread_index, ts->opaque);
   hs->tx_buf = 0;
-  hs->resp_headers = 0;
+  vec_reset_length (hs->resp_headers);
+  http_reset_header_table (&hs->req_headers);
 
   /* Read the http message header */
   rv = svm_fifo_dequeue (ts->rx_fifo, sizeof (msg), (u8 *) &msg);
@@ -413,30 +415,22 @@ hcs_ts_rx_callback (session_t *ts)
 
   if (msg.data.headers_len)
     {
-      u8 *headers = 0;
-      http_header_table_t *ht;
-      vec_validate (headers, msg.data.headers_len - 1);
+      http_init_header_table_buf (&hs->req_headers, msg);
       rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset,
-                         msg.data.headers_len, headers);
+                         msg.data.headers_len, hs->req_headers.buf);
       ASSERT (rv == msg.data.headers_len);
-      if (http_parse_headers (headers, &ht))
+      http_build_header_table (&hs->req_headers, msg);
+      const http_header_t *accept = http_get_header (
+       &hs->req_headers, http_header_name_token (HTTP_HEADER_ACCEPT));
+      if (accept)
        {
-         start_send_data (hs, HTTP_STATUS_BAD_REQUEST);
-         vec_free (args.buf);
-         vec_free (headers);
-         goto done;
-       }
-      const char *accept_value =
-       http_get_header (ht, http_header_name_str (HTTP_HEADER_ACCEPT));
-      if (accept_value)
-       {
-         HCS_DBG ("client accept: %s", accept_value);
+         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 (strstr (accept_value, "text/plain"))
+         if (http_token_contains (accept->value.base, accept->value.len,
+                                  http_token_lit ("text/plain")))
            args.plain_text = 1;
        }
-      http_free_header_table (ht);
-      vec_free (headers);
     }
 
   args.hs_index = hs->session_index;
@@ -547,6 +541,8 @@ hcs_ts_cleanup_callback (session_t *s, session_cleanup_ntf_t ntf)
     return;
 
   vec_free (hs->tx_buf);
+  vec_free (hs->resp_headers);
+  http_free_header_table (&hs->req_headers);
   hcs_session_free (hs);
 }
 
index a86bf1f..cfa5e9c 100644 (file)
@@ -38,6 +38,12 @@ typedef struct
   http_msg_t msg;
 } hc_worker_t;
 
+typedef struct
+{
+  u8 *name;
+  u8 *value;
+} hc_http_header_t;
+
 typedef struct
 {
   u32 app_index;
@@ -52,7 +58,7 @@ typedef struct
   u8 *resp_headers;
   u8 *http_response;
   u8 *response_status;
-  http_header_ht_t *custom_header;
+  hc_http_header_t *custom_header;
   u8 is_file;
   u8 use_ptr;
   u8 *filename;
@@ -181,7 +187,7 @@ hc_session_connected_callback (u32 app_index, u32 hc_session_index,
   hc_main_t *hcm = &hc_main;
   hc_worker_t *wrk;
   u32 new_hc_index;
-  http_header_ht_t *header;
+  hc_http_header_t *header;
   HTTP_DBG (1, "ho hc_index: %d", hc_session_index);
 
   if (err)
@@ -344,7 +350,6 @@ hc_rx_callback (session_t *s)
            format (0, "%U", format_http_status_code, msg.code);
          svm_fifo_dequeue_drop (s->rx_fifo, msg.data.headers_offset);
 
-         http_header_table_t *ht;
          vec_validate (hcm->resp_headers, msg.data.headers_len - 1);
          vec_set_len (hcm->resp_headers, msg.data.headers_len);
          rv = svm_fifo_dequeue (s->rx_fifo, msg.data.headers_len,
@@ -352,14 +357,6 @@ hc_rx_callback (session_t *s)
 
          ASSERT (rv == msg.data.headers_len);
          HTTP_DBG (1, (char *) format (0, "%v", hcm->resp_headers));
-         if (http_parse_headers (hcm->resp_headers, &ht))
-           {
-             clib_warning ("invalid headers received");
-             vlib_process_signal_event_mt (
-               wrk->vlib_main, hcm->cli_node_index, HC_GENERIC_ERR, 0);
-             return -1;
-           }
-         http_free_header_table (ht);
          msg.data.body_offset -=
            msg.data.headers_len + msg.data.headers_offset;
        }
@@ -666,7 +663,7 @@ hc_cleanup ()
   HTTP_DBG (1, "cleanup");
   hc_main_t *hcm = &hc_main;
   hc_worker_t *wrk;
-  http_header_ht_t *header;
+  hc_http_header_t *header;
 
   vec_foreach (wrk, hcm->wrk)
     hcc_worker_cleanup (wrk);
@@ -696,7 +693,7 @@ hc_command_fn (vlib_main_t *vm, unformat_input_t *input,
   unformat_input_t _line_input, *line_input = &_line_input;
   u8 *path = 0;
   u8 *file_data;
-  http_header_ht_t new_header;
+  hc_http_header_t new_header;
   u8 *name;
   u8 *value;
   int rv;
index 7c0b55e..c5cd894 100644 (file)
@@ -17,6 +17,7 @@
 #include <vnet/session/session.h>
 #include <http/http_timer.h>
 #include <http/http_status_codes.h>
+#include <http/http_header_names.h>
 
 static http_main_t http_main;
 
@@ -35,6 +36,12 @@ const http_buffer_type_t msg_to_buf_type[] = {
   [HTTP_MSG_DATA_PTR] = HTTP_BUFFER_PTR,
 };
 
+const char *http_upgrade_proto_str[] = { "",
+#define _(sym, str) str,
+                                        foreach_http_upgrade_proto
+#undef _
+};
+
 static u8 *
 format_http_req_state (u8 *s, va_list *va)
 {
@@ -446,6 +453,9 @@ static const char *http_response_template = "HTTP/1.1 %s\r\n"
 
 static const char *content_len_template = "Content-Length: %llu\r\n";
 
+static const char *connection_upgrade_template = "Connection: upgrade\r\n"
+                                                "Upgrade: %s\r\n";
+
 /**
  * http request boilerplate
  */
@@ -706,6 +716,7 @@ http_parse_request_line (http_req_t *req, http_status_code_t *ec)
     {
       HTTP_DBG (0, "CONNECT method");
       req->method = HTTP_REQ_CONNECT;
+      req->upgrade_proto = HTTP_UPGRADE_PROTO_NA;
       req->target_path_offset = method_offset + 8;
       req->is_tunnel = 1;
     }
@@ -892,7 +903,17 @@ http_parse_status_line (http_req_t *req)
 static int
 http_identify_headers (http_req_t *req, http_status_code_t *ec)
 {
-  int i;
+  int rv;
+  u8 *p, *end, *name_start, *value_start;
+  u32 name_len, value_len;
+  http_field_line_t *field_line;
+  uword header_index;
+
+  vec_reset_length (req->headers);
+  req->content_len_header_index = ~0;
+  req->connection_header_index = ~0;
+  req->upgrade_header_index = ~0;
+  req->headers_offset = req->rx_buf_offset;
 
   /* check if we have any header */
   if ((req->rx_buf[req->rx_buf_offset] == '\r') &&
@@ -905,16 +926,53 @@ http_identify_headers (http_req_t *req, http_status_code_t *ec)
       return 0;
     }
 
-  /* find empty line indicating end of header section */
-  i = v_find_index (req->rx_buf, req->rx_buf_offset, 0, "\r\n\r\n");
-  if (i < 0)
+  end = req->rx_buf + vec_len (req->rx_buf);
+  p = req->rx_buf + req->rx_buf_offset;
+
+  while (1)
     {
-      clib_warning ("cannot find header section end");
-      *ec = HTTP_STATUS_BAD_REQUEST;
-      return -1;
-    }
-  req->headers_offset = req->rx_buf_offset;
-  req->headers_len = i - req->rx_buf_offset + 2;
+      rv = _parse_field_name (&p, end, &name_start, &name_len);
+      if (rv != 0)
+       {
+         *ec = HTTP_STATUS_BAD_REQUEST;
+         return -1;
+       }
+      rv = _parse_field_value (&p, end, &value_start, &value_len);
+      if (rv != 0 || (end - p) < 2)
+       {
+         *ec = HTTP_STATUS_BAD_REQUEST;
+         return -1;
+       }
+
+      vec_add2 (req->headers, field_line, 1);
+      field_line->name_offset =
+       (name_start - req->rx_buf) - req->headers_offset;
+      field_line->name_len = name_len;
+      field_line->value_offset =
+       (value_start - req->rx_buf) - req->headers_offset;
+      field_line->value_len = value_len;
+      header_index = field_line - req->headers;
+
+      /* find headers that will be used later in preprocessing */
+      if (req->content_len_header_index == ~0 &&
+         http_token_is ((const char *) name_start, name_len,
+                        http_header_name_token (HTTP_HEADER_CONTENT_LENGTH)))
+       req->content_len_header_index = header_index;
+      else if (req->connection_header_index == ~0 &&
+              http_token_is ((const char *) name_start, name_len,
+                             http_header_name_token (HTTP_HEADER_CONNECTION)))
+       req->connection_header_index = header_index;
+      else if (req->upgrade_header_index == ~0 &&
+              http_token_is ((const char *) name_start, name_len,
+                             http_header_name_token (HTTP_HEADER_UPGRADE)))
+       req->upgrade_header_index = header_index;
+
+      /* are we done? */
+      if (*p == '\r' && *(p + 1) == '\n')
+       break;
+    }
+
+  req->headers_len = p - (req->rx_buf + req->headers_offset);
   req->control_data_len += (req->headers_len + 2);
   HTTP_DBG (2, "headers length: %u", req->headers_len);
   HTTP_DBG (2, "headers offset: %u", req->headers_offset);
@@ -925,9 +983,10 @@ http_identify_headers (http_req_t *req, http_status_code_t *ec)
 static int
 http_identify_message_body (http_req_t *req, http_status_code_t *ec)
 {
-  int i, value_len;
-  u8 *end, *p, *value_start;
+  int i;
+  u8 *p;
   u64 body_len = 0, digit;
+  http_field_line_t *field_line;
 
   req->body_len = 0;
 
@@ -944,67 +1003,15 @@ http_identify_message_body (http_req_t *req, http_status_code_t *ec)
 
   /* TODO check for chunked transfer coding */
 
-  /* try to find Content-Length header */
-  i = v_find_index (req->rx_buf, req->headers_offset, req->headers_len,
-                   "Content-Length:");
-  if (i < 0)
+  if (req->content_len_header_index == ~0)
     {
       HTTP_DBG (2, "Content-Length header not present, no message-body");
       return 0;
     }
-  req->rx_buf_offset = i + 15;
+  field_line = vec_elt_at_index (req->headers, req->content_len_header_index);
 
-  i = v_find_index (req->rx_buf, req->rx_buf_offset, req->headers_len, "\r\n");
-  if (i < 0)
-    {
-      clib_warning ("end of line missing");
-      *ec = HTTP_STATUS_BAD_REQUEST;
-      return -1;
-    }
-  value_len = i - req->rx_buf_offset;
-  if (value_len < 1)
-    {
-      clib_warning ("invalid header, content length value missing");
-      *ec = HTTP_STATUS_BAD_REQUEST;
-      return -1;
-    }
-
-  end = req->rx_buf + req->rx_buf_offset + value_len;
-  p = req->rx_buf + req->rx_buf_offset;
-  /* skip leading whitespace */
-  while (1)
-    {
-      if (p == end)
-       {
-         clib_warning ("value not found");
-         *ec = HTTP_STATUS_BAD_REQUEST;
-         return -1;
-       }
-      else if (*p != ' ' && *p != '\t')
-       {
-         break;
-       }
-      p++;
-      value_len--;
-    }
-  value_start = p;
-  /* skip trailing whitespace */
-  p = value_start + value_len - 1;
-  while (*p == ' ' || *p == '\t')
-    {
-      p--;
-      value_len--;
-    }
-
-  if (value_len < 1)
-    {
-      clib_warning ("value not found");
-      *ec = HTTP_STATUS_BAD_REQUEST;
-      return -1;
-    }
-
-  p = value_start;
-  for (i = 0; i < value_len; i++)
+  p = req->rx_buf + req->headers_offset + field_line->value_offset;
+  for (i = 0; i < field_line->value_len; i++)
     {
       /* check for digit */
       if (!isdigit (*p))
@@ -1095,6 +1102,7 @@ http_req_state_wait_transport_reply (http_conn_t *hc,
   msg.data.body_len = hc->req.body_len;
   msg.data.type = HTTP_MSG_DATA_INLINE;
   msg.data.len = len;
+  msg.data.headers_ctx = pointer_to_uword (hc->req.headers);
 
   svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) },
                             { hc->req.rx_buf, len } };
@@ -1130,6 +1138,49 @@ error:
   return HTTP_SM_ERROR;
 }
 
+#define http_field_line_value_token(_fl, _req)                                \
+  (const char *) ((_req)->rx_buf + (_req)->headers_offset +                   \
+                 (_fl)->value_offset),                                       \
+    (_fl)->value_len
+
+static void
+http_check_connection_upgrade (http_req_t *req)
+{
+  http_field_line_t *connection, *upgrade;
+  u8 skip;
+
+  skip = (req->method != HTTP_REQ_GET) + (req->connection_header_index == ~0) +
+        (req->upgrade_header_index == ~0);
+  if (skip)
+    return;
+
+  connection = vec_elt_at_index (req->headers, req->connection_header_index);
+  /* connection options are case-insensitive (RFC9110 7.6.1) */
+  if (http_token_is_case (http_field_line_value_token (connection, req),
+                         http_token_lit ("upgrade")))
+    {
+      upgrade = vec_elt_at_index (req->headers, req->upgrade_header_index);
+
+      /* check upgrade protocol, we want to ignore something like upgrade to
+       * newer HTTP version, only tunnels are supported */
+      if (0)
+       ;
+#define _(sym, str)                                                           \
+  else if (http_token_is (http_field_line_value_token (upgrade, req),         \
+                         http_token_lit (str))) req->upgrade_proto =         \
+    HTTP_UPGRADE_PROTO_##sym;
+      foreach_http_upgrade_proto
+#undef _
+       else return;
+
+      HTTP_DBG (1, "connection upgrade: %U", format_http_bytes,
+               req->rx_buf + req->headers_offset + upgrade->value_offset,
+               upgrade->value_len);
+      req->is_tunnel = 1;
+      req->method = HTTP_REQ_CONNECT;
+    }
+}
+
 static http_sm_result_t
 http_req_state_wait_transport_method (http_conn_t *hc,
                                      transport_send_params_t *sp)
@@ -1164,6 +1215,8 @@ http_req_state_wait_transport_method (http_conn_t *hc,
   if (rv)
     goto error;
 
+  http_check_connection_upgrade (&hc->req);
+
   rv = http_identify_message_body (&hc->req, &ec);
   if (rv)
     goto error;
@@ -1196,6 +1249,8 @@ http_req_state_wait_transport_method (http_conn_t *hc,
   msg.data.headers_len = hc->req.headers_len;
   msg.data.body_offset = hc->req.body_offset;
   msg.data.body_len = hc->req.body_len;
+  msg.data.headers_ctx = pointer_to_uword (hc->req.headers);
+  msg.data.upgrade_proto = hc->req.upgrade_proto;
 
   svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) },
                             { hc->req.rx_buf, len } };
@@ -1286,15 +1341,21 @@ http_req_state_wait_app_reply (http_conn_t *hc, transport_send_params_t *sp)
                     /* Server */
                     hc->app_name);
 
-  /* RFC9110 9.3.6: A server MUST NOT send Content-Length header field in a
-   * 2xx (Successful) response to CONNECT. */
-  if (hc->req.is_tunnel && http_status_code_str[msg.code][0] == '2')
+  /* RFC9110 8.6: A server MUST NOT send Content-Length header field in a
+   * 2xx (Successful) response to CONNECT or with a status code of 101
+   * (Switching Protocols). */
+  if (hc->req.is_tunnel && (http_status_code_str[msg.code][0] == '2' ||
+                           msg.code == HTTP_STATUS_SWITCHING_PROTOCOLS))
     {
       ASSERT (msg.data.body_len == 0);
       next_state = HTTP_REQ_STATE_TUNNEL;
+      if (hc->req.upgrade_proto > HTTP_UPGRADE_PROTO_NA)
+       response = format (response, connection_upgrade_template,
+                          http_upgrade_proto_str[hc->req.upgrade_proto]);
       /* cleanup some stuff we don't need anymore in tunnel mode */
       http_conn_timer_stop (hc);
       vec_free (hc->req.rx_buf);
+      vec_free (hc->req.headers);
       http_buffer_free (&hc->req.tx_buf);
     }
   else
@@ -1860,6 +1921,7 @@ http_ts_cleanup_callback (session_t *ts, session_cleanup_ntf_t ntf)
   HTTP_DBG (1, "going to free hc [%u]%x", ts->thread_index, ts->opaque);
 
   vec_free (hc->req.rx_buf);
+  vec_free (hc->req.headers);
 
   http_buffer_free (&hc->req.tx_buf);
 
index f54b4a0..bcee69b 100644 (file)
@@ -352,12 +352,36 @@ typedef enum http_header_name_
 #undef _
 } http_header_name_t;
 
+#define HTTP_BOOLEAN_TRUE "?1"
+
+#define foreach_http_upgrade_proto                                            \
+  _ (CONNECT_UDP, "connect-udp")                                              \
+  _ (CONNECT_IP, "connect-ip")                                                \
+  _ (WEBSOCKET, "websocket")
+
+typedef enum http_upgrade_proto_
+{
+  HTTP_UPGRADE_PROTO_NA =
+    0, /* indicating standard CONNECT where protocol is omitted */
+#define _(sym, str) HTTP_UPGRADE_PROTO_##sym,
+  foreach_http_upgrade_proto
+#undef _
+} http_upgrade_proto_t;
+
 typedef enum http_msg_data_type_
 {
   HTTP_MSG_DATA_INLINE,
   HTTP_MSG_DATA_PTR
 } http_msg_data_type_t;
 
+typedef struct http_field_line_
+{
+  u32 name_offset;
+  u32 name_len;
+  u32 value_offset;
+  u32 value_len;
+} http_field_line_t;
+
 typedef struct http_msg_data_
 {
   http_msg_data_type_t type;
@@ -371,6 +395,8 @@ typedef struct http_msg_data_
   u32 headers_len;
   u32 body_offset;
   u64 body_len;
+  uword headers_ctx;
+  http_upgrade_proto_t upgrade_proto;
   u8 data[0];
 } http_msg_data_t;
 
@@ -422,6 +448,13 @@ typedef struct http_req_
 
   u32 body_offset;
   u64 body_len;
+
+  http_field_line_t *headers;
+  uword content_len_header_index;
+  uword connection_header_index;
+  uword upgrade_header_index;
+
+  http_upgrade_proto_t upgrade_proto;
 } http_req_t;
 
 typedef struct http_tc_
@@ -477,6 +510,27 @@ typedef struct http_main_
   u32 fifo_size;
 } http_main_t;
 
+always_inline u8 *
+format_http_bytes (u8 *s, va_list *va)
+{
+  u8 *bytes = va_arg (*va, u8 *);
+  int n_bytes = va_arg (*va, int);
+  uword i;
+
+  if (n_bytes == 0)
+    return s;
+
+  for (i = 0; i < n_bytes; i++)
+    {
+      if (isprint (bytes[i]))
+       s = format (s, "%c", bytes[i]);
+      else
+       s = format (s, "\\x%02x", bytes[i]);
+    }
+
+  return s;
+}
+
 always_inline int
 _validate_target_syntax (u8 *target, u32 len, int is_query, int *is_encoded)
 {
@@ -765,12 +819,6 @@ _parse_field_value (u8 **pos, u8 *end, u8 **field_value_start,
   return -1;
 }
 
-typedef struct
-{
-  u8 *name;
-  u8 *value;
-} http_header_ht_t;
-
 typedef struct
 {
   http_token_t name;
@@ -779,10 +827,88 @@ typedef struct
 
 typedef struct
 {
-  http_header_ht_t *headers;
+  http_header_t *headers;
   uword *value_by_name;
+  u8 *buf;
+  char **concatenated_values;
 } http_header_table_t;
 
+#define HTTP_HEADER_TABLE_NULL                                                \
+  {                                                                           \
+    .headers = 0, .value_by_name = 0, .buf = 0, .concatenated_values = 0,     \
+  }
+
+always_inline u8
+http_token_is (const char *actual, uword actual_len, const char *expected,
+              uword expected_len)
+{
+  ASSERT (actual != 0);
+  if (actual_len != expected_len)
+    return 0;
+  return memcmp (actual, expected, expected_len) == 0 ? 1 : 0;
+}
+
+always_inline u8
+http_token_is_case (const char *actual, uword actual_len, const char *expected,
+                   uword expected_len)
+{
+  uword i;
+  ASSERT (actual != 0);
+  if (actual_len != expected_len)
+    return 0;
+  for (i = 0; i < expected_len; i++)
+    {
+      if (tolower (actual[i]) != expected[i])
+       return 0;
+    }
+  return 1;
+}
+
+always_inline u8
+http_token_contains (const char *haystack, uword haystack_len,
+                    const char *needle, uword needle_len)
+{
+  uword end_index, i;
+  ASSERT (haystack != 0);
+  if (haystack_len < needle_len)
+    return 0;
+  end_index = haystack_len - needle_len;
+  for (i = 0; i <= end_index; i++)
+    {
+      if (!memcmp (haystack + i, needle, needle_len))
+       return 1;
+    }
+  return 0;
+}
+
+/**
+ * Reset header table before reuse.
+ *
+ * @param ht Header table to reset.
+ */
+always_inline void
+http_reset_header_table (http_header_table_t *ht)
+{
+  int i;
+  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->buf);
+  hash_free (ht->value_by_name);
+}
+
+/**
+ * Initialize header table input buffer.
+ * @param ht  Header table.
+ * @param msg HTTP transport message metadata.
+ */
+always_inline void
+http_init_header_table_buf (http_header_table_t *ht, http_msg_t msg)
+{
+  vec_validate (ht->buf, msg.data.headers_len - 1);
+}
+
 /**
  * Free header table's memory.
  *
@@ -791,86 +917,88 @@ typedef struct
 always_inline void
 http_free_header_table (http_header_table_t *ht)
 {
-  http_header_ht_t *header;
-  vec_foreach (header, ht->headers)
-    {
-      vec_free (header->name);
-      vec_free (header->value);
-    }
+  int i;
+  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->buf);
   hash_free (ht->value_by_name);
   clib_mem_free (ht);
 }
 
+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);
+}
+
+static uword
+_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);
+}
+
 /**
- * Parse headers in given vector.
+ * Build header table.
  *
- * @param headers Vector to parse.
- * @param [out] header_table Parsed headers in case of success.
- *
- * @return @c 0 on success.
+ * @param header_table Header table with loaded buffer.
+ * @param msg HTTP transport message metadata.
  *
- * The caller is responsible to free the returned @c header_table
- * using @c http_free_header_table .
+ * @note If reusing already allocated header table use
+ * @c http_reset_header_table first.
  */
-always_inline int
-http_parse_headers (u8 *headers, http_header_table_t **header_table)
+always_inline void
+http_build_header_table (http_header_table_t *ht, http_msg_t msg)
 {
-  u8 *pos, *end, *name_start, *value_start, *name;
-  u32 name_len, value_len;
-  int rv;
-  http_header_ht_t *header;
-  http_header_table_t *ht;
+  http_token_t name;
+  http_header_t *header;
+  http_field_line_t *field_lines, *field_line;
   uword *p;
 
-  end = headers + vec_len (headers);
-  pos = headers;
+  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 = clib_mem_alloc (sizeof (*ht));
-  ht->value_by_name = hash_create_string (0, sizeof (uword));
-  ht->headers = 0;
-  do
+  vec_foreach (field_line, field_lines)
     {
-      rv = _parse_field_name (&pos, end, &name_start, &name_len);
-      if (rv != 0)
-       {
-         http_free_header_table (ht);
-         return rv;
-       }
-      rv = _parse_field_value (&pos, end, &value_start, &value_len);
-      if (rv != 0)
-       {
-         http_free_header_table (ht);
-         return rv;
-       }
-      name = vec_new (u8, name_len);
-      clib_memcpy (name, name_start, name_len);
-      vec_terminate_c_string (name);
+      name.base = (char *) (ht->buf + field_line->name_offset);
+      name.len = field_line->name_len;
       /* check if header is repeated */
-      p = hash_get_mem (ht->value_by_name, name);
+      p = hash_get_mem (ht->value_by_name, &name);
       if (p)
        {
-         /* if yes combine values */
+         char *new_value = 0;
          header = vec_elt_at_index (ht->headers, p[0]);
-         vec_pop (header->value); /* drop null byte */
-         header->value = format (header->value, ", %U%c", format_ascii_bytes,
-                                 value_start, value_len, 0);
-         vec_free (name);
+         u32 new_len = header->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,
+                      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;
          continue;
        }
       /* or create new record */
-      vec_add2 (ht->headers, header, sizeof (*header));
-      header->name = name;
-      header->value = vec_new (u8, value_len);
-      clib_memcpy (header->value, value_start, value_len);
-      vec_terminate_c_string (header->value);
-      hash_set_mem (ht->value_by_name, header->name, header - ht->headers);
+      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;
+      HTTP_DBG (1, "value: %U", format_http_bytes, header->value.base,
+               header->value.len);
+      hash_set_mem (ht->value_by_name, &header->name, header - ht->headers);
     }
-  while (pos != end);
-
-  *header_table = ht;
-
-  return 0;
 }
 
 /**
@@ -881,17 +1009,19 @@ http_parse_headers (u8 *headers, http_header_table_t **header_table)
  *
  * @return Header's value in case of success, @c 0 otherwise.
  */
-always_inline const char *
-http_get_header (http_header_table_t *header_table, const char *name)
+always_inline const http_header_t *
+http_get_header (http_header_table_t *header_table, const char *name,
+                uword name_len)
 {
   uword *p;
-  http_header_ht_t *header;
+  http_header_t *header;
+  http_token_t name_token = { (char *) name, name_len };
 
-  p = hash_get_mem (header_table->value_by_name, name);
+  p = hash_get_mem (header_table->value_by_name, &name_token);
   if (p)
     {
       header = vec_elt_at_index (header_table->headers, p[0]);
-      return (const char *) header->value;
+      return header;
     }
 
   return 0;
index f86c796..bf414cf 100644 (file)
@@ -15,9 +15,11 @@ Usage
 -----
 
 The plugin exposes following inline functions: ``http_validate_abs_path_syntax``, ``http_validate_query_syntax``,
-``http_percent_decode``, ``http_path_remove_dot_segments``, ``http_parse_headers``, ``http_get_header``,
-``http_free_header_table``, ``http_add_header``, ``http_serialize_headers``, ``http_parse_authority_form_target``,
-``http_serialize_authority_form_target``, ``http_parse_absolute_form``, ``http_parse_masque_host_port``.
+``http_percent_decode``, ``http_path_remove_dot_segments``, ``http_build_header_table``, ``http_get_header``,
+``http_reset_header_table``, ``http_free_header_table``, ``http_add_header``,
+``http_serialize_headers``, ``http_parse_authority_form_target``, ``http_serialize_authority_form_target``,
+``http_parse_absolute_form``, ``http_parse_masque_host_port``, ``http_decap_udp_payload_datagram``,
+``http_encap_udp_payload_datagram``. ``http_token_is``, ``http_token_is_case``, ``http_token_contains``
 
 It relies on the hoststack constructs and uses ``http_msg_data_t`` data structure for passing metadata to/from applications.
 
@@ -125,24 +127,60 @@ Following example shows how to parse headers:
   #include <http/http_header_names.h>
   if (msg.data.headers_len)
     {
-      u8 *headers = 0;
-      http_header_table_t *ht;
-      vec_validate (headers, msg.data.headers_len - 1);
+      http_header_table_t ht = HTTP_HEADER_TABLE_NULL;
+      http_init_header_table_buf (&ht, msg);
       rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset,
-                         msg.data.headers_len, headers);
+                         msg.data.headers_len, ht.buf);
       ASSERT (rv == msg.data.headers_len);
-      if (http_parse_headers (headers, &ht))
+      http_build_header_table (&ht, msg);
+      /* get Accept header */
+      const http_header_t *accept = http_get_header (&ht, http_header_name_token (HTTP_HEADER_ACCEPT));
+      if (accept_value)
         {
-          /* your error handling */
+          /* do something interesting */
         }
+      http_free_header_table (&ht);
+    }
+
+Allocated header table memory can be reused, you just need to reset it using ``http_reset_header_table`` before reuse.
+We will add following member to our session context structure:
+
+.. code-block:: C
+
+  typedef struct
+  {
+    /* ... */
+    http_header_table_t ht;
+  } session_ctx_t;
+
+Don't forget to zero allocated session context.
+
+And in ``session_cleanup_callback`` we free header table memory:
+
+.. code-block:: C
+
+  http_free_header_table (&ctx->ht);
+
+Modified example above:
+
+.. code-block:: C
+
+  #include <http/http_header_names.h>
+  http_reset_header_table (&ctx->ht);
+  /* ... */
+  if (msg.data.headers_len)
+    {
+      http_init_header_table_buf (&ctx->ht, msg);
+      rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset,
+                         msg.data.headers_len, ctx->ht.buf);
+      ASSERT (rv == msg.data.headers_len);
+      http_build_header_table (&ctx->ht, msg);
       /* get Accept header */
-      const char *accept_value = http_get_header (ht, http_header_name_str (HTTP_HEADER_ACCEPT));
+      const http_header_t *accept = http_get_header (&ctx->ht, http_header_name_token (HTTP_HEADER_ACCEPT));
       if (accept_value)
         {
           /* do something interesting */
         }
-      http_free_header_table (ht);
-      vec_free (headers);
     }
 
 Finally application reads body  (if any), which might be received in multiple pieces (depends on size), so we might need some state machine in ``builtin_app_rx_callback``.
@@ -437,24 +475,19 @@ Following example shows how to parse headers:
   #include <http/http_header_names.h>
   if (msg.data.headers_len)
     {
-      u8 *headers = 0;
-      http_header_table_t *ht;
-      vec_validate (headers, msg.data.headers_len - 1);
+      http_header_table_t ht = HTTP_HEADER_TABLE_NULL;
+      http_init_header_table_buf (&ht, msg);
       rv = svm_fifo_peek (ts->rx_fifo, msg.data.headers_offset,
-                         msg.data.headers_len, headers);
+                         msg.data.headers_len, ht.buf);
       ASSERT (rv == msg.data.headers_len);
-      if (http_parse_headers (headers, &ht))
-        {
-          /* your error handling */
-        }
+      http_build_header_table (&ht, msg);
       /* get Content-Type header */
-      const char *content_type = http_get_header (ht, http_header_name_str (HTTP_HEADER_CONTENT_TYPE));
+      const http_header_t *content_type = http_get_header (&ht, http_header_name_token (HTTP_HEADER_CONTENT_TYPE));
       if (content_type)
         {
           /* do something interesting */
         }
-      http_free_header_table (ht);
-      vec_free (headers);
+      http_free_header_table (&ht);
     }
 
 Finally application reads body, which might be received in multiple pieces (depends on size), so we might need some state machine in ``builtin_app_rx_callback``.