mirror of
https://github.com/golang/go.git
synced 2026-04-03 01:40:30 +09:00
net/http: add basic unexported pluggable HTTP/3 support
Following #77440, this CL adds a basic support for plugging in an HTTP/3 implementation to net/http. As the proposal is not accepted yet, this CL does not add any exported symbols. Access to plug HTTP/3 support is locked behind net/http.protocolSetHTTP3, which can only be used via linkname by golang.org/x/net/internal/http3_test.protocolSetHTTP3. This will allow us to run our HTTP/3 implementation in x/net againts various tests in net/http to support development, without expanding the API surface for any users. Support for closeIdleConnectionser will be added separately in the future. For #77440 Change-Id: I6e3a0c2e9b329cef43e4682463ed5e2093d04256 Reviewed-on: https://go-review.googlesource.com/c/go/+/740120 Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
committed by
Nicholas Husin
parent
fdf3bee342
commit
0b9bcbc58c
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
_ "unsafe"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
@@ -35,6 +36,7 @@ const (
|
||||
protoHTTP1 = 1 << iota
|
||||
protoHTTP2
|
||||
protoUnencryptedHTTP2
|
||||
protoHTTP3
|
||||
)
|
||||
|
||||
// HTTP1 reports whether p includes HTTP/1.
|
||||
@@ -55,6 +57,15 @@ func (p Protocols) UnencryptedHTTP2() bool { return p.bits&protoUnencryptedHTTP2
|
||||
// SetUnencryptedHTTP2 adds or removes unencrypted HTTP/2 from p.
|
||||
func (p *Protocols) SetUnencryptedHTTP2(ok bool) { p.setBit(protoUnencryptedHTTP2, ok) }
|
||||
|
||||
// http3 reports whether p includes HTTP/3.
|
||||
func (p Protocols) http3() bool { return p.bits&protoHTTP3 != 0 }
|
||||
|
||||
// setHTTP3 adds or removes HTTP/3 from p.
|
||||
func (p *Protocols) setHTTP3(ok bool) { p.setBit(protoHTTP3, ok) }
|
||||
|
||||
//go:linkname protocolSetHTTP3 golang.org/x/net/internal/http3_test.protocolSetHTTP3
|
||||
func protocolSetHTTP3(p *Protocols) { p.setHTTP3(true) }
|
||||
|
||||
func (p *Protocols) setBit(bit uint8, ok bool) {
|
||||
if ok {
|
||||
p.bits |= bit
|
||||
@@ -63,6 +74,11 @@ func (p *Protocols) setBit(bit uint8, ok bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// empty returns true if p has no protocol set at all.
|
||||
func (p Protocols) empty() bool {
|
||||
return p.bits == 0
|
||||
}
|
||||
|
||||
func (p Protocols) String() string {
|
||||
var s []string
|
||||
if p.HTTP1() {
|
||||
|
||||
@@ -3476,6 +3476,22 @@ func (s *Server) Serve(l net.Listener) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) setupTLSConfig(certFile, keyFile string, nextProtos []string) (*tls.Config, error) {
|
||||
config := cloneTLSConfig(s.TLSConfig)
|
||||
config.NextProtos = nextProtos
|
||||
|
||||
configHasCert := len(config.Certificates) > 0 || config.GetCertificate != nil || config.GetConfigForClient != nil
|
||||
if !configHasCert || certFile != "" || keyFile != "" {
|
||||
var err error
|
||||
config.Certificates = make([]tls.Certificate, 1)
|
||||
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ServeTLS accepts incoming connections on the Listener l, creating a
|
||||
// new service goroutine for each. The service goroutines perform TLS
|
||||
// setup and then read requests, calling s.Handler to reply to them.
|
||||
@@ -3497,17 +3513,9 @@ func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
config := cloneTLSConfig(s.TLSConfig)
|
||||
config.NextProtos = adjustNextProtos(config.NextProtos, s.protocols())
|
||||
|
||||
configHasCert := len(config.Certificates) > 0 || config.GetCertificate != nil || config.GetConfigForClient != nil
|
||||
if !configHasCert || certFile != "" || keyFile != "" {
|
||||
var err error
|
||||
config.Certificates = make([]tls.Certificate, 1)
|
||||
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := s.setupTLSConfig(certFile, keyFile, adjustNextProtos(s.TLSConfig.NextProtos, s.protocols()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlsListener := tls.NewListener(l, config)
|
||||
@@ -3516,6 +3524,15 @@ func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
|
||||
|
||||
func (s *Server) protocols() Protocols {
|
||||
if s.Protocols != nil {
|
||||
// Historically, even when Protocols for a Server was set to be empty,
|
||||
// the Server can still run normally with just HTTP/1.
|
||||
// To keep backward-compatibility, the zero value of Protocols is
|
||||
// defined as having only HTTP/1 enabled.
|
||||
if s.Protocols.empty() {
|
||||
var p Protocols
|
||||
p.SetHTTP1(true)
|
||||
return p
|
||||
}
|
||||
return *s.Protocols // user-configured set
|
||||
}
|
||||
|
||||
@@ -3696,6 +3713,51 @@ func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error {
|
||||
return server.ListenAndServeTLS(certFile, keyFile)
|
||||
}
|
||||
|
||||
// http3ServerHandler implements an interface in an external library that
|
||||
// supports HTTP/3, allowing an external implementation of HTTP/3 to be used
|
||||
// via net/http. See https://go.dev/issue/77440 for details.
|
||||
//
|
||||
// This is currently only used with golang.org/x/net/internal/http3, to allow
|
||||
// us to test our HTTP/3 implementation againts tests in net/http. HTTP/3 is
|
||||
// not yet accessible to end-users.
|
||||
type http3ServerHandler struct {
|
||||
handler serverHandler
|
||||
tlsConfig *tls.Config
|
||||
baseCtx context.Context
|
||||
errc chan error
|
||||
}
|
||||
|
||||
// ServeHTTP ensures that http3ServerHandler implements the Handler interface,
|
||||
// and gives an HTTP/3 server implementation access to the net/http handler.
|
||||
func (h http3ServerHandler) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Addr gives an HTTP/3 server implementation the address that it should listen
|
||||
// on.
|
||||
func (h http3ServerHandler) Addr() string {
|
||||
return h.handler.srv.Addr
|
||||
}
|
||||
|
||||
// TLSConfig gives an HTTP/3 server implementation the *tls.Config that it
|
||||
// should use.
|
||||
func (h http3ServerHandler) TLSConfig() *tls.Config {
|
||||
return h.tlsConfig
|
||||
}
|
||||
|
||||
// BaseContext gives an HTTP/3 server implementation the base context to use
|
||||
// for server requests.
|
||||
func (h http3ServerHandler) BaseContext() context.Context {
|
||||
return h.baseCtx
|
||||
}
|
||||
|
||||
// ListenErrHook should be called by an HTTP/3 server implementation to
|
||||
// propagate any error it encounters when trying to listen, if any, to
|
||||
// net/http.
|
||||
func (h http3ServerHandler) ListenErrHook(err error) {
|
||||
h.errc <- err
|
||||
}
|
||||
|
||||
// ListenAndServeTLS listens on the TCP network address s.Addr and
|
||||
// then calls [ServeTLS] to handle requests on incoming TLS connections.
|
||||
// Accepted connections are configured to enable TCP keep-alives.
|
||||
@@ -3720,13 +3782,37 @@ func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
|
||||
addr = ":https"
|
||||
}
|
||||
|
||||
p := s.protocols()
|
||||
if p.http3() {
|
||||
fn, ok := s.TLSNextProto["http/3"]
|
||||
if !ok {
|
||||
return errors.New("http: Server.Protocols contains HTTP3, but Server does not support HTTP/3")
|
||||
}
|
||||
config, err := s.setupTLSConfig(certFile, keyFile, []string{"h3"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errc := make(chan error, 1)
|
||||
go fn(s, nil, http3ServerHandler{
|
||||
handler: serverHandler{s},
|
||||
tlsConfig: config,
|
||||
baseCtx: context.WithValue(context.Background(), ServerContextKey, s),
|
||||
errc: errc,
|
||||
})
|
||||
if err := <-errc; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Only start a TCP listener if HTTP/1 or HTTP/2 is used.
|
||||
if !p.HTTP1() && !p.HTTP2() && !p.UnencryptedHTTP2() {
|
||||
return nil
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer ln.Close()
|
||||
|
||||
return s.ServeTLS(ln, certFile, keyFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -288,8 +288,9 @@ type Transport struct {
|
||||
// nextProtoOnce guards initialization of TLSNextProto and
|
||||
// h2transport (via onceSetNextProtoDefaults)
|
||||
nextProtoOnce sync.Once
|
||||
h2transport h2Transport // non-nil if http2 wired up
|
||||
tlsNextProtoWasNil bool // whether TLSNextProto was nil when the Once fired
|
||||
h2transport h2Transport // non-nil if http2 wired up
|
||||
h3transport dialClientConner // non-nil if http3 wired up
|
||||
tlsNextProtoWasNil bool // whether TLSNextProto was nil when the Once fired
|
||||
|
||||
// ForceAttemptHTTP2 controls whether HTTP/2 is enabled when a non-zero
|
||||
// Dial, DialTLS, or DialContext func or TLSClientConfig is provided.
|
||||
@@ -380,6 +381,36 @@ func (t *Transport) Clone() *Transport {
|
||||
return t2
|
||||
}
|
||||
|
||||
type dialClientConner interface {
|
||||
// DialClientConn creates a new client connection to address.
|
||||
//
|
||||
// If proxy is non-nil, the connection should use the provided proxy.
|
||||
// If HTTP/3 proxies are not supported, DialClientConn should return
|
||||
// an error wrapping [errors.ErrUnsupported].
|
||||
//
|
||||
// The RoundTripper returned by DialClientConn may implement
|
||||
// any of the following methods to support the [ClientConn]
|
||||
// method of the same name:
|
||||
// Close() error
|
||||
// Err() error
|
||||
// Reserve() error
|
||||
// Release() error
|
||||
// Available() int
|
||||
// InFlight() int
|
||||
//
|
||||
// The client connection should arrange to call internalStateHook
|
||||
// when the connection closes, when requests complete, and when the
|
||||
// connection concurrency limit changes.
|
||||
//
|
||||
// The client connection must call the internal state hook when
|
||||
// the connection state changes asynchronously, such as when a request completes.
|
||||
//
|
||||
// The internal state hook need not be called after synchronous changes
|
||||
// to the state: Close, Reserve, Release, and RoundTrip calls
|
||||
// which don't start a request do not need to call the hook.
|
||||
DialClientConn(ctx context.Context, address string, proxy *url.URL, internalStateHook func()) (RoundTripper, error)
|
||||
}
|
||||
|
||||
// h2Transport is the interface we expect to be able to call from
|
||||
// net/http against an *http2.Transport that's either bundled into
|
||||
// h2_bundle.go or supplied by the user via x/net/http2.
|
||||
@@ -878,6 +909,14 @@ var ErrSkipAltProtocol = errors.New("net/http: skip alternate protocol")
|
||||
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {
|
||||
t.altMu.Lock()
|
||||
defer t.altMu.Unlock()
|
||||
|
||||
if scheme == "http/3" {
|
||||
var ok bool
|
||||
if t.h3transport, ok = rt.(dialClientConner); !ok {
|
||||
panic("http: HTTP/3 RoundTripper does not implement DialClientConn")
|
||||
}
|
||||
}
|
||||
|
||||
oldMap, _ := t.altProto.Load().(map[string]RoundTripper)
|
||||
if _, exists := oldMap[scheme]; exists {
|
||||
panic("protocol " + scheme + " already registered")
|
||||
@@ -1767,6 +1806,28 @@ type erringRoundTripper interface {
|
||||
var testHookProxyConnectTimeout = context.WithTimeout
|
||||
|
||||
func (t *Transport) dialConn(ctx context.Context, cm connectMethod, isClientConn bool, internalStateHook func()) (pconn *persistConn, err error) {
|
||||
// TODO: actually support HTTP/3. Among other things:
|
||||
// - make HTTP/3 play well with proxy.
|
||||
// - implement happy eyeball between HTTP/3 and HTTP/1 & HTTP/2.
|
||||
// - clean up the connection pooling logic.
|
||||
if p := t.protocols(); p.http3() {
|
||||
if p.HTTP1() || p.HTTP2() || p.UnencryptedHTTP2() {
|
||||
return nil, errors.New("http: when using HTTP3, Transport.Protocols must contain only HTTP3")
|
||||
}
|
||||
if t.h3transport == nil {
|
||||
return nil, errors.New("http: Transport.Protocols contains HTTP3, but Transport does not support HTTP/3")
|
||||
}
|
||||
rt, err := t.h3transport.DialClientConn(ctx, cm.addr(), cm.proxyURL, internalStateHook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &persistConn{
|
||||
t: t,
|
||||
cacheKey: cm.key(),
|
||||
alt: rt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
pconn = &persistConn{
|
||||
t: t,
|
||||
cacheKey: cm.key(),
|
||||
|
||||
Reference in New Issue
Block a user