From d4d16688641738c2ea8bd69a86ee9c10195c898d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 19 Oct 2015 15:30:21 -0700 Subject: [PATCH] net/http: add Transport.TLSNextProto, ErrSkipAltProtocol This is the start of wiring up the HTTP/2 Transport. It is still disabled in this commit. This change does two main things: 1) Transport.RegisterProtocol now permits registering "http" or "https" (they previously paniced), and the semantics of the registered RoundTripper have been extended to say that the new sentinel error value (ErrSkipAltProtocol, added in this CL) means that the Transport's RoundTrip method proceeds as if the alternate protocol had not been registered. This gives us a place to register an alternate "https" RoundTripper which gets first dibs on using HTTP/2 if there's already a cached connection. 2) adds Transport.TLSNextProto, a map keyed by TLS NPN/ALPN protocol strings, similar in feel to the existing Server.TLSNextProto map. This map is the glue between the HTTP/1 and HTTP/2 clients, since we don't know which protocol we're going to speak (and thus which Transport type to use) until we've already made the TCP connection. Updates #6891 Change-Id: I7328c7ff24f52d9fe4899facabf7ecc5dcb989f3 Reviewed-on: https://go-review.googlesource.com/16090 Reviewed-by: Andrew Gerrand Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot --- src/net/http/transport.go | 99 ++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/src/net/http/transport.go b/src/net/http/transport.go index 31599237e0..ad8aa19c2e 100644 --- a/src/net/http/transport.go +++ b/src/net/http/transport.go @@ -25,6 +25,10 @@ import ( "time" ) +// h2DefaultTransport is the HTTP/2 version of DefaultTransport. +// DefaultTransport and h2DefaultTransport are wired up together. +var h2DefaultTransport = &http2Transport{} + // DefaultTransport is the default implementation of Transport and is // used by DefaultClient. It establishes network connections as needed // and caches them for reuse by subsequent calls. It uses HTTP proxies @@ -40,6 +44,41 @@ var DefaultTransport RoundTripper = &Transport{ ExpectContinueTimeout: 1 * time.Second, } +func init() { + // TODO(bradfitz,adg): remove the following line before Go 1.6 + // ships. This just gives us a mechanism to temporarily + // enable the http2 client during development. + if !strings.Contains(os.Getenv("GODEBUG"), "h2client=1") { + return + } + + t := DefaultTransport.(*Transport) + + // TODO(bradfitz,adg): move all this up to DefaultTransport before Go 1.6: + t.RegisterProtocol("https", noDialH2Transport{h2DefaultTransport}) + t.TLSClientConfig = &tls.Config{ + NextProtos: []string{"h2"}, + } + t.TLSNextProto = map[string]func(string, *tls.Conn) RoundTripper{ + "h2": http2TransportForConn, + } +} + +// noDialH2Transport is a RoundTripper which only tries to complete the request if +// the wrapped *http2Transport already has a cached connection to the host. +type noDialH2Transport struct{ rt *http2Transport } + +func (t noDialH2Transport) RoundTrip(req *Request) (*Response, error) { + // TODO(bradfitz): wire up http2.Transport + return nil, ErrSkipAltProtocol +} + +func http2TransportForConn(authority string, c *tls.Conn) RoundTripper { + // TODO(bradfitz): donate c to h2DefaultTransport: + // h2DefaultTransport.AddIdleConn(authority, c) + return h2DefaultTransport +} + // DefaultMaxIdleConnsPerHost is the default value of Transport's // MaxIdleConnsPerHost. const DefaultMaxIdleConnsPerHost = 2 @@ -121,6 +160,16 @@ type Transport struct { // This time does not include the time to send the request header. ExpectContinueTimeout time.Duration + // TLSNextProto specifies how the Transport switches to an + // alternate protocol (such as HTTP/2) after a TLS NPN/ALPN + // protocol negotiation. If Transport dials an TLS connection + // with a non-empty protocol name and TLSNextProto contains a + // map entry for that key (such as "h2"), then the func is + // called with the request's authority (such as "example.com" + // or "example.com:1234") and the TLS connection. The function + // must return a RoundTripper that then handles the request. + TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper + // TODO: tunable on global max cached connections // TODO: tunable on timeout on cached connections } @@ -196,7 +245,7 @@ func (tr *transportRequest) extraHeaders() Header { // // For higher-level HTTP client support (such as handling of cookies // and redirects), see Get, Post, and the Client type. -func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { +func (t *Transport) RoundTrip(req *Request) (*Response, error) { if req.URL == nil { req.closeBody() return nil, errors.New("http: nil Request.URL") @@ -205,18 +254,18 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { req.closeBody() return nil, errors.New("http: nil Request.Header") } - if req.URL.Scheme != "http" && req.URL.Scheme != "https" { - t.altMu.RLock() - var rt RoundTripper - if t.altProto != nil { - rt = t.altProto[req.URL.Scheme] + // TODO(bradfitz): switch to atomic.Value for this map instead of RWMutex + t.altMu.RLock() + altRT := t.altProto[req.URL.Scheme] + t.altMu.RUnlock() + if altRT != nil { + if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol { + return resp, err } - t.altMu.RUnlock() - if rt == nil { - req.closeBody() - return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme} - } - return rt.RoundTrip(req) + } + if s := req.URL.Scheme; s != "http" && s != "https" { + req.closeBody() + return nil, &badStringError{"unsupported protocol scheme", s} } if req.URL.Host == "" { req.closeBody() @@ -239,20 +288,27 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { req.closeBody() return nil, err } - + if pconn.alt != nil { + // HTTP/2 path. + return pconn.alt.RoundTrip(req) + } return pconn.roundTrip(treq) } +// ErrSkipAltProtocol is a sentinel error value defined by Transport.RegisterProtocol. +var ErrSkipAltProtocol = errors.New("net/http: skip alternate protocol") + // RegisterProtocol registers a new protocol with scheme. // The Transport will pass requests using the given scheme to rt. // It is rt's responsibility to simulate HTTP request semantics. // // RegisterProtocol can be used by other packages to provide // implementations of protocol schemes like "ftp" or "file". +// +// If rt.RoundTrip returns ErrSkipAltProtocol, the Transport will +// handle the RoundTrip itself for that one request, as if the +// protocol were not registered. func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) { - if scheme == "http" || scheme == "https" { - panic("protocol " + scheme + " already registered") - } t.altMu.Lock() defer t.altMu.Unlock() if t.altProto == nil { @@ -688,6 +744,12 @@ func (t *Transport) dialConn(cm connectMethod) (*persistConn, error) { pconn.conn = tlsConn } + if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" { + if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok { + return &persistConn{alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil + } + } + pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF}) pconn.bw = bufio.NewWriter(pconn.conn) go pconn.readLoop() @@ -817,6 +879,11 @@ func (k connectMethodKey) String() string { // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well) type persistConn struct { + // alt optionally specifies the TLS NextProto RoundTripper. + // This is used for HTTP/2 today and future protocol laters. + // If it's non-nil, the rest of the fields are unused. + alt RoundTripper + t *Transport cacheKey connectMethodKey conn net.Conn