Files
quic-go/http3/conn_test.go

398 lines
12 KiB
Go

package http3
import (
"bytes"
"context"
"errors"
"testing"
"time"
"github.com/quic-go/quic-go"
mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/quicvarint"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestConnReceiveSettings(t *testing.T) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
qconn.EXPECT().ReceiveDatagram(gomock.Any()).Return(nil, errors.New("no datagrams")).MaxTimes(1)
conn := newConnection(
context.Background(),
qconn,
false,
protocol.PerspectiveServer,
nil,
0,
)
b := quicvarint.Append(nil, streamTypeControlStream)
b = (&settingsFrame{
Datagram: true,
ExtendedConnect: true,
Other: map[uint64]uint64{1337: 42},
}).Append(b)
r := bytes.NewReader(b)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(r.Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
done := make(chan struct{})
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-conn.ReceivedSettings():
case <-time.After(time.Second):
t.Fatal("timeout waiting for settings")
}
settings := conn.Settings()
require.True(t, settings.EnableDatagrams)
require.True(t, settings.EnableExtendedConnect)
require.Equal(t, map[uint64]uint64{1337: 42}, settings.Other)
}
func TestConnRejectDuplicateStreams(t *testing.T) {
t.Run("control stream", func(t *testing.T) {
testConnRejectDuplicateStreams(t, streamTypeControlStream)
})
t.Run("encoder stream", func(t *testing.T) {
testConnRejectDuplicateStreams(t, streamTypeQPACKEncoderStream)
})
t.Run("decoder stream", func(t *testing.T) {
testConnRejectDuplicateStreams(t, streamTypeQPACKDecoderStream)
})
}
func testConnRejectDuplicateStreams(t *testing.T, typ uint64) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
false,
protocol.PerspectiveServer,
nil,
0,
)
b := quicvarint.Append(nil, typ)
if typ == streamTypeControlStream {
b = (&settingsFrame{}).Append(b)
}
controlStr1 := mockquic.NewMockStream(mockCtrl)
controlStr1.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
controlStr2 := mockquic.NewMockStream(mockCtrl)
controlStr2.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
done := make(chan struct{})
closed := make(chan struct{})
qconn.EXPECT().CloseWithError(qerr.ApplicationErrorCode(ErrCodeStreamCreationError), gomock.Any()).Do(func(qerr.ApplicationErrorCode, string) error {
close(closed)
return nil
})
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr1, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr2, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("timeout waiting for duplicate stream")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnResetUnknownUniStream(t *testing.T) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
false,
protocol.PerspectiveServer,
nil,
0,
)
buf := bytes.NewBuffer(quicvarint.Append(nil, 0x1337))
str := mockquic.NewMockStream(mockCtrl)
str.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes()
reset := make(chan struct{})
str.EXPECT().CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError)).Do(func(quic.StreamErrorCode) { close(reset) })
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(str, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
done := make(chan struct{})
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-reset:
case <-time.After(time.Second):
t.Fatal("timeout waiting for reset")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnControlStreamFailures(t *testing.T) {
t.Run("missing settings", func(t *testing.T) {
testConnControlStreamFailures(t, (&dataFrame{}).Append(nil), ErrCodeMissingSettings)
})
t.Run("frame error", func(t *testing.T) {
testConnControlStreamFailures(t,
(&settingsFrame{Other: map[uint64]uint64{settingExtendedConnect: 1337}}).Append(nil),
ErrCodeFrameError,
)
})
}
func testConnControlStreamFailures(t *testing.T, data []byte, expectedErr ErrCode) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
false,
protocol.PerspectiveServer,
nil,
0,
)
b := quicvarint.Append(nil, streamTypeControlStream)
b = append(b, data...)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
closed := make(chan struct{})
qconn.EXPECT().CloseWithError(quic.ApplicationErrorCode(expectedErr), gomock.Any()).Do(func(quic.ApplicationErrorCode, string) error {
close(closed)
return nil
})
done := make(chan struct{})
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("timeout waiting for close")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnRejectPushStream(t *testing.T) {
t.Run("client", func(t *testing.T) {
testConnRejectPushStream(t, protocol.PerspectiveClient, ErrCodeStreamCreationError)
})
t.Run("server", func(t *testing.T) {
testConnRejectPushStream(t, protocol.PerspectiveServer, ErrCodeIDError)
})
}
func testConnRejectPushStream(t *testing.T, pers protocol.Perspective, expectedErr ErrCode) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
false,
pers.Opposite(),
nil,
0,
)
buf := bytes.NewBuffer(quicvarint.Append(nil, streamTypePushStream))
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
closed := make(chan struct{})
qconn.EXPECT().CloseWithError(quic.ApplicationErrorCode(expectedErr), gomock.Any()).Do(func(quic.ApplicationErrorCode, string) error {
close(closed)
return nil
})
done := make(chan struct{})
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("timeout waiting for close")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnInconsistentDatagramSupport(t *testing.T) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
true,
protocol.PerspectiveClient,
nil,
0,
)
b := quicvarint.Append(nil, streamTypeControlStream)
b = (&settingsFrame{Datagram: true}).Append(b)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
qconn.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: false})
closed := make(chan struct{})
qconn.EXPECT().CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support").Do(func(quic.ApplicationErrorCode, string) error {
close(closed)
return nil
})
done := make(chan struct{})
go func() {
defer close(done)
conn.handleUnidirectionalStreams(nil)
}()
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("timeout waiting for close")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnSendAndReceiveDatagram(t *testing.T) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
true,
protocol.PerspectiveClient,
nil,
0,
)
b := quicvarint.Append(nil, streamTypeControlStream)
b = (&settingsFrame{Datagram: true}).Append(b)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil).MaxTimes(1)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done")).MaxTimes(1)
qconn.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: true}).MaxTimes(1)
const strID = 4
// first deliver a datagram...
// since the stream is not open yet, it will be dropped
quarterStreamID := quicvarint.Append([]byte{}, strID/4)
delivered := make(chan struct{})
qconn.EXPECT().ReceiveDatagram(gomock.Any()).DoAndReturn(func(context.Context) ([]byte, error) {
close(delivered)
return append(quarterStreamID, []byte("foo")...), nil
})
streamOpened := make(chan struct{})
qconn.EXPECT().ReceiveDatagram(gomock.Any()).DoAndReturn(func(context.Context) ([]byte, error) {
<-streamOpened
return append(quarterStreamID, []byte("bar")...), nil
}).MaxTimes(1)
qconn.EXPECT().ReceiveDatagram(gomock.Any()).Return(nil, errors.New("test done")).MaxTimes(1)
go func() { conn.handleUnidirectionalStreams(nil) }()
select {
case <-delivered:
case <-time.After(time.Second):
t.Fatal("timeout waiting for datagram delivery")
}
// now open the stream...
qstr := mockquic.NewMockStream(mockCtrl)
qstr.EXPECT().StreamID().Return(strID).MinTimes(1)
qstr.EXPECT().Context().Return(context.Background()).AnyTimes()
qconn.EXPECT().OpenStreamSync(gomock.Any()).Return(qstr, nil)
str, err := conn.openRequestStream(context.Background(), nil, nil, true, 1000)
require.NoError(t, err)
// ... then deliver another datagram
close(streamOpened)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
data, err := str.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, []byte("bar"), data)
// now send a datagram
const strID2 = 404
expected := quicvarint.Append([]byte{}, strID2/4)
expected = append(expected, []byte("foobaz")...)
qconn.EXPECT().SendDatagram(expected).Return(assert.AnError)
require.ErrorIs(t, conn.sendDatagram(strID2, []byte("foobaz")), assert.AnError)
}
func TestConnDatagramFailures(t *testing.T) {
t.Run("invalid varint", func(t *testing.T) {
testConnDatagramFailures(t, []byte{128})
})
t.Run("invalid quarter stream ID", func(t *testing.T) {
testConnDatagramFailures(t, quicvarint.Append([]byte{}, maxQuarterStreamID+1))
})
}
func testConnDatagramFailures(t *testing.T, datagram []byte) {
mockCtrl := gomock.NewController(t)
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
conn := newConnection(
context.Background(),
qconn,
true,
protocol.PerspectiveClient,
nil,
0,
)
b := quicvarint.Append(nil, streamTypeControlStream)
b = (&settingsFrame{Datagram: true}).Append(b)
r := bytes.NewReader(b)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(r.Read).AnyTimes()
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil).MaxTimes(1)
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done")).MaxTimes(1)
qconn.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: true}).MaxTimes(1)
qconn.EXPECT().ReceiveDatagram(gomock.Any()).Return(datagram, nil)
done := make(chan struct{})
qconn.EXPECT().CloseWithError(qerr.ApplicationErrorCode(ErrCodeDatagramError), gomock.Any()).Do(func(qerr.ApplicationErrorCode, string) error {
close(done)
return nil
})
go func() { conn.handleUnidirectionalStreams(nil) }()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for close")
}
}