From e863f751f08e59ed6492f59e8406b6763cadd0af Mon Sep 17 00:00:00 2001 From: TheBabu Date: Thu, 2 Apr 2026 16:13:51 -0600 Subject: [PATCH] lib/http: Add HTTP/2 cleartext support in server configuration Enable HTTP/2 cleartext support for non-TLS listeners. --- lib/http/server.go | 17 +++++++++++++++- lib/http/server_test.go | 44 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/http/server.go b/lib/http/server.go index 835030cf7..bd356076c 100644 --- a/lib/http/server.go +++ b/lib/http/server.go @@ -23,6 +23,8 @@ import ( "github.com/rclone/rclone/lib/atexit" sdActivation "github.com/rclone/rclone/lib/sdactivation" "github.com/spf13/pflag" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" ) // Help returns text describing the http server to add to the command @@ -61,6 +63,12 @@ identically. ` + "`--{{ .Prefix }}disable-zip`" + ` may be set to disable the zipping download option. +#### Protocol + +The server supports HTTP/1.1 and HTTP/2. HTTP/2 is used automatically +for TLS connections. For non-TLS connections, HTTP/2 cleartext (h2c) +is supported, allowing HTTP/2 without encryption. + #### TLS (SSL) By default this will serve over http. If you want you can serve over @@ -273,11 +281,18 @@ func newInstance(ctx context.Context, s *Server, listener net.Listener, tlsCfg * listener = tls.NewListener(listener, tlsCfg) } + var handler http.Handler = s.mux + // Enable h2c (HTTP/2 cleartext) for non-TLS listeners + if tlsCfg == nil { + h2s := &http2.Server{} + handler = h2c.NewHandler(s.mux, h2s) + } + return &instance{ url: url, listener: listener, httpServer: &http.Server{ - Handler: s.mux, + Handler: handler, ReadTimeout: time.Duration(s.cfg.ServerReadTimeout), WriteTimeout: time.Duration(s.cfg.ServerWriteTimeout), MaxHeaderBytes: s.cfg.MaxHeaderBytes, diff --git a/lib/http/server_test.go b/lib/http/server_test.go index 22e49580b..d675420d1 100644 --- a/lib/http/server_test.go +++ b/lib/http/server_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "golang.org/x/net/http2" ) func testEmptyHandler() http.Handler { @@ -550,6 +551,49 @@ func TestNewServerTLS(t *testing.T) { } } +func TestH2CServer(t *testing.T) { + ctx := context.Background() + + cfg := DefaultCfg() + cfg.ListenAddr = []string{"127.0.0.1:0"} + + s, err := NewServer(ctx, WithConfig(cfg)) + require.NoError(t, err) + defer func() { + require.NoError(t, s.Shutdown()) + }() + + expected := []byte("h2c-test-response") + s.Router().Mount("/", testEchoHandler(expected)) + s.Serve() + + url := testGetServerURL(t, s) + require.True(t, strings.HasPrefix(url, "http://"), "url should have http scheme (no TLS)") + + // Create an HTTP/2 cleartext client + client := &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + }, + } + + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { + _ = resp.Body.Close() + }() + + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 2, resp.ProtoMajor, "response should be HTTP/2") + testExpectRespBody(t, resp, expected) +} + func TestHelpPrefixServer(t *testing.T) { // This test assumes template variables are placed correctly. const testPrefix = "server-help-test"