From: Matus Fabian Date: Wed, 11 Jun 2025 15:02:25 +0000 (-0400) Subject: http: http/2 extended connect method X-Git-Url: https://gerrit.fd.io/r/gitweb?a=commitdiff_plain;h=2eb0e479bf76b5202d24d4088ea97d86ee5aa0ee;p=vpp.git http: http/2 extended connect method Type: feature Change-Id: I42e94b6282fa693d3c69f938ec9d3a290b71b9fa Signed-off-by: Matus Fabian --- diff --git a/extras/hs-test/h2spec_extras/h2spec_extras.go b/extras/hs-test/h2spec_extras/h2spec_extras.go index 6957557a01b..b1d86a0b3ad 100644 --- a/extras/hs-test/h2spec_extras/h2spec_extras.go +++ b/extras/hs-test/h2spec_extras/h2spec_extras.go @@ -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 } diff --git a/extras/hs-test/infra/suite_vpp_proxy.go b/extras/hs-test/infra/suite_vpp_proxy.go index ae3f203d71b..db6b6bedf7a 100644 --- a/extras/hs-test/infra/suite_vpp_proxy.go +++ b/extras/hs-test/infra/suite_vpp_proxy.go @@ -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 { diff --git a/extras/hs-test/infra/suite_vpp_udp_proxy.go b/extras/hs-test/infra/suite_vpp_udp_proxy.go index 7043864a23d..29ee5b7e6da 100644 --- a/extras/hs-test/infra/suite_vpp_udp_proxy.go +++ b/extras/hs-test/infra/suite_vpp_udp_proxy.go @@ -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)) + } + +}) diff --git a/src/plugins/http/http2/hpack.c b/src/plugins/http/http2/hpack.c index 76021ae14a6..324602ce346 100644 --- a/src/plugins/http/http2/hpack.c +++ b/src/plugins/http/http2/hpack.c @@ -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; diff --git a/src/plugins/http/http2/hpack.h b/src/plugins/http/http2/hpack.h index 69144de133a..fef2a96c2df 100644 --- a/src/plugins/http/http2/hpack.h +++ b/src/plugins/http/http2/hpack.h @@ -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; diff --git a/src/plugins/http/http2/http2.c b/src/plugins/http/http2/http2.c index d1b6915ac1f..19a43314f31 100644 --- a/src/plugins/http/http2/http2.c +++ b/src/plugins/http/http2/http2.c @@ -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; diff --git a/src/plugins/http/http2/http2.h b/src/plugins/http/http2/http2.h index 9fc95344771..b48c2a27156 100644 --- a/src/plugins/http/http2/http2.h +++ b/src/plugins/http/http2/http2.h @@ -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 {