|
|
@@ -1371,6 +1371,269 @@ func testInvalidTrailer(t *testing.T, trailers headerType, wantErr error, writeT
|
|
|
ct.run()
|
|
|
}
|
|
|
|
|
|
+// headerListSize returns the HTTP2 header list size of h.
|
|
|
+// http://httpwg.org/specs/rfc7540.html#SETTINGS_MAX_HEADER_LIST_SIZE
|
|
|
+// http://httpwg.org/specs/rfc7540.html#MaxHeaderBlock
|
|
|
+func headerListSize(h http.Header) (size uint32) {
|
|
|
+ for k, vv := range h {
|
|
|
+ for _, v := range vv {
|
|
|
+ hf := hpack.HeaderField{Name: k, Value: v}
|
|
|
+ size += hf.Size()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return size
|
|
|
+}
|
|
|
+
|
|
|
+// padHeaders adds data to an http.Header until headerListSize(h) ==
|
|
|
+// limit. Due to the way header list sizes are calculated, padHeaders
|
|
|
+// cannot add fewer than len("Pad-Headers") + 32 bytes to h, and will
|
|
|
+// call t.Fatal if asked to do so. PadHeaders first reserves enough
|
|
|
+// space for an empty "Pad-Headers" key, then adds as many copies of
|
|
|
+// filler as possible. Any remaining bytes necessary to push the
|
|
|
+// header list size up to limit are added to h["Pad-Headers"].
|
|
|
+func padHeaders(t *testing.T, h http.Header, limit uint64, filler string) {
|
|
|
+ if limit > 0xffffffff {
|
|
|
+ t.Fatalf("padHeaders: refusing to pad to more than 2^32-1 bytes. limit = %v", limit)
|
|
|
+ }
|
|
|
+ hf := hpack.HeaderField{Name: "Pad-Headers", Value: ""}
|
|
|
+ minPadding := uint64(hf.Size())
|
|
|
+ size := uint64(headerListSize(h))
|
|
|
+
|
|
|
+ minlimit := size + minPadding
|
|
|
+ if limit < minlimit {
|
|
|
+ t.Fatalf("padHeaders: limit %v < %v", limit, minlimit)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use a fixed-width format for name so that fieldSize
|
|
|
+ // remains constant.
|
|
|
+ nameFmt := "Pad-Headers-%06d"
|
|
|
+ hf = hpack.HeaderField{Name: fmt.Sprintf(nameFmt, 1), Value: filler}
|
|
|
+ fieldSize := uint64(hf.Size())
|
|
|
+
|
|
|
+ // Add as many complete filler values as possible, leaving
|
|
|
+ // room for at least one empty "Pad-Headers" key.
|
|
|
+ limit = limit - minPadding
|
|
|
+ for i := 0; size+fieldSize < limit; i++ {
|
|
|
+ name := fmt.Sprintf(nameFmt, i)
|
|
|
+ h.Add(name, filler)
|
|
|
+ size += fieldSize
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add enough bytes to reach limit.
|
|
|
+ remain := limit - size
|
|
|
+ lastValue := strings.Repeat("*", int(remain))
|
|
|
+ h.Add("Pad-Headers", lastValue)
|
|
|
+}
|
|
|
+
|
|
|
+func TestPadHeaders(t *testing.T) {
|
|
|
+ check := func(h http.Header, limit uint32, fillerLen int) {
|
|
|
+ if h == nil {
|
|
|
+ h = make(http.Header)
|
|
|
+ }
|
|
|
+ filler := strings.Repeat("f", fillerLen)
|
|
|
+ padHeaders(t, h, uint64(limit), filler)
|
|
|
+ gotSize := headerListSize(h)
|
|
|
+ if gotSize != limit {
|
|
|
+ t.Errorf("Got size = %v; want %v", gotSize, limit)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Try all possible combinations for small fillerLen and limit.
|
|
|
+ hf := hpack.HeaderField{Name: "Pad-Headers", Value: ""}
|
|
|
+ minLimit := hf.Size()
|
|
|
+ for limit := minLimit; limit <= 128; limit++ {
|
|
|
+ for fillerLen := 0; uint32(fillerLen) <= limit; fillerLen++ {
|
|
|
+ check(nil, limit, fillerLen)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try a few tests with larger limits, plus cumulative
|
|
|
+ // tests. Since these tests are cumulative, tests[i+1].limit
|
|
|
+ // must be >= tests[i].limit + minLimit. See the comment on
|
|
|
+ // padHeaders for more info on why the limit arg has this
|
|
|
+ // restriction.
|
|
|
+ tests := []struct {
|
|
|
+ fillerLen int
|
|
|
+ limit uint32
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ fillerLen: 64,
|
|
|
+ limit: 1024,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ fillerLen: 1024,
|
|
|
+ limit: 1286,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ fillerLen: 256,
|
|
|
+ limit: 2048,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ fillerLen: 1024,
|
|
|
+ limit: 10 * 1024,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ fillerLen: 1023,
|
|
|
+ limit: 11 * 1024,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ h := make(http.Header)
|
|
|
+ for _, tc := range tests {
|
|
|
+ check(nil, tc.limit, tc.fillerLen)
|
|
|
+ check(h, tc.limit, tc.fillerLen)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestTransportChecksRequestHeaderListSize(t *testing.T) {
|
|
|
+ st := newServerTester(t,
|
|
|
+ func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ // Consume body & force client to send
|
|
|
+ // trailers before writing response.
|
|
|
+ // ioutil.ReadAll returns non-nil err for
|
|
|
+ // requests that attempt to send greater than
|
|
|
+ // maxHeaderListSize bytes of trailers, since
|
|
|
+ // those requests generate a stream reset.
|
|
|
+ ioutil.ReadAll(r.Body)
|
|
|
+ r.Body.Close()
|
|
|
+ },
|
|
|
+ func(ts *httptest.Server) {
|
|
|
+ ts.Config.MaxHeaderBytes = 16 << 10
|
|
|
+ },
|
|
|
+ optOnlyServer,
|
|
|
+ optQuiet,
|
|
|
+ )
|
|
|
+ defer st.Close()
|
|
|
+
|
|
|
+ tr := &Transport{TLSClientConfig: tlsConfigInsecure}
|
|
|
+ defer tr.CloseIdleConnections()
|
|
|
+
|
|
|
+ checkRoundTrip := func(req *http.Request, wantErr error, desc string) {
|
|
|
+ res, err := tr.RoundTrip(req)
|
|
|
+ if err != wantErr {
|
|
|
+ if res != nil {
|
|
|
+ res.Body.Close()
|
|
|
+ }
|
|
|
+ t.Errorf("%v: RoundTrip err = %v; want %v", desc, err, wantErr)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err == nil {
|
|
|
+ if res == nil {
|
|
|
+ t.Errorf("%v: response nil; want non-nil.", desc)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer res.Body.Close()
|
|
|
+ if res.StatusCode != http.StatusOK {
|
|
|
+ t.Errorf("%v: response status = %v; want %v", desc, res.StatusCode, http.StatusOK)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if res != nil {
|
|
|
+ t.Errorf("%v: RoundTrip err = %v but response non-nil", desc, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ headerListSizeForRequest := func(req *http.Request) (size uint64) {
|
|
|
+ contentLen := actualContentLength(req)
|
|
|
+ trailers, err := commaSeparatedTrailers(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("headerListSizeForRequest: %v", err)
|
|
|
+ }
|
|
|
+ cc := &ClientConn{peerMaxHeaderListSize: 0xffffffffffffffff}
|
|
|
+ cc.henc = hpack.NewEncoder(&cc.hbuf)
|
|
|
+ cc.mu.Lock()
|
|
|
+ hdrs, err := cc.encodeHeaders(req, true, trailers, contentLen)
|
|
|
+ cc.mu.Unlock()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("headerListSizeForRequest: %v", err)
|
|
|
+ }
|
|
|
+ hpackDec := hpack.NewDecoder(initialHeaderTableSize, func(hf hpack.HeaderField) {
|
|
|
+ size += uint64(hf.Size())
|
|
|
+ })
|
|
|
+ if len(hdrs) > 0 {
|
|
|
+ if _, err := hpackDec.Write(hdrs); err != nil {
|
|
|
+ t.Fatalf("headerListSizeForRequest: %v", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return size
|
|
|
+ }
|
|
|
+ // Create a new Request for each test, rather than reusing the
|
|
|
+ // same Request, to avoid a race when modifying req.Headers.
|
|
|
+ // See https://github.com/golang/go/issues/21316
|
|
|
+ newRequest := func() *http.Request {
|
|
|
+ // Body must be non-nil to enable writing trailers.
|
|
|
+ body := strings.NewReader("hello")
|
|
|
+ req, err := http.NewRequest("POST", st.ts.URL, body)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("newRequest: NewRequest: %v", err)
|
|
|
+ }
|
|
|
+ return req
|
|
|
+ }
|
|
|
+
|
|
|
+ // Make an arbitrary request to ensure we get the server's
|
|
|
+ // settings frame and initialize peerMaxHeaderListSize.
|
|
|
+ req := newRequest()
|
|
|
+ checkRoundTrip(req, nil, "Initial request")
|
|
|
+
|
|
|
+ // Get the ClientConn associated with the request and validate
|
|
|
+ // peerMaxHeaderListSize.
|
|
|
+ addr := authorityAddr(req.URL.Scheme, req.URL.Host)
|
|
|
+ cc, err := tr.connPool().GetClientConn(req, addr)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("GetClientConn: %v", err)
|
|
|
+ }
|
|
|
+ cc.mu.Lock()
|
|
|
+ peerSize := cc.peerMaxHeaderListSize
|
|
|
+ cc.mu.Unlock()
|
|
|
+ st.scMu.Lock()
|
|
|
+ wantSize := uint64(st.sc.maxHeaderListSize())
|
|
|
+ st.scMu.Unlock()
|
|
|
+ if peerSize != wantSize {
|
|
|
+ t.Errorf("peerMaxHeaderListSize = %v; want %v", peerSize, wantSize)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sanity check peerSize. (*serverConn) maxHeaderListSize adds
|
|
|
+ // 320 bytes of padding.
|
|
|
+ wantHeaderBytes := uint64(st.ts.Config.MaxHeaderBytes) + 320
|
|
|
+ if peerSize != wantHeaderBytes {
|
|
|
+ t.Errorf("peerMaxHeaderListSize = %v; want %v.", peerSize, wantHeaderBytes)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Pad headers & trailers, but stay under peerSize.
|
|
|
+ req = newRequest()
|
|
|
+ req.Header = make(http.Header)
|
|
|
+ req.Trailer = make(http.Header)
|
|
|
+ filler := strings.Repeat("*", 1024)
|
|
|
+ padHeaders(t, req.Trailer, peerSize, filler)
|
|
|
+ // cc.encodeHeaders adds some default headers to the request,
|
|
|
+ // so we need to leave room for those.
|
|
|
+ defaultBytes := headerListSizeForRequest(req)
|
|
|
+ padHeaders(t, req.Header, peerSize-defaultBytes, filler)
|
|
|
+ checkRoundTrip(req, nil, "Headers & Trailers under limit")
|
|
|
+
|
|
|
+ // Add enough header bytes to push us over peerSize.
|
|
|
+ req = newRequest()
|
|
|
+ req.Header = make(http.Header)
|
|
|
+ padHeaders(t, req.Header, peerSize, filler)
|
|
|
+ checkRoundTrip(req, errRequestHeaderListSize, "Headers over limit")
|
|
|
+
|
|
|
+ // Push trailers over the limit.
|
|
|
+ req = newRequest()
|
|
|
+ req.Trailer = make(http.Header)
|
|
|
+ padHeaders(t, req.Trailer, peerSize+1, filler)
|
|
|
+ checkRoundTrip(req, errRequestHeaderListSize, "Trailers over limit")
|
|
|
+
|
|
|
+ // Send headers with a single large value.
|
|
|
+ req = newRequest()
|
|
|
+ filler = strings.Repeat("*", int(peerSize))
|
|
|
+ req.Header = make(http.Header)
|
|
|
+ req.Header.Set("Big", filler)
|
|
|
+ checkRoundTrip(req, errRequestHeaderListSize, "Single large header")
|
|
|
+
|
|
|
+ // Send trailers with a single large value.
|
|
|
+ req = newRequest()
|
|
|
+ req.Trailer = make(http.Header)
|
|
|
+ req.Trailer.Set("Big", filler)
|
|
|
+ checkRoundTrip(req, errRequestHeaderListSize, "Single large trailer")
|
|
|
+}
|
|
|
+
|
|
|
func TestTransportChecksResponseHeaderListSize(t *testing.T) {
|
|
|
ct := newClientTester(t)
|
|
|
ct.client = func() error {
|
|
|
@@ -2663,7 +2926,7 @@ func TestTransportRequestPathPseudo(t *testing.T) {
|
|
|
},
|
|
|
}
|
|
|
for i, tt := range tests {
|
|
|
- cc := &ClientConn{}
|
|
|
+ cc := &ClientConn{peerMaxHeaderListSize: 0xffffffffffffffff}
|
|
|
cc.henc = hpack.NewEncoder(&cc.hbuf)
|
|
|
cc.mu.Lock()
|
|
|
hdrs, err := cc.encodeHeaders(tt.req, false, "", -1)
|
|
|
@@ -3373,3 +3636,51 @@ func TestTransportNoBodyMeansNoDATA(t *testing.T) {
|
|
|
}
|
|
|
ct.run()
|
|
|
}
|
|
|
+
|
|
|
+func benchSimpleRoundTrip(b *testing.B, nHeaders int) {
|
|
|
+ defer disableGoroutineTracking()()
|
|
|
+ b.ReportAllocs()
|
|
|
+ st := newServerTester(b,
|
|
|
+ func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ },
|
|
|
+ optOnlyServer,
|
|
|
+ optQuiet,
|
|
|
+ )
|
|
|
+ defer st.Close()
|
|
|
+
|
|
|
+ tr := &Transport{TLSClientConfig: tlsConfigInsecure}
|
|
|
+ defer tr.CloseIdleConnections()
|
|
|
+
|
|
|
+ req, err := http.NewRequest("GET", st.ts.URL, nil)
|
|
|
+ if err != nil {
|
|
|
+ b.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ for i := 0; i < nHeaders; i++ {
|
|
|
+ name := fmt.Sprint("A-", i)
|
|
|
+ req.Header.Set(name, "*")
|
|
|
+ }
|
|
|
+
|
|
|
+ b.ResetTimer()
|
|
|
+
|
|
|
+ for i := 0; i < b.N; i++ {
|
|
|
+ res, err := tr.RoundTrip(req)
|
|
|
+ if err != nil {
|
|
|
+ if res != nil {
|
|
|
+ res.Body.Close()
|
|
|
+ }
|
|
|
+ b.Fatalf("RoundTrip err = %v; want nil", err)
|
|
|
+ }
|
|
|
+ res.Body.Close()
|
|
|
+ if res.StatusCode != http.StatusOK {
|
|
|
+ b.Fatalf("Response code = %v; want %v", res.StatusCode, http.StatusOK)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func BenchmarkClientRequestHeaders(b *testing.B) {
|
|
|
+ b.Run(" 0 Headers", func(b *testing.B) { benchSimpleRoundTrip(b, 0) })
|
|
|
+ b.Run(" 10 Headers", func(b *testing.B) { benchSimpleRoundTrip(b, 10) })
|
|
|
+ b.Run(" 100 Headers", func(b *testing.B) { benchSimpleRoundTrip(b, 100) })
|
|
|
+ b.Run("1000 Headers", func(b *testing.B) { benchSimpleRoundTrip(b, 1000) })
|
|
|
+}
|