Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

httpserver: Add H2C support #3289

Merged
merged 7 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 0 additions & 13 deletions modules/caddyhttp/caddyhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,6 @@ func (ws WeakString) String() string {
return string(ws)
}

// CopyHeader copies HTTP headers by completely
// replacing dest with src. (This allows deletions
// to be propagated, assuming src started as a
// consistent copy of dest.)
func CopyHeader(dest, src http.Header) {
for field := range dest {
delete(dest, field)
}
for field, val := range src {
dest[field] = val
}
}

// StatusCodeMatches returns true if a real HTTP status code matches
// the configured status code, which may be either a real HTTP status
// code or an integer representing a class of codes (e.g. 4 for all
Expand Down
37 changes: 11 additions & 26 deletions modules/caddyhttp/responsewriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ type responseRecorder struct {
buf *bytes.Buffer
shouldBuffer ShouldBufferFunc
size int
header http.Header
wroteHeader bool
stream bool
}
Expand Down Expand Up @@ -122,46 +121,34 @@ type responseRecorder struct {
// }
// // process the buffered response here
//
// After a response has been buffered, remember that any upstream header
// manipulations are only manifest in the recorder's Header(), not the
// Header() of the underlying ResponseWriter. Thus if you wish to inspect
// or change response headers, you either need to use rec.Header(), or
// copy rec.Header() into w.Header() first (see caddyhttp.CopyHeader).
// The header map is not buffered; i.e. the ResponseRecorder's Header()
// method returns the same header map of the underlying ResponseWriter.
// This is a crucial design decision to allow HTTP trailers to be
// flushed properly (https://github.com/caddyserver/caddy/issues/3236).
francislavoie marked this conversation as resolved.
Show resolved Hide resolved
//
// Once you are ready to write the response, there are two ways you can do
// it. The easier way is to have the recorder do it:
// Once you are ready to write the response, there are two ways you can
// do it. The easier way is to have the recorder do it:
//
// rec.WriteResponse()
//
// This writes the recorded response headers as well as the buffered body.
// Or, you may wish to do it yourself, especially if you manipulated the
// buffered body. First you will need to copy the recorded headers, then
// write the headers with the recorded status code, then write the body
// (this example writes the recorder's body buffer, but you might have
// your own body to write instead):
// buffered body. First you will need to write the headers with the
// recorded status code, then write the body (this example writes the
// recorder's body buffer, but you might have your own body to write
// instead):
//
// caddyhttp.CopyHeader(w.Header(), rec.Header())
// w.WriteHeader(rec.Status())
// io.Copy(w, rec.Buffer())
//
func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer ShouldBufferFunc) ResponseRecorder {
// copy the current response header into this buffer so
// that any header manipulations on the buffered header
// are consistent with what would be written out
hdr := make(http.Header)
CopyHeader(hdr, w.Header())
return &responseRecorder{
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
buf: buf,
shouldBuffer: shouldBuffer,
header: hdr,
}
}

func (rr *responseRecorder) Header() http.Header {
return rr.header
}

func (rr *responseRecorder) WriteHeader(statusCode int) {
if rr.wroteHeader {
return
Expand All @@ -173,12 +160,11 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
if rr.shouldBuffer == nil {
rr.stream = true
} else {
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.header)
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
mholt marked this conversation as resolved.
Show resolved Hide resolved
}

// if not buffered, immediately write header
if rr.stream {
CopyHeader(rr.ResponseWriterWrapper.Header(), rr.header)
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
}
}
Expand Down Expand Up @@ -224,7 +210,6 @@ func (rr *responseRecorder) WriteResponse() error {
if rr.stream {
return nil
}
CopyHeader(rr.ResponseWriterWrapper.Header(), rr.header)
if rr.statusCode == 0 {
// could happen if no handlers actually wrote anything,
// and this prevents a panic; status must be > 0
Expand Down
7 changes: 7 additions & 0 deletions modules/caddyhttp/reverseproxy/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// tls_trusted_ca_certs <cert_files...>
// keepalive [off|<duration>]
// keepalive_idle_conns <max_count>
// versions <versions...>
// }
//
func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
Expand Down Expand Up @@ -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())
}
Expand Down
36 changes: 35 additions & 1 deletion modules/caddyhttp/reverseproxy/httptransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand Down
11 changes: 11 additions & 0 deletions modules/caddyhttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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
Expand Down