Skip to content

Commit 66abc55

Browse files
committed
net/http: add support for unencrypted HTTP/2
Add an UnencryptedHTTP2 protocol value. Both Server and Transport implement "HTTP/2 with prior knowledge" as described in RFC 9113, section 3.3. Neither supports the deprecated HTTP/2 upgrade mechanism (RFC 7540, section 3.2 "h2c"). For Server, UnencryptedHTTP2 controls whether the server will accept HTTP/2 connections on unencrypted ports. When enabled, the server checks new connections for the HTTP/2 preface and routes them appropriately. For Transport, enabling UnencryptedHTTP2 and disabling HTTP1 causes http:// requests to be made over unencrypted HTTP/2 connections. For #67816 Change-Id: I2763c4cdec1c2bc6bb8157edb93b94377de8a59b Reviewed-on: https://go-review.googlesource.com/c/go/+/622976 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Keith Randall <khr@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1 parent c0bccdd commit 66abc55

File tree

6 files changed

+198
-2
lines changed

6 files changed

+198
-2
lines changed

api/next/67816.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pkg net/http, method (*Protocols) SetUnencryptedHTTP2(bool) #67816
2+
pkg net/http, method (Protocols) UnencryptedHTTP2() bool #67816
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
The server and client may be configured to support unencrypted HTTP/2
2+
connections.
3+
4+
When [Server.Protocols] contains UnencryptedHTTP2, the server will accept
5+
HTTP/2 connections on unencrypted ports. The server can accept both
6+
HTTP/1 and unencrypted HTTP/2 on the same port.
7+
8+
When [Transport.Protocols] contains UnencryptedHTTP2 and does not contain
9+
HTTP1, the transport will use unencrypted HTTP/2 for http:// URLs.
10+
If the transport is configured to use both HTTP/1 and unencrypted HTTP/2,
11+
it will use HTTP/1.
12+
13+
Unencrypted HTTP/2 support uses "HTTP/2 with Prior Knowledge"
14+
(RFC 9113, section 3.3). The deprecated "Upgrade: h2c" header
15+
is not supported.

src/net/http/http.go

+12
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ import (
2424
// HTTP1 is supported on both unsecured TCP and secured TLS connections.
2525
//
2626
// - HTTP2 is the HTTP/2 protcol over a TLS connection.
27+
//
28+
// - UnencryptedHTTP2 is the HTTP/2 protocol over an unsecured TCP connection.
2729
type Protocols struct {
2830
bits uint8
2931
}
3032

3133
const (
3234
protoHTTP1 = 1 << iota
3335
protoHTTP2
36+
protoUnencryptedHTTP2
3437
)
3538

3639
// HTTP1 reports whether p includes HTTP/1.
@@ -45,6 +48,12 @@ func (p Protocols) HTTP2() bool { return p.bits&protoHTTP2 != 0 }
4548
// SetHTTP2 adds or removes HTTP/2 from p.
4649
func (p *Protocols) SetHTTP2(ok bool) { p.setBit(protoHTTP2, ok) }
4750

51+
// UnencryptedHTTP2 reports whether p includes unencrypted HTTP/2.
52+
func (p Protocols) UnencryptedHTTP2() bool { return p.bits&protoUnencryptedHTTP2 != 0 }
53+
54+
// SetUnencryptedHTTP2 adds or removes unencrypted HTTP/2 from p.
55+
func (p *Protocols) SetUnencryptedHTTP2(ok bool) { p.setBit(protoUnencryptedHTTP2, ok) }
56+
4857
func (p *Protocols) setBit(bit uint8, ok bool) {
4958
if ok {
5059
p.bits |= bit
@@ -61,6 +70,9 @@ func (p Protocols) String() string {
6170
if p.HTTP2() {
6271
s = append(s, "HTTP2")
6372
}
73+
if p.UnencryptedHTTP2() {
74+
s = append(s, "UnencryptedHTTP2")
75+
}
6476
return "{" + strings.Join(s, ",") + "}"
6577
}
6678

src/net/http/server.go

+83-1
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,16 @@ func (c *conn) serve(ctx context.Context) {
20132013
c.bufr = newBufioReader(c.r)
20142014
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
20152015

2016+
protos := c.server.protocols()
2017+
if c.tlsState == nil && protos.UnencryptedHTTP2() {
2018+
if c.maybeServeUnencryptedHTTP2(ctx) {
2019+
return
2020+
}
2021+
}
2022+
if !protos.HTTP1() {
2023+
return
2024+
}
2025+
20162026
for {
20172027
w, err := c.readRequest(ctx)
20182028
if c.r.remain != c.server.initialReadLimitSize() {
@@ -2132,6 +2142,70 @@ func (c *conn) serve(ctx context.Context) {
21322142
}
21332143
}
21342144

2145+
// unencryptedHTTP2Request is an HTTP handler that initializes
2146+
// certain uninitialized fields in its *Request.
2147+
//
2148+
// It's the unencrypted version of initALPNRequest.
2149+
type unencryptedHTTP2Request struct {
2150+
ctx context.Context
2151+
c net.Conn
2152+
h serverHandler
2153+
}
2154+
2155+
func (h unencryptedHTTP2Request) BaseContext() context.Context { return h.ctx }
2156+
2157+
func (h unencryptedHTTP2Request) ServeHTTP(rw ResponseWriter, req *Request) {
2158+
if req.Body == nil {
2159+
req.Body = NoBody
2160+
}
2161+
if req.RemoteAddr == "" {
2162+
req.RemoteAddr = h.c.RemoteAddr().String()
2163+
}
2164+
h.h.ServeHTTP(rw, req)
2165+
}
2166+
2167+
// unencryptedNetConnInTLSConn is used to pass an unencrypted net.Conn to
2168+
// functions that only accept a *tls.Conn.
2169+
type unencryptedNetConnInTLSConn struct {
2170+
net.Conn // panic on all net.Conn methods
2171+
conn net.Conn
2172+
}
2173+
2174+
func (c unencryptedNetConnInTLSConn) UnencryptedNetConn() net.Conn {
2175+
return c.conn
2176+
}
2177+
2178+
func unencryptedTLSConn(c net.Conn) *tls.Conn {
2179+
return tls.Client(unencryptedNetConnInTLSConn{conn: c}, nil)
2180+
}
2181+
2182+
// TLSNextProto key to use for unencrypted HTTP/2 connections.
2183+
// Not actually a TLS-negotiated protocol.
2184+
const nextProtoUnencryptedHTTP2 = "unencrypted_http2"
2185+
2186+
func (c *conn) maybeServeUnencryptedHTTP2(ctx context.Context) bool {
2187+
fn, ok := c.server.TLSNextProto[nextProtoUnencryptedHTTP2]
2188+
if !ok {
2189+
return false
2190+
}
2191+
hasPreface := func(c *conn, preface []byte) bool {
2192+
c.r.setReadLimit(int64(len(preface)) - int64(c.bufr.Buffered()))
2193+
got, err := c.bufr.Peek(len(preface))
2194+
c.r.setInfiniteReadLimit()
2195+
return err == nil && bytes.Equal(got, preface)
2196+
}
2197+
if !hasPreface(c, []byte("PRI * HTTP/2.0")) {
2198+
return false
2199+
}
2200+
if !hasPreface(c, []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")) {
2201+
return false
2202+
}
2203+
c.setState(c.rwc, StateActive, skipHooks)
2204+
h := unencryptedHTTP2Request{ctx, c.rwc, serverHandler{c.server}}
2205+
fn(c.server, unencryptedTLSConn(c.rwc), h)
2206+
return true
2207+
}
2208+
21352209
func (w *response) sendExpectationFailed() {
21362210
// TODO(bradfitz): let ServeHTTP handlers handle
21372211
// requests with non-standard expectation[s]? Seems
@@ -2981,6 +3055,10 @@ type Server struct {
29813055

29823056
// Protocols is the set of protocols accepted by the server.
29833057
//
3058+
// If Protocols includes UnencryptedHTTP2, the server will accept
3059+
// unencrypted HTTP/2 connections. The server can serve both
3060+
// HTTP/1 and unencrypted HTTP/2 on the same address and port.
3061+
//
29843062
// If Protocols is nil, the default is usually HTTP/1 and HTTP/2.
29853063
// If TLSNextProto is non-nil and does not contain an "h2" entry,
29863064
// the default is HTTP/1 only.
@@ -3286,6 +3364,9 @@ func (s *Server) shouldConfigureHTTP2ForServe() bool {
32863364
// in case the listener returns an "h2" *tls.Conn.
32873365
return true
32883366
}
3367+
if s.protocols().UnencryptedHTTP2() {
3368+
return true
3369+
}
32893370
// The user specified a TLSConfig on their http.Server.
32903371
// In this, case, only configure HTTP/2 if their tls.Config
32913372
// explicitly mentions "h2". Otherwise http2.ConfigureServer
@@ -3658,7 +3739,8 @@ func (s *Server) onceSetNextProtoDefaults() {
36583739
if omitBundledHTTP2 {
36593740
return
36603741
}
3661-
if !s.protocols().HTTP2() {
3742+
p := s.protocols()
3743+
if !p.HTTP2() && !p.UnencryptedHTTP2() {
36623744
return
36633745
}
36643746
if http2server.Value() == "0" {

src/net/http/transport.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ type Transport struct {
302302

303303
// Protocols is the set of protocols supported by the transport.
304304
//
305+
// If Protocols includes UnencryptedHTTP2 and does not include HTTP1,
306+
// the transport will use unencrypted HTTP/2 for requests for http:// URLs.
307+
//
305308
// If Protocols is nil, the default is usually HTTP/1 only.
306309
// If ForceAttemptHTTP2 is true, or if TLSNextProto contains an "h2" entry,
307310
// the default is HTTP/1 and HTTP/2.
@@ -410,7 +413,7 @@ func (t *Transport) onceSetNextProtoDefaults() {
410413
}
411414

412415
protocols := t.protocols()
413-
if !protocols.HTTP2() {
416+
if !protocols.HTTP2() && !protocols.UnencryptedHTTP2() {
414417
return
415418
}
416419
if omitBundledHTTP2 {
@@ -1902,6 +1905,24 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers
19021905
}
19031906
}
19041907

1908+
// Possible unencrypted HTTP/2 with prior knowledge.
1909+
unencryptedHTTP2 := pconn.tlsState == nil &&
1910+
t.Protocols != nil &&
1911+
t.Protocols.UnencryptedHTTP2() &&
1912+
!t.Protocols.HTTP1()
1913+
if unencryptedHTTP2 {
1914+
next, ok := t.TLSNextProto[nextProtoUnencryptedHTTP2]
1915+
if !ok {
1916+
return nil, errors.New("http: Transport does not support unencrypted HTTP/2")
1917+
}
1918+
alt := next(cm.targetAddr, unencryptedTLSConn(pconn.conn))
1919+
if e, ok := alt.(erringRoundTripper); ok {
1920+
// pconn.conn was closed by next (http2configureTransports.upgradeFn).
1921+
return nil, e.RoundTripErr()
1922+
}
1923+
return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: alt}, nil
1924+
}
1925+
19051926
if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
19061927
if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
19071928
alt := next(cm.targetAddr, pconn.conn.(*tls.Conn))

src/net/http/transport_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -7312,6 +7312,67 @@ func TestTransportServerProtocols(t *testing.T) {
73127312
tr.Protocols.SetHTTP2(true)
73137313
},
73147314
want: "HTTP/1.1",
7315+
}, {
7316+
name: "unencrypted HTTP2 with prior knowledge",
7317+
scheme: "http",
7318+
transport: func(tr *Transport) {
7319+
tr.Protocols = &Protocols{}
7320+
tr.Protocols.SetUnencryptedHTTP2(true)
7321+
},
7322+
server: func(srv *Server) {
7323+
srv.Protocols = &Protocols{}
7324+
srv.Protocols.SetHTTP1(true)
7325+
srv.Protocols.SetUnencryptedHTTP2(true)
7326+
},
7327+
want: "HTTP/2.0",
7328+
}, {
7329+
name: "unencrypted HTTP2 only on server",
7330+
scheme: "http",
7331+
transport: func(tr *Transport) {
7332+
tr.Protocols = &Protocols{}
7333+
tr.Protocols.SetUnencryptedHTTP2(true)
7334+
},
7335+
server: func(srv *Server) {
7336+
srv.Protocols = &Protocols{}
7337+
srv.Protocols.SetUnencryptedHTTP2(true)
7338+
},
7339+
want: "HTTP/2.0",
7340+
}, {
7341+
name: "unencrypted HTTP2 with no server support",
7342+
scheme: "http",
7343+
transport: func(tr *Transport) {
7344+
tr.Protocols = &Protocols{}
7345+
tr.Protocols.SetUnencryptedHTTP2(true)
7346+
},
7347+
server: func(srv *Server) {
7348+
srv.Protocols = &Protocols{}
7349+
srv.Protocols.SetHTTP1(true)
7350+
},
7351+
want: "error",
7352+
}, {
7353+
name: "HTTP1 with no server support",
7354+
scheme: "http",
7355+
transport: func(tr *Transport) {
7356+
tr.Protocols = &Protocols{}
7357+
tr.Protocols.SetHTTP1(true)
7358+
},
7359+
server: func(srv *Server) {
7360+
srv.Protocols = &Protocols{}
7361+
srv.Protocols.SetUnencryptedHTTP2(true)
7362+
},
7363+
want: "error",
7364+
}, {
7365+
name: "HTTPS1 with no server support",
7366+
scheme: "https",
7367+
transport: func(tr *Transport) {
7368+
tr.Protocols = &Protocols{}
7369+
tr.Protocols.SetHTTP1(true)
7370+
},
7371+
server: func(srv *Server) {
7372+
srv.Protocols = &Protocols{}
7373+
srv.Protocols.SetHTTP2(true)
7374+
},
7375+
want: "error",
73157376
}} {
73167377
t.Run(test.name, func(t *testing.T) {
73177378
// We don't use httptest here because it makes its own decisions
@@ -7362,6 +7423,9 @@ func TestTransportServerProtocols(t *testing.T) {
73627423
client := &Client{Transport: tr}
73637424
resp, err := client.Get(test.scheme + "://" + listener.Addr().String())
73647425
if err != nil {
7426+
if test.want == "error" {
7427+
return
7428+
}
73657429
t.Fatal(err)
73667430
}
73677431
if got := resp.Header.Get("X-Proto"); got != test.want {

0 commit comments

Comments
 (0)