From 1482f338456ff463d1828e7af7475807be85d148 Mon Sep 17 00:00:00 2001 From: Elim Tsiagbey Date: Thu, 20 Jul 2023 09:58:52 -0400 Subject: [PATCH] feat: Perform asciicast conversion sanity checks (#3481) * feat: Perform asciicast conversion sanity checks Check that the conversion to asciicast is done on `exec` or `shell` session program. Unsupported session programs will throw an error. --- internal/bsr/convert/convert.go | 31 +- internal/bsr/convert/convert_test.go | 564 +++++++++++++++++++++++++++ 2 files changed, 584 insertions(+), 11 deletions(-) create mode 100644 internal/bsr/convert/convert_test.go diff --git a/internal/bsr/convert/convert.go b/internal/bsr/convert/convert.go index bb6538a58..d6847a434 100644 --- a/internal/bsr/convert/convert.go +++ b/internal/bsr/convert/convert.go @@ -55,20 +55,29 @@ func ToAsciicast(ctx context.Context, session *bsr.Session, tmp storage.TempFile return nil, fmt.Errorf("%s: %w", op, err) } - // TODO sanity checks before getting the data files: - // - check connection summary to see if there was an exec or shell request + switch chs := ch.Summary.(type) { + case *ssh.ChannelSummary: + switch chs.SessionProgram { + case ssh.Shell, ssh.Exec: + reqScanner, err := ch.OpenRequestScanner(ctx, bsr.Inbound) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } - reqScanner, err := ch.OpenRequestScanner(ctx, bsr.Inbound) - if err != nil { - return nil, fmt.Errorf("%s: %w", op, err) + msgScanner, err := ch.OpenMessageScanner(ctx, bsr.Outbound) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + return sshChannelToAsciicast(ctx, reqScanner, msgScanner, tmp, options...) + case "": + return nil, fmt.Errorf("%s: session program not set for asciicast conversion", op) + default: + return nil, fmt.Errorf("%s: unsupported %q session program for asciicast conversion", op, chs.SessionProgram) + } + default: + return nil, fmt.Errorf("%s: unexpected error occurred with channel summary. possibly a malformed Boundary Session Recording", op) } - msgScanner, err := ch.OpenMessageScanner(ctx, bsr.Outbound) - if err != nil { - return nil, fmt.Errorf("%s: %w", op, err) - } - - return sshChannelToAsciicast(ctx, reqScanner, msgScanner, tmp, options...) default: return nil, fmt.Errorf("%s: %w", op, ErrUnsupportedProtocol) } diff --git a/internal/bsr/convert/convert_test.go b/internal/bsr/convert/convert_test.go new file mode 100644 index 000000000..f1adf520a --- /dev/null +++ b/internal/bsr/convert/convert_test.go @@ -0,0 +1,564 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package convert_test + +import ( + "context" + "errors" + "fmt" + "io" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/bsr" + "github.com/hashicorp/boundary/internal/bsr/convert" + "github.com/hashicorp/boundary/internal/bsr/internal/fstest" + "github.com/hashicorp/boundary/internal/bsr/kms" + "github.com/hashicorp/boundary/internal/bsr/ssh" + "github.com/stretchr/testify/require" +) + +func testChunks(s string, d bsr.Direction, p bsr.Protocol) []bsr.Chunk { + ts := time.Date(2023, time.March, 16, 10, 47, 3, 14, time.UTC) + + return []bsr.Chunk{ + &bsr.HeaderChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: p, + Direction: d, + Timestamp: bsr.NewTimestamp(ts), + Type: bsr.ChunkHeader, + }, + Compression: bsr.NoCompression, + Encryption: bsr.NoEncryption, + SessionId: s, + }, + &bsr.EndChunk{ + BaseChunk: &bsr.BaseChunk{ + Protocol: p, + Direction: d, + Timestamp: bsr.NewTimestamp(ts.Add(time.Second)), + Type: bsr.ChunkEnd, + }, + }, + } +} + +func writeToChannels(ctx context.Context, w io.Writer, chunks ...bsr.Chunk) error { + w.Write(bsr.Magic.Bytes()) + enc, err := bsr.NewChunkEncoder(ctx, w, bsr.NoCompression, bsr.NoEncryption) + if err != nil { + return err + } + + for _, c := range chunks { + _, err := enc.Encode(ctx, c) + if err != nil { + return err + } + } + + return nil +} + +func TestConvert_ToAsciicast_SessionProgram(t *testing.T) { + ctx := context.Background() + + fs := &fstest.MemFS{} + tmpfile, err := fstest.NewTempFile(t.Name()) + require.NoError(t, err) + + connectionId := "test_connection" + channelId := "test_channel" + + cases := []struct { + name string + chs bsr.ChannelSummary + protocol bsr.Protocol + id string + wantErr error + }{ + { + name: "exec", + id: "01234567890", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.Exec, + }, + wantErr: nil, + }, + { + name: "shell", + id: "11234567890", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.Shell, + }, + wantErr: nil, + }, + { + name: "unsupported session program - subsystem", + id: "21234567890", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.Subsystem, + }, + wantErr: errors.New("convert.ToAsciicast: unsupported \"subsystem\" session program for asciicast conversion"), + }, + { + name: "unsupported session program - not applicable", + id: "21234567892", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.NotApplicable, + }, + wantErr: errors.New("convert.ToAsciicast: unsupported \"not applicable\" session program for asciicast conversion"), + }, + { + name: "unsupported session program - none", + id: "21234567891", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.None, + }, + wantErr: errors.New("convert.ToAsciicast: unsupported \"none\" session program for asciicast conversion"), + }, + { + name: "nil session program", + id: "31234567890", + protocol: ssh.Protocol, + chs: &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + }, + wantErr: errors.New("convert.ToAsciicast: session program not set for asciicast conversion"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Setup keys + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), fmt.Sprintf("s_%s", tc.id)) + require.NoError(t, err) + + keyFn := func(w kms.WrappedKeys) (kms.UnwrappedKeys, error) { + u := kms.UnwrappedKeys{ + BsrKey: keys.BsrKey, + PrivKey: keys.PrivKey, + } + return u, nil + } + + // Set up session + srm := &bsr.SessionRecordingMeta{ + Id: fmt.Sprintf("sr_%s", tc.id), + Protocol: tc.protocol, + } + sessionMeta := bsr.TestSessionMeta(fmt.Sprintf("s_%s", tc.id)) + + sesh, err := bsr.NewSession(ctx, srm, sessionMeta, fs, keys, bsr.WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, sesh) + + // Encode session summary + sesh.EncodeSummary(ctx, &bsr.BaseSessionSummary{ + Id: channelId, + }) + + // Set up connection + connMeta := &bsr.ConnectionRecordingMeta{Id: connectionId} + conn, err := sesh.NewConnection(ctx, connMeta) + require.NoError(t, err) + require.NotNil(t, conn) + + // Encode connection summary + err = conn.EncodeSummary(ctx, &bsr.BaseConnectionSummary{ + Id: connectionId, + ChannelCount: 1, + }) + require.NoError(t, err) + + // Setup Channel + chanMeta := &bsr.ChannelRecordingMeta{ + Id: channelId, + Type: "chan", + } + ch, err := conn.NewChannel(ctx, chanMeta) + require.NoError(t, err) + require.NotNil(t, ch) + + // Encode channel summary + err = ch.EncodeSummary(ctx, tc.chs) + require.NoError(t, err) + + // Write request-inbound.data file + requestInboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Inbound, tc.protocol) + inW, err := ch.NewRequestsWriter(ctx, bsr.Inbound) + require.NoError(t, err) + require.NotNil(t, inW) + + writeToChannels(ctx, inW, requestInboundBsrChunks...) + require.NoError(t, err) + + inWC := inW.(io.Closer) + inWC.Close() + + // Write message-outbound.data file + messageOutboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Outbound, tc.protocol) + outW, err := ch.NewMessagesWriter(ctx, bsr.Outbound) + require.NoError(t, err) + require.NotNil(t, outW) + + err = writeToChannels(ctx, outW, messageOutboundBsrChunks...) + require.NoError(t, err) + + outWC := outW.(io.Closer) + outWC.Close() + + ch.Close(ctx) + conn.Close(ctx) + sesh.Close(ctx) + + opSesh, err := bsr.OpenSession(ctx, srm.Id, fs, keyFn) + require.NoError(t, err) + require.NotNil(t, opSesh) + + _, err = convert.ToAsciicast(ctx, opSesh, tmpfile, connectionId, convert.WithChannelId(channelId)) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + }) + } +} + +func TestConvert_ToAsciicast_Protocol(t *testing.T) { + ctx := context.Background() + + fs := &fstest.MemFS{} + tmpfile, err := fstest.NewTempFile(t.Name()) + require.NoError(t, err) + + connectionId := "test_connection" + channelId := "test_channel" + + cases := []struct { + name string + protocol bsr.Protocol + registerSummaryAlloc bool + id string + wantErr error + }{ + { + name: "unsupported protocol", + id: "41234567890", + protocol: bsr.Protocol("UNSUPPORTED_PROTOCOL"), + registerSummaryAlloc: true, + wantErr: errors.New("convert.ToAsciicast: unsupported protocol"), + }, + { + name: "BSSH Protocol", + id: "51234567890", + protocol: ssh.Protocol, + registerSummaryAlloc: false, + wantErr: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Setup keys + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), fmt.Sprintf("s_%s", tc.id)) + require.NoError(t, err) + + keyFn := func(w kms.WrappedKeys) (kms.UnwrappedKeys, error) { + u := kms.UnwrappedKeys{ + BsrKey: keys.BsrKey, + PrivKey: keys.PrivKey, + } + return u, nil + } + + // Register custom summary alloc func for custom protocol + if tc.registerSummaryAlloc { + err = bsr.RegisterSummaryAllocFunc(tc.protocol, bsr.ChannelContainer, func(ctx context.Context) bsr.Summary { + return &bsr.BaseChannelSummary{Id: channelId, ConnectionRecordingId: connectionId} + }) + require.NoError(t, err) + + err = bsr.RegisterSummaryAllocFunc(tc.protocol, bsr.SessionContainer, func(ctx context.Context) bsr.Summary { + return &bsr.BaseSessionSummary{Id: fmt.Sprintf("s_%s", tc.id), ConnectionCount: 1} + }) + require.NoError(t, err) + + err = bsr.RegisterSummaryAllocFunc(tc.protocol, bsr.ConnectionContainer, func(ctx context.Context) bsr.Summary { + return &bsr.BaseConnectionSummary{Id: connectionId, ChannelCount: 1} + }) + require.NoError(t, err) + } + + // Set up session + srm := &bsr.SessionRecordingMeta{ + Id: fmt.Sprintf("sr_%s", tc.id), + Protocol: tc.protocol, + } + sessionMeta := bsr.TestSessionMeta(fmt.Sprintf("s_%s", tc.id)) + + sesh, err := bsr.NewSession(ctx, srm, sessionMeta, fs, keys, bsr.WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, sesh) + + // Encode session summary + sesh.EncodeSummary(ctx, &bsr.BaseSessionSummary{ + Id: channelId, + }) + + // Set up connection + connMeta := &bsr.ConnectionRecordingMeta{Id: connectionId} + conn, err := sesh.NewConnection(ctx, connMeta) + require.NoError(t, err) + require.NotNil(t, conn) + + // Encode connection summary + err = conn.EncodeSummary(ctx, &bsr.BaseConnectionSummary{ + Id: connectionId, + ChannelCount: 1, + }) + require.NoError(t, err) + + // Setup Channel + chanMeta := &bsr.ChannelRecordingMeta{ + Id: channelId, + Type: "chan", + } + ch, err := conn.NewChannel(ctx, chanMeta) + require.NoError(t, err) + require.NotNil(t, ch) + + // Encode channel summary + err = ch.EncodeSummary(ctx, &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.Shell, + }) + require.NoError(t, err) + + // Write request-inbound.data file + requestInboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Inbound, tc.protocol) + inW, err := ch.NewRequestsWriter(ctx, bsr.Inbound) + require.NoError(t, err) + require.NotNil(t, inW) + + writeToChannels(ctx, inW, requestInboundBsrChunks...) + require.NoError(t, err) + + inWC := inW.(io.Closer) + inWC.Close() + + // Write message-outbound.data file + messageOutboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Outbound, tc.protocol) + outW, err := ch.NewMessagesWriter(ctx, bsr.Outbound) + require.NoError(t, err) + require.NotNil(t, outW) + + err = writeToChannels(ctx, outW, messageOutboundBsrChunks...) + require.NoError(t, err) + + outWC := outW.(io.Closer) + outWC.Close() + + ch.Close(ctx) + conn.Close(ctx) + sesh.Close(ctx) + + opSesh, err := bsr.OpenSession(ctx, srm.Id, fs, keyFn) + require.NoError(t, err) + require.NotNil(t, opSesh) + + _, err = convert.ToAsciicast(ctx, opSesh, tmpfile, connectionId, convert.WithChannelId(channelId)) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + }) + } +} + +func TestConvert_ToAsciicast_Scanner(t *testing.T) { + ctx := context.Background() + + fs := &fstest.MemFS{} + tmpfile, err := fstest.NewTempFile(t.Name()) + require.NoError(t, err) + + connectionId := "test_connection" + channelId := "test_channel" + + cases := []struct { + name string + protocol bsr.Protocol + writeRequest bool + writeMessage bool + id string + wantErr error + }{ + { + name: "request and message scanner", + id: "61234567890", + protocol: ssh.Protocol, + writeRequest: true, + writeMessage: true, + wantErr: nil, + }, + { + name: "missing requests data file", + id: "71234567890", + protocol: ssh.Protocol, + writeRequest: false, + writeMessage: true, + wantErr: errors.New("convert.ToAsciicast: bsr.(Channel).OpenRequestScanner: file requests-inbound.data does not exist: does not exist"), + }, + { + name: "missing messages data file", + id: "81234567890", + protocol: ssh.Protocol, + writeRequest: true, + writeMessage: false, + wantErr: errors.New("convert.ToAsciicast: bsr.(Channel).OpenMessageScanner: file messages-outbound.data does not exist: does not exist"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Setup keys + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), fmt.Sprintf("s_%s", tc.id)) + require.NoError(t, err) + + keyFn := func(w kms.WrappedKeys) (kms.UnwrappedKeys, error) { + u := kms.UnwrappedKeys{ + BsrKey: keys.BsrKey, + PrivKey: keys.PrivKey, + } + return u, nil + } + + // Set up session + srm := &bsr.SessionRecordingMeta{ + Id: fmt.Sprintf("sr_%s", tc.id), + Protocol: tc.protocol, + } + sessionMeta := bsr.TestSessionMeta(fmt.Sprintf("s_%s", tc.id)) + + sesh, err := bsr.NewSession(ctx, srm, sessionMeta, fs, keys, bsr.WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, sesh) + + // Encode session summary + sesh.EncodeSummary(ctx, &bsr.BaseSessionSummary{ + Id: channelId, + }) + + // Set up connection + connMeta := &bsr.ConnectionRecordingMeta{Id: connectionId} + conn, err := sesh.NewConnection(ctx, connMeta) + require.NoError(t, err) + require.NotNil(t, conn) + + // Encode connection summary + err = conn.EncodeSummary(ctx, &bsr.BaseConnectionSummary{ + Id: connectionId, + ChannelCount: 1, + }) + require.NoError(t, err) + + // Setup Channel + chanMeta := &bsr.ChannelRecordingMeta{ + Id: channelId, + Type: "chan", + } + ch, err := conn.NewChannel(ctx, chanMeta) + require.NoError(t, err) + require.NotNil(t, ch) + + // Encode channel summary + err = ch.EncodeSummary(ctx, &ssh.ChannelSummary{ + ChannelSummary: &bsr.BaseChannelSummary{ + Id: channelId, + ConnectionRecordingId: connectionId, + }, + SessionProgram: ssh.Shell, + }) + require.NoError(t, err) + + if tc.writeRequest { + // Write request-inbound.data file + requestInboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Inbound, tc.protocol) + inW, err := ch.NewRequestsWriter(ctx, bsr.Inbound) + require.NoError(t, err) + require.NotNil(t, inW) + + writeToChannels(ctx, inW, requestInboundBsrChunks...) + require.NoError(t, err) + + inWC := inW.(io.Closer) + inWC.Close() + } + + if tc.writeMessage { + // Write message-outbound.data file + messageOutboundBsrChunks := testChunks(fmt.Sprintf("s_%s", tc.id), bsr.Outbound, tc.protocol) + outW, err := ch.NewMessagesWriter(ctx, bsr.Outbound) + require.NoError(t, err) + require.NotNil(t, outW) + + err = writeToChannels(ctx, outW, messageOutboundBsrChunks...) + require.NoError(t, err) + + outWC := outW.(io.Closer) + outWC.Close() + } + + ch.Close(ctx) + conn.Close(ctx) + sesh.Close(ctx) + + opSesh, err := bsr.OpenSession(ctx, srm.Id, fs, keyFn) + require.NoError(t, err) + require.NotNil(t, opSesh) + + _, err = convert.ToAsciicast(ctx, opSesh, tmpfile, connectionId, convert.WithChannelId(channelId)) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + }) + } +}