From 60183f4fecf0beeea17cfc9b0139afa96d85dde8 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Sat, 18 Jan 2020 21:51:19 +0700 Subject: [PATCH] implement marshalling of frames --- qlog/frame.go | 360 +++++++++++++++++++++++++++++++++++++++ qlog/frame_test.go | 367 ++++++++++++++++++++++++++++++++++++++++ qlog/qlog_suite_test.go | 13 ++ 3 files changed, 740 insertions(+) create mode 100644 qlog/frame.go create mode 100644 qlog/frame_test.go create mode 100644 qlog/qlog_suite_test.go diff --git a/qlog/frame.go b/qlog/frame.go new file mode 100644 index 00000000..e5c4653e --- /dev/null +++ b/qlog/frame.go @@ -0,0 +1,360 @@ +package qlog + +import ( + "encoding/json" + "fmt" + + "github.com/lucas-clemente/quic-go/internal/protocol" + "github.com/lucas-clemente/quic-go/internal/wire" +) + +type frame struct { + Frame interface{} +} + +type streamType protocol.StreamType + +func (s streamType) String() string { + switch protocol.StreamType(s) { + case protocol.StreamTypeUni: + return "unidirectional" + case protocol.StreamTypeBidi: + return "bidirectional" + default: + panic("unknown stream type") + } +} + +func escapeStr(str string) []byte { return []byte("\"" + str + "\"") } + +type connectionID protocol.ConnectionID + +func (c connectionID) MarshalJSON() ([]byte, error) { + return escapeStr(fmt.Sprintf("%x", c)), nil +} + +type cryptoFrame struct { + Offset protocol.ByteCount + Length protocol.ByteCount +} + +type streamFrame struct { + StreamID protocol.StreamID + Offset protocol.ByteCount + Length protocol.ByteCount + FinBit bool +} + +func transformFrame(wf wire.Frame) *frame { + // We don't want to store CRYPTO and STREAM frames, for multiple reasons: + // * They both contain data, and we want to make this byte slice GC'able as soon as possible. + // * STREAM frames use a slice from the buffer pool, which is released as soon as the frame is processed. + switch f := wf.(type) { + case *wire.CryptoFrame: + return &frame{Frame: &cryptoFrame{ + Offset: f.Offset, + Length: protocol.ByteCount(len(f.Data)), + }} + case *wire.StreamFrame: + return &frame{Frame: &streamFrame{ + StreamID: f.StreamID, + Offset: f.Offset, + Length: f.DataLen(), + FinBit: f.FinBit, + }} + default: + return &frame{Frame: wf} + } +} + +// MarshalJSON marshals to JSON +func (f frame) MarshalJSON() ([]byte, error) { + switch frame := f.Frame.(type) { + case *wire.PingFrame: + return marshalPingFrame(frame) + case *wire.AckFrame: + return marshalAckFrame(frame) + case *wire.ResetStreamFrame: + return marshalResetStreamFrame(frame) + case *wire.StopSendingFrame: + return marshalStopSendingFrame(frame) + case *cryptoFrame: + return marshalCryptoFrame(frame) + case *wire.NewTokenFrame: + return marshalNewTokenFrame(frame) + case *streamFrame: + return marshalStreamFrame(frame) + case *wire.MaxDataFrame: + return marshalMaxDataFrame(frame) + case *wire.MaxStreamDataFrame: + return marshalMaxStreamDataFrame(frame) + case *wire.MaxStreamsFrame: + return marshalMaxStreamsFrame(frame) + case *wire.DataBlockedFrame: + return marshalDataBlockedFrame(frame) + case *wire.StreamDataBlockedFrame: + return marshalStreamDataBlockedFrame(frame) + case *wire.StreamsBlockedFrame: + return marshalStreamsBlockedFrame(frame) + case *wire.NewConnectionIDFrame: + return marshalNewConnectionIDFrame(frame) + case *wire.RetireConnectionIDFrame: + return marshalRetireConnectionIDFrame(frame) + case *wire.PathChallengeFrame: + return marshalPathChallengeFrame(frame) + case *wire.PathResponseFrame: + return marshalPathResponseFrame(frame) + case *wire.ConnectionCloseFrame: + return marshalConnectionCloseFrame(frame) + case *wire.HandshakeDoneFrame: + return marshalHandshakeDoneFrame(frame) + default: + panic("unknown frame type") + } +} + +func marshalPingFrame(_ *wire.PingFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + }{ + FrameType: "ping", + }) +} + +type ackRange struct { + Smallest protocol.PacketNumber + Largest protocol.PacketNumber +} + +func (ar ackRange) MarshalJSON() ([]byte, error) { + if ar.Smallest == ar.Largest { + return json.Marshal([]string{fmt.Sprintf("%d", ar.Smallest)}) + } + return json.Marshal([]string{fmt.Sprintf("%d", ar.Smallest), fmt.Sprintf("%d", ar.Largest)}) +} + +func marshalAckFrame(f *wire.AckFrame) ([]byte, error) { + ranges := make([]ackRange, len(f.AckRanges)) + for i, r := range f.AckRanges { + ranges[i] = ackRange{Smallest: r.Smallest, Largest: r.Largest} + } + return json.Marshal(struct { + FrameType string `json:"frame_type"` + AckDelay int64 `json:"ack_delay,string,omitempty"` + AckRanges []ackRange `json:"acked_ranges"` + }{ + FrameType: "ack", + AckDelay: f.DelayTime.Milliseconds(), + AckRanges: ranges, + }) +} + +func marshalResetStreamFrame(f *wire.ResetStreamFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamID protocol.StreamID `json:"stream_id,string"` + ErrorCode protocol.ApplicationErrorCode `json:"error_code"` + FinalSize protocol.ByteCount `json:"final_size,string"` + }{ + FrameType: "reset_stream", + StreamID: f.StreamID, + ErrorCode: f.ErrorCode, + FinalSize: f.ByteOffset, + }) +} + +func marshalStopSendingFrame(f *wire.StopSendingFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamID protocol.StreamID `json:"stream_id,string"` + ErrorCode protocol.ApplicationErrorCode `json:"error_code"` + }{ + FrameType: "stop_sending", + StreamID: f.StreamID, + ErrorCode: f.ErrorCode, + }) +} + +func marshalCryptoFrame(f *cryptoFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Offset protocol.ByteCount `json:"offset,string"` + Length protocol.ByteCount `json:"length"` + }{ + FrameType: "crypto", + Offset: f.Offset, + Length: f.Length, + }) +} + +func marshalNewTokenFrame(f *wire.NewTokenFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Length int `json:"length"` + Token string `json:"token"` + }{ + FrameType: "new_token", + Length: len(f.Token), + Token: fmt.Sprintf("%x", f.Token), + }) +} + +func marshalStreamFrame(f *streamFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamID protocol.StreamID `json:"stream_id,string"` + Offset protocol.ByteCount `json:"offset,string"` + Length protocol.ByteCount `json:"length"` + Fin bool `json:"fin,omitempty"` + }{ + FrameType: "stream", + StreamID: f.StreamID, + Offset: f.Offset, + Length: f.Length, + Fin: f.FinBit, + }) +} + +func marshalMaxDataFrame(f *wire.MaxDataFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Maximum protocol.ByteCount `json:"maximum,string"` + }{ + FrameType: "max_data", + Maximum: f.ByteOffset, + }) +} + +func marshalMaxStreamDataFrame(f *wire.MaxStreamDataFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamID protocol.StreamID `json:"stream_id,string"` + Maximum protocol.ByteCount `json:"maximum,string"` + }{ + FrameType: "max_stream_data", + StreamID: f.StreamID, + Maximum: f.ByteOffset, + }) +} + +func marshalMaxStreamsFrame(f *wire.MaxStreamsFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamType string `json:"stream_type"` + Maximum protocol.StreamNum `json:"maximum,string"` + }{ + FrameType: "max_streams", + StreamType: streamType(f.Type).String(), + Maximum: f.MaxStreamNum, + }) +} + +func marshalDataBlockedFrame(f *wire.DataBlockedFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Limit protocol.ByteCount `json:"limit,string"` + }{ + FrameType: "data_blocked", + Limit: f.DataLimit, + }) +} + +func marshalStreamDataBlockedFrame(f *wire.StreamDataBlockedFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamID protocol.StreamID `json:"stream_id,string"` + Limit protocol.ByteCount `json:"limit,string"` + }{ + FrameType: "stream_data_blocked", + StreamID: f.StreamID, + Limit: f.DataLimit, + }) +} + +func marshalStreamsBlockedFrame(f *wire.StreamsBlockedFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + StreamType string `json:"stream_type"` + Limit protocol.StreamNum `json:"limit,string"` + }{ + FrameType: "streams_blocked", + StreamType: streamType(f.Type).String(), + Limit: f.StreamLimit, + }) +} + +func marshalNewConnectionIDFrame(f *wire.NewConnectionIDFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + SequenceNumber uint64 `json:"sequence_number,string"` + RetirePriorTo uint64 `json:"retire_prior_to,string"` + Length int `json:"length"` + ConnectionID connectionID `json:"connection_id"` + ResetToken string `json:"reset_token"` + }{ + FrameType: "new_connection_id", + SequenceNumber: f.SequenceNumber, + RetirePriorTo: f.RetirePriorTo, + Length: f.ConnectionID.Len(), + ConnectionID: connectionID(f.ConnectionID), + ResetToken: fmt.Sprintf("%x", f.StatelessResetToken), + }) +} + +func marshalRetireConnectionIDFrame(f *wire.RetireConnectionIDFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + SequenceNumber uint64 `json:"sequence_number,string"` + }{ + FrameType: "retire_connection_id", + SequenceNumber: f.SequenceNumber, + }) +} + +func marshalPathChallengeFrame(f *wire.PathChallengeFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Data string `json:"data"` + }{ + FrameType: "path_challenge", + Data: fmt.Sprintf("%x", f.Data[:]), + }) +} + +func marshalPathResponseFrame(f *wire.PathResponseFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + Data string `json:"data"` + }{ + FrameType: "path_response", + Data: fmt.Sprintf("%x", f.Data[:]), + }) +} + +func marshalConnectionCloseFrame(f *wire.ConnectionCloseFrame) ([]byte, error) { + errorSpace := "transport" + if f.IsApplicationError { + errorSpace = "application" + } + return json.Marshal(struct { + FrameType string `json:"frame_type"` + ErrorSpace string `json:"error_space"` + ErrorCode uint64 `json:"error_code"` + RawErrorCode uint64 `json:"raw_error_code"` + Reason string `json:"reason"` + }{ + FrameType: "connection_close", + ErrorSpace: errorSpace, + ErrorCode: uint64(f.ErrorCode), + RawErrorCode: uint64(f.ErrorCode), + Reason: f.ReasonPhrase, + }) +} + +func marshalHandshakeDoneFrame(_ *wire.HandshakeDoneFrame) ([]byte, error) { + return json.Marshal(struct { + FrameType string `json:"frame_type"` + }{ + FrameType: "handshake_done", + }) +} diff --git a/qlog/frame_test.go b/qlog/frame_test.go new file mode 100644 index 00000000..bae6ea11 --- /dev/null +++ b/qlog/frame_test.go @@ -0,0 +1,367 @@ +package qlog + +import ( + "encoding/json" + "time" + + "github.com/lucas-clemente/quic-go/internal/protocol" + "github.com/lucas-clemente/quic-go/internal/wire" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Frames", func() { + // marshal the frame + check := func(f wire.Frame, expected map[string]interface{}) { + data, err := transformFrame(f).MarshalJSON() + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, json.Valid(data)).To(BeTrue()) + // unmarshal the frame + m := make(map[string](interface{})) + ExpectWithOffset(1, json.Unmarshal(data, &m)).To(Succeed()) + ExpectWithOffset(1, m).To(HaveLen(len(expected))) + for key, value := range expected { + switch value.(type) { + case string: + ExpectWithOffset(1, m).To(HaveKeyWithValue(key, value)) + case int: + ExpectWithOffset(1, m).To(HaveKeyWithValue(key, float64(value.(int)))) + case bool: + ExpectWithOffset(1, m).To(HaveKeyWithValue(key, value.(bool))) + case [][]string: // used in the ACK frame + ExpectWithOffset(1, m).To(HaveKey(key)) + for i, l := range value.([][]string) { + for j, s := range l { + ExpectWithOffset(1, m[key].([]interface{})[i].([]interface{})[j].(string)).To(Equal(s)) + } + } + default: + Fail("unexpected type") + } + } + } + + It("marshals PING frames", func() { + check( + &wire.PingFrame{}, + map[string]interface{}{ + "frame_type": "ping", + }, + ) + }) + + It("marshals ACK frames with a range acknowledging a single packet", func() { + check( + &wire.AckFrame{ + DelayTime: 86 * time.Millisecond, + AckRanges: []wire.AckRange{{Smallest: 120, Largest: 120}}, + }, + map[string]interface{}{ + "frame_type": "ack", + "ack_delay": "86", + "acked_ranges": [][]string{[]string{"120"}}, + }, + ) + }) + + It("marshals ACK frames without a delay", func() { + check( + &wire.AckFrame{ + AckRanges: []wire.AckRange{{Smallest: 120, Largest: 120}}, + }, + map[string]interface{}{ + "frame_type": "ack", + "acked_ranges": [][]string{[]string{"120"}}, + }, + ) + }) + + It("marshals ACK frames with a range acknowledging ranges of packets", func() { + check( + &wire.AckFrame{ + DelayTime: 86 * time.Millisecond, + AckRanges: []wire.AckRange{ + {Smallest: 5, Largest: 50}, + {Smallest: 100, Largest: 120}, + }, + }, + map[string]interface{}{ + "frame_type": "ack", + "ack_delay": "86", + "acked_ranges": [][]string{ + []string{"5", "50"}, + []string{"100", "120"}, + }, + }, + ) + }) + + It("marshals RESET_STREAM frames", func() { + check( + &wire.ResetStreamFrame{ + StreamID: 987, + ByteOffset: 1234, + ErrorCode: 42, + }, + map[string]interface{}{ + "frame_type": "reset_stream", + "stream_id": "987", + "error_code": 42, + "final_size": "1234", + }, + ) + }) + + It("marshals STOP_SENDING frames", func() { + check( + &wire.StopSendingFrame{ + StreamID: 987, + ErrorCode: 42, + }, + map[string]interface{}{ + "frame_type": "stop_sending", + "stream_id": "987", + "error_code": 42, + }, + ) + }) + + It("marshals CRYPTO frames", func() { + check( + &wire.CryptoFrame{ + Offset: 1337, + Data: []byte("foobar"), + }, + map[string]interface{}{ + "frame_type": "crypto", + "offset": "1337", + "length": 6, + }, + ) + }) + + It("marshals NEW_TOKEN frames", func() { + check( + &wire.NewTokenFrame{ + Token: []byte{0xde, 0xad, 0xbe, 0xef}, + }, + map[string]interface{}{ + "frame_type": "new_token", + "length": 4, + "token": "deadbeef", + }, + ) + }) + + It("marshals STREAM frames with FIN", func() { + check( + &wire.StreamFrame{ + StreamID: 42, + Offset: 1337, + FinBit: true, + Data: []byte("foobar"), + }, + map[string]interface{}{ + "frame_type": "stream", + "stream_id": "42", + "offset": "1337", + "fin": true, + "length": 6, + }, + ) + }) + + It("marshals STREAM frames without FIN", func() { + check( + &wire.StreamFrame{ + StreamID: 42, + Offset: 1337, + Data: []byte("foo"), + }, + map[string]interface{}{ + "frame_type": "stream", + "stream_id": "42", + "offset": "1337", + "length": 3, + }, + ) + }) + + It("marshals MAX_DATA frames", func() { + check( + &wire.MaxDataFrame{ + ByteOffset: 1337, + }, + map[string]interface{}{ + "frame_type": "max_data", + "maximum": "1337", + }, + ) + }) + + It("marshals MAX_STREAM_DATA frames", func() { + check( + &wire.MaxStreamDataFrame{ + StreamID: 1234, + ByteOffset: 1337, + }, + map[string]interface{}{ + "frame_type": "max_stream_data", + "stream_id": "1234", + "maximum": "1337", + }, + ) + }) + + It("marshals MAX_STREAMS frames", func() { + check( + &wire.MaxStreamsFrame{ + Type: protocol.StreamTypeBidi, + MaxStreamNum: 42, + }, + map[string]interface{}{ + "frame_type": "max_streams", + "stream_type": "bidirectional", + "maximum": "42", + }, + ) + }) + + It("marshals DATA_BLOCKED frames", func() { + check( + &wire.DataBlockedFrame{ + DataLimit: 1337, + }, + map[string]interface{}{ + "frame_type": "data_blocked", + "limit": "1337", + }, + ) + }) + + It("marshals STREAM_DATA_BLOCKED frames", func() { + check( + &wire.StreamDataBlockedFrame{ + StreamID: 42, + DataLimit: 1337, + }, + map[string]interface{}{ + "frame_type": "stream_data_blocked", + "stream_id": "42", + "limit": "1337", + }, + ) + }) + + It("marshals STREAMS_BLOCKED frames", func() { + check( + &wire.StreamsBlockedFrame{ + Type: protocol.StreamTypeUni, + StreamLimit: 123, + }, + map[string]interface{}{ + "frame_type": "streams_blocked", + "stream_type": "unidirectional", + "limit": "123", + }, + ) + }) + + It("marshals NEW_CONNECTION_ID frames", func() { + check( + &wire.NewConnectionIDFrame{ + SequenceNumber: 42, + RetirePriorTo: 24, + ConnectionID: protocol.ConnectionID{0xde, 0xad, 0xbe, 0xef}, + StatelessResetToken: [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, + }, + map[string]interface{}{ + "frame_type": "new_connection_id", + "sequence_number": "42", + "retire_prior_to": "24", + "length": 4, + "connection_id": "deadbeef", + "reset_token": "000102030405060708090a0b0c0d0e0f", + }, + ) + }) + + It("marshals RETIRE_CONNECTION_ID frames", func() { + check( + &wire.RetireConnectionIDFrame{ + SequenceNumber: 1337, + }, + map[string]interface{}{ + "frame_type": "retire_connection_id", + "sequence_number": "1337", + }, + ) + }) + + It("marshals PATH_CHALLENGE frames", func() { + check( + &wire.PathChallengeFrame{ + Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01}, + }, + map[string]interface{}{ + "frame_type": "path_challenge", + "data": "deadbeefcafec001", + }, + ) + }) + + It("marshals PATH_RESPONSE frames", func() { + check( + &wire.PathResponseFrame{ + Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01}, + }, + map[string]interface{}{ + "frame_type": "path_response", + "data": "deadbeefcafec001", + }, + ) + }) + + It("marshals CONNECTION_CLOSE frames, for application error codes", func() { + check( + &wire.ConnectionCloseFrame{ + IsApplicationError: true, + ErrorCode: 1337, + ReasonPhrase: "lorem ipsum", + }, + map[string]interface{}{ + "frame_type": "connection_close", + "error_space": "application", + "error_code": 1337, + "raw_error_code": 1337, + "reason": "lorem ipsum", + }, + ) + }) + + It("marshals CONNECTION_CLOSE frames, for transport error codes", func() { + check( + &wire.ConnectionCloseFrame{ + ErrorCode: 1337, + ReasonPhrase: "lorem ipsum", + }, + map[string]interface{}{ + "frame_type": "connection_close", + "error_space": "transport", + "error_code": 1337, + "raw_error_code": 1337, + "reason": "lorem ipsum", + }, + ) + }) + + It("marshals HANDSHAKE_DONE frames", func() { + check( + &wire.HandshakeDoneFrame{}, + map[string]interface{}{ + "frame_type": "handshake_done", + }, + ) + }) +}) diff --git a/qlog/qlog_suite_test.go b/qlog/qlog_suite_test.go new file mode 100644 index 00000000..935475dd --- /dev/null +++ b/qlog/qlog_suite_test.go @@ -0,0 +1,13 @@ +package qlog + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestQlog(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "qlog Suite") +}