From 80f387214c0424db4d31d6aeee6bb8e4f72565bd Mon Sep 17 00:00:00 2001 From: Areg Harutyunyan Date: Tue, 10 Mar 2020 01:35:11 +0000 Subject: [PATCH] TUN-2796: Implement HTTP2 CONTINUATION headers correctly --- h2mux/muxwriter.go | 58 +++++++++++++++++++++++++++++------------ h2mux/muxwriter_test.go | 26 ++++++++++++++++++ 2 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 h2mux/muxwriter_test.go diff --git a/h2mux/muxwriter.go b/h2mux/muxwriter.go index 80888d45..dfe52d7f 100644 --- a/h2mux/muxwriter.go +++ b/h2mux/muxwriter.go @@ -250,28 +250,52 @@ func (w *MuxWriter) encodeHeaders(headers []Header) ([]byte, error) { // writeHeaders writes a block of encoded headers, splitting it into multiple frames if necessary. func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error { encodedHeaders, err := w.encodeHeaders(headers) - if err != nil { + if err != nil || len(encodedHeaders) == 0 { return err } + blockSize := int(w.maxFrameSize) - endHeaders := len(encodedHeaders) == 0 - for !endHeaders && err == nil { - blockFragment := encodedHeaders - if len(encodedHeaders) > blockSize { - blockFragment = blockFragment[:blockSize] - encodedHeaders = encodedHeaders[blockSize:] - // Send CONTINUATION frame if the headers can't be fit into 1 frame - err = w.f.WriteContinuation(streamID, endHeaders, blockFragment) - } else { - endHeaders = true - err = w.f.WriteHeaders(http2.HeadersFrameParam{ - StreamID: streamID, - EndHeaders: endHeaders, - BlockFragment: blockFragment, - }) + // CONTINUATION is unnecessary; the headers fit within the blockSize + if len(encodedHeaders) < blockSize { + return w.f.WriteHeaders(http2.HeadersFrameParam{ + StreamID: streamID, + EndHeaders: true, + BlockFragment: encodedHeaders, + }) + } + + choppedHeaders := chopEncodedHeaders(encodedHeaders, blockSize) + // len(choppedHeaders) is at least 2 + if err := w.f.WriteHeaders(http2.HeadersFrameParam{StreamID: streamID, EndHeaders: false, BlockFragment: choppedHeaders[0]}); err != nil { + return err + } + for i := 1; i < len(choppedHeaders)-1; i++ { + if err := w.f.WriteContinuation(streamID, false, choppedHeaders[i]); err != nil { + return err } } - return err + if err := w.f.WriteContinuation(streamID, true, choppedHeaders[len(choppedHeaders)-1]); err != nil { + return err + } + + return nil +} + +// Partition a slice of bytes into `len(slice) / blockSize` slices of length `blockSize` +func chopEncodedHeaders(headers []byte, chunkSize int) [][]byte { + var divided [][]byte + + for i := 0; i < len(headers); i += chunkSize { + end := i + chunkSize + + if end > len(headers) { + end = len(headers) + } + + divided = append(divided, headers[i:end]) + } + + return divided } func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error { diff --git a/h2mux/muxwriter_test.go b/h2mux/muxwriter_test.go new file mode 100644 index 00000000..07e23bdc --- /dev/null +++ b/h2mux/muxwriter_test.go @@ -0,0 +1,26 @@ +package h2mux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChopEncodedHeaders(t *testing.T) { + mockEncodedHeaders := make([]byte, 5) + for i := range mockEncodedHeaders { + mockEncodedHeaders[i] = byte(i) + } + chopped := chopEncodedHeaders(mockEncodedHeaders, 4) + + assert.Equal(t, 2, len(chopped)) + assert.Equal(t, []byte{0, 1, 2, 3}, chopped[0]) + assert.Equal(t, []byte{4}, chopped[1]) +} + +func TestChopEncodedEmptyHeaders(t *testing.T) { + mockEncodedHeaders := make([]byte, 0) + chopped := chopEncodedHeaders(mockEncodedHeaders, 3) + + assert.Equal(t, 0, len(chopped)) +}