Skip to content

Commit 77f622b

Browse files
committed
http2: client side SETTINGS_ENABLE_CONNECT_PROTOCOL support
1 parent d1683ac commit 77f622b

File tree

3 files changed

+120
-22
lines changed

3 files changed

+120
-22
lines changed

Diff for: http2/http2.go

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

Diff for: http2/transport.go

+57-22
Original file line numberDiff line numberDiff line change
@@ -350,31 +350,33 @@ type ClientConn struct {
350350
idleTimeout time.Duration // or 0 for never
351351
idleTimer timer
352352

353-
mu sync.Mutex // guards following
354-
cond *sync.Cond // hold mu; broadcast on flow/closed changes
355-
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
356-
inflow inflow // peer's conn-level flow control
357-
doNotReuse bool // whether conn is marked to not be reused for any future requests
358-
closing bool
359-
closed bool
360-
seenSettings bool // true if we've seen a settings frame, false otherwise
361-
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
362-
goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
363-
goAwayDebug string // goAway frame's debug data, retained as a string
364-
streams map[uint32]*clientStream // client-initiated
365-
streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
366-
nextStreamID uint32
367-
pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
368-
pings map[[8]byte]chan struct{} // in flight ping data to notification channel
369-
br *bufio.Reader
370-
lastActive time.Time
371-
lastIdle time.Time // time last idle
353+
mu sync.Mutex // guards following
354+
cond *sync.Cond // hold mu; broadcast on flow/closed changes
355+
flow outflow // our conn-level flow control quota (cs.outflow is per stream)
356+
inflow inflow // peer's conn-level flow control
357+
doNotReuse bool // whether conn is marked to not be reused for any future requests
358+
closing bool
359+
closed bool
360+
seenSettings bool // true if we've seen a settings frame, false otherwise
361+
seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails
362+
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
363+
goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
364+
goAwayDebug string // goAway frame's debug data, retained as a string
365+
streams map[uint32]*clientStream // client-initiated
366+
streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
367+
nextStreamID uint32
368+
pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
369+
pings map[[8]byte]chan struct{} // in flight ping data to notification channel
370+
br *bufio.Reader
371+
lastActive time.Time
372+
lastIdle time.Time // time last idle
372373
// Settings from peer: (also guarded by wmu)
373374
maxFrameSize uint32
374375
maxConcurrentStreams uint32
375376
peerMaxHeaderListSize uint64
376377
peerMaxHeaderTableSize uint32
377378
initialWindowSize uint32
379+
extendedConnecAllowed bool
378380

379381
// reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests.
380382
// Write to reqHeaderMu to lock it, read from it to unlock.
@@ -788,6 +790,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
788790
peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
789791
streams: make(map[uint32]*clientStream),
790792
singleUse: singleUse,
793+
seenSettingsChan: make(chan struct{}),
791794
wantSettingsAck: true,
792795
pings: make(map[[8]byte]chan struct{}),
793796
reqHeaderMu: make(chan struct{}, 1),
@@ -1411,6 +1414,8 @@ func (cs *clientStream) doRequest(req *http.Request, streamf func(*clientStream)
14111414
cs.cleanupWriteRequest(err)
14121415
}
14131416

1417+
var errExtendedConnectNotSupported = errors.New("net/http: extended connect not supported by peer")
1418+
14141419
// writeRequest sends a request.
14151420
//
14161421
// It returns nil after the request is written, the response read,
@@ -1440,7 +1445,20 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre
14401445
return ctx.Err()
14411446
}
14421447

1448+
// wait for setting frames to be received, a server can change this value later,
1449+
// but we just wait for the first settings frame
1450+
var isExtendedConnect bool
1451+
if req.Method == "CONNECT" && req.Header.Get(":protocol") != "" {
1452+
isExtendedConnect = true
1453+
<-cc.seenSettingsChan
1454+
}
1455+
14431456
cc.mu.Lock()
1457+
if isExtendedConnect && !cc.extendedConnecAllowed {
1458+
cc.mu.Unlock()
1459+
<-cc.reqHeaderMu
1460+
return errExtendedConnectNotSupported
1461+
}
14441462
if cc.idleTimer != nil {
14451463
cc.idleTimer.Stop()
14461464
}
@@ -1945,7 +1963,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
19451963

19461964
func validateHeaders(hdrs http.Header) string {
19471965
for k, vv := range hdrs {
1948-
if !httpguts.ValidHeaderFieldName(k) {
1966+
if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
19491967
return fmt.Sprintf("name %q", k)
19501968
}
19511969
for _, v := range vv {
@@ -1961,6 +1979,10 @@ func validateHeaders(hdrs http.Header) string {
19611979

19621980
var errNilRequestURL = errors.New("http2: Request.URI is nil")
19631981

1982+
func isNormalConnect(req *http.Request) bool {
1983+
return req.Method == "CONNECT" && req.Header.Get(":protocol") == ""
1984+
}
1985+
19641986
// requires cc.wmu be held.
19651987
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
19661988
cc.hbuf.Reset()
@@ -1981,7 +2003,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
19812003
}
19822004

19832005
var path string
1984-
if req.Method != "CONNECT" {
2006+
if !isNormalConnect(req) {
19852007
path = req.URL.RequestURI()
19862008
if !validPseudoPath(path) {
19872009
orig := path
@@ -2018,7 +2040,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
20182040
m = http.MethodGet
20192041
}
20202042
f(":method", m)
2021-
if req.Method != "CONNECT" {
2043+
if !isNormalConnect(req) {
20222044
f(":path", path)
20232045
f(":scheme", req.URL.Scheme)
20242046
}
@@ -2405,6 +2427,9 @@ func (rl *clientConnReadLoop) run() error {
24052427
if VerboseLogs {
24062428
cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err)
24072429
}
2430+
if !cc.seenSettings {
2431+
close(cc.seenSettingsChan)
2432+
}
24082433
return err
24092434
}
24102435
}
@@ -2952,6 +2977,15 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
29522977
case SettingHeaderTableSize:
29532978
cc.henc.SetMaxDynamicTableSize(s.Val)
29542979
cc.peerMaxHeaderTableSize = s.Val
2980+
case SettingEnableConnectProtocol:
2981+
if err := s.Valid(); err != nil {
2982+
return err
2983+
}
2984+
// RFC 8441 section, https://datatracker.ietf.org/doc/html/rfc8441#section-3
2985+
if s.Val == 0 && cc.extendedConnecAllowed {
2986+
return ConnectionError(ErrCodeProtocol)
2987+
}
2988+
cc.extendedConnecAllowed = s.Val == 1
29552989
default:
29562990
cc.vlogf("Unhandled Setting: %v", s)
29572991
}
@@ -2969,6 +3003,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
29693003
// connection can establish to our default.
29703004
cc.maxConcurrentStreams = defaultMaxConcurrentStreams
29713005
}
3006+
close(cc.seenSettingsChan)
29723007
cc.seenSettings = true
29733008
}
29743009

Diff for: http2/transport_test.go

+59
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)