From 0e478a2a5f6c65aefdbab3e736a5c54b40654627 Mon Sep 17 00:00:00 2001 From: Eli Lindsey Date: Fri, 30 Sep 2022 12:53:47 -0400 Subject: [PATCH] http2: add SETTINGS_HEADER_TABLE_SIZE support Add support for handling of SETTINGS_HEADER_TABLESIZE in SETTINGS frames. Add http2.Transport.MaxDecoderHeaderTableSize to set the advertised table size for new client connections. Add http2.Transport.MaxEncoderHeaderTableSize to cap the accepted size for new client connections. Add http2.Server.MaxDecoderHeaderTableSize and MaxEncoderHeaderTableSize to do the same on the server. Fixes golang/go#29356 Fixes golang/go#56054 Change-Id: I16ae0f84b8527dc1e09dfce081e9f408fd514513 Reviewed-on: https://go-review.googlesource.com/c/net/+/435899 Reviewed-by: Damien Neil Reviewed-by: Joedian Reid TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- http2/hpack/encode.go | 5 ++ http2/server.go | 34 ++++++++-- http2/server_test.go | 37 +++++++++++ http2/transport.go | 50 +++++++++++--- http2/transport_test.go | 144 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 12 deletions(-) diff --git a/http2/hpack/encode.go b/http2/hpack/encode.go index 6886dc163..46219da2b 100644 --- a/http2/hpack/encode.go +++ b/http2/hpack/encode.go @@ -116,6 +116,11 @@ func (e *Encoder) SetMaxDynamicTableSize(v uint32) { e.dynTab.setMaxSize(v) } +// MaxDynamicTableSize returns the current dynamic header table size. +func (e *Encoder) MaxDynamicTableSize() (v uint32) { + return e.dynTab.maxSize +} + // SetMaxDynamicTableSizeLimit changes the maximum value that can be // specified in SetMaxDynamicTableSize to v. By default, it is set to // 4096, which is the same size of the default dynamic header table diff --git a/http2/server.go b/http2/server.go index d8a17aa9b..e35a76c07 100644 --- a/http2/server.go +++ b/http2/server.go @@ -98,6 +98,19 @@ type Server struct { // the HTTP/2 spec's recommendations. MaxConcurrentStreams uint32 + // MaxDecoderHeaderTableSize optionally specifies the http2 + // SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It + // informs the remote endpoint of the maximum size of the header compression + // table used to decode header blocks, in octets. If zero, the default value + // of 4096 is used. + MaxDecoderHeaderTableSize uint32 + + // MaxEncoderHeaderTableSize optionally specifies an upper limit for the + // header compression table used for encoding request headers. Received + // SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero, + // the default value of 4096 is used. + MaxEncoderHeaderTableSize uint32 + // MaxReadFrameSize optionally specifies the largest frame // this server is willing to read. A valid value is between // 16k and 16M, inclusive. If zero or otherwise invalid, a @@ -170,6 +183,20 @@ func (s *Server) maxConcurrentStreams() uint32 { return defaultMaxStreams } +func (s *Server) maxDecoderHeaderTableSize() uint32 { + if v := s.MaxDecoderHeaderTableSize; v > 0 { + return v + } + return initialHeaderTableSize +} + +func (s *Server) maxEncoderHeaderTableSize() uint32 { + if v := s.MaxEncoderHeaderTableSize; v > 0 { + return v + } + return initialHeaderTableSize +} + // maxQueuedControlFrames is the maximum number of control frames like // SETTINGS, PING and RST_STREAM that will be queued for writing before // the connection is closed to prevent memory exhaustion attacks. @@ -394,7 +421,6 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) { advMaxStreams: s.maxConcurrentStreams(), initialStreamSendWindowSize: initialWindowSize, maxFrameSize: initialMaxFrameSize, - headerTableSize: initialHeaderTableSize, serveG: newGoroutineLock(), pushEnabled: true, sawClientPreface: opts.SawClientPreface, @@ -424,12 +450,13 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) { sc.flow.add(initialWindowSize) sc.inflow.add(initialWindowSize) sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf) + sc.hpackEncoder.SetMaxDynamicTableSizeLimit(s.maxEncoderHeaderTableSize()) fr := NewFramer(sc.bw, c) if s.CountError != nil { fr.countError = s.CountError } - fr.ReadMetaHeaders = hpack.NewDecoder(initialHeaderTableSize, nil) + fr.ReadMetaHeaders = hpack.NewDecoder(s.maxDecoderHeaderTableSize(), nil) fr.MaxHeaderListSize = sc.maxHeaderListSize() fr.SetMaxReadFrameSize(s.maxReadFrameSize()) sc.framer = fr @@ -559,7 +586,6 @@ type serverConn struct { streams map[uint32]*stream initialStreamSendWindowSize int32 maxFrameSize int32 - headerTableSize uint32 peerMaxHeaderListSize uint32 // zero means unknown (default) canonHeader map[string]string // http2-lower-case -> Go-Canonical-Case writingFrame bool // started writing a frame (on serve goroutine or separate) @@ -864,6 +890,7 @@ func (sc *serverConn) serve() { {SettingMaxFrameSize, sc.srv.maxReadFrameSize()}, {SettingMaxConcurrentStreams, sc.advMaxStreams}, {SettingMaxHeaderListSize, sc.maxHeaderListSize()}, + {SettingHeaderTableSize, sc.srv.maxDecoderHeaderTableSize()}, {SettingInitialWindowSize, uint32(sc.srv.initialStreamRecvWindowSize())}, }, }) @@ -1661,7 +1688,6 @@ func (sc *serverConn) processSetting(s Setting) error { } switch s.ID { case SettingHeaderTableSize: - sc.headerTableSize = s.Val sc.hpackEncoder.SetMaxDynamicTableSize(s.Val) case SettingEnablePush: sc.pushEnabled = s.Val != 0 diff --git a/http2/server_test.go b/http2/server_test.go index 757bd2949..376087106 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -2736,6 +2736,43 @@ func TestServerWithH2Load(t *testing.T) { } } +func TestServer_MaxDecoderHeaderTableSize(t *testing.T) { + wantHeaderTableSize := uint32(initialHeaderTableSize * 2) + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {}, func(s *Server) { + s.MaxDecoderHeaderTableSize = wantHeaderTableSize + }) + defer st.Close() + + var advHeaderTableSize *uint32 + st.greetAndCheckSettings(func(s Setting) error { + switch s.ID { + case SettingHeaderTableSize: + advHeaderTableSize = &s.Val + } + return nil + }) + + if advHeaderTableSize == nil { + t.Errorf("server didn't advertise a header table size") + } else if got, want := *advHeaderTableSize, wantHeaderTableSize; got != want { + t.Errorf("server advertised a header table size of %d, want %d", got, want) + } +} + +func TestServer_MaxEncoderHeaderTableSize(t *testing.T) { + wantHeaderTableSize := uint32(initialHeaderTableSize / 2) + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {}, func(s *Server) { + s.MaxEncoderHeaderTableSize = wantHeaderTableSize + }) + defer st.Close() + + st.greet() + + if got, want := st.sc.hpackEncoder.MaxDynamicTableSize(), wantHeaderTableSize; got != want { + t.Errorf("server encoder is using a header table size of %d, want %d", got, want) + } +} + // Issue 12843 func TestServerDoS_MaxHeaderListSize(t *testing.T) { st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {}) diff --git a/http2/transport.go b/http2/transport.go index 46dda4dc3..91f4370cc 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -118,6 +118,19 @@ type Transport struct { // to mean no limit. MaxHeaderListSize uint32 + // MaxDecoderHeaderTableSize optionally specifies the http2 + // SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It + // informs the remote endpoint of the maximum size of the header compression + // table used to decode header blocks, in octets. If zero, the default value + // of 4096 is used. + MaxDecoderHeaderTableSize uint32 + + // MaxEncoderHeaderTableSize optionally specifies an upper limit for the + // header compression table used for encoding request headers. Received + // SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero, + // the default value of 4096 is used. + MaxEncoderHeaderTableSize uint32 + // StrictMaxConcurrentStreams controls whether the server's // SETTINGS_MAX_CONCURRENT_STREAMS should be respected // globally. If false, new TCP connections are created to the @@ -293,10 +306,11 @@ type ClientConn struct { lastActive time.Time lastIdle time.Time // time last idle // Settings from peer: (also guarded by wmu) - maxFrameSize uint32 - maxConcurrentStreams uint32 - peerMaxHeaderListSize uint64 - initialWindowSize uint32 + maxFrameSize uint32 + maxConcurrentStreams uint32 + peerMaxHeaderListSize uint64 + peerMaxHeaderTableSize uint32 + initialWindowSize uint32 // reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests. // Write to reqHeaderMu to lock it, read from it to unlock. @@ -681,6 +695,20 @@ func (t *Transport) expectContinueTimeout() time.Duration { return t.t1.ExpectContinueTimeout } +func (t *Transport) maxDecoderHeaderTableSize() uint32 { + if v := t.MaxDecoderHeaderTableSize; v > 0 { + return v + } + return initialHeaderTableSize +} + +func (t *Transport) maxEncoderHeaderTableSize() uint32 { + if v := t.MaxEncoderHeaderTableSize; v > 0 { + return v + } + return initialHeaderTableSize +} + func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) { return t.newClientConn(c, t.disableKeepAlives()) } @@ -724,12 +752,13 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro if t.CountError != nil { cc.fr.countError = t.CountError } - cc.fr.ReadMetaHeaders = hpack.NewDecoder(initialHeaderTableSize, nil) + maxHeaderTableSize := t.maxDecoderHeaderTableSize() + cc.fr.ReadMetaHeaders = hpack.NewDecoder(maxHeaderTableSize, nil) cc.fr.MaxHeaderListSize = t.maxHeaderListSize() - // TODO: SetMaxDynamicTableSize, SetMaxDynamicTableSizeLimit on - // henc in response to SETTINGS frames? cc.henc = hpack.NewEncoder(&cc.hbuf) + cc.henc.SetMaxDynamicTableSizeLimit(t.maxEncoderHeaderTableSize()) + cc.peerMaxHeaderTableSize = initialHeaderTableSize if t.AllowHTTP { cc.nextStreamID = 3 @@ -747,6 +776,9 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro if max := t.maxHeaderListSize(); max != 0 { initialSettings = append(initialSettings, Setting{ID: SettingMaxHeaderListSize, Val: max}) } + if maxHeaderTableSize != initialHeaderTableSize { + initialSettings = append(initialSettings, Setting{ID: SettingHeaderTableSize, Val: maxHeaderTableSize}) + } cc.bw.Write(clientPreface) cc.fr.WriteSettings(initialSettings...) @@ -2773,8 +2805,10 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { cc.cond.Broadcast() cc.initialWindowSize = s.Val + case SettingHeaderTableSize: + cc.henc.SetMaxDynamicTableSize(s.Val) + cc.peerMaxHeaderTableSize = s.Val default: - // TODO(bradfitz): handle more settings? SETTINGS_HEADER_TABLE_SIZE probably. cc.vlogf("Unhandled Setting: %v", s) } return nil diff --git a/http2/transport_test.go b/http2/transport_test.go index 9eaf7bfb3..ee852b619 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -4223,6 +4223,150 @@ func TestTransportRequestsStallAtServerLimit(t *testing.T) { ct.run() } +func TestTransportMaxDecoderHeaderTableSize(t *testing.T) { + ct := newClientTester(t) + var reqSize, resSize uint32 = 8192, 16384 + ct.tr.MaxDecoderHeaderTableSize = reqSize + ct.client = func() error { + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + cc, err := ct.tr.NewClientConn(ct.cc) + if err != nil { + return err + } + _, err = cc.RoundTrip(req) + if err != nil { + return err + } + if got, want := cc.peerMaxHeaderTableSize, resSize; got != want { + return fmt.Errorf("peerHeaderTableSize = %d, want %d", got, want) + } + return nil + } + ct.server = func() error { + buf := make([]byte, len(ClientPreface)) + _, err := io.ReadFull(ct.sc, buf) + if err != nil { + return fmt.Errorf("reading client preface: %v", err) + } + f, err := ct.fr.ReadFrame() + if err != nil { + return err + } + sf, ok := f.(*SettingsFrame) + if !ok { + ct.t.Fatalf("wanted client settings frame; got %v", f) + _ = sf // stash it away? + } + var found bool + err = sf.ForeachSetting(func(s Setting) error { + if s.ID == SettingHeaderTableSize { + found = true + if got, want := s.Val, reqSize; got != want { + return fmt.Errorf("received SETTINGS_HEADER_TABLE_SIZE = %d, want %d", got, want) + } + } + return nil + }) + if err != nil { + return err + } + if !found { + return fmt.Errorf("missing SETTINGS_HEADER_TABLE_SIZE setting") + } + if err := ct.fr.WriteSettings(Setting{SettingHeaderTableSize, resSize}); err != nil { + ct.t.Fatal(err) + } + if err := ct.fr.WriteSettingsAck(); err != nil { + ct.t.Fatal(err) + } + + for { + f, err := ct.fr.ReadFrame() + if err != nil { + return err + } + switch f := f.(type) { + case *HeadersFrame: + var buf bytes.Buffer + enc := hpack.NewEncoder(&buf) + enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) + ct.fr.WriteHeaders(HeadersFrameParam{ + StreamID: f.StreamID, + EndHeaders: true, + EndStream: true, + BlockFragment: buf.Bytes(), + }) + return nil + } + } + } + ct.run() +} + +func TestTransportMaxEncoderHeaderTableSize(t *testing.T) { + ct := newClientTester(t) + var peerAdvertisedMaxHeaderTableSize uint32 = 16384 + ct.tr.MaxEncoderHeaderTableSize = 8192 + ct.client = func() error { + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + cc, err := ct.tr.NewClientConn(ct.cc) + if err != nil { + return err + } + _, err = cc.RoundTrip(req) + if err != nil { + return err + } + if got, want := cc.henc.MaxDynamicTableSize(), ct.tr.MaxEncoderHeaderTableSize; got != want { + return fmt.Errorf("henc.MaxDynamicTableSize() = %d, want %d", got, want) + } + return nil + } + ct.server = func() error { + buf := make([]byte, len(ClientPreface)) + _, err := io.ReadFull(ct.sc, buf) + if err != nil { + return fmt.Errorf("reading client preface: %v", err) + } + f, err := ct.fr.ReadFrame() + if err != nil { + return err + } + sf, ok := f.(*SettingsFrame) + if !ok { + ct.t.Fatalf("wanted client settings frame; got %v", f) + _ = sf // stash it away? + } + if err := ct.fr.WriteSettings(Setting{SettingHeaderTableSize, peerAdvertisedMaxHeaderTableSize}); err != nil { + ct.t.Fatal(err) + } + if err := ct.fr.WriteSettingsAck(); err != nil { + ct.t.Fatal(err) + } + + for { + f, err := ct.fr.ReadFrame() + if err != nil { + return err + } + switch f := f.(type) { + case *HeadersFrame: + var buf bytes.Buffer + enc := hpack.NewEncoder(&buf) + enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) + ct.fr.WriteHeaders(HeadersFrameParam{ + StreamID: f.StreamID, + EndHeaders: true, + EndStream: true, + BlockFragment: buf.Bytes(), + }) + return nil + } + } + } + ct.run() +} + func TestAuthorityAddr(t *testing.T) { tests := []struct { scheme, authority string