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