feat(bsr/convert): Add logic to convert ssh channel to asciicast
This commit is contained in:
3 files changed
+865
No files matched your search
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package convert
|
||||
|
||||
import "errors"
|
||||
|
||||
// Common convert errors.
|
||||
var (
|
||||
ErrUnsupportedProtocol = errors.New("unsupported protocol")
|
||||
ErrMalformedBsr = errors.New("malformed bsr data file")
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
"github.com/hashicorp/boundary/internal/bsr/convert/internal/asciicast"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
"github.com/hashicorp/boundary/internal/bsr/ssh"
|
||||
)
|
||||
|
||||
// sshChannelToAsciicast will convert a recording of an ssh channel from a BSR
|
||||
// into an asciicast. This expects two bsr.ChunkScanners. One for the recording
|
||||
// of ssh requests and one for the recording of messages. In order to generate
|
||||
// a useful asciicast, it should be the inbound requests and outbound messages.
|
||||
// This also expects a io.ReadWriteSeeker that will be used to write the
|
||||
// asciicast. This is then reset and returned as a io.ReadCloser. The caller
|
||||
// should call Close on the returned io.ReadCloser after reading the asciicast.
|
||||
func sshChannelToAsciicast(ctx context.Context, requestScanner *bsr.ChunkScanner, messagesScanner *bsr.ChunkScanner, w io.ReadWriteSeeker) (io.ReadCloser, error) {
|
||||
const op = "convert.sshChannelToAsciicast"
|
||||
|
||||
switch {
|
||||
case is.Nil(requestScanner):
|
||||
return nil, fmt.Errorf("%s: missing request scanner: %w", op, bsr.ErrInvalidParameter)
|
||||
case is.Nil(messagesScanner):
|
||||
return nil, fmt.Errorf("%s: missing message scanner: %w", op, bsr.ErrInvalidParameter)
|
||||
case is.Nil(w):
|
||||
return nil, fmt.Errorf("%s: missing read write seeker: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
header := asciicast.NewHeader()
|
||||
|
||||
// Walk the requests to extract any information for asciicast header. This
|
||||
// will come from either a Pty and/or Env request. There could also be
|
||||
// additional WindowChange requests to resize the terminal. In picking an
|
||||
// initial height/width for the asciicast, this will pick the largest heigh
|
||||
// and width seen between a Pty and any WindowChange requests
|
||||
//
|
||||
// There may not be either request, so the header defaults to some sane values.
|
||||
// We would expect at most one Pty request per channel. Although this is
|
||||
// not required by and RFC. If there happen to be multiple, it is probably
|
||||
// ok to just use the last one.
|
||||
if err := bsr.ChunkWalk(ctx, requestScanner, func(ctx context.Context, c bsr.Chunk) error {
|
||||
switch c.GetProtocol() {
|
||||
case ssh.Protocol:
|
||||
switch c.GetType() {
|
||||
case ssh.PtyReqChunkType:
|
||||
cc := c.(*ssh.PtyRequest)
|
||||
|
||||
if cc.GetTerminalWidthCharacters() > asciicast.MinWidth {
|
||||
header.Width = cc.GetTerminalWidthCharacters()
|
||||
}
|
||||
if cc.GetTerminalHeightRows() > asciicast.MinHeight {
|
||||
header.Height = cc.GetTerminalHeightRows()
|
||||
}
|
||||
if cc.GetTermEnvVar() != "" {
|
||||
header.Env.Term = cc.GetTermEnvVar()
|
||||
}
|
||||
case ssh.WindowChangeReqChunkType:
|
||||
cc := c.(*ssh.WindowChangeRequest)
|
||||
|
||||
if cc.GetTerminalWidthColumns() > header.Width {
|
||||
header.Width = cc.GetTerminalWidthColumns()
|
||||
}
|
||||
if cc.GetTerminalHeightRows() > header.Height {
|
||||
header.Height = cc.GetTerminalHeightRows()
|
||||
}
|
||||
|
||||
case ssh.EnvReqChunkType:
|
||||
cc := c.(*ssh.EnvRequest)
|
||||
if cc.GetVariableName() == "SHELL" && cc.GetVariableValue() != "" {
|
||||
header.Env.Shell = cc.GetVariableValue()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return ErrUnsupportedProtocol
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
// Now walk the message chunks. The ChunkHeader should contain the last bit
|
||||
// of information needed to complete the asciicast header; the start timestamp.
|
||||
// After that it should just be creating asciicast.Event for each DataChunk.
|
||||
enc := json.NewEncoder(w)
|
||||
var wroteHeader bool
|
||||
if err := bsr.ChunkWalk(ctx, messagesScanner, func(ctx context.Context, c bsr.Chunk) error {
|
||||
switch c.GetProtocol() {
|
||||
case ssh.Protocol:
|
||||
switch c.GetType() {
|
||||
case bsr.ChunkHeader:
|
||||
cc := c.(*bsr.HeaderChunk)
|
||||
if wroteHeader {
|
||||
return fmt.Errorf("multiple header chunks: %w", ErrMalformedBsr)
|
||||
}
|
||||
header.Timestamp = asciicast.Time(cc.GetTimestamp().AsTime())
|
||||
if err := enc.Encode(&header); err != nil {
|
||||
return err
|
||||
}
|
||||
wroteHeader = true
|
||||
case ssh.DataChunkType:
|
||||
if !wroteHeader {
|
||||
return fmt.Errorf("data chunk before header: %w", ErrMalformedBsr)
|
||||
}
|
||||
cc := c.(*ssh.DataChunk)
|
||||
tt := cc.GetTimestamp().AsTime()
|
||||
ts := float64(tt.Sub(time.Time(header.Timestamp))) / float64(time.Second)
|
||||
data := cc.Data
|
||||
|
||||
e, err := asciicast.NewEvent(asciicast.Output, ts, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := enc.Encode(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return ErrUnsupportedProtocol
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
if _, err := w.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r io.ReadCloser
|
||||
if v, ok := w.(io.ReadCloser); ok {
|
||||
r = v
|
||||
} else {
|
||||
r = io.NopCloser(w)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
sshv1 "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/ssh"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_sshChannelToAsciicast(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ts := time.Date(2023, time.March, 16, 10, 47, 3, 14, time.UTC)
|
||||
newW := func() io.ReadWriteSeeker {
|
||||
f, err := os.CreateTemp("", "*.asciicast")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
return f
|
||||
}
|
||||
newScanner := func(chunks ...bsr.Chunk) *bsr.ChunkScanner {
|
||||
var buf bytes.Buffer
|
||||
buf.Write(bsr.Magic.Bytes())
|
||||
enc, err := bsr.NewChunkEncoder(ctx, &buf, bsr.NoCompression, bsr.NoEncryption)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range chunks {
|
||||
_, err := enc.Encode(ctx, c)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
s, err := bsr.NewChunkScanner(ctx, bytes.NewBuffer(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
requestScanner *bsr.ChunkScanner
|
||||
messageScanner *bsr.ChunkScanner
|
||||
w io.ReadWriteSeeker
|
||||
want []byte
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"no-pty-no-env-no-messages",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":80,"height":120,"timestamp":1678963623,"env":{"SHELL":"/bin/bash","TERM":"xterm"}}
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"pty-no-env-no-messages",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&ssh.PtyRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.PtyReqChunkType,
|
||||
},
|
||||
PtyRequest: &sshv1.PtyRequest{
|
||||
RequestType: ssh.PtyRequestType,
|
||||
WantReply: false,
|
||||
TermEnvVar: "kitty",
|
||||
TerminalWidthCharacters: 160,
|
||||
TerminalHeightRows: 200,
|
||||
TerminalWidthPixels: 0,
|
||||
TerminalHeightPixels: 0,
|
||||
EncodedTerminalMode: []byte{},
|
||||
},
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":160,"height":200,"timestamp":1678963623,"env":{"SHELL":"/bin/bash","TERM":"kitty"}}
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"pty-no-env-no-messages-window-change",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&ssh.PtyRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.PtyReqChunkType,
|
||||
},
|
||||
PtyRequest: &sshv1.PtyRequest{
|
||||
RequestType: ssh.PtyRequestType,
|
||||
WantReply: false,
|
||||
TermEnvVar: "kitty",
|
||||
TerminalWidthCharacters: 160,
|
||||
TerminalHeightRows: 200,
|
||||
TerminalWidthPixels: 0,
|
||||
TerminalHeightPixels: 0,
|
||||
EncodedTerminalMode: []byte{},
|
||||
},
|
||||
},
|
||||
&ssh.WindowChangeRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.WindowChangeReqChunkType,
|
||||
},
|
||||
WindowChangeRequest: &sshv1.WindowChangeRequest{
|
||||
RequestType: ssh.WindowChangeRequestType,
|
||||
WantReply: false,
|
||||
TerminalWidthColumns: 220,
|
||||
TerminalHeightRows: 100,
|
||||
},
|
||||
},
|
||||
&ssh.WindowChangeRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.WindowChangeReqChunkType,
|
||||
},
|
||||
WindowChangeRequest: &sshv1.WindowChangeRequest{
|
||||
RequestType: ssh.WindowChangeRequestType,
|
||||
WantReply: false,
|
||||
TerminalWidthColumns: 100,
|
||||
TerminalHeightRows: 500,
|
||||
},
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":220,"height":500,"timestamp":1678963623,"env":{"SHELL":"/bin/bash","TERM":"kitty"}}
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"pty-env-no-messages",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&ssh.PtyRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.PtyReqChunkType,
|
||||
},
|
||||
PtyRequest: &sshv1.PtyRequest{
|
||||
RequestType: ssh.PtyRequestType,
|
||||
WantReply: false,
|
||||
TermEnvVar: "kitty",
|
||||
TerminalWidthCharacters: 160,
|
||||
TerminalHeightRows: 200,
|
||||
TerminalWidthPixels: 0,
|
||||
TerminalHeightPixels: 0,
|
||||
EncodedTerminalMode: []byte{},
|
||||
},
|
||||
},
|
||||
&ssh.EnvRequest{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond + time.Nanosecond)),
|
||||
Type: ssh.EnvReqChunkType,
|
||||
},
|
||||
EnvRequest: &sshv1.EnvRequest{
|
||||
RequestType: ssh.EnvRequestType,
|
||||
WantReply: false,
|
||||
VariableName: "SHELL",
|
||||
VariableValue: "/bin/fish",
|
||||
},
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":160,"height":200,"timestamp":1678963623,"env":{"SHELL":"/bin/fish","TERM":"kitty"}}
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no-pty-no-env-messages",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&ssh.DataChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.DataChunkType,
|
||||
},
|
||||
Data: []byte("ls -lash"),
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(2 * time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":80,"height":120,"timestamp":1678963623,"env":{"SHELL":"/bin/bash","TERM":"xterm"}}
|
||||
[0.000001,"o","ls -lash"]
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no-pty-no-env-multiple-messages",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&ssh.DataChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.DataChunkType,
|
||||
},
|
||||
Data: []byte("ls -lash"),
|
||||
},
|
||||
&ssh.DataChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(2 * time.Microsecond)),
|
||||
Type: ssh.DataChunkType,
|
||||
},
|
||||
Data: []byte("foo\r\n"),
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(2 * time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
[]byte(`{"version":2,"width":80,"height":120,"timestamp":1678963623,"env":{"SHELL":"/bin/bash","TERM":"xterm"}}
|
||||
[0.000001,"o","ls -lash"]
|
||||
[0.000002,"o","foo\r\n"]
|
||||
`),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"nil-requestScanner",
|
||||
nil,
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
nil,
|
||||
errors.New("convert.sshChannelToAsciicast: missing request scanner: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"nil-messageScanner",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
nil,
|
||||
newW(),
|
||||
nil,
|
||||
errors.New("convert.sshChannelToAsciicast: missing message scanner: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"nil-w",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
nil,
|
||||
nil,
|
||||
errors.New("convert.sshChannelToAsciicast: missing read write seeker: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"data-before-header",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&ssh.DataChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Microsecond)),
|
||||
Type: ssh.DataChunkType,
|
||||
},
|
||||
Data: []byte("ls -lash"),
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(2 * time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
nil,
|
||||
errors.New("convert.sshChannelToAsciicast: bsr.ChunkWalk: data chunk before header: malformed bsr data file"),
|
||||
},
|
||||
{
|
||||
"multiple-header",
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newScanner(
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.HeaderChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts),
|
||||
Type: bsr.ChunkHeader,
|
||||
},
|
||||
Compression: bsr.NoCompression,
|
||||
Encryption: bsr.NoEncryption,
|
||||
SessionId: "sess_123456789",
|
||||
},
|
||||
&bsr.EndChunk{
|
||||
BaseChunk: &bsr.BaseChunk{
|
||||
Protocol: ssh.Protocol,
|
||||
Direction: bsr.Inbound,
|
||||
Timestamp: bsr.NewTimestamp(ts.Add(time.Second)),
|
||||
Type: bsr.ChunkEnd,
|
||||
},
|
||||
},
|
||||
),
|
||||
newW(),
|
||||
nil,
|
||||
errors.New("convert.sshChannelToAsciicast: bsr.ChunkWalk: multiple header chunks: malformed bsr data file"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r, err := sshChannelToAsciicast(
|
||||
ctx,
|
||||
tc.requestScanner,
|
||||
tc.messageScanner,
|
||||
tc.w,
|
||||
)
|
||||
if tc.wantErr != nil {
|
||||
require.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
got, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(tc.want), string(got))
|
||||
|
||||
err = r.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user