Skip to content

Commit

Permalink
http2: add SETTINGS_HEADER_TABLE_SIZE support
Browse files Browse the repository at this point in the history
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 <[email protected]>
Reviewed-by: Joedian Reid <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Run-TryBot: Damien Neil <[email protected]>
  • Loading branch information
elindsey authored and neild committed Nov 15, 2022
1 parent a2d827a commit 0e478a2
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 12 deletions.
5 changes: 5 additions & 0 deletions http2/hpack/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 30 additions & 4 deletions http2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())},
},
})
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions http2/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {})
Expand Down
50 changes: 42 additions & 8 deletions http2/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
Expand All @@ -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...)
Expand Down Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions http2/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0e478a2

Please sign in to comment.