Skip to content

Commit

Permalink
Enable HTTP/2 Cleartext in HTTP Server (dapr#6601)
Browse files Browse the repository at this point in the history
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com>
Co-authored-by: Mukundan Sundararajan <65565396+mukundansundar@users.noreply.github.com>
  • Loading branch information
3 people committed Aug 6, 2023
1 parent 41760a2 commit 7c5c9c3
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 1 deletion.
12 changes: 11 additions & 1 deletion pkg/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
Expand All @@ -33,13 +34,16 @@ import (

chi "github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"

"github.com/dapr/dapr/pkg/config"
corsDapr "github.com/dapr/dapr/pkg/cors"
diag "github.com/dapr/dapr/pkg/diagnostics"
diagUtils "github.com/dapr/dapr/pkg/diagnostics/utils"
httpMiddleware "github.com/dapr/dapr/pkg/middleware/http"
"github.com/dapr/dapr/pkg/security"
"github.com/dapr/dapr/utils"
"github.com/dapr/kit/logger"
)

Expand Down Expand Up @@ -128,11 +132,17 @@ func (s *server) StartNonBlocking() error {
return errors.New("could not listen on any endpoint")
}

// Create a handler with support for HTTP/2 Cleartext
var handler http.Handler = r
if !utils.IsTruthy(os.Getenv("DAPR_HTTP_DISABLE_H2C")) {
handler = h2c.NewHandler(r, &http2.Server{})
}

for _, listener := range listeners {
// srv is created in a loop because each instance
// has a handle on the underlying listener.
srv := &http.Server{
Handler: r,
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
MaxHeaderBytes: s.config.ReadBufferSizeKB << 10, // To bytes
}
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import (
"github.com/dapr/dapr/tests/integration/framework/binary"
"github.com/dapr/dapr/tests/integration/suite"

// Register all tests
_ "github.com/dapr/dapr/tests/integration/suite/daprd"
_ "github.com/dapr/dapr/tests/integration/suite/healthz"
_ "github.com/dapr/dapr/tests/integration/suite/httpserver"
_ "github.com/dapr/dapr/tests/integration/suite/metadata"
_ "github.com/dapr/dapr/tests/integration/suite/ports"
_ "github.com/dapr/dapr/tests/integration/suite/sentry"
Expand Down
159 changes: 159 additions & 0 deletions tests/integration/suite/httpserver/httpserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package httpserver

import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"

"github.com/dapr/dapr/tests/integration/framework"
procdaprd "github.com/dapr/dapr/tests/integration/framework/process/daprd"
"github.com/dapr/dapr/tests/integration/suite"
)

func init() {
suite.Register(new(httpServer))
}

// httpServer tests Dapr's HTTP server features.
type httpServer struct {
proc *procdaprd.Daprd
}

func (h *httpServer) Setup(t *testing.T) []framework.Option {
h.proc = procdaprd.New(t)
return []framework.Option{
framework.WithProcesses(h.proc),
}
}

func (h *httpServer) Run(t *testing.T, ctx context.Context) {
h1Client := http.Client{Transport: &http.Transport{}}
h2cClient := &http.Client{
Transport: &http2.Transport{
// Allow http2.Transport to use protocol "http"
AllowHTTP: true,
// Pretend we are dialing a TLS endpoint
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}

// Wait for daprd to be up
assert.Eventually(t, func() bool {
reqCtx, reqCancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer reqCancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, fmt.Sprintf("http://localhost:%d/v1.0/healthz", h.proc.HTTPPort()), nil)
if err != nil {
return false
}

// Body is closed below but the linter isn't seeing that
//nolint:bodyclose
res, err := h1Client.Do(req)
if err != nil {
return false
}

err = closeBody(res.Body)
if err != nil {
return false
}

return res.StatusCode == http.StatusNoContent
}, time.Second*5, 200*time.Millisecond)

t.Run("test with HTTP1", func(t *testing.T) {
reqCtx, reqCancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer reqCancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, fmt.Sprintf("http://localhost:%d/v1.0/healthz", h.proc.HTTPPort()), nil)
require.NoError(t, err)

// Body is closed below but the linter isn't seeing that
//nolint:bodyclose
res, err := h1Client.Do(req)
require.NoError(t, err)
defer closeBody(res.Body)
require.Equal(t, http.StatusNoContent, res.StatusCode)

assert.Equal(t, 1, res.ProtoMajor)
})

t.Run("test with HTTP2 Cleartext with prior knowledge", func(t *testing.T) {
reqCtx, reqCancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer reqCancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, fmt.Sprintf("http://localhost:%d/v1.0/healthz", h.proc.HTTPPort()), nil)
require.NoError(t, err)

// Body is closed below but the linter isn't seeing that
//nolint:bodyclose
res, err := h2cClient.Do(req)
require.NoError(t, err)
defer closeBody(res.Body)
require.Equal(t, http.StatusNoContent, res.StatusCode)

assert.Equal(t, 2, res.ProtoMajor)
})

t.Run("server allows upgrading from HTTP1 to HTTP2 Cleartext", func(t *testing.T) {
// The Go HTTP client doesn't handle upgrades from HTTP/1 to HTTP/2
// The best we can do for now is to verify that the server responds with a 101 response when we signal that we are able to upgrade
// See: https://github.com/golang/go/issues/46249

reqCtx, reqCancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer reqCancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, fmt.Sprintf("http://localhost:%d/v1.0/healthz", h.proc.HTTPPort()), nil)
require.NoError(t, err)

req.Header.Set("Connection", "Upgrade, HTTP2-Settings")
req.Header.Set("Upgrade", "h2c")
req.Header.Set("HTTP2-Settings", "AAMAAABkAARAAAAAAAIAAAAA")

// Body is closed below but the linter isn't seeing that
//nolint:bodyclose
res, err := h1Client.Do(req)
require.NoError(t, err)
defer closeBody(res.Body)

// This response should have arrived over HTTP/1
assert.Equal(t, 1, res.ProtoMajor)
assert.Equal(t, http.StatusSwitchingProtocols, res.StatusCode)
assert.NotEmpty(t, res.Header)
assert.Equal(t, "Upgrade", res.Header.Get("Connection"))
assert.Equal(t, "h2c", res.Header.Get("Upgrade"))
})
}

// Drain body before closing
func closeBody(body io.ReadCloser) error {
_, err := io.Copy(io.Discard, body)
if err != nil {
return err
}
return body.Close()
}

0 comments on commit 7c5c9c3

Please sign in to comment.