diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go index 19291792250..4a3b472d5e2 100644 --- a/config/confighttp/confighttp.go +++ b/config/confighttp/confighttp.go @@ -24,6 +24,8 @@ import ( "github.com/rs/cors" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config" @@ -198,6 +200,24 @@ func (hss *HTTPServerSettings) ToServer(handler http.Handler, settings component o(serverOpts) } + // Declare an HTTP/1 server which: + // Handles only HTTP/1 traffic over TLS or non-TLS + h1Server := &http.Server{ + Addr: hss.Endpoint, + } + + // Declare an HTTP/2 server which will handle any H2 protocol traffic + // The PriorityWriteScheduler setting is used to match + // the default scheduler used by the HTTP/1 server for ALPN h2 traffic. + // https://github.com/golang/go/blob/5da2010840a3b4d99fcdccb7cdef0ffbd6e9a29f/src/net/http/server.go#L3269 + h2Server := &http2.Server{ + NewWriteScheduler: func() http2.WriteScheduler { return http2.NewPriorityWriteScheduler(nil) }, + } + + // Configure the HTTP/1 server to serve TLS ALPN=h2 traffic onto the H2 server + // The handler specified on the HTTP/1 server will be via H2 transport + http2.ConfigureServer(h1Server, h2Server) + handler = middleware.HTTPContentDecompressor( handler, middleware.WithErrorHandler(serverOpts.errorHandler), @@ -226,7 +246,17 @@ func (hss *HTTPServerSettings) ToServer(handler http.Handler, settings component }), ) - return &http.Server{ - Handler: handler, + // Enable the H2C handler if and only if there are no TLS settings in + // accordance with rfc7540 section-3.4 + if hss.TLSSetting == nil { + // Configure the H1 server to intercept HTTP/2 with prior + // knowledge and handle that with the H2 server. + // This allows H2 upgrade via the HTTP/1 server path + handler = h2c.NewHandler(handler, h2Server) } + + // Set the handler on the HTTP1 server + h1Server.Handler = handler + + return h1Server } diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go index 82c26900a50..0d963f95735 100644 --- a/config/confighttp/confighttp_test.go +++ b/config/confighttp/confighttp_test.go @@ -15,9 +15,11 @@ package confighttp import ( + "crypto/tls" "errors" "fmt" "io/ioutil" + "net" "net/http" "net/http/httptest" "net/url" @@ -27,6 +29,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/net/http2" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" @@ -378,7 +381,24 @@ func TestHttpReception(t *testing.T) { hasError: true, }, } + // prepare + runServer := func(t *testing.T, hss *HTTPServerSettings) (s *http.Server, addr string) { + ln, err := hss.ToListener() + assert.NoError(t, err) + s = hss.ToServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + assert.NoError(t, errWrite) + }), componenttest.NewNopTelemetrySettings()) + + go func() { + _ = s.Serve(ln) + }() + + // Wait for the servers to start + <-time.After(10 * time.Millisecond) + return s, ln.Addr().String() + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -386,19 +406,8 @@ func TestHttpReception(t *testing.T) { Endpoint: "localhost:0", TLSSetting: tt.tlsServerCreds, } - ln, err := hss.ToListener() - assert.NoError(t, err) - s := hss.ToServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, errWrite := fmt.Fprint(w, "test") - assert.NoError(t, errWrite) - }), componenttest.NewNopTelemetrySettings()) - - go func() { - _ = s.Serve(ln) - }() - // Wait for the servers to start - <-time.After(10 * time.Millisecond) + s, addr := runServer(t, hss) prefix := "https://" if tt.tlsClientCreds.Insecure { @@ -406,7 +415,7 @@ func TestHttpReception(t *testing.T) { } hcs := &HTTPClientSettings{ - Endpoint: prefix + ln.Addr().String(), + Endpoint: prefix + addr, TLSSetting: *tt.tlsClientCreds, } client, errClient := hcs.ToClient(map[config.ComponentID]component.Extension{}) @@ -420,9 +429,41 @@ func TestHttpReception(t *testing.T) { assert.NoError(t, errRead) assert.Equal(t, "test", string(body)) } + require.NoError(t, s.Close()) }) } + + t.Run("h2c", func(t *testing.T) { + hss := &HTTPServerSettings{ + Endpoint: "localhost:0", + } + + s, addr := runServer(t, hss) + // Check that h2c payloads are handled when serving insecure traffic + client := http.Client{ + Transport: &http2.Transport{ + // So http2.Transport doesn't complain the URL scheme isn't 'https' + AllowHTTP: true, + // Pretend we are dialing a TLS endpoint. + // Note, we ignore the passed tls.Config + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + }, + } + + hcs := &HTTPClientSettings{ + Endpoint: "http://" + addr, + } + resp, errResp := client.Get(hcs.Endpoint) + assert.NoError(t, errResp) + body, errRead := ioutil.ReadAll(resp.Body) + assert.NoError(t, errRead) + assert.Equal(t, "test", string(body)) + require.NoError(t, s.Close()) + + }) } func TestHttpCors(t *testing.T) { @@ -598,3 +639,61 @@ func TestHttpHeaders(t *testing.T) { }) } } + +func BenchmarkHttpRequest(b *testing.B) { + + tests := []struct { + name string + tlsServerCreds *configtls.TLSServerSetting + tlsClientCreds *configtls.TLSClientSetting + hasError bool + }{ + { + name: "noTLS", + tlsServerCreds: nil, + tlsClientCreds: &configtls.TLSClientSetting{ + Insecure: true, + }, + }, + { + name: "TLS", + tlsServerCreds: &configtls.TLSServerSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: path.Join(".", "testdata", "ca.crt"), + CertFile: path.Join(".", "testdata", "server.crt"), + KeyFile: path.Join(".", "testdata", "server.key"), + }, + }, + tlsClientCreds: &configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: path.Join(".", "testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + }, + } + + for _, bb := range tests { + b.Run(bb.name, func(b *testing.B) { + hss := &HTTPServerSettings{ + Endpoint: "localhost:0", + TLSSetting: bb.tlsServerCreds, + } + s := hss.ToServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + assert.NoError(b, errWrite) + }), componenttest.NewNopTelemetrySettings()) + + req := httptest.NewRequest("GET", "/", nil) + + for i := 0; i < b.N; i++ { + rw := httptest.NewRecorder() + + // Use the handler generated by ToServer for benchmarking + // to avoid network artifacts + s.Handler.ServeHTTP(rw, req) + } + }) + } + +} diff --git a/go.mod b/go.mod index 147ddf6101f..60e10637c27 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( go.opentelemetry.io/otel/trace v1.0.0 go.uber.org/atomic v1.9.0 go.uber.org/zap v1.19.1 + golang.org/x/net v0.0.0-20210917221730-978cfadd31cf golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 google.golang.org/grpc v1.40.0 @@ -66,7 +67,6 @@ require ( go.opentelemetry.io/contrib v0.23.0 // indirect go.opentelemetry.io/otel/internal/metric v0.23.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/text v0.3.6 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 40c86604c95..7cfa6493d20 100644 --- a/go.sum +++ b/go.sum @@ -518,8 +518,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=