diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 8e7f3eaac..f695276a7 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -27,6 +27,8 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" ) func init() { @@ -282,6 +284,14 @@ func (app *App) Start() error { Handler: srv, } + // enable h2c if configured + if srv.AllowH2C { + h2server := &http2.Server{ + IdleTimeout: time.Duration(srv.IdleTimeout), + } + s.Handler = h2c.NewHandler(srv, h2server) + } + for _, lnAddr := range srv.Listen { listenAddr, err := caddy.ParseNetworkAddress(lnAddr) if err != nil { diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 9636936d5..56e2698e2 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -584,6 +584,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // tls_trusted_ca_certs // keepalive [off|] // keepalive_idle_conns +// versions // } // func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { @@ -701,6 +702,12 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { h.KeepAlive.MaxIdleConns = num h.KeepAlive.MaxIdleConnsPerHost = num + case "versions": + h.Versions = d.RemainingArgs() + if len(h.Versions) == 0 { + return d.ArgErr() + } + default: return d.Errf("unrecognized subdirective %s", d.Val()) } diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 4b5111c0a..f0fbbd6ee 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -82,11 +82,16 @@ type HTTPTransport struct { // The size of the read buffer in bytes. ReadBufferSize int `json:"read_buffer_size,omitempty"` - // The versions of HTTP to support. Default: ["1.1", "2"] + // The versions of HTTP to support. As a special case, "h2c" + // can be specified to use H2C (HTTP/2 over Cleartext) to the + // upstream (this feature is experimental and subject to + // change or removal). Default: ["1.1", "2"] Versions []string `json:"versions,omitempty"` // The pre-configured underlying HTTP transport. Transport *http.Transport `json:"-"` + + h2cTransport *http2.Transport } // CaddyModule returns the Caddy module information. @@ -110,6 +115,28 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error { } h.Transport = rt + // if h2c is enabled, configure its transport (std lib http.Transport + // does not "HTTP/2 over cleartext TCP") + if sliceContains(h.Versions, "h2c") { + // crafting our own http2.Transport doesn't allow us to utilize + // most of the customizations/preferences on the http.Transport, + // because, for some reason, only http2.ConfigureTransport() + // is allowed to set the unexported field that refers to a base + // http.Transport config; oh well + h2t := &http2.Transport{ + // kind of a hack, but for plaintext/H2C requests, pretend to dial TLS + DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { + // TODO: no context, thus potentially wrong dial info + return net.Dial(network, addr) + }, + AllowHTTP: true, + } + if h.Compression != nil { + h2t.DisableCompression = !*h.Compression + } + h.h2cTransport = h2t + } + return nil } @@ -182,6 +209,13 @@ func (h *HTTPTransport) NewTransport(_ caddy.Context) (*http.Transport, error) { // RoundTrip implements http.RoundTripper. func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { h.SetScheme(req) + + // if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is + // HTTP/2 without TLS, use the alternate H2C-capable transport instead + if req.ProtoMajor == 2 && req.URL.Scheme == "http" && h.h2cTransport != nil { + return h.h2cTransport.RoundTrip(req) + } + return h.Transport.RoundTrip(req) } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 36e70422a..a4fda28ca 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -111,6 +111,17 @@ type Server struct { // This field is not subject to compatibility promises. ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"` + // Enables H2C ("Cleartext HTTP/2" or "H2 over TCP") support, + // which will serve HTTP/2 over plaintext TCP connections if + // a client support it. Because this is not implemented by the + // Go standard library, using H2C is incompatible with most + // of the other options for this server. Do not enable this + // only to achieve maximum client compatibility. In practice, + // very few clients implement H2C, and even fewer require it. + // This setting applies only to unencrypted HTTP listeners. + // ⚠️ Experimental feature; subject to change or removal. + AllowH2C bool `json:"allow_h2c,omitempty"` + primaryHandlerChain Handler errorHandlerChain Handler listenerWrappers []caddy.ListenerWrapper