http: http/2 extended connect method 47/43147/5
authorMatus Fabian <[email protected]>
Wed, 11 Jun 2025 15:02:25 +0000 (11:02 -0400)
committerFlorin Coras <[email protected]>
Wed, 18 Jun 2025 01:41:31 +0000 (01:41 +0000)
Type: feature

Change-Id: I42e94b6282fa693d3c69f938ec9d3a290b71b9fa
Signed-off-by: Matus Fabian <[email protected]>
extras/hs-test/h2spec_extras/h2spec_extras.go
extras/hs-test/infra/suite_vpp_proxy.go
extras/hs-test/infra/suite_vpp_udp_proxy.go
src/plugins/http/http2/hpack.c
src/plugins/http/http2/hpack.h
src/plugins/http/http2/http2.c
src/plugins/http/http2/http2.h

index 6957557..b1d86a0 100644 (file)
@@ -3,6 +3,7 @@ package h2spec_extras
 import (
        "fmt"
        "slices"
+       "strconv"
 
        "github.com/summerwind/h2spec/config"
        "github.com/summerwind/h2spec/spec"
@@ -28,6 +29,7 @@ func Spec() *spec.TestGroup {
 
        tg.AddTestGroup(FlowControl())
        tg.AddTestGroup(ConnectMethod())
+       tg.AddTestGroup(ExtendedConnectMethod())
 
        return tg
 }
@@ -394,5 +396,89 @@ func ConnectMethod() *spec.TestGroup {
                        return nil
                },
        })
+
+       tg.AddTestCase(&spec.TestCase{
+               Desc:        "The \":scheme\" and \":path\" pseudo-header fields MUST be omitted.",
+               Requirement: "A CONNECT request that does not conform to these restrictions is malformed.",
+               Run: func(c *config.Config, conn *spec.Conn) error {
+                       var streamID uint32 = 1
+
+                       err := conn.Handshake()
+                       if err != nil {
+                               return err
+                       }
+
+                       headers := ConnectHeaders(c)
+                       headers = append(headers, spec.HeaderField(":scheme", "https"))
+                       headers = append(headers, spec.HeaderField(":path", "/"))
+                       hp := http2.HeadersFrameParam{
+                               StreamID:      streamID,
+                               EndStream:     false,
+                               EndHeaders:    true,
+                               BlockFragment: conn.EncodeHeaders(headers),
+                       }
+                       conn.WriteHeaders(hp)
+
+                       return spec.VerifyStreamError(conn, http2.ErrCodeProtocol)
+               },
+       })
+       return tg
+}
+
+func ExtendedConnectMethod() *spec.TestGroup {
+       tg := NewTestGroup("3", "Extended CONNECT method")
+
+       tg.AddTestCase(&spec.TestCase{
+               Desc:        "SETTINGS_ENABLE_CONNECT_PROTOCOL parameter with value 1 received.",
+               Requirement: "Using a SETTINGS parameter to opt into an otherwise incompatible protocol change is a use of \"Extending HTTP/2\" defined by Section 5.5 of RFC9113.",
+               Run: func(c *config.Config, conn *spec.Conn) error {
+
+                       err := conn.Handshake()
+                       if err != nil {
+                               return err
+                       }
+
+                       enabled, ok := conn.Settings[http2.SettingEnableConnectProtocol]
+                       if !ok {
+                               return &spec.TestError{
+                                       Expected: []string{"SETTINGS_ENABLE_CONNECT_PROTOCOL received"},
+                                       Actual:   "SETTINGS_ENABLE_CONNECT_PROTOCOL not received",
+                               }
+                       }
+                       if enabled != uint32(1) {
+                               return &spec.TestError{
+                                       Expected: []string{"SETTINGS_ENABLE_CONNECT_PROTOCOL parameter with value 1 received"},
+                                       Actual:   "SETTINGS_ENABLE_CONNECT_PROTOCOL parameter with value " + strconv.Itoa(int(enabled)) + " received",
+                               }
+                       }
+
+                       return nil
+               },
+       })
+
+       tg.AddTestCase(&spec.TestCase{
+               Desc:        "The \":scheme\" and \":path\" pseudo-header fields MUST be included.",
+               Requirement: "A CONNECT request bearing the \":protocol\" pseudo-header that does not conform is malformed.",
+               Run: func(c *config.Config, conn *spec.Conn) error {
+                       var streamID uint32 = 1
+
+                       err := conn.Handshake()
+                       if err != nil {
+                               return err
+                       }
+
+                       headers := ConnectHeaders(c)
+                       headers = append(headers, spec.HeaderField(":protocol", "connect-udp"))
+                       hp := http2.HeadersFrameParam{
+                               StreamID:      streamID,
+                               EndStream:     false,
+                               EndHeaders:    true,
+                               BlockFragment: conn.EncodeHeaders(headers),
+                       }
+                       conn.WriteHeaders(hp)
+
+                       return spec.VerifyStreamError(conn, http2.ErrCodeProtocol)
+               },
+       })
        return tg
 }
index ae3f203..db6b6be 100644 (file)
@@ -367,6 +367,7 @@ var _ = Describe("H2SpecProxySuite", Ordered, ContinueOnFailure, func() {
                {desc: "extras/2/2"},
                {desc: "extras/2/3"},
                {desc: "extras/2/4"},
+               {desc: "extras/2/5"},
        }
 
        for _, test := range testCases {
index 7043864..29ee5b7 100644 (file)
@@ -1,16 +1,22 @@
 package hst
 
 import (
+       "bytes"
        "fmt"
+       "io"
        "net"
+       "os"
        "reflect"
        "runtime"
        "strconv"
        "strings"
        "time"
 
+       "fd.io/hs-test/h2spec_extras"
+
        . "fd.io/hs-test/infra/common"
        . "github.com/onsi/ginkgo/v2"
+       "github.com/summerwind/h2spec/config"
 )
 
 type VppUdpProxySuite struct {
@@ -243,3 +249,73 @@ var _ = Describe("VppUdpProxyMWSuite", Ordered, ContinueOnFailure, Serial, func(
                }
        }
 })
+
+var _ = Describe("H2SpecUdpProxySuite", Ordered, ContinueOnFailure, func() {
+       var s VppUdpProxySuite
+       BeforeAll(func() {
+               s.SetupSuite()
+       })
+       BeforeEach(func() {
+               s.SetupTest()
+       })
+       AfterAll(func() {
+               s.TeardownSuite()
+       })
+       AfterEach(func() {
+               s.TeardownTest()
+       })
+
+       testCases := []struct {
+               desc string
+       }{
+               {desc: "extras/3/1"},
+               {desc: "extras/3/2"},
+       }
+
+       for _, test := range testCases {
+               test := test
+               testName := "proxy_test.go/h2spec_" + strings.ReplaceAll(test.desc, "/", "_")
+               It(testName, func(ctx SpecContext) {
+                       s.Log(testName + ": BEGIN")
+                       vppProxy := s.Containers.VppProxy.VppInstance
+                       remoteServerConn := s.StartEchoServer()
+                       defer remoteServerConn.Close()
+                       cmd := fmt.Sprintf("test proxy server fifo-size 512k server-uri https://%s/%d", s.VppProxyAddr(), s.Ports.Proxy)
+                       s.Log(vppProxy.Vppctl(cmd))
+                       path := fmt.Sprintf("/.well-known/masque/udp/%s/%d/", s.ServerAddr(), s.Ports.Server)
+                       conf := &config.Config{
+                               Host:         s.VppProxyAddr(),
+                               Port:         s.Ports.Proxy,
+                               Path:         path,
+                               Timeout:      time.Second * s.MaxTimeout,
+                               MaxHeaderLen: 4096,
+                               TLS:          true,
+                               Insecure:     true,
+                               Sections:     []string{test.desc},
+                               Verbose:      true,
+                       }
+                       // capture h2spec output so it will be in log
+                       oldStdout := os.Stdout
+                       r, w, _ := os.Pipe()
+                       os.Stdout = w
+
+                       tg := h2spec_extras.Spec()
+                       tg.Test(conf)
+
+                       oChan := make(chan string)
+                       go func() {
+                               var buf bytes.Buffer
+                               io.Copy(&buf, r)
+                               oChan <- buf.String()
+                       }()
+
+                       // restore to normal state
+                       w.Close()
+                       os.Stdout = oldStdout
+                       o := <-oChan
+                       s.Log(o)
+                       s.AssertEqual(0, tg.FailedCount)
+               }, SpecTimeout(TestTimeout))
+       }
+
+})
index 76021ae..324602c 100644 (file)
@@ -813,6 +813,18 @@ hpack_parse_req_pseudo_header (u8 *name, u32 name_len, u8 *value,
          return HTTP2_ERROR_PROTOCOL_ERROR;
        }
       break;
+    case 9:
+      if (!memcmp (name + 1, "protocol", 8))
+       {
+         if (control_data->parsed_bitmap &
+             HPACK_PSEUDO_HEADER_PROTOCOL_PARSED)
+           return HTTP2_ERROR_PROTOCOL_ERROR;
+         control_data->parsed_bitmap |= HPACK_PSEUDO_HEADER_PROTOCOL_PARSED;
+         control_data->protocol = value;
+         control_data->protocol_len = value_len;
+         break;
+       }
+      break;
     case 10:
       if (!memcmp (name + 1, "authority", 9))
        {
@@ -874,7 +886,7 @@ hpack_preprocess_header (u8 *name, u32 name_len, u8 *value, u32 value_len,
        }
       break;
     case 14:
-      if (!memcmp (name, "content-length", 7) &&
+      if (!memcmp (name, "content-length", 14) &&
          control_data->content_len_header_index == ~0)
        control_data->content_len_header_index = index;
       break;
index 69144de..fef2a96 100644 (file)
@@ -54,6 +54,8 @@ typedef struct
   u8 *path;
   u32 path_len;
   u8 *headers;
+  u8 *protocol;
+  u32 protocol_len;
   uword content_len_header_index;
   u32 headers_len;
   u32 control_data_len;
index d1b6915..19a4331 100644 (file)
@@ -909,6 +909,10 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
   HTTP_DBG (1, "decompressed headers size %u", control_data.headers_len);
   HTTP_DBG (1, "dynamic table size %u", h2c->decoder_dynamic_table.used);
 
+  req->base.control_data_len = control_data.control_data_len;
+  req->base.headers_offset = control_data.headers - wrk->header_list;
+  req->base.headers_len = control_data.headers_len;
+
   if (!(control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_METHOD_PARSED))
     {
       HTTP_DBG (1, ":method pseudo-header missing in request");
@@ -949,42 +953,77 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
     }
   if (control_data.method == HTTP_REQ_CONNECT)
     {
-      if (control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_SCHEME_PARSED ||
-         control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_PATH_PARSED)
-       {
-         HTTP_DBG (1, ":scheme and :path pseudo-header must be omitted for "
-                      "CONNECT method");
-         http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
-         return HTTP_SM_STOP;
-       }
-      /* quick check if port is present */
-      p = control_data.authority + control_data.authority_len;
-      p--;
-      if (!isdigit (*p))
+      if (control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_PROTOCOL_PARSED)
        {
-         HTTP_DBG (1, "port not present in authority");
-         http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
-         return HTTP_SM_STOP;
+         /* extended CONNECT (RFC8441) */
+         if (!(control_data.parsed_bitmap &
+               HPACK_PSEUDO_HEADER_SCHEME_PARSED) ||
+             !(control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_PATH_PARSED))
+           {
+             HTTP_DBG (1,
+                       ":scheme and :path pseudo-header must be present for "
+                       "extended CONNECT method");
+             http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
+             return HTTP_SM_STOP;
+           }
+         /* parse protocol header value */
+         if (0)
+           ;
+#define _(sym, str)                                                           \
+  else if (http_token_is_case ((const char *) control_data.protocol,          \
+                              control_data.protocol_len,                     \
+                              http_token_lit (str)))                         \
+    req->base.upgrade_proto = HTTP_UPGRADE_PROTO_##sym;
+         foreach_http_upgrade_proto
+#undef _
+           else
+         {
+           HTTP_DBG (1, "unsupported extended connect protocol %U",
+                     format_http_bytes, control_data.protocol,
+                     control_data.protocol_len);
+           http2_stream_error (hc, req, HTTP2_ERROR_INTERNAL_ERROR, sp);
+           return HTTP_SM_STOP;
+         }
        }
-      p--;
-      for (; p > control_data.authority; p--)
+      else
        {
+         if (control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_SCHEME_PARSED ||
+             control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_PATH_PARSED)
+           {
+             HTTP_DBG (1,
+                       ":scheme and :path pseudo-header must be omitted for "
+                       "CONNECT method");
+             http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
+             return HTTP_SM_STOP;
+           }
+         /* quick check if port is present */
+         p = control_data.authority + control_data.authority_len;
+         p--;
          if (!isdigit (*p))
-           break;
-       }
-      if (*p != ':')
-       {
-         HTTP_DBG (1, "port not present in authority");
-         http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
-         return HTTP_SM_STOP;
+           {
+             HTTP_DBG (1, "port not present in authority");
+             http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
+             return HTTP_SM_STOP;
+           }
+         p--;
+         for (; p > control_data.authority; p--)
+           {
+             if (!isdigit (*p))
+               break;
+           }
+         if (*p != ':')
+           {
+             HTTP_DBG (1, "port not present in authority");
+             http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
+             return HTTP_SM_STOP;
+           }
+         req->base.upgrade_proto = HTTP_UPGRADE_PROTO_NA;
        }
+
       req->base.is_tunnel = 1;
       http_io_as_add_want_read_ntf (&req->base);
     }
 
-  req->base.control_data_len = control_data.control_data_len;
-  req->base.headers_offset = control_data.headers - wrk->header_list;
-  req->base.headers_len = control_data.headers_len;
   if (control_data.content_len_header_index != ~0)
     {
       req->base.content_len_header_index =
@@ -1043,6 +1082,7 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
   msg.data.upgrade_proto = HTTP_UPGRADE_PROTO_NA;
   msg.data.body_offset = req->base.control_data_len;
   msg.data.body_len = req->base.body_len;
+  msg.data.upgrade_proto = req->base.upgrade_proto;
 
   svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) },
                             { wrk->header_list,
@@ -2362,6 +2402,7 @@ http2_init (vlib_main_t *vm)
   h2m->settings = http2_default_conn_settings;
   h2m->settings.max_concurrent_streams = 100; /* by default unlimited */
   h2m->settings.max_header_list_size = 1 << 14; /* by default unlimited */
+  h2m->settings.enable_connect_protocol = 1;   /* enable extended connect */
   http_register_engine (&http2_engine, HTTP_VERSION_2);
 
   return 0;
index 9fc9534..b48c2a2 100644 (file)
@@ -56,7 +56,8 @@ format_http2_error (u8 *s, va_list *va)
   _ (1, SCHEME, "scheme")                                                     \
   _ (2, AUTHORITY, "authority")                                               \
   _ (3, PATH, "path")                                                         \
-  _ (4, STATUS, "status")
+  _ (4, STATUS, "status")                                                     \
+  _ (5, PROTOCOL, "protocol")
 
 /* value, label, member, min, max, default_value, err_code */
 #define foreach_http2_settings                                                \
@@ -70,7 +71,9 @@ format_http2_error (u8 *s, va_list *va)
   _ (5, MAX_FRAME_SIZE, max_frame_size, 16384, 16777215, 16384,               \
      HTTP2_ERROR_PROTOCOL_ERROR)                                              \
   _ (6, MAX_HEADER_LIST_SIZE, max_header_list_size, 0, CLIB_U32_MAX,          \
-     CLIB_U32_MAX, HTTP2_ERROR_NO_ERROR)
+     CLIB_U32_MAX, HTTP2_ERROR_NO_ERROR)                                      \
+  _ (8, ENABLE_CONNECT_PROTOCOL, enable_connect_protocol, 0, 1, 0,            \
+     HTTP2_ERROR_NO_ERROR)
 
 typedef enum
 {