import (
"fmt"
"slices"
+ "strconv"
"github.com/summerwind/h2spec/config"
"github.com/summerwind/h2spec/spec"
tg.AddTestGroup(FlowControl())
tg.AddTestGroup(ConnectMethod())
+ tg.AddTestGroup(ExtendedConnectMethod())
return tg
}
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
}
{desc: "extras/2/2"},
{desc: "extras/2/3"},
{desc: "extras/2/4"},
+ {desc: "extras/2/5"},
}
for _, test := range testCases {
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 {
}
}
})
+
+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))
+ }
+
+})
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))
{
}
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;
u8 *path;
u32 path_len;
u8 *headers;
+ u8 *protocol;
+ u32 protocol_len;
uword content_len_header_index;
u32 headers_len;
u32 control_data_len;
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");
}
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 =
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,
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;
_ (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 \
_ (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
{