mirror of
https://github.com/caddyserver/caddy.git
synced 2025-05-24 02:02:26 -04:00
use the new http.Protocols to handle h1, h2 and h2c requests
This commit is contained in:
parent
a76d005a94
commit
f4b57b0a32
@ -28,7 +28,6 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||||
@ -231,15 +230,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
for _, srvProtocol := range srv.Protocols {
|
for _, srvProtocol := range srv.Protocols {
|
||||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||||
}
|
}
|
||||||
_, h1ok := srvProtocolsUnique["h1"]
|
|
||||||
_, h2ok := srvProtocolsUnique["h2"]
|
|
||||||
_, h2cok := srvProtocolsUnique["h2c"]
|
|
||||||
|
|
||||||
// the Go standard library does not let us serve only HTTP/2 using
|
|
||||||
// http.Server; we would probably need to write our own server
|
|
||||||
if !h1ok && (h2ok || h2cok) {
|
|
||||||
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.ListenProtocols != nil {
|
if srv.ListenProtocols != nil {
|
||||||
if len(srv.ListenProtocols) != len(srv.Listen) {
|
if len(srv.ListenProtocols) != len(srv.Listen) {
|
||||||
@ -273,19 +263,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lnProtocolsIncludeUnique := map[string]struct{}{}
|
|
||||||
for _, lnProtocol := range lnProtocolsInclude {
|
|
||||||
lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
|
|
||||||
}
|
|
||||||
_, h1ok := lnProtocolsIncludeUnique["h1"]
|
|
||||||
_, h2ok := lnProtocolsIncludeUnique["h2"]
|
|
||||||
_, h2cok := lnProtocolsIncludeUnique["h2c"]
|
|
||||||
|
|
||||||
// check if any listener protocols contain h2 or h2c without h1
|
|
||||||
if !h1ok && (h2ok || h2cok) {
|
|
||||||
return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.ListenProtocols[i] = lnProtocolsInclude
|
srv.ListenProtocols[i] = lnProtocolsInclude
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -461,11 +438,8 @@ func (app *App) Start() error {
|
|||||||
MaxHeaderBytes: srv.MaxHeaderBytes,
|
MaxHeaderBytes: srv.MaxHeaderBytes,
|
||||||
Handler: srv,
|
Handler: srv,
|
||||||
ErrorLog: serverLogger,
|
ErrorLog: serverLogger,
|
||||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
Protocols: new(http.Protocols),
|
||||||
return context.WithValue(ctx, ConnCtxKey, c)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
h2server := new(http2.Server)
|
|
||||||
|
|
||||||
// disable HTTP/2, which we enabled by default during provisioning
|
// disable HTTP/2, which we enabled by default during provisioning
|
||||||
if !srv.protocol("h2") {
|
if !srv.protocol("h2") {
|
||||||
@ -486,7 +460,24 @@ func (app *App) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// configure the http versions the server will serve
|
||||||
|
if srv.protocol("h1") {
|
||||||
|
srv.server.Protocols.SetHTTP1(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.protocol("h2") || srv.protocol("h2c") {
|
||||||
|
// skip setting h2 because if NextProtos is present, it's list of alpn versions will take precedence.
|
||||||
|
// it will always be present because http2.ConfigureServer will populate that field
|
||||||
|
// enabling h2c because some listener wrapper will wrap the connection that is no longer *tls.Conn
|
||||||
|
// However, we need to handle the case that if the connection is h2c but h2c is not enabled. We identify
|
||||||
|
// this type of connection by checking if it's behind a TLS listener wrapper or if it implements tls.ConnectionState.
|
||||||
|
srv.server.Protocols.SetUnencryptedHTTP2(true)
|
||||||
|
// when h2c is enabled but h2 disabled, we already removed h2 from NextProtos
|
||||||
|
// the handshake will never succeed with h2
|
||||||
|
// http2.ConfigureServer will enable the server to handle both h2 and h2c
|
||||||
|
h2server := new(http2.Server)
|
||||||
//nolint:errcheck
|
//nolint:errcheck
|
||||||
http2.ConfigureServer(srv.server, h2server)
|
http2.ConfigureServer(srv.server, h2server)
|
||||||
}
|
}
|
||||||
@ -496,11 +487,6 @@ func (app *App) Start() error {
|
|||||||
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
||||||
srv.configureServer(srv.server)
|
srv.configureServer(srv.server)
|
||||||
|
|
||||||
// enable H2C if configured
|
|
||||||
if srv.protocol("h2c") {
|
|
||||||
srv.server.Handler = h2c.NewHandler(srv, h2server)
|
|
||||||
}
|
|
||||||
|
|
||||||
for lnIndex, lnAddr := range srv.Listen {
|
for lnIndex, lnAddr := range srv.Listen {
|
||||||
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
|
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -560,15 +546,11 @@ func (app *App) Start() error {
|
|||||||
ln = srv.listenerWrappers[i].WrapListener(ln)
|
ln = srv.listenerWrappers[i].WrapListener(ln)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle http2 if use tls listener wrapper
|
// check if the connection is h2c
|
||||||
if h2ok {
|
ln = &http2Listener{
|
||||||
http2lnWrapper := &http2Listener{
|
useTLS: useTLS,
|
||||||
Listener: ln,
|
Listener: ln,
|
||||||
server: srv.server,
|
logger: app.logger,
|
||||||
h2server: h2server,
|
|
||||||
}
|
|
||||||
srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
|
|
||||||
ln = http2lnWrapper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if binding to port 0, the OS chooses a port for us;
|
// if binding to port 0, the OS chooses a port for us;
|
||||||
@ -586,11 +568,8 @@ func (app *App) Start() error {
|
|||||||
|
|
||||||
srv.listeners = append(srv.listeners, ln)
|
srv.listeners = append(srv.listeners, ln)
|
||||||
|
|
||||||
// enable HTTP/1 if configured
|
//nolint:errcheck
|
||||||
if h1ok {
|
go srv.server.Serve(ln)
|
||||||
//nolint:errcheck
|
|
||||||
go srv.server.Serve(ln)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h2ok && !useTLS {
|
if h2ok && !useTLS {
|
||||||
@ -723,25 +702,12 @@ func (app *App) Stop() error {
|
|||||||
zap.Strings("addresses", server.Listen))
|
zap.Strings("addresses", server.Listen))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopH2Listener := func(server *Server) {
|
|
||||||
defer finishedShutdown.Done()
|
|
||||||
startedShutdown.Done()
|
|
||||||
|
|
||||||
for i, s := range server.h2listeners {
|
|
||||||
if err := s.Shutdown(ctx); err != nil {
|
|
||||||
app.logger.Error("http2 listener shutdown",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.Int("index", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, server := range app.Servers {
|
for _, server := range app.Servers {
|
||||||
startedShutdown.Add(3)
|
startedShutdown.Add(2)
|
||||||
finishedShutdown.Add(3)
|
finishedShutdown.Add(2)
|
||||||
go stopServer(server)
|
go stopServer(server)
|
||||||
go stopH3Server(server)
|
go stopH3Server(server)
|
||||||
go stopH2Listener(server)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// block until all the goroutines have been run by the scheduler;
|
// block until all the goroutines have been run by the scheduler;
|
||||||
|
@ -1,102 +1,87 @@
|
|||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
weakrand "math/rand"
|
"go.uber.org/zap"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// http2Listener wraps the listener to solve the following problems:
|
type connectionStater interface {
|
||||||
// 1. server h2 natively without using h2c hack when listener handles tls connection but
|
|
||||||
// don't return *tls.Conn
|
|
||||||
// 2. graceful shutdown. the shutdown logic is copied from stdlib http.Server, it's an extra maintenance burden but
|
|
||||||
// whatever, the shutdown logic maybe extracted to be used with h2c graceful shutdown. http2.Server supports graceful shutdown
|
|
||||||
// sending GO_AWAY frame to connected clients, but doesn't track connection status. It requires explicit call of http2.ConfigureServer
|
|
||||||
type http2Listener struct {
|
|
||||||
cnt uint64
|
|
||||||
net.Listener
|
|
||||||
server *http.Server
|
|
||||||
h2server *http2.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
type connectionStateConn interface {
|
|
||||||
net.Conn
|
|
||||||
ConnectionState() tls.ConnectionState
|
ConnectionState() tls.ConnectionState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// http2Listener wraps the listener to solve the following problems:
|
||||||
|
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
|
||||||
|
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
|
||||||
|
// isn't http2.
|
||||||
|
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
|
||||||
|
// can do this.
|
||||||
|
// 2. After wrapping the connection doesn't implement connectionStater, emit a warning so that listener
|
||||||
|
// wrapper authors will hopefully implement it.
|
||||||
|
type http2Listener struct {
|
||||||
|
useTLS bool
|
||||||
|
net.Listener
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
func (h *http2Listener) Accept() (net.Conn, error) {
|
func (h *http2Listener) Accept() (net.Conn, error) {
|
||||||
for {
|
conn, err := h.Listener.Accept()
|
||||||
conn, err := h.Listener.Accept()
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if csc, ok := conn.(connectionStateConn); ok {
|
|
||||||
// *tls.Conn will return empty string because it's only populated after handshake is complete
|
|
||||||
if csc.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
|
|
||||||
go h.serveHttp2(csc)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *http2Listener) serveHttp2(csc connectionStateConn) {
|
|
||||||
atomic.AddUint64(&h.cnt, 1)
|
|
||||||
h.runHook(csc, http.StateNew)
|
|
||||||
defer func() {
|
|
||||||
csc.Close()
|
|
||||||
atomic.AddUint64(&h.cnt, ^uint64(0))
|
|
||||||
h.runHook(csc, http.StateClosed)
|
|
||||||
}()
|
|
||||||
h.h2server.ServeConn(csc, &http2.ServeConnOpts{
|
|
||||||
Context: h.server.ConnContext(context.Background(), csc),
|
|
||||||
BaseConfig: h.server,
|
|
||||||
Handler: h.server.Handler,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const shutdownPollIntervalMax = 500 * time.Millisecond
|
|
||||||
|
|
||||||
func (h *http2Listener) Shutdown(ctx context.Context) error {
|
|
||||||
pollIntervalBase := time.Millisecond
|
|
||||||
nextPollInterval := func() time.Duration {
|
|
||||||
// Add 10% jitter.
|
|
||||||
//nolint:gosec
|
|
||||||
interval := pollIntervalBase + time.Duration(weakrand.Intn(int(pollIntervalBase/10)))
|
|
||||||
// Double and clamp for next time.
|
|
||||||
pollIntervalBase *= 2
|
|
||||||
if pollIntervalBase > shutdownPollIntervalMax {
|
|
||||||
pollIntervalBase = shutdownPollIntervalMax
|
|
||||||
}
|
|
||||||
return interval
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := time.NewTimer(nextPollInterval())
|
if h.useTLS {
|
||||||
defer timer.Stop()
|
// emit a warning
|
||||||
for {
|
if _, ok := conn.(connectionStater); !ok {
|
||||||
if atomic.LoadUint64(&h.cnt) == 0 {
|
h.logger.Warn("tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater")
|
||||||
return nil
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-timer.C:
|
|
||||||
timer.Reset(nextPollInterval())
|
|
||||||
}
|
}
|
||||||
|
return &http2Conn{
|
||||||
|
idx: len(http2.ClientPreface),
|
||||||
|
Conn: conn,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := conn.(connectionStater); ok {
|
||||||
|
h.logger.Warn("tls is disabled, but listener wrapper returns a connection that implements connectionStater")
|
||||||
|
return &http2Conn{
|
||||||
|
idx: len(http2.ClientPreface),
|
||||||
|
Conn: conn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http2Conn{
|
||||||
|
Conn: conn,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *http2Listener) runHook(conn net.Conn, state http.ConnState) {
|
type http2Conn struct {
|
||||||
if h.server.ConnState != nil {
|
// check h2 preface if it's smaller that the preface
|
||||||
h.server.ConnState(conn, state)
|
idx int
|
||||||
}
|
// log if one such connection is detected
|
||||||
|
logger *zap.Logger
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *http2Conn) Read(p []byte) (int, error) {
|
||||||
|
if c.idx >= len(http2.ClientPreface) {
|
||||||
|
return c.Conn.Read(p)
|
||||||
|
}
|
||||||
|
n, err := c.Conn.Read(p)
|
||||||
|
for i := range n {
|
||||||
|
// mismatch
|
||||||
|
if p[i] != http2.ClientPreface[c.idx] {
|
||||||
|
c.idx = len(http2.ClientPreface)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
c.idx++
|
||||||
|
if c.idx == len(http2.ClientPreface) {
|
||||||
|
c.logger.Warn("h2c connection detected, but h2c is not enabled")
|
||||||
|
_ = c.Conn.Close()
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
@ -245,10 +245,9 @@ type Server struct {
|
|||||||
traceLogger *zap.Logger
|
traceLogger *zap.Logger
|
||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
|
|
||||||
server *http.Server
|
server *http.Server
|
||||||
h3server *http3.Server
|
h3server *http3.Server
|
||||||
h2listeners []*http2Listener
|
addresses []caddy.NetworkAddress
|
||||||
addresses []caddy.NetworkAddress
|
|
||||||
|
|
||||||
trustedProxies IPRangeSource
|
trustedProxies IPRangeSource
|
||||||
|
|
||||||
@ -264,18 +263,6 @@ type Server struct {
|
|||||||
|
|
||||||
// ServeHTTP is the entry point for all HTTP requests.
|
// ServeHTTP is the entry point for all HTTP requests.
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil.
|
|
||||||
// TODO: Can be removed if https://github.com/golang/go/pull/56110 is ever merged.
|
|
||||||
if r.TLS == nil {
|
|
||||||
// not all requests have a conn (like virtual requests) - see #5698
|
|
||||||
if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok {
|
|
||||||
if csc, ok := conn.(connectionStateConn); ok {
|
|
||||||
r.TLS = new(tls.ConnectionState)
|
|
||||||
*r.TLS = csc.ConnectionState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Server", "Caddy")
|
w.Header().Set("Server", "Caddy")
|
||||||
|
|
||||||
// advertise HTTP/3, if enabled
|
// advertise HTTP/3, if enabled
|
||||||
@ -1080,6 +1067,8 @@ const (
|
|||||||
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
OriginalRequestCtxKey caddy.CtxKey = "original_request"
|
||||||
|
|
||||||
// For referencing underlying net.Conn
|
// For referencing underlying net.Conn
|
||||||
|
// DEPRECATED: Not used anymore. To refer to the underlying connection, implement a middleware plugin
|
||||||
|
// that RegisterConnContext during provisioning.
|
||||||
ConnCtxKey caddy.CtxKey = "conn"
|
ConnCtxKey caddy.CtxKey = "conn"
|
||||||
|
|
||||||
// For tracking whether the client is a trusted proxy
|
// For tracking whether the client is a trusted proxy
|
||||||
|
Loading…
x
Reference in New Issue
Block a user