Skip to content

Commit 4e0170f

Browse files
committed
http2: client side SETTINGS_ENABLE_CONNECT_PROTOCOL support
1 parent f238a07 commit 4e0170f

File tree

3 files changed

+120
-22
lines changed

3 files changed

+120
-22
lines changed

http2/http2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ func (s Setting) Valid() error {
145145
if s.Val < 16384 || s.Val > 1<<24-1 {
146146
return ConnectionError(ErrCodeProtocol)
147147
}
148+
case SettingEnableConnectProtocol:
149+
if s.Val != 1 && s.Val != 0 {
150+
return ConnectionError(ErrCodeProtocol)
151+
}
148152
}
149153
return nil
150154
}

http2/transport.go

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -335,25 +335,26 @@ type ClientConn struct {
335335
idleTimeout time.Duration // or 0 for never
336336
idleTimer timer
337337

338-
mu sync.Mutex // guards following
339-
cond *sync.Cond // hold mu; broadcast on flow/closed changes
340-
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
341-
inflow inflow // peer's conn-level flow control
342-
doNotReuse bool // whether conn is marked to not be reused for any future requests
343-
closing bool
344-
closed bool
345-
seenSettings bool // true if we've seen a settings frame, false otherwise
346-
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
347-
goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
348-
goAwayDebug string // goAway frame's debug data, retained as a string
349-
streams map[uint32]*clientStream // client-initiated
350-
streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
351-
nextStreamID uint32
352-
pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
353-
pings map[[8]byte]chan struct{} // in flight ping data to notification channel
354-
br *bufio.Reader
355-
lastActive time.Time
356-
lastIdle time.Time // time last idle
338+
mu sync.Mutex // guards following
339+
cond *sync.Cond // hold mu; broadcast on flow/closed changes
340+
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
341+
inflow inflow // peer's conn-level flow control
342+
doNotReuse bool // whether conn is marked to not be reused for any future requests
343+
closing bool
344+
closed bool
345+
seenSettings bool // true if we've seen a settings frame, false otherwise
346+
seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails
347+
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
348+
goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
349+
goAwayDebug string // goAway frame's debug data, retained as a string
350+
streams map[uint32]*clientStream // client-initiated
351+
streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
352+
nextStreamID uint32
353+
pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
354+
pings map[[8]byte]chan struct{} // in flight ping data to notification channel
355+
br *bufio.Reader
356+
lastActive time.Time
357+
lastIdle time.Time // time last idle
357358
// Settings from peer: (also guarded by wmu)
358359
maxFrameSize uint32
359360
maxConcurrentStreams uint32
@@ -363,6 +364,7 @@ type ClientConn struct {
363364
initialStreamRecvWindowSize int32
364365
readIdleTimeout time.Duration
365366
pingTimeout time.Duration
367+
extendedConnecAllowed bool
366368

367369
// reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests.
368370
// Write to reqHeaderMu to lock it, read from it to unlock.
@@ -752,6 +754,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
752754
peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
753755
streams: make(map[uint32]*clientStream),
754756
singleUse: singleUse,
757+
seenSettingsChan: make(chan struct{}),
755758
wantSettingsAck: true,
756759
readIdleTimeout: conf.SendPingTimeout,
757760
pingTimeout: conf.PingTimeout,
@@ -1376,6 +1379,8 @@ func (cs *clientStream) doRequest(req *http.Request, streamf func(*clientStream)
13761379
cs.cleanupWriteRequest(err)
13771380
}
13781381

1382+
var errExtendedConnectNotSupported = errors.New("net/http: extended connect not supported by peer")
1383+
13791384
// writeRequest sends a request.
13801385
//
13811386
// It returns nil after the request is written, the response read,
@@ -1405,7 +1410,20 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre
14051410
return ctx.Err()
14061411
}
14071412

1413+
// wait for setting frames to be received, a server can change this value later,
1414+
// but we just wait for the first settings frame
1415+
var isExtendedConnect bool
1416+
if req.Method == "CONNECT" && req.Header.Get(":protocol") != "" {
1417+
isExtendedConnect = true
1418+
<-cc.seenSettingsChan
1419+
}
1420+
14081421
cc.mu.Lock()
1422+
if isExtendedConnect && !cc.extendedConnecAllowed {
1423+
cc.mu.Unlock()
1424+
<-cc.reqHeaderMu
1425+
return errExtendedConnectNotSupported
1426+
}
14091427
if cc.idleTimer != nil {
14101428
cc.idleTimer.Stop()
14111429
}
@@ -1910,7 +1928,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
19101928

19111929
func validateHeaders(hdrs http.Header) string {
19121930
for k, vv := range hdrs {
1913-
if !httpguts.ValidHeaderFieldName(k) {
1931+
if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
19141932
return fmt.Sprintf("name %q", k)
19151933
}
19161934
for _, v := range vv {
@@ -1926,6 +1944,10 @@ func validateHeaders(hdrs http.Header) string {
19261944

19271945
var errNilRequestURL = errors.New("http2: Request.URI is nil")
19281946

1947+
func isNormalConnect(req *http.Request) bool {
1948+
return req.Method == "CONNECT" && req.Header.Get(":protocol") == ""
1949+
}
1950+
19291951
// requires cc.wmu be held.
19301952
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
19311953
cc.hbuf.Reset()
@@ -1946,7 +1968,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
19461968
}
19471969

19481970
var path string
1949-
if req.Method != "CONNECT" {
1971+
if !isNormalConnect(req) {
19501972
path = req.URL.RequestURI()
19511973
if !validPseudoPath(path) {
19521974
orig := path
@@ -1983,7 +2005,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
19832005
m = http.MethodGet
19842006
}
19852007
f(":method", m)
1986-
if req.Method != "CONNECT" {
2008+
if !isNormalConnect(req) {
19872009
f(":path", path)
19882010
f(":scheme", req.URL.Scheme)
19892011
}
@@ -2370,6 +2392,9 @@ func (rl *clientConnReadLoop) run() error {
23702392
if VerboseLogs {
23712393
cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err)
23722394
}
2395+
if !cc.seenSettings {
2396+
close(cc.seenSettingsChan)
2397+
}
23732398
return err
23742399
}
23752400
}
@@ -2917,6 +2942,15 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
29172942
case SettingHeaderTableSize:
29182943
cc.henc.SetMaxDynamicTableSize(s.Val)
29192944
cc.peerMaxHeaderTableSize = s.Val
2945+
case SettingEnableConnectProtocol:
2946+
if err := s.Valid(); err != nil {
2947+
return err
2948+
}
2949+
// RFC 8441 section, https://datatracker.ietf.org/doc/html/rfc8441#section-3
2950+
if s.Val == 0 && cc.extendedConnecAllowed {
2951+
return ConnectionError(ErrCodeProtocol)
2952+
}
2953+
cc.extendedConnecAllowed = s.Val == 1
29202954
default:
29212955
cc.vlogf("Unhandled Setting: %v", s)
29222956
}
@@ -2934,6 +2968,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
29342968
// connection can establish to our default.
29352969
cc.maxConcurrentStreams = defaultMaxConcurrentStreams
29362970
}
2971+
close(cc.seenSettingsChan)
29372972
cc.seenSettings = true
29382973
}
29392974

http2/transport_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5421,3 +5421,62 @@ func TestIssue67671(t *testing.T) {
54215421
res.Body.Close()
54225422
}
54235423
}
5424+
5425+
func TestExtendedConnectClientWithServerSupport(t *testing.T) {
5426+
disableExtendedConnectProtocol = false
5427+
ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
5428+
t.Log(io.Copy(w, r.Body))
5429+
})
5430+
tr := &Transport{
5431+
TLSClientConfig: tlsConfigInsecure,
5432+
AllowHTTP: true,
5433+
}
5434+
defer tr.CloseIdleConnections()
5435+
pr, pw := io.Pipe()
5436+
pwDone := make(chan struct{})
5437+
req, _ := http.NewRequest("CONNECT", ts.URL, pr)
5438+
req.Header.Set(":protocol", "extended-connect")
5439+
go func() {
5440+
pw.Write([]byte("hello, extended connect"))
5441+
pw.Close()
5442+
close(pwDone)
5443+
}()
5444+
5445+
res, err := tr.RoundTrip(req)
5446+
if err != nil {
5447+
t.Fatal(err)
5448+
}
5449+
body, err := io.ReadAll(res.Body)
5450+
if err != nil {
5451+
t.Fatal(err)
5452+
}
5453+
if !bytes.Equal(body, []byte("hello, extended connect")) {
5454+
t.Fatal("unexpected body received")
5455+
}
5456+
}
5457+
5458+
func TestExtendedConnectClientWithoutServerSupport(t *testing.T) {
5459+
disableExtendedConnectProtocol = true
5460+
ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
5461+
io.Copy(w, r.Body)
5462+
})
5463+
tr := &Transport{
5464+
TLSClientConfig: tlsConfigInsecure,
5465+
AllowHTTP: true,
5466+
}
5467+
defer tr.CloseIdleConnections()
5468+
pr, pw := io.Pipe()
5469+
pwDone := make(chan struct{})
5470+
req, _ := http.NewRequest("CONNECT", ts.URL, pr)
5471+
req.Header.Set(":protocol", "extended-connect")
5472+
go func() {
5473+
pw.Write([]byte("hello, extended connect"))
5474+
pw.Close()
5475+
close(pwDone)
5476+
}()
5477+
5478+
_, err := tr.RoundTrip(req)
5479+
if !errors.Is(err, errExtendedConnectNotSupported) {
5480+
t.Fatalf("expected error errExtendedConnectNotSupported, got: %v", err)
5481+
}
5482+
}

0 commit comments

Comments
 (0)