package http3 import ( "bytes" "context" "io" "math" "net/http" "net/http/httptest" "net/http/httptrace" "strings" "testing" "time" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3/qlog" "github.com/quic-go/quic-go/qlogwriter" "github.com/quic-go/quic-go/testutils/events" "github.com/quic-go/qpack" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) func getDataFrame(data []byte) []byte { b := (&dataFrame{Length: uint64(len(data))}).Append(nil) return append(b, data...) } func TestStreamReadDataFrames(t *testing.T) { var buf bytes.Buffer mockCtrl := gomock.NewController(t) qstr := NewMockDatagramStream(mockCtrl) qstr.EXPECT().StreamID().Return(quic.StreamID(42)).AnyTimes() qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes() qstr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes() var eventRecorder events.Recorder clientConn, _ := newConnPairWithRecorder(t, &eventRecorder, nil) str := newStream( qstr, newConnection( clientConn.Context(), clientConn, false, false, // client nil, 0, ), nil, func(io.Reader, *headersFrame) error { return nil }, &eventRecorder, ) buf.Write(getDataFrame([]byte("foobar"))) b := make([]byte, 3) n, err := str.Read(b) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte("foo"), b) n, err = str.Read(b) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte("bar"), b) expectedLen, _ := expectedFrameLength(t, &dataFrame{Length: 6}) require.Equal(t, []qlogwriter.Event{ qlog.FrameParsed{ StreamID: 42, Raw: qlog.RawInfo{Length: expectedLen, PayloadLength: 6}, Frame: qlog.Frame{Frame: qlog.DataFrame{}}, }, }, eventRecorder.Events(qlog.FrameParsed{}), ) eventRecorder.Clear() buf.Write(getDataFrame([]byte("baz"))) b = make([]byte, 10) n, err = str.Read(b) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte("baz"), b[:n]) require.Len(t, eventRecorder.Events(qlog.FrameParsed{}), 1) eventRecorder.Clear() buf.Write(getDataFrame([]byte("lorem"))) buf.Write(getDataFrame([]byte("ipsum"))) data, err := io.ReadAll(str) require.NoError(t, err) require.Equal(t, "loremipsum", string(data)) require.Len(t, eventRecorder.Events(qlog.FrameParsed{}), 2) eventRecorder.Clear() // invalid frame buf.Write([]byte("invalid")) _, err = str.Read([]byte{0}) require.Error(t, err) require.Empty(t, eventRecorder.Events(qlog.FrameParsed{})) } func TestStreamInvalidFrame(t *testing.T) { var buf bytes.Buffer b := (&settingsFrame{}).Append(nil) buf.Write(b) mockCtrl := gomock.NewController(t) qstr := NewMockDatagramStream(mockCtrl) qstr.EXPECT().StreamID().Return(quic.StreamID(42)).AnyTimes() qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes() qstr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes() clientConn, serverConn := newConnPair(t) str := newStream( qstr, newConnection(context.Background(), clientConn, false, false, nil, 0), nil, func(io.Reader, *headersFrame) error { return nil }, nil, ) _, err := str.Read([]byte{0}) require.ErrorContains(t, err, "peer sent an unexpected frame") select { case <-serverConn.Context().Done(): var appErr *quic.ApplicationError require.ErrorAs(t, context.Cause(serverConn.Context()), &appErr) require.Equal(t, quic.ApplicationErrorCode(ErrCodeFrameUnexpected), appErr.ErrorCode) case <-time.After(time.Second): t.Fatal("timeout") } } func TestStreamWrite(t *testing.T) { var buf bytes.Buffer mockCtrl := gomock.NewController(t) qstr := NewMockDatagramStream(mockCtrl) qstr.EXPECT().StreamID().Return(quic.StreamID(42)).AnyTimes() qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes() var eventRecorder events.Recorder str := newStream(qstr, nil, nil, func(io.Reader, *headersFrame) error { return nil }, &eventRecorder) str.Write([]byte("foo")) str.Write([]byte("foobar")) fp := frameParser{r: &buf} f, err := fp.ParseNext(nil) require.NoError(t, err) f1Len, f1PayloadLen := expectedFrameLength(t, &dataFrame{Length: 3}) require.Equal(t, &dataFrame{Length: 3}, f) b := make([]byte, 3) _, err = io.ReadFull(&buf, b) require.NoError(t, err) require.Equal(t, []byte("foo"), b) fp = frameParser{r: &buf} f, err = fp.ParseNext(nil) require.NoError(t, err) f2Len, f2PayloadLen := expectedFrameLength(t, &dataFrame{Length: 6}) require.Equal(t, &dataFrame{Length: 6}, f) b = make([]byte, 6) _, err = io.ReadFull(&buf, b) require.NoError(t, err) require.Equal(t, []byte("foobar"), b) require.Equal(t, []qlogwriter.Event{ qlog.FrameCreated{ StreamID: 42, Raw: qlog.RawInfo{Length: f1Len, PayloadLength: f1PayloadLen}, Frame: qlog.Frame{Frame: qlog.DataFrame{}}, }, qlog.FrameCreated{ StreamID: 42, Raw: qlog.RawInfo{Length: f2Len, PayloadLength: f2PayloadLen}, Frame: qlog.Frame{Frame: qlog.DataFrame{}}, }, }, eventRecorder.Events(qlog.FrameCreated{}), ) } func TestRequestStream(t *testing.T) { mockCtrl := gomock.NewController(t) qstr := NewMockDatagramStream(mockCtrl) qstr.EXPECT().StreamID().Return(quic.StreamID(42)).AnyTimes() requestWriter := newRequestWriter() clientConn, _ := newConnPair(t) str := newRequestStream( newStream( qstr, newConnection(context.Background(), clientConn, false, false, nil, 0), &httptrace.ClientTrace{}, func(io.Reader, *headersFrame) error { return nil }, nil, ), requestWriter, make(chan struct{}), qpack.NewDecoder(func(qpack.HeaderField) {}), true, math.MaxUint64, &http.Response{}, ) _, err := str.Read([]byte{0}) require.EqualError(t, err, "http3: invalid use of RequestStream.Read before ReadResponse") _, err = str.Write([]byte{0}) require.EqualError(t, err, "http3: invalid use of RequestStream.Write before SendRequestHeader") // calling ReadResponse before SendRequestHeader is not valid _, err = str.ReadResponse() require.EqualError(t, err, "http3: invalid duplicate use of RequestStream.ReadResponse before SendRequestHeader") // SendRequestHeader can't be used for requests that have a request body require.EqualError(t, str.SendRequestHeader( httptest.NewRequest(http.MethodGet, "https://quic-go.net", strings.NewReader("foobar")), ), "http3: invalid use of RequestStream.SendRequestHeader with a request that has a request body", ) req := httptest.NewRequest(http.MethodGet, "https://quic-go.net", nil) qstr.EXPECT().Write(gomock.Any()).AnyTimes() require.NoError(t, str.SendRequestHeader(req)) // duplicate calls are not allowed require.EqualError(t, str.SendRequestHeader(req), "http3: invalid duplicate use of RequestStream.SendRequestHeader") buf := bytes.NewBuffer(encodeResponse(t, 200)) buf.Write((&dataFrame{Length: 6}).Append(nil)) buf.Write([]byte("foobar")) qstr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes() rsp, err := str.ReadResponse() require.NoError(t, err) require.Equal(t, http.StatusOK, rsp.StatusCode) b := make([]byte, 10) n, err := str.Read(b) require.NoError(t, err) require.Equal(t, 6, n) require.Equal(t, []byte("foobar"), b[:n]) }