From b6cc662e49cadcd47ba4b4d1f7ed25c172a56fc7 Mon Sep 17 00:00:00 2001 From: Matus Fabian Date: Thu, 10 Jul 2025 12:39:37 -0400 Subject: [PATCH] http: http2 client side - only with TLS - request are serialized within one app session (no multiplexing) - http version can be specified in http client Type: feature Change-Id: I2fe11bd3252985d1bd1732616837f7a91f37f6a3 Signed-off-by: Matus Fabian --- extras/hs-test/docker/Dockerfile.nginx-server | 1 + extras/hs-test/http2_test.go | 91 +++- extras/hs-test/infra/suite_envoy_proxy.go | 6 + extras/hs-test/infra/suite_http1.go | 10 +- extras/hs-test/infra/suite_http2.go | 38 +- extras/hs-test/infra/suite_no_topo.go | 12 +- extras/hs-test/infra/suite_no_topo6.go | 10 +- extras/hs-test/infra/suite_veth.go | 5 +- extras/hs-test/infra/suite_vpp_proxy.go | 10 +- extras/hs-test/resources/nginx/nginx_server.conf | 14 + extras/hs-test/topo-containers/envoyProxy.yaml | 2 + extras/hs-test/topo-containers/single.yaml | 2 + extras/hs-test/topo-containers/vppProxy.yaml | 2 + src/plugins/hs_apps/http_client.c | 44 +- src/plugins/http/http.c | 12 +- src/plugins/http/http.h | 69 +++ src/plugins/http/http2/frame.c | 3 +- src/plugins/http/http2/http2.c | 558 ++++++++++++++++++++--- src/plugins/http/http2/http2.h | 26 +- src/plugins/http/http_private.h | 20 - 20 files changed, 818 insertions(+), 117 deletions(-) diff --git a/extras/hs-test/docker/Dockerfile.nginx-server b/extras/hs-test/docker/Dockerfile.nginx-server index 5284ea77636..30f32a5a267 100644 --- a/extras/hs-test/docker/Dockerfile.nginx-server +++ b/extras/hs-test/docker/Dockerfile.nginx-server @@ -9,6 +9,7 @@ COPY script/nginx_server_entrypoint.sh /usr/bin/nginx_server_entrypoint.sh COPY resources/nginx/html/index.html /usr/share/nginx/index.html RUN fallocate -l 10MB /usr/share/nginx/httpTestFile +RUN touch /usr/share/nginx/test_upload RUN mkdir /usr/share/nginx/upload && chmod 777 /usr/share/nginx/upload ENTRYPOINT ["nginx_server_entrypoint.sh"] diff --git a/extras/hs-test/http2_test.go b/extras/hs-test/http2_test.go index 011e4b907a3..238d335be84 100644 --- a/extras/hs-test/http2_test.go +++ b/extras/hs-test/http2_test.go @@ -10,8 +10,10 @@ import ( ) func init() { - RegisterH2Tests(Http2TcpGetTest, Http2TcpPostTest, Http2MultiplexingTest, Http2TlsTest, Http2ContinuationTxTest, Http2ServerMemLeakTest) + RegisterH2Tests(Http2TcpGetTest, Http2TcpPostTest, Http2MultiplexingTest, Http2TlsTest, Http2ContinuationTxTest, Http2ServerMemLeakTest, + Http2ClientGetTest, Http2ClientPostTest, Http2ClientPostPtrTest, Http2ClientGetRepeatTest) RegisterH2MWTests(Http2MultiplexingMWTest) + RegisterVethTests(Http2CliTlsTest, Http2ClientContinuationTest) } func Http2TcpGetTest(s *Http2Suite) { @@ -155,3 +157,90 @@ func Http2ServerMemLeakTest(s *Http2Suite) { s.AssertNil(err, fmt.Sprint(err)) vpp.MemLeakCheck(traces1, traces2) } + +func Http2CliTlsTest(s *VethsSuite) { + uri := "https://" + s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + + s.Containers.ServerVpp.VppInstance.Vppctl("http cli server uri " + uri) + + o := s.Containers.ClientVpp.VppInstance.Vppctl("http cli client" + + " uri " + uri + "/show/version") + s.Log(o) + s.AssertContains(o, "", " not found in the result!") + s.AssertContains(o, "", " not found in the result!") + + /* second request to test postponed ho-cleanup */ + o = s.Containers.ClientVpp.VppInstance.Vppctl("http cli client" + + " uri " + uri + "/show/vlib/graph") + s.Log(o) + s.AssertContains(o, "", " not found in the result!") + s.AssertContains(o, "", " not found in the result!") +} + +func Http2ClientGetTest(s *Http2Suite) { + vpp := s.Containers.Vpp.VppInstance + serverAddress := s.HostAddr() + ":" + s.Ports.Port2 + + s.CreateNginxServer() + s.AssertNil(s.Containers.NginxServer.Start()) + + uri := "https://" + serverAddress + "/httpTestFile" + o := vpp.Vppctl("http client save-to response.txt verbose uri " + uri) + s.Log(o) + s.AssertContains(o, "HTTP/2 200 OK") + s.AssertContains(o, "10000000 bytes saved to file") +} + +func http2ClientPostFile(s *Http2Suite, usePtr bool, fileSize int) { + vpp := s.Containers.Vpp.VppInstance + serverAddress := s.HostAddr() + ":" + s.Ports.Port2 + + fileName := "/tmp/test_file.txt" + s.Log(vpp.Container.Exec(false, "fallocate -l "+strconv.Itoa(fileSize)+" "+fileName)) + s.Log(vpp.Container.Exec(false, "ls -la "+fileName)) + + s.CreateNginxServer() + s.AssertNil(s.Containers.NginxServer.Start()) + + uri := "https://" + serverAddress + "/test_upload" + cmd := "http client post verbose uri " + uri + " file " + fileName + if usePtr { + cmd += " use-ptr" + } + o := vpp.Vppctl(cmd) + s.Log(o) + s.AssertContains(o, "HTTP/2 200 OK") +} + +func Http2ClientPostTest(s *Http2Suite) { + http2ClientPostFile(s, false, 131072) +} + +func Http2ClientPostPtrTest(s *Http2Suite) { + http2ClientPostFile(s, true, 131072) +} + +func Http2ClientGetRepeatTest(s *Http2Suite) { + vpp := s.Containers.Vpp.VppInstance + serverAddress := s.HostAddr() + ":" + s.Ports.Port2 + + s.CreateNginxServer() + s.AssertNil(s.Containers.NginxServer.Start()) + + uri := "https://" + serverAddress + "/64B" + cmd := fmt.Sprintf("http client http2 repeat %d uri %s", 10, uri) + o := vpp.Vppctl(cmd) + s.Log(o) +} + +func Http2ClientContinuationTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + + s.Containers.ServerVpp.VppInstance.Vppctl("http tps uri tls://" + serverAddress + " no-zc") + + uri := fmt.Sprintf("https://%s/test_file_64?test_header=32k", serverAddress) + o := s.Containers.ClientVpp.VppInstance.Vppctl("http client fifo-size 64k verbose save-to response.txt uri " + uri) + s.Log(o) + s.AssertContains(o, "HTTP/2 200 OK") + s.AssertGreaterThan(strings.Count(o, "x"), 32768) +} diff --git a/extras/hs-test/infra/suite_envoy_proxy.go b/extras/hs-test/infra/suite_envoy_proxy.go index 42a5b937688..1932f18116f 100644 --- a/extras/hs-test/infra/suite_envoy_proxy.go +++ b/extras/hs-test/infra/suite_envoy_proxy.go @@ -31,6 +31,7 @@ type EnvoyProxySuite struct { } Ports struct { Nginx uint16 + NginxSsl uint16 Proxy uint16 EnvoyAdmin uint16 } @@ -64,6 +65,7 @@ func (s *EnvoyProxySuite) SetupSuite() { s.Containers.EnvoyProxy = s.GetContainerByName("envoy-vcl") s.Containers.Curl = s.GetContainerByName("curl") s.Ports.Nginx = s.GeneratePortAsInt() + s.Ports.NginxSsl = s.GeneratePortAsInt() s.Ports.Proxy = s.GeneratePortAsInt() s.Ports.EnvoyAdmin = s.GeneratePortAsInt() } @@ -89,11 +91,15 @@ func (s *EnvoyProxySuite) SetupTest() { LogPrefix string Address string Port uint16 + PortSsl uint16 + Http2 string Timeout int }{ LogPrefix: s.Containers.NginxServerTransient.Name, Address: s.Interfaces.Server.Ip4AddressString(), Port: s.Ports.Nginx, + PortSsl: s.Ports.NginxSsl, + Http2: "off", Timeout: s.maxTimeout, } s.Containers.NginxServerTransient.CreateConfigFromTemplate( diff --git a/extras/hs-test/infra/suite_http1.go b/extras/hs-test/infra/suite_http1.go index ff2a65f90f5..248a413bce4 100644 --- a/extras/hs-test/infra/suite_http1.go +++ b/extras/hs-test/infra/suite_http1.go @@ -24,8 +24,9 @@ type Http1Suite struct { Wrk *Container } Ports struct { - NginxServer string - Http string + NginxServer string + NginxServerSsl string + Http string } } @@ -49,6 +50,7 @@ func (s *Http1Suite) SetupSuite() { s.Containers.Wrk = s.GetContainerByName("wrk") s.Ports.Http = s.GeneratePort() s.Ports.NginxServer = s.GeneratePort() + s.Ports.NginxServerSsl = s.GeneratePort() } func (s *Http1Suite) SetupTest() { @@ -90,11 +92,15 @@ func (s *Http1Suite) CreateNginxServer() { LogPrefix string Address string Port string + PortSsl string + Http2 string Timeout int }{ LogPrefix: s.Containers.NginxServer.Name, Address: s.Interfaces.Tap.Ip4AddressString(), Port: s.Ports.NginxServer, + PortSsl: s.Ports.NginxServerSsl, + Http2: "off", Timeout: 600, } s.Containers.NginxServer.CreateConfigFromTemplate( diff --git a/extras/hs-test/infra/suite_http2.go b/extras/hs-test/infra/suite_http2.go index a0507a38d40..d67399b577c 100644 --- a/extras/hs-test/infra/suite_http2.go +++ b/extras/hs-test/infra/suite_http2.go @@ -30,13 +30,15 @@ type Http2Suite struct { Tap *NetInterface } Containers struct { - Vpp *Container - Curl *Container - H2load *Container + Vpp *Container + Curl *Container + H2load *Container + NginxServer *Container } Ports struct { Port1 string Port1AsInt int + Port2 string } } @@ -58,7 +60,9 @@ func (s *Http2Suite) SetupSuite() { s.Containers.Vpp = s.GetContainerByName("vpp") s.Containers.Curl = s.GetContainerByName("curl") s.Containers.H2load = s.GetContainerByName("h2load") + s.Containers.NginxServer = s.GetTransientContainerByName("nginx-server") s.Ports.Port1 = s.GeneratePort() + s.Ports.Port2 = s.GeneratePort() var err error s.Ports.Port1AsInt, err = strconv.Atoi(s.Ports.Port1) s.AssertNil(err) @@ -92,6 +96,34 @@ func (s *Http2Suite) VppAddr() string { return s.Interfaces.Tap.Peer.Ip4AddressString() } +func (s *Http2Suite) HostAddr() string { + return s.Interfaces.Tap.Ip4AddressString() +} + +func (s *Http2Suite) CreateNginxServer() { + s.AssertNil(s.Containers.NginxServer.Create()) + nginxSettings := struct { + LogPrefix string + Address string + Port string + PortSsl string + Http2 string + Timeout int + }{ + LogPrefix: s.Containers.NginxServer.Name, + Address: s.Interfaces.Tap.Ip4AddressString(), + Port: s.Ports.Port1, + PortSsl: s.Ports.Port2, + Http2: "on", + Timeout: 600, + } + s.Containers.NginxServer.CreateConfigFromTemplate( + "/nginx.conf", + "./resources/nginx/nginx_server.conf", + nginxSettings, + ) +} + var _ = Describe("Http2Suite", Ordered, ContinueOnFailure, func() { var s Http2Suite BeforeAll(func() { diff --git a/extras/hs-test/infra/suite_no_topo.go b/extras/hs-test/infra/suite_no_topo.go index 1749d13d53f..5a411adc294 100644 --- a/extras/hs-test/infra/suite_no_topo.go +++ b/extras/hs-test/infra/suite_no_topo.go @@ -29,9 +29,10 @@ type NoTopoSuite struct { Ab *Container } Ports struct { - NginxServer string - NginxHttp3 string - Http string + NginxServer string + NginxServerSsl string + NginxHttp3 string + Http string } } @@ -59,6 +60,7 @@ func (s *NoTopoSuite) SetupSuite() { s.Containers.Ab = s.GetContainerByName("ab") s.Ports.Http = s.GeneratePort() s.Ports.NginxServer = s.GeneratePort() + s.Ports.NginxServerSsl = s.GeneratePort() s.Ports.NginxHttp3 = s.GeneratePort() } @@ -125,11 +127,15 @@ func (s *NoTopoSuite) CreateNginxServer() { LogPrefix string Address string Port string + PortSsl string + Http2 string Timeout int }{ LogPrefix: s.Containers.NginxServer.Name, Address: s.Interfaces.Tap.Ip4AddressString(), Port: s.Ports.NginxServer, + PortSsl: s.Ports.NginxServerSsl, + Http2: "off", Timeout: 600, } s.Containers.NginxServer.CreateConfigFromTemplate( diff --git a/extras/hs-test/infra/suite_no_topo6.go b/extras/hs-test/infra/suite_no_topo6.go index a57cb29721e..0997e8a1720 100644 --- a/extras/hs-test/infra/suite_no_topo6.go +++ b/extras/hs-test/infra/suite_no_topo6.go @@ -28,8 +28,9 @@ type NoTopo6Suite struct { Ab *Container } Ports struct { - NginxServer string - Http string + NginxServer string + NginxServerSsl string + Http string } } @@ -54,6 +55,7 @@ func (s *NoTopo6Suite) SetupSuite() { s.Containers.Ab = s.GetContainerByName("ab") s.Ports.Http = s.GeneratePort() s.Ports.NginxServer = s.GeneratePort() + s.Ports.NginxServerSsl = s.GeneratePort() } func (s *NoTopo6Suite) SetupTest() { @@ -119,11 +121,15 @@ func (s *NoTopo6Suite) CreateNginxServer() { LogPrefix string Address string Port string + PortSsl string + Http2 string Timeout int }{ LogPrefix: s.Containers.NginxServer.Name, Address: "[" + s.Interfaces.Tap.Ip6AddressString() + "]", Port: s.Ports.NginxServer, + PortSsl: s.Ports.NginxServerSsl, + Http2: "off", Timeout: 600, } s.Containers.NginxServer.CreateConfigFromTemplate( diff --git a/extras/hs-test/infra/suite_veth.go b/extras/hs-test/infra/suite_veth.go index 2b7832e2880..65506c448b3 100644 --- a/extras/hs-test/infra/suite_veth.go +++ b/extras/hs-test/infra/suite_veth.go @@ -70,13 +70,16 @@ func (s *VethsSuite) SetupTest() { } else { sessionConfig.Close() } + // For http/2 continuation frame test between http tps and http client + var httpConfig Stanza + httpConfig.NewStanza("http").NewStanza("http2").Append("max-header-list-size 65536") // ... For server serverVpp, err := s.Containers.ServerVpp.newVppInstance(s.Containers.ServerVpp.AllocatedCpus, sessionConfig) s.AssertNotNil(serverVpp, fmt.Sprint(err)) // ... For client - clientVpp, err := s.Containers.ClientVpp.newVppInstance(s.Containers.ClientVpp.AllocatedCpus, sessionConfig) + clientVpp, err := s.Containers.ClientVpp.newVppInstance(s.Containers.ClientVpp.AllocatedCpus, sessionConfig, httpConfig) s.AssertNotNil(clientVpp, fmt.Sprint(err)) s.SetupServerVpp() diff --git a/extras/hs-test/infra/suite_vpp_proxy.go b/extras/hs-test/infra/suite_vpp_proxy.go index d43a588fe45..7b108b966f3 100644 --- a/extras/hs-test/infra/suite_vpp_proxy.go +++ b/extras/hs-test/infra/suite_vpp_proxy.go @@ -41,8 +41,9 @@ type VppProxySuite struct { IperfC *Container } Ports struct { - Server uint16 - Proxy uint16 + Server uint16 + ServerSsl uint16 + Proxy uint16 } } @@ -65,6 +66,7 @@ func (s *VppProxySuite) SetupSuite() { s.LoadNetworkTopology("2taps") s.LoadContainerTopology("vppProxy") s.Ports.Server = s.GeneratePortAsInt() + s.Ports.ServerSsl = s.GeneratePortAsInt() s.Ports.Proxy = s.GeneratePortAsInt() if *IsVppDebug { @@ -117,11 +119,15 @@ func (s *VppProxySuite) SetupNginxServer() { LogPrefix string Address string Port uint16 + PortSsl uint16 + Http2 string Timeout int }{ LogPrefix: s.Containers.NginxServerTransient.Name, Address: s.Interfaces.Server.Ip4AddressString(), Port: s.Ports.Server, + PortSsl: s.Ports.ServerSsl, + Http2: "off", Timeout: s.maxTimeout, } s.Containers.NginxServerTransient.CreateConfigFromTemplate( diff --git a/extras/hs-test/resources/nginx/nginx_server.conf b/extras/hs-test/resources/nginx/nginx_server.conf index 26d58348039..a40ed7c309a 100644 --- a/extras/hs-test/resources/nginx/nginx_server.conf +++ b/extras/hs-test/resources/nginx/nginx_server.conf @@ -22,9 +22,15 @@ http { server { access_log /tmp/nginx/{{.LogPrefix}}-access.log; listen {{.Port}}; + listen {{.PortSsl}} ssl; server_name {{.Address}}; root /usr/share/nginx; + ssl_certificate /etc/nginx/ssl/localhost.crt; + ssl_certificate_key /etc/nginx/ssl/localhost.key; + http2 {{.Http2}}; index index.html index.htm; + # to allow POST on static pages + error_page 405 =200 $uri; location ~ "/upload/([0-9a-zA-Z-.]*)$" { alias /usr/share/nginx/upload/$1; client_body_temp_path /tmp; @@ -39,5 +45,13 @@ http { location / { sendfile on; } + # HTTP2 will not wait for the post body and return 200 + location = /test_upload { + proxy_pass http://127.0.0.1:{{.Port}}/dev-null; + } + location = /dev-null { + return 200; + } + # HTTP2 upload fix end } } diff --git a/extras/hs-test/topo-containers/envoyProxy.yaml b/extras/hs-test/topo-containers/envoyProxy.yaml index cb2d673b788..3189080cb92 100644 --- a/extras/hs-test/topo-containers/envoyProxy.yaml +++ b/extras/hs-test/topo-containers/envoyProxy.yaml @@ -28,6 +28,8 @@ containers: - <<: *shared-vol container-dir: "/tmp/nginx" is-default-work-dir: true + - host-dir: $HST_DIR/resources/cert + container-dir: "/etc/nginx/ssl" image: "hs-test/nginx-server" is-optional: true - name: "curl" diff --git a/extras/hs-test/topo-containers/single.yaml b/extras/hs-test/topo-containers/single.yaml index d1f43ad07b4..c862a7991bc 100644 --- a/extras/hs-test/topo-containers/single.yaml +++ b/extras/hs-test/topo-containers/single.yaml @@ -33,6 +33,8 @@ containers: - <<: *shared-vol container-dir: "/tmp/nginx" is-default-work-dir: true + - host-dir: $HST_DIR/resources/cert + container-dir: "/etc/nginx/ssl" image: "hs-test/nginx-server" is-optional: true diff --git a/extras/hs-test/topo-containers/vppProxy.yaml b/extras/hs-test/topo-containers/vppProxy.yaml index 5f0248027a1..54aac03f037 100644 --- a/extras/hs-test/topo-containers/vppProxy.yaml +++ b/extras/hs-test/topo-containers/vppProxy.yaml @@ -26,6 +26,8 @@ containers: - <<: *shared-vol container-dir: "/tmp/nginx" is-default-work-dir: true + - host-dir: $HST_DIR/resources/cert + container-dir: "/etc/nginx/ssl" image: "hs-test/nginx-server" is-optional: true - name: "curl" diff --git a/src/plugins/hs_apps/http_client.c b/src/plugins/hs_apps/http_client.c index ddb92b3a3af..6ddf0b52fed 100644 --- a/src/plugins/hs_apps/http_client.c +++ b/src/plugins/hs_apps/http_client.c @@ -10,6 +10,7 @@ #include #include #include +#include #define foreach_hc_s_flag \ _ (1, IS_CLOSED) \ @@ -100,6 +101,7 @@ typedef struct bool was_transport_closed; u32 ckpair_index; u64 max_body_size; + http_version_t http_version; } hc_main_t; typedef enum @@ -411,6 +413,7 @@ hc_rx_callback (session_t *s) u32 max_deq; session_error_t session_err = 0; int send_err = 0; + http_version_t http_version; if (hc_session->session_flags & HC_S_FLAG_IS_CLOSED) { @@ -441,21 +444,24 @@ hc_rx_callback (session_t *s) http_init_header_table_buf (&hc_session->resp_headers, msg); if (!hcm->repeat) - hc_session->response_status = - format (0, "%U", format_http_status_code, msg.code); + { + http_version = http_session_get_version (s); + hc_session->response_status = + format (0, "%U %U", format_http_version, http_version, + format_http_status_code, msg.code); + } svm_fifo_dequeue_drop (s->rx_fifo, msg.data.headers_offset); rv = svm_fifo_dequeue (s->rx_fifo, msg.data.headers_len, hc_session->resp_headers.buf); ASSERT (rv == msg.data.headers_len); - HTTP_DBG (1, - (char *) format (0, "%U", format_hash, - hc_session->resp_headers.value_by_name)); msg.data.body_offset -= msg.data.headers_len + msg.data.headers_offset; http_build_header_table (&hc_session->resp_headers, msg); + HTTP_DBG (2, "%U", format_hash, + hc_session->resp_headers.value_by_name); const http_token_t *content_type = http_get_header ( &hc_session->resp_headers, http_header_name_token (HTTP_HEADER_CONTENT_TYPE)); @@ -522,6 +528,11 @@ hc_rx_callback (session_t *s) } ASSERT (rv == n_deq); + if (svm_fifo_needs_deq_ntf (s->rx_fifo, n_deq)) + { + svm_fifo_clear_deq_ntf (s->rx_fifo); + session_program_transport_io_evt (s->handle, SESSION_IO_EVT_RX); + } if (!(hc_session->session_flags & HC_S_FLAG_CHUNKED_BODY)) vec_set_len (hc_session->http_response, curr + n_deq); ASSERT (hc_session->to_recv >= rv); @@ -552,10 +563,12 @@ done: if (hc_session->stats.elapsed_time >= hcm->duration && hc_session->stats.request_count >= hc_session->stats.req_per_wrk) { + HTTP_DBG (1, "repeat done"); hc_session_disconnect_callback (s); } else { + HTTP_DBG (1, "doing another repeat"); send_err = hc_request (s, wrk, hc_session, session_err); if (send_err) clib_warning ("failed to send request, error %d", send_err); @@ -633,7 +646,7 @@ hc_attach () a->options[APP_OPTIONS_SEGMENT_SIZE] = segment_size; a->options[APP_OPTIONS_ADD_SEGMENT_SIZE] = segment_size; a->options[APP_OPTIONS_RX_FIFO_SIZE] = - hcm->fifo_size ? hcm->fifo_size : 8 << 10; + hcm->fifo_size ? hcm->fifo_size : 32 << 10; a->options[APP_OPTIONS_TX_FIFO_SIZE] = hcm->fifo_size ? hcm->fifo_size : 32 << 10; a->options[APP_OPTIONS_FLAGS] = APP_OPTIONS_FLAGS_IS_BUILTIN; @@ -707,6 +720,17 @@ hc_connect () &a->sep_ext, TRANSPORT_ENDPT_EXT_CFG_CRYPTO, sizeof (transport_endpt_crypto_cfg_t)); ext_cfg->crypto.ckpair_index = hcm->ckpair_index; + switch (hcm->http_version) + { + case HTTP_VERSION_1: + ext_cfg->crypto.alpn_protos[0] = TLS_ALPN_PROTO_HTTP_1_1; + break; + case HTTP_VERSION_2: + ext_cfg->crypto.alpn_protos[1] = TLS_ALPN_PROTO_HTTP_2; + break; + default: + break; + } } session_send_rpc_evt_to_thread_force (transport_cl_thread (), hc_connect_rpc, @@ -955,6 +979,7 @@ hc_command_fn (vlib_main_t *vm, unformat_input_t *input, hcm->fifo_size = 0; hcm->was_transport_closed = false; hcm->verbose = false; + hcm->http_version = HTTP_VERSION_NA; /* default max - 64MB */ hcm->max_body_size = 64 << 20; hc_stats.request_count = 0; @@ -1035,7 +1060,10 @@ hc_command_fn (vlib_main_t *vm, unformat_input_t *input, ; else if (unformat (line_input, "secret %lu", &hcm->appns_secret)) ; - + else if (unformat (line_input, "http1")) + hcm->http_version = HTTP_VERSION_1; + else if (unformat (line_input, "http2")) + hcm->http_version = HTTP_VERSION_2; else { err = clib_error_return (0, "unknown input `%U'", @@ -1140,7 +1168,7 @@ VLIB_CLI_COMMAND (hc_command, static) = { "[timeout (default = 10)] [repeat | duration ] " "[sessions <# of sessions>] [appns secret ] " "[fifo-size ] [private-segment-size ] [prealloc-fifos ]" - "[max-body-size ]", + "[max-body-size ] [http1|http2]", .function = hc_command_fn, .is_mp_safe = 1, }; diff --git a/src/plugins/http/http.c b/src/plugins/http/http.c index 5a61b0d717c..ccf987a6ad0 100644 --- a/src/plugins/http/http.c +++ b/src/plugins/http/http.c @@ -869,6 +869,7 @@ http_transport_connect (transport_endpoint_cfg_t *tep) u32 hc_index; session_t *ho; transport_endpt_ext_cfg_t *ext_cfg; + segment_manager_props_t *props; app_worker_t *app_wrk = app_worker_get (sep->app_wrk_index); clib_memset (cargs, 0, sizeof (*cargs)); @@ -883,7 +884,6 @@ http_transport_connect (transport_endpoint_cfg_t *tep) hc->hc_pa_wrk_index = sep->app_wrk_index; hc->hc_pa_app_api_ctx = sep->opaque; hc->state = HTTP_CONN_STATE_CONNECTING; - /* TODO: set to HTTP_VERSION_NA in case of TLS */ hc->version = HTTP_VERSION_1; cargs->api_context = hc_index; @@ -900,7 +900,15 @@ http_transport_connect (transport_endpoint_cfg_t *tep) if (ext_cfg) { HTTP_DBG (1, "app set tls"); + hc->version = HTTP_VERSION_NA; cargs->sep.transport_proto = TRANSPORT_PROTO_TLS; + if (ext_cfg->crypto.alpn_protos[0] == TLS_ALPN_PROTO_NONE) + { + HTTP_DBG (1, + "app do not set alpn list, using default (h2,http/1.1)"); + ext_cfg->crypto.alpn_protos[0] = TLS_ALPN_PROTO_HTTP_2; + ext_cfg->crypto.alpn_protos[1] = TLS_ALPN_PROTO_HTTP_1_1; + } } if (vec_len (app->name)) @@ -928,6 +936,8 @@ http_transport_connect (transport_endpoint_cfg_t *tep) session_type_from_proto_and_ip (TRANSPORT_PROTO_HTTP, sep->is_ip4); hc->hc_tc_session_handle = cargs->sh; hc->c_s_index = ho->session_index; + props = application_segment_manager_properties (app); + hc->app_rx_fifo_size = props->rx_fifo_size; return 0; } diff --git a/src/plugins/http/http.h b/src/plugins/http/http.h index 5777bd520a5..61c387bf781 100644 --- a/src/plugins/http/http.h +++ b/src/plugins/http/http.h @@ -20,6 +20,7 @@ #include #include #include +#include #define HTTP_DEBUG 0 @@ -31,6 +32,14 @@ #define HTTP_DBG(_lvl, _fmt, _args...) #endif +typedef enum http_version_ +{ + HTTP_VERSION_1, + HTTP_VERSION_2, + HTTP_VERSION_3, + HTTP_VERSION_NA = 7, +} http_version_t; + typedef enum http_udp_tunnel_mode_ { HTTP_UDP_TUNNEL_CAPSULE, /**< app receive raw capsule */ @@ -393,6 +402,66 @@ typedef struct http_msg_ http_msg_data_t data; } http_msg_t; +typedef union +{ + struct + { + u32 version : 3; + u32 req_index : 29; + }; + u32 as_u32; +} http_req_handle_t; + +STATIC_ASSERT (sizeof (http_req_handle_t) == sizeof (u32), "must fit in u32"); + +always_inline http_version_t +http_session_get_version (session_t *s) +{ + http_req_handle_t h; + h.as_u32 = s->connection_index; + return (http_version_t) h.version; +} + +always_inline u8 * +format_http_version (u8 *s, va_list *va) +{ + http_version_t v = va_arg (*va, http_version_t); + switch (v) + { + case HTTP_VERSION_1: + s = format (s, "HTTP/1.1"); + break; + case HTTP_VERSION_2: + s = format (s, "HTTP/2"); + break; + case HTTP_VERSION_3: + s = format (s, "HTTP/3"); + break; + default: + s = format (s, "unknown"); + break; + } + return s; +} + +always_inline u8 * +format_http_method (u8 *s, va_list *va) +{ + http_req_method_t method = va_arg (*va, http_req_method_t); + u8 *t = 0; + + switch (method) + { +#define _(s, str) \ + case HTTP_REQ_##s: \ + t = (u8 *) str; \ + break; + foreach_http_method +#undef _ + default : return format (s, "unknown"); + } + return format (s, "%s", t); +} always_inline u8 * format_http_bytes (u8 *s, va_list *va) { diff --git a/src/plugins/http/http2/frame.c b/src/plugins/http/http2/frame.c index 07821de3be7..fe593a00150 100644 --- a/src/plugins/http/http2/frame.c +++ b/src/plugins/http/http2/frame.c @@ -73,7 +73,8 @@ http2_frame_read_settings (http2_conn_settings_t *settings, u8 *payload, entry = (http2_settings_entry_t *) payload; switch (clib_net_to_host_u16 (entry->identifier)) { -#define _(v, label, member, min, max, default_value, err_code) \ +#define _(v, label, member, min, max, default_value, err_code, server, \ + client) \ case HTTP2_SETTINGS_##label: \ value = clib_net_to_host_u32 (entry->value); \ if (!(value >= min && value <= max)) \ diff --git a/src/plugins/http/http2/http2.c b/src/plugins/http/http2/http2.c index 8e9e6d923a8..9060a73bb11 100644 --- a/src/plugins/http/http2/http2.c +++ b/src/plugins/http/http2/http2.c @@ -30,7 +30,8 @@ typedef enum http2_stream_state_ #define foreach_http2_req_flags \ _ (APP_CLOSED, "app-closed") \ - _ (NEED_WINDOW_UPDATE, "need-window-update") + _ (NEED_WINDOW_UPDATE, "need-window-update") \ + _ (IS_PARENT, "is-parent") typedef enum http2_req_flags_bit_ { @@ -67,6 +68,7 @@ typedef struct http2_req_ #define foreach_http2_conn_flags \ _ (EXPECT_PREFACE, "expect-preface") \ _ (EXPECT_CONTINUATION, "expect-continuation") \ + _ (EXPECT_SERVER_SETTINGS, "expect-server-settings") \ _ (PREFACE_VERIFIED, "preface-verified") \ _ (TS_DESCHED, "ts-descheduled") @@ -206,7 +208,7 @@ http2_conn_ctx_free (http_conn_t *hc) } static inline http2_req_t * -http2_conn_alloc_req (http_conn_t *hc, u32 stream_id) +http2_conn_alloc_req (http_conn_t *hc) { http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index); http2_conn_ctx_t *h2c; @@ -224,19 +226,29 @@ http2_conn_alloc_req (http_conn_t *hc, u32 stream_id) req->base.hr_hc_index = hc->hc_hc_index; req->base.c_thread_index = hc->c_thread_index; req->base.c_flags |= TRANSPORT_CONNECTION_F_NO_LOOKUP; - req->stream_id = stream_id; req->stream_state = HTTP2_STREAM_STATE_IDLE; req->sched_list.next = CLIB_LLIST_INVALID_INDEX; req->sched_list.prev = CLIB_LLIST_INVALID_INDEX; h2c = http2_conn_ctx_get_w_thread (hc); - HTTP_DBG (1, "h2c [%u]%x req_index %x stream_id %u", hc->c_thread_index, - h2c - wrk->conn_pool, req_index, stream_id); + HTTP_DBG (1, "h2c [%u]%x req_index %x", hc->c_thread_index, + h2c - wrk->conn_pool, req_index); req->peer_window = h2c->peer_settings.initial_window_size; req->our_window = h2c->settings.initial_window_size; - hash_set (h2c->req_by_stream_id, stream_id, req_index); return req; } +static_always_inline void +http2_req_set_stream_id (http2_req_t *req, http2_conn_ctx_t *h2c, + u32 stream_id) +{ + HTTP_DBG (1, "req_index [%u]%x stream_id %u", req->base.c_thread_index, + ((http_req_handle_t) req->base.hr_req_handle).req_index, + stream_id); + req->stream_id = stream_id; + hash_set (h2c->req_by_stream_id, stream_id, + ((http_req_handle_t) req->base.hr_req_handle).req_index); +} + static inline void http2_conn_free_req (http2_conn_ctx_t *h2c, http2_req_t *req, clib_thread_index_t thread_index) @@ -252,12 +264,30 @@ http2_conn_free_req (http2_conn_ctx_t *h2c, http2_req_t *req, vec_free (req->base.headers); vec_free (req->base.target); http_buffer_free (&req->base.tx_buf); - hash_unset (h2c->req_by_stream_id, req->stream_id); + if (req->stream_id) + hash_unset (h2c->req_by_stream_id, req->stream_id); if (CLIB_DEBUG) memset (req, 0xba, sizeof (*req)); pool_put (wrk->req_pool, req); } +static inline void +http2_conn_reset_req (http2_conn_ctx_t *h2c, http2_req_t *req, + clib_thread_index_t thread_index) +{ + http2_worker_ctx_t *wrk = http2_get_worker (thread_index); + + if (clib_llist_elt_is_linked (req, sched_list)) + clib_llist_remove (wrk->req_pool, sched_list, req); + http_buffer_free (&req->base.tx_buf); + if (req->stream_id) + hash_unset (h2c->req_by_stream_id, req->stream_id); + req->flags = 0; + req->stream_state = HTTP2_STREAM_STATE_IDLE; + req->peer_window = h2c->peer_settings.initial_window_size; + req->our_window = h2c->settings.initial_window_size; +} + http2_req_t * http2_conn_get_req (http_conn_t *hc, u32 stream_id) { @@ -287,6 +317,16 @@ http2_req_get (u32 req_index, clib_thread_index_t thread_index) return pool_elt_at_index (wrk->req_pool, req_index); } +always_inline u32 +http2_conn_get_next_stream_id (http2_conn_ctx_t *h2c) +{ + if (h2c->last_opened_stream_id) + h2c->last_opened_stream_id += 2; + else + h2c->last_opened_stream_id = 1; + return h2c->last_opened_stream_id; +} + always_inline void http2_conn_schedule (http2_conn_ctx_t *h2c, clib_thread_index_t thread_index) { @@ -451,8 +491,9 @@ http2_send_server_preface (http_conn_t *hc) http2_settings_entry_t *setting, *settings_list = 0; http2_conn_ctx_t *h2c = http2_conn_ctx_get_w_thread (hc); -#define _(v, label, member, min, max, default_value, err_code) \ - if (h2c->settings.member != default_value) \ +#define _(v, label, member, min, max, default_value, err_code, server, \ + client) \ + if (h2c->settings.member != default_value && server) \ { \ vec_add2 (settings_list, setting, 1); \ setting->identifier = HTTP2_SETTINGS_##label; \ @@ -471,6 +512,37 @@ http2_send_server_preface (http_conn_t *hc) vec_free (settings_list); } +always_inline void +http2_send_client_preface (http_conn_t *hc) +{ + u8 *response, *p; + http2_settings_entry_t *setting, *settings_list = 0; + http2_conn_ctx_t *h2c = http2_conn_ctx_get_w_thread (hc); + + response = http_get_tx_buf (hc); + vec_add2 (response, p, http2_conn_preface.len); + clib_memcpy_fast (p, http2_conn_preface.base, http2_conn_preface.len); + +#define _(v, label, member, min, max, default_value, err_code, server, \ + client) \ + if (h2c->settings.member != default_value && client) \ + { \ + vec_add2 (settings_list, setting, 1); \ + setting->identifier = HTTP2_SETTINGS_##label; \ + setting->value = h2c->settings.member; \ + } + foreach_http2_settings +#undef _ + + http2_frame_write_settings (settings_list, &response); + /* send also connection window update */ + http2_frame_write_window_update (h2c->our_window - HTTP2_INITIAL_WIN_SIZE, 0, + &response); + http_io_ts_write (hc, response, vec_len (response), 0); + http_io_ts_after_write (hc, 1); + vec_free (settings_list); +} + /***********************/ /* stream TX scheduler */ /***********************/ @@ -530,10 +602,15 @@ http2_sched_dispatch_data (http2_req_t *req, http_conn_t *hc, u8 *n_emissions) if (hc->flags & HTTP_CONN_F_IS_SERVER) http2_stream_close (req, hc); else - req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED; + { + req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED; + http_req_state_change (&req->base, + HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY); + } } else { + http_io_as_dequeue_notify (&req->base, n_written); if (req->peer_window == 0) { /* mark that we need window update on stream */ @@ -546,7 +623,6 @@ http2_sched_dispatch_data (http2_req_t *req, http_conn_t *hc, u8 *n_emissions) HTTP_DBG (1, "adding to data queue req_index %x", ((http_req_handle_t) req->base.hr_req_handle).req_index); http2_req_schedule_data_tx (hc, req); - http_io_as_dequeue_notify (&req->base, n_written); } } @@ -748,7 +824,6 @@ http2_sched_dispatch_continuation (http2_req_t *req, http_conn_t *hc, vec_free (h2c->unsent_headers); *next_ri = clib_llist_next_index (req, sched_list); clib_llist_remove (wrk->req_pool, sched_list, req); - flags |= HTTP2_FRAME_FLAG_END_HEADERS; if (http_buffer_bytes_left (&req->base.tx_buf)) { /* start sending the actual data */ @@ -758,7 +833,16 @@ http2_sched_dispatch_continuation (http2_req_t *req, http_conn_t *hc, http2_req_schedule_data_tx (hc, req); } else - http2_stream_close (req, hc); + { + if (hc->flags & HTTP_CONN_F_IS_SERVER) + http2_stream_close (req, hc); + else + { + req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED; + http_req_state_change (&req->base, + HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY); + } + } } else { @@ -769,8 +853,9 @@ http2_sched_dispatch_continuation (http2_req_t *req, http_conn_t *hc, } static void -http2_sched_dispatch_headers (http2_req_t *req, http_conn_t *hc, - u8 *n_emissions, clib_llist_index_t *next_ri) +http2_sched_dispatch_resp_headers (http2_req_t *req, http_conn_t *hc, + u8 *n_emissions, + clib_llist_index_t *next_ri) { http_msg_t msg; u8 *response, *date, *app_headers = 0; @@ -903,6 +988,122 @@ http2_sched_dispatch_headers (http2_req_t *req, http_conn_t *hc, http_io_ts_after_write (hc, 0); } +static void +http2_sched_dispatch_req_headers (http2_req_t *req, http_conn_t *hc, + u8 *n_emissions, clib_llist_index_t *next_ri) +{ + http_msg_t msg; + u8 *request, *app_headers = 0; + u8 fh[HTTP2_FRAME_HEADER_SIZE]; + hpack_request_control_data_t control_data; + u8 flags = 0; + u32 n_written, stream_id, n_deq, max_write, headers_len, headers_left; + http2_conn_ctx_t *h2c; + http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index); + + req->stream_state = HTTP2_STREAM_STATE_OPEN; + + http_get_app_msg (&req->base, &msg); + ASSERT (msg.type == HTTP_MSG_REQUEST); + n_deq = sizeof (msg); + *n_emissions += msg.data.type == HTTP_MSG_DATA_PTR ? + HTTP2_SCHED_WEIGHT_HEADERS_PTR : + HTTP2_SCHED_WEIGHT_HEADERS_INLINE; + + request = http_get_tx_buf (hc); + + control_data.method = msg.method_type; + control_data.parsed_bitmap = HPACK_PSEUDO_HEADER_SCHEME_PARSED; + control_data.scheme = HTTP_URL_SCHEME_HTTPS; + control_data.parsed_bitmap |= HPACK_PSEUDO_HEADER_PATH_PARSED; + control_data.path = http_get_app_target (&req->base, &msg); + control_data.path_len = msg.data.target_path_len; + control_data.parsed_bitmap |= HPACK_PSEUDO_HEADER_AUTHORITY_PARSED; + control_data.authority = hc->host; + control_data.authority_len = vec_len (hc->host); + control_data.user_agent = hc->app_name; + control_data.user_agent_len = vec_len (hc->app_name); + + HTTP_DBG (1, "%U %U", format_http_method, control_data.method, + format_http_bytes, control_data.path, control_data.path_len); + if (msg.data.body_len) + { + control_data.content_len = msg.data.body_len; + http_req_tx_buffer_init (&req->base, &msg); + } + else + { + control_data.content_len = HPACK_ENCODER_SKIP_CONTENT_LEN; + flags |= HTTP2_FRAME_FLAG_END_STREAM; + } + + if (msg.data.headers_len) + { + n_deq += msg.data.type == HTTP_MSG_DATA_PTR ? sizeof (uword) : + msg.data.headers_len; + app_headers = http_get_app_header_list (&req->base, &msg); + } + + hpack_serialize_request (app_headers, msg.data.headers_len, &control_data, + &request); + headers_len = vec_len (request); + + h2c = http2_conn_ctx_get_w_thread (hc); + + max_write = http_io_ts_max_write (hc, 0); + max_write -= HTTP2_FRAME_HEADER_SIZE; + max_write = clib_min (max_write, h2c->peer_settings.max_frame_size); + + stream_id = http2_conn_get_next_stream_id (h2c); + http2_req_set_stream_id (req, h2c, stream_id); + + http_io_as_dequeue_notify (&req->base, n_deq); + + if (headers_len <= max_write) + { + *next_ri = clib_llist_next_index (req, sched_list); + clib_llist_remove (wrk->req_pool, sched_list, req); + flags |= HTTP2_FRAME_FLAG_END_HEADERS; + if (msg.data.body_len) + { + /* start sending the actual data */ + req->dispatch_data_cb = http2_sched_dispatch_data; + HTTP_DBG (1, "adding to data queue req_index %x", + ((http_req_handle_t) req->base.hr_req_handle).req_index); + http2_req_schedule_data_tx (hc, req); + } + else + { + req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED; + http_req_state_change (&req->base, + HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY); + } + } + else + { + /* we need to send CONTINUATION frame as next */ + HTTP_DBG (1, "response headers need to be fragmented"); + *next_ri = clib_llist_entry_index (wrk->req_pool, req); + headers_len = max_write; + headers_left = vec_len (request) - headers_len; + req->dispatch_headers_cb = http2_sched_dispatch_continuation; + /* move unsend portion of headers to connection ctx */ + ASSERT (h2c->unsent_headers == 0); + vec_validate (h2c->unsent_headers, headers_left - 1); + clib_memcpy_fast (h2c->unsent_headers, request + headers_len, + headers_left); + h2c->unsent_headers_offset = 0; + *n_emissions += HTTP2_SCHED_WEIGHT_HEADERS_CONTINUATION; + } + + http2_frame_write_headers_header (headers_len, stream_id, flags, fh); + svm_fifo_seg_t segs[2] = { { fh, HTTP2_FRAME_HEADER_SIZE }, + { request, headers_len } }; + n_written = http_io_ts_write_segs (hc, segs, 2, 0); + ASSERT (n_written == (HTTP2_FRAME_HEADER_SIZE + headers_len)); + http_io_ts_after_write (hc, 0); +} + static void http2_update_time_callback (f64 now, u8 thread_index) { @@ -992,6 +1193,101 @@ http2_update_time_callback (f64 now, u8 thread_index) /* request state machine handlers RX */ /*************************************/ +static http_sm_result_t +http2_req_state_wait_transport_reply (http_conn_t *hc, http2_req_t *req, + transport_send_params_t *sp, + http2_error_t *error) +{ + http2_conn_ctx_t *h2c; + hpack_response_control_data_t control_data; + http_msg_t msg; + int rv; + http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index); + + h2c = http2_conn_ctx_get_w_thread (hc); + + vec_reset_length (req->base.headers); + *error = + hpack_parse_response (req->payload, req->payload_len, wrk->header_list, + vec_len (wrk->header_list), &control_data, + &req->base.headers, &h2c->decoder_dynamic_table); + if (*error != HTTP2_ERROR_NO_ERROR) + { + HTTP_DBG (1, "hpack_parse_response failed"); + return HTTP_SM_ERROR; + } + + 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; + req->base.status_code = control_data.sc; + + if (!(control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_STATUS_PARSED)) + { + HTTP_DBG (1, ":status pseudo-header missing in request"); + http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp); + return HTTP_SM_STOP; + } + + if (control_data.content_len_header_index != ~0) + { + req->base.content_len_header_index = + control_data.content_len_header_index; + rv = http_parse_content_length (&req->base, wrk->header_list); + if (rv) + { + http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp); + return HTTP_SM_STOP; + } + } + /* TODO: message framing without content length using END_STREAM flag */ + if (req->base.body_len == 0 && + req->stream_state == HTTP2_STREAM_STATE_HALF_CLOSED) + { + HTTP_DBG (1, "no content-length and DATA frame expected"); + *error = HTTP2_ERROR_INTERNAL_ERROR; + return HTTP_SM_ERROR; + } + req->base.to_recv = req->base.body_len; + + msg.type = HTTP_MSG_REPLY; + msg.code = req->base.status_code; + msg.data.headers_offset = req->base.headers_offset; + msg.data.headers_len = req->base.headers_len; + msg.data.headers_ctx = pointer_to_uword (req->base.headers); + msg.data.body_offset = req->base.control_data_len; + msg.data.body_len = req->base.body_len; + msg.data.type = HTTP_MSG_DATA_INLINE; + + svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) }, + { wrk->header_list, + req->base.control_data_len } }; + HTTP_DBG (3, "%U", format_http_bytes, wrk->header_list, + req->base.control_data_len); + http_io_as_write_segs (&req->base, segs, 2); + + if (req->base.body_len) + { + http_req_state_change (&req->base, + HTTP_REQ_STATE_TRANSPORT_IO_MORE_DATA); + http_io_as_add_want_read_ntf (&req->base); + } + else + { + /* we are done wait for the next app request */ + http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_APP_METHOD); + transport_connection_reschedule (&req->base.connection); + http2_conn_reset_req (h2c, req, hc->c_thread_index); + } + + http_app_worker_rx_notify (&req->base); + + return HTTP_SM_STOP; +} + static http_sm_result_t http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req, transport_send_params_t *sp, @@ -1222,6 +1518,8 @@ http2_req_state_transport_io_more_data (http_conn_t *hc, http2_req_t *req, http2_error_t *error) { u32 max_enq; + http2_stream_state_t expected_state; + http2_conn_ctx_t *h2c; if (req->payload_len > req->base.to_recv) { @@ -1230,8 +1528,10 @@ http2_req_state_transport_io_more_data (http_conn_t *hc, http2_req_t *req, return HTTP_SM_STOP; } req->base.to_recv -= req->payload_len; - if (req->stream_state == HTTP2_STREAM_STATE_HALF_CLOSED && - req->base.to_recv != 0) + expected_state = hc->flags & HTTP_CONN_F_IS_SERVER ? + HTTP2_STREAM_STATE_HALF_CLOSED : + HTTP2_STREAM_STATE_CLOSED; + if (req->stream_state == expected_state && req->base.to_recv != 0) { HTTP_DBG (1, "peer closed stream but don't send all data"); http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp); @@ -1245,7 +1545,23 @@ http2_req_state_transport_io_more_data (http_conn_t *hc, http2_req_t *req, return HTTP_SM_STOP; } if (req->base.to_recv == 0) - http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_APP_REPLY); + { + if (hc->flags & HTTP_CONN_F_IS_SERVER) + { + http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_APP_REPLY); + } + else + { + /* we are done wait for the next app request */ + http_req_state_change (&req->base, + hc->flags & HTTP_CONN_F_IS_SERVER ? + HTTP_REQ_STATE_WAIT_APP_REPLY : + HTTP_REQ_STATE_WAIT_APP_METHOD); + transport_connection_reschedule (&req->base.connection); + h2c = http2_conn_ctx_get_w_thread (hc); + http2_conn_reset_req (h2c, req, hc->c_thread_index); + } + } http_io_as_write (&req->base, req->payload, req->payload_len); http_app_worker_rx_notify (&req->base); @@ -1348,6 +1664,7 @@ http2_req_state_udp_tunnel_rx (http_conn_t *hc, http2_req_t *req, return HTTP_SM_STOP; } + /*************************************/ /* request state machine handlers TX */ /*************************************/ @@ -1399,6 +1716,40 @@ http2_req_state_app_io_more_data (http_conn_t *hc, http2_req_t *req, return HTTP_SM_STOP; } +static http_sm_result_t +http2_req_state_wait_app_method (http_conn_t *hc, http2_req_t *req, + transport_send_params_t *sp, + http2_error_t *error) +{ + http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index); + http2_req_t *he; + http2_conn_ctx_t *h2c; + + ASSERT (!clib_llist_elt_is_linked (req, sched_list)); + + h2c = http2_conn_ctx_get_w_thread (hc); + + if (!(hc->flags & HTTP_CONN_F_HAS_REQUEST)) + { + hc->flags |= HTTP_CONN_F_HAS_REQUEST; + hpack_dynamic_table_init (&h2c->decoder_dynamic_table, + http2_default_conn_settings.header_table_size); + } + + /* add response to stream scheduler */ + HTTP_DBG (1, "adding to headers queue req_index %x", + ((http_req_handle_t) req->base.hr_req_handle).req_index); + he = clib_llist_elt (wrk->req_pool, h2c->new_tx_streams); + clib_llist_add_tail (wrk->req_pool, sched_list, req, he); + http2_conn_schedule (h2c, hc->c_thread_index); + + req->dispatch_headers_cb = http2_sched_dispatch_req_headers; + http_req_state_change (&req->base, HTTP_REQ_STATE_APP_IO_MORE_DATA); + http_req_deschedule (&req->base, sp); + + return HTTP_SM_STOP; +} + static http_sm_result_t http2_req_state_tunnel_tx (http_conn_t *hc, http2_req_t *req, transport_send_params_t *sp, http2_error_t *error) @@ -1433,7 +1784,7 @@ typedef http_sm_result_t (*http2_sm_handler) (http_conn_t *hc, static http2_sm_handler tx_state_funcs[HTTP_REQ_N_STATES] = { 0, /* idle */ - 0, /* wait app method */ + http2_req_state_wait_app_method, 0, /* wait transport reply */ 0, /* transport io more data */ 0, /* wait transport method */ @@ -1447,7 +1798,7 @@ static http2_sm_handler tx_state_funcs[HTTP_REQ_N_STATES] = { static http2_sm_handler rx_state_funcs[HTTP_REQ_N_STATES] = { 0, /* idle */ 0, /* wait app method */ - 0, /* wait transport reply */ + http2_req_state_wait_transport_reply, http2_req_state_transport_io_more_data, http2_req_state_wait_transport_method, 0, /* wait app reply */ @@ -1462,6 +1813,12 @@ http2_req_state_is_tx_valid (http2_req_t *req) return tx_state_funcs[req->base.state] ? 1 : 0; } +static_always_inline int +http2_req_state_is_rx_valid (http2_req_t *req) +{ + return rx_state_funcs[req->base.state] ? 1 : 0; +} + static_always_inline http2_error_t http2_req_run_state_machine (http_conn_t *hc, http2_req_t *req, transport_send_params_t *sp, u8 is_tx) @@ -1501,9 +1858,10 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh) http2_error_t rv; http2_conn_ctx_t *h2c; + h2c = http2_conn_ctx_get_w_thread (hc); + if (hc->flags & HTTP_CONN_F_IS_SERVER) { - h2c = http2_conn_ctx_get_w_thread (hc); /* streams initiated by client must use odd-numbered stream id */ if ((fh->stream_id & 1) == 0) { @@ -1526,8 +1884,9 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh) HTTP2_ERROR_REFUSED_STREAM, 0); return HTTP2_ERROR_NO_ERROR; } - req = http2_conn_alloc_req (hc, fh->stream_id); - req->dispatch_headers_cb = http2_sched_dispatch_headers; + req = http2_conn_alloc_req (hc); + http2_req_set_stream_id (req, h2c, fh->stream_id); + req->dispatch_headers_cb = http2_sched_dispatch_resp_headers; http_conn_accept_request (hc, &req->base); http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_TRANSPORT_METHOD); req->stream_state = HTTP2_STREAM_STATE_OPEN; @@ -1569,8 +1928,54 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh) } else { - /* TODO: client */ - return HTTP2_ERROR_INTERNAL_ERROR; + req = http2_conn_get_req (hc, fh->stream_id); + if (!req) + return HTTP2_ERROR_PROTOCOL_ERROR; + + if (!http2_req_state_is_rx_valid (req)) + { + if (req->base.state == HTTP_REQ_STATE_APP_IO_MORE_DATA) + { + /* client can receive error response from server when still + * sending content */ + /* TODO: 100 continue support */ + HTTP_DBG (1, "server send response while client sending data"); + http_io_as_drain_all (&req->base); + hc->state = HTTP_CONN_STATE_CLOSED; + http_req_state_change (&req->base, + HTTP_REQ_STATE_WAIT_TRANSPORT_REPLY); + } + else + return HTTP2_ERROR_INTERNAL_ERROR; + } + + if (fh->flags & HTTP2_FRAME_FLAG_END_STREAM) + req->stream_state = HTTP2_STREAM_STATE_CLOSED; + + if (!(fh->flags & HTTP2_FRAME_FLAG_END_HEADERS)) + { + HTTP_DBG (1, "fragmented headers stream id %u", fh->stream_id); + h2c->flags |= HTTP2_CONN_F_EXPECT_CONTINUATION; + vec_validate (h2c->unparsed_headers, fh->length - 1); + http_io_ts_read (hc, h2c->unparsed_headers, fh->length, 0); + rv = http2_frame_read_headers (&headers_start, &headers_len, + h2c->unparsed_headers, fh->length, + fh->flags); + if (rv != HTTP2_ERROR_NO_ERROR) + return rv; + + /* in case frame has padding */ + if (PREDICT_FALSE (headers_start != h2c->unparsed_headers)) + { + n_dec = fh->length - headers_len; + n_del = headers_start - h2c->unparsed_headers; + n_dec -= n_del; + vec_delete (h2c->unparsed_headers, n_del, 0); + vec_dec_len (h2c->unparsed_headers, n_dec); + } + + return HTTP2_ERROR_NO_ERROR; + } } rx_buf = http_get_rx_buf (hc); @@ -1594,42 +1999,34 @@ http2_handle_continuation_frame (http_conn_t *hc, http2_frame_header_t *fh) u8 *p; http2_error_t rv = HTTP2_ERROR_NO_ERROR; - if (hc->flags & HTTP_CONN_F_IS_SERVER) - { - h2c = http2_conn_ctx_get_w_thread (hc); + h2c = http2_conn_ctx_get_w_thread (hc); - if (!(h2c->flags & HTTP2_CONN_F_EXPECT_CONTINUATION)) - { - HTTP_DBG (1, "unexpected CONTINUATION frame"); - return HTTP2_ERROR_PROTOCOL_ERROR; - } + if (!(h2c->flags & HTTP2_CONN_F_EXPECT_CONTINUATION)) + { + HTTP_DBG (1, "unexpected CONTINUATION frame"); + return HTTP2_ERROR_PROTOCOL_ERROR; + } - if (fh->stream_id != h2c->last_opened_stream_id) - { - HTTP_DBG (1, "invalid stream id %u", fh->stream_id); - return HTTP2_ERROR_PROTOCOL_ERROR; - } + if (fh->stream_id != h2c->last_opened_stream_id) + { + HTTP_DBG (1, "invalid stream id %u", fh->stream_id); + return HTTP2_ERROR_PROTOCOL_ERROR; + } - vec_add2 (h2c->unparsed_headers, p, fh->length); - http_io_ts_read (hc, p, fh->length, 0); + vec_add2 (h2c->unparsed_headers, p, fh->length); + http_io_ts_read (hc, p, fh->length, 0); - if (fh->flags & HTTP2_FRAME_FLAG_END_HEADERS) - { - req = http2_conn_get_req (hc, fh->stream_id); - if (!req) - return HTTP2_ERROR_PROTOCOL_ERROR; - h2c->flags &= ~HTTP2_CONN_F_EXPECT_CONTINUATION; - req->payload = h2c->unparsed_headers; - req->payload_len = vec_len (h2c->unparsed_headers); - HTTP_DBG (1, "run state machine"); - rv = http2_req_run_state_machine (hc, req, 0, 0); - vec_free (h2c->unparsed_headers); - } - } - else + if (fh->flags & HTTP2_FRAME_FLAG_END_HEADERS) { - /* TODO: client */ - return HTTP2_ERROR_INTERNAL_ERROR; + req = http2_conn_get_req (hc, fh->stream_id); + if (!req) + return HTTP2_ERROR_PROTOCOL_ERROR; + h2c->flags &= ~HTTP2_CONN_F_EXPECT_CONTINUATION; + req->payload = h2c->unparsed_headers; + req->payload_len = vec_len (h2c->unparsed_headers); + HTTP_DBG (1, "run state machine"); + rv = http2_req_run_state_machine (hc, req, 0, 0); + vec_free (h2c->unparsed_headers); } return rv; @@ -1712,7 +2109,9 @@ http2_handle_data_frame (http_conn_t *hc, http2_frame_header_t *fh) } } else - req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED; + req->stream_state = hc->flags & HTTP_CONN_F_IS_SERVER ? + HTTP2_STREAM_STATE_HALF_CLOSED : + HTTP2_STREAM_STATE_CLOSED; } rx_buf = http_get_rx_buf (hc); @@ -1820,8 +2219,12 @@ http2_handle_settings_frame (http_conn_t *hc, http2_frame_header_t *fh) if (fh->stream_id != 0) return HTTP2_ERROR_PROTOCOL_ERROR; + h2c = http2_conn_ctx_get_w_thread (hc); + if (fh->flags == HTTP2_FRAME_FLAG_ACK) { + if (h2c->flags & HTTP2_CONN_F_EXPECT_SERVER_SETTINGS) + return HTTP2_ERROR_PROTOCOL_ERROR; if (fh->length != 0) return HTTP2_ERROR_FRAME_SIZE_ERROR; /* TODO: we can start using non-default settings */ @@ -1835,12 +2238,21 @@ http2_handle_settings_frame (http_conn_t *hc, http2_frame_header_t *fh) vec_validate (rx_buf, fh->length - 1); http_io_ts_read (hc, rx_buf, fh->length, 0); - h2c = http2_conn_ctx_get_w_thread (hc); new_settings = h2c->peer_settings; rv = http2_frame_read_settings (&new_settings, rx_buf, fh->length); if (rv != HTTP2_ERROR_NO_ERROR) return rv; + if (h2c->flags & HTTP2_CONN_F_EXPECT_SERVER_SETTINGS) + { + h2c->flags &= ~HTTP2_CONN_F_EXPECT_SERVER_SETTINGS; + /* client connection is now established */ + req = http2_conn_alloc_req (hc); + http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_APP_METHOD); + if (http_conn_established (hc, &req->base)) + return HTTP2_ERROR_INTERNAL_ERROR; + } + /* ACK peer settings */ http2_frame_write_settings_ack (&resp); http_io_ts_write (hc, resp, vec_len (resp), 0); @@ -2135,6 +2547,7 @@ http2_app_rx_evt_callback (http_conn_t *hc, u32 req_index, http2_req_t *req; u8 *response; u32 increment; + http2_stream_state_t expected_state; req = http2_req_get (req_index, thread_index); if (!req) @@ -2145,7 +2558,10 @@ http2_app_rx_evt_callback (http_conn_t *hc, u32 req_index, HTTP_DBG (1, "received app read notification stream id %u", req->stream_id); /* send stream window update if app read data in rx fifo and we expect more * data (stream is still open) */ - if (req->stream_state == HTTP2_STREAM_STATE_OPEN) + expected_state = hc->flags & HTTP_CONN_F_IS_SERVER ? + HTTP2_STREAM_STATE_OPEN : + HTTP2_STREAM_STATE_HALF_CLOSED; + if (req->stream_state == expected_state) { http_io_as_reset_has_read_ntf (&req->base); response = http_get_tx_buf (hc); @@ -2176,6 +2592,7 @@ http2_app_close_callback (http_conn_t *hc, u32 req_index, } if (req->stream_state == HTTP2_STREAM_STATE_CLOSED || + req->stream_state == HTTP2_STREAM_STATE_IDLE || hc->state == HTTP_CONN_STATE_CLOSED) { HTTP_DBG (1, "nothing more to send, confirm close"); @@ -2232,8 +2649,15 @@ http2_app_reset_callback (http_conn_t *hc, u32 req_index, static int http2_transport_connected_callback (http_conn_t *hc) { - /* TODO */ - return -1; + http2_conn_ctx_t *h2c; + + HTTP_DBG (1, "hc [%u]%x", hc->c_thread_index, hc->hc_hc_index); + h2c = http2_conn_ctx_alloc_w_thread (hc); + h2c->flags |= HTTP2_CONN_F_EXPECT_SERVER_SETTINGS; + + http2_send_client_preface (hc); + + return 0; } static void @@ -2324,6 +2748,14 @@ http2_transport_rx_callback (http_conn_t *hc) return; } + if ((h2c->flags & HTTP2_CONN_F_EXPECT_SERVER_SETTINGS) && + fh.type != HTTP2_FRAME_TYPE_SETTINGS) + { + HTTP_DBG (1, "expected SETTINGS frame (server preface)"); + http2_connection_error (hc, HTTP2_ERROR_PROTOCOL_ERROR, 0); + return; + } + switch (fh.type) { case HTTP2_FRAME_TYPE_HEADERS: @@ -2530,7 +2962,8 @@ http2_update_settings (http_settings_t type, u32 value) switch (type) { -#define _(v, label, member, min, max, default_value, err_code) \ +#define _(v, label, member, min, max, default_value, err_code, server, \ + client) \ case HTTP2_SETTINGS_##label: \ if (!(value >= min && value <= max)) \ return -1; \ @@ -2611,6 +3044,7 @@ http2_init (vlib_main_t *vm) 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 */ + h2m->settings.enable_push = 0; /* by default enabled */ 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 b48c2a27156..67277f1fa70 100644 --- a/src/plugins/http/http2/http2.h +++ b/src/plugins/http/http2/http2.h @@ -59,25 +59,26 @@ format_http2_error (u8 *s, va_list *va) _ (4, STATUS, "status") \ _ (5, PROTOCOL, "protocol") -/* value, label, member, min, max, default_value, err_code */ +/* value, label, member, min, max, default_value, err_code, server, client */ #define foreach_http2_settings \ _ (1, HEADER_TABLE_SIZE, header_table_size, 0, CLIB_U32_MAX, 4096, \ - HTTP2_ERROR_NO_ERROR) \ - _ (2, ENABLE_PUSH, enable_push, 0, 1, 1, HTTP2_ERROR_PROTOCOL_ERROR) \ + HTTP2_ERROR_NO_ERROR, 1, 1) \ + _ (2, ENABLE_PUSH, enable_push, 0, 1, 1, HTTP2_ERROR_PROTOCOL_ERROR, 0, 1) \ _ (3, MAX_CONCURRENT_STREAMS, max_concurrent_streams, 0, CLIB_U32_MAX, \ - CLIB_U32_MAX, HTTP2_ERROR_NO_ERROR) \ + CLIB_U32_MAX, HTTP2_ERROR_NO_ERROR, 1, 1) \ _ (4, INITIAL_WINDOW_SIZE, initial_window_size, 0, 0x7FFFFFFF, 65535, \ - HTTP2_ERROR_FLOW_CONTROL_ERROR) \ + HTTP2_ERROR_FLOW_CONTROL_ERROR, 1, 1) \ _ (5, MAX_FRAME_SIZE, max_frame_size, 16384, 16777215, 16384, \ - HTTP2_ERROR_PROTOCOL_ERROR) \ + HTTP2_ERROR_PROTOCOL_ERROR, 1, 1) \ _ (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, 1, 1) \ _ (8, ENABLE_CONNECT_PROTOCOL, enable_connect_protocol, 0, 1, 0, \ - HTTP2_ERROR_NO_ERROR) + HTTP2_ERROR_NO_ERROR, 1, 0) typedef enum { -#define _(value, label, member, min, max, default_value, err_code) \ +#define _(value, label, member, min, max, default_value, err_code, server, \ + client) \ HTTP2_SETTINGS_##label = value, foreach_http2_settings #undef _ @@ -85,13 +86,16 @@ typedef enum typedef struct { -#define _(value, label, member, min, max, default_value, err_code) u32 member; +#define _(value, label, member, min, max, default_value, err_code, server, \ + client) \ + u32 member; foreach_http2_settings #undef _ } http2_conn_settings_t; static const http2_conn_settings_t http2_default_conn_settings = { -#define _(value, label, member, min, max, default_value, err_code) \ +#define _(value, label, member, min, max, default_value, err_code, server, \ + client) \ default_value, foreach_http2_settings #undef _ diff --git a/src/plugins/http/http_private.h b/src/plugins/http/http_private.h index 5ad940d5c48..51fdfd65d32 100644 --- a/src/plugins/http/http_private.h +++ b/src/plugins/http/http_private.h @@ -29,18 +29,6 @@ typedef union STATIC_ASSERT (sizeof (http_conn_handle_t) == sizeof (u32), "must fit in u32"); -typedef union -{ - struct - { - u32 version : 3; - u32 req_index : 29; - }; - u32 as_u32; -} http_req_handle_t; - -STATIC_ASSERT (sizeof (http_req_handle_t) == sizeof (u32), "must fit in u32"); - #define foreach_http_conn_state \ _ (LISTEN, "LISTEN") \ _ (CONNECTING, "CONNECTING") \ @@ -84,14 +72,6 @@ typedef enum http_target_form_ HTTP_TARGET_ASTERISK_FORM } http_target_form_t; -typedef enum http_version_ -{ - HTTP_VERSION_1, - HTTP_VERSION_2, - HTTP_VERSION_3, - HTTP_VERSION_NA = 7, -} http_version_t; - typedef struct http_req_id_ { session_handle_t app_session_handle; -- 2.16.6