feat(bsr): Define container and the required fs interfaces
Co-authored-by: Irena Rindos <irenarindos@users.noreply.github.com>
This commit is contained in:
1 parent
9302705d1b
commit
2a31ddd881
56 files changed
+2549
-71
No files matched your search
@@ -3,12 +3,261 @@
|
||||
|
||||
package bsr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
"github.com/hashicorp/boundary/internal/bsr/kms"
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
bsrFile = "%s.bsr"
|
||||
connectionFile = "%s.connection"
|
||||
channelFile = "%s.channel"
|
||||
messagesFile = "messages-%s.data"
|
||||
requestsFile = "requests-%s.data"
|
||||
)
|
||||
|
||||
// SessionMeta contains metadata about a session in a BSR.
|
||||
type SessionMeta struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
// Session is the top level container in a bsr that contains the files for
|
||||
// a recorded session.
|
||||
type Session struct {
|
||||
*container
|
||||
multiplexed bool
|
||||
|
||||
Meta *SessionMeta
|
||||
}
|
||||
|
||||
// NewSession creates a Session container for a given session id.
|
||||
func NewSession(ctx context.Context, meta *SessionMeta, f storage.FS, keys *kms.Keys, options ...Option) (*Session, error) {
|
||||
const op = "bsr.NewSession"
|
||||
|
||||
switch {
|
||||
case is.Nil(meta):
|
||||
return nil, fmt.Errorf("%s: missing session meta: %w", op, ErrInvalidParameter)
|
||||
case meta.Id == "":
|
||||
return nil, fmt.Errorf("%s: missing session id: %w", op, ErrInvalidParameter)
|
||||
case is.Nil(f):
|
||||
return nil, fmt.Errorf("%s: missing storage fs: %w", op, ErrInvalidParameter)
|
||||
case is.Nil(keys):
|
||||
return nil, fmt.Errorf("%s: missing kms keys: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
opts := getOpts(options...)
|
||||
|
||||
c, err := f.New(ctx, fmt.Sprintf(bsrFile, meta.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc, err := newContainer(ctx, sessionContainer, c, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = nc.WriteMeta(ctx, "id", meta.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Session{
|
||||
container: nc,
|
||||
multiplexed: opts.withSupportsMultiplex,
|
||||
Meta: meta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewConnection creates a Connection container for a given connection id.
|
||||
func (s *Session) NewConnection(ctx context.Context, meta *ConnectionMeta) (*Connection, error) {
|
||||
const op = "bsr.(Session).NewConnection"
|
||||
|
||||
switch {
|
||||
case is.Nil(meta):
|
||||
return nil, fmt.Errorf("%s: missing connection meta: %w", op, ErrInvalidParameter)
|
||||
case meta.Id == "":
|
||||
return nil, fmt.Errorf("%s: missing connection id: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf(connectionFile, meta.Id)
|
||||
sc, err := s.container.container.SubContainer(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.WriteMeta(ctx, "connection", name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc, err := newContainer(ctx, connectionContainer, sc, s.keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Connection{
|
||||
container: nc,
|
||||
multiplexed: s.multiplexed,
|
||||
Meta: meta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the Session container.
|
||||
func (s *Session) Close(ctx context.Context) error {
|
||||
return s.container.close(ctx)
|
||||
}
|
||||
|
||||
// ConnectionMeta contains metadata about a connection in a BSR.
|
||||
type ConnectionMeta struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
// Connection is a container in a bsr for a specific connection in a session
|
||||
// container. It contains the files for the recorded connection.
|
||||
type Connection struct {
|
||||
*container
|
||||
multiplexed bool
|
||||
|
||||
Meta *ConnectionMeta
|
||||
}
|
||||
|
||||
// NewChannel creates a Channel container for a given channel id.
|
||||
func (c *Connection) NewChannel(ctx context.Context, meta *ChannelMeta) (*Channel, error) {
|
||||
const op = "bsr.(Connection).NewChannel"
|
||||
|
||||
if !c.multiplexed {
|
||||
return nil, fmt.Errorf("%s: connection cannot make channels: %w", op, ErrNotSupported)
|
||||
}
|
||||
|
||||
switch {
|
||||
case is.Nil(meta):
|
||||
return nil, fmt.Errorf("%s: missing channel meta: %w", op, ErrInvalidParameter)
|
||||
case meta.Id == "":
|
||||
return nil, fmt.Errorf("%s: missing channel id: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf(channelFile, meta.Id)
|
||||
sc, err := c.container.container.SubContainer(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := c.WriteMeta(ctx, "channel", name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nc, err := newContainer(ctx, channelContainer, sc, c.keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Channel{
|
||||
container: nc,
|
||||
Meta: meta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewMessagesWriter creates a writer for recording channel messages.
|
||||
func (c *Connection) NewMessagesWriter(ctx context.Context, dir Direction) (io.Writer, error) {
|
||||
const op = "bsr.(Connection).NewMessagesWriter"
|
||||
|
||||
switch {
|
||||
case !ValidDirection(dir):
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
messagesName := fmt.Sprintf(messagesFile, dir.String())
|
||||
c.container.WriteMeta(ctx, "messages", dir.String())
|
||||
m, err := c.container.create(ctx, messagesName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return checksum.NewFile(ctx, m, c.checksums)
|
||||
}
|
||||
|
||||
// NewRequestsWriter creates a writer for recording connection requests.
|
||||
func (c *Connection) NewRequestsWriter(ctx context.Context, dir Direction) (io.Writer, error) {
|
||||
const op = "bsr.(Connection).NewRequestsWriter"
|
||||
|
||||
switch {
|
||||
case !ValidDirection(dir):
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
requestName := fmt.Sprintf(requestsFile, dir.String())
|
||||
c.container.WriteMeta(ctx, "requests", dir.String())
|
||||
m, err := c.container.create(ctx, requestName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return checksum.NewFile(ctx, m, c.checksums)
|
||||
}
|
||||
|
||||
// Close closes the Connection container.
|
||||
func (c *Connection) Close(ctx context.Context) error {
|
||||
return c.container.close(ctx)
|
||||
}
|
||||
|
||||
// ChannelMeta contains metadata about a channel in a BSR.
|
||||
type ChannelMeta struct {
|
||||
Id string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Channel is a container in a bsr for a specific channel in a session
|
||||
// container. It contains the files for the recorded channel.
|
||||
type Channel struct {
|
||||
*container
|
||||
|
||||
Meta *ChannelMeta
|
||||
}
|
||||
|
||||
// Close closes the Channel container.
|
||||
func (c *Channel) Close(ctx context.Context) error {
|
||||
return c.container.close(ctx)
|
||||
}
|
||||
|
||||
// NewMessagesWriter creates a writer for recording channel messages.
|
||||
func (c *Channel) NewMessagesWriter(ctx context.Context, dir Direction) (io.Writer, error) {
|
||||
const op = "bsr.(Channel).NewMessagesWriter"
|
||||
|
||||
switch {
|
||||
case !ValidDirection(dir):
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
messagesName := fmt.Sprintf(messagesFile, dir.String())
|
||||
c.container.WriteMeta(ctx, "messages", dir.String())
|
||||
m, err := c.container.create(ctx, messagesName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return checksum.NewFile(ctx, m, c.checksums)
|
||||
}
|
||||
|
||||
// NewRequestsWriter creates a writer for recording channel requests.
|
||||
func (c *Channel) NewRequestsWriter(ctx context.Context, dir Direction) (io.Writer, error) {
|
||||
const op = "bsr.(Channel).NewRequestsWriter"
|
||||
|
||||
switch {
|
||||
case !ValidDirection(dir):
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
|
||||
requestName := fmt.Sprintf(requestsFile, dir.String())
|
||||
c.container.WriteMeta(ctx, "requests", dir.String())
|
||||
m, err := c.container.create(ctx, requestName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return checksum.NewFile(ctx, m, c.checksums)
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
|
||||
"github.com/hashicorp/boundary/internal/bsr/kms"
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func assertContainer(ctx context.Context, t *testing.T, path, state string, typ string, fs *fstest.MemContainer, keys *kms.Keys) {
|
||||
t.Helper()
|
||||
|
||||
td := filepath.Join("testdata", t.Name(), state, path)
|
||||
|
||||
// meta
|
||||
wantMeta, err := os.ReadFile(filepath.Join(td, string(typ)+".meta"))
|
||||
require.NoError(t, err, "unable to find test data file")
|
||||
meta, ok := fs.Files[string(typ)+".meta"]
|
||||
require.True(t, ok, "container is missing meta file")
|
||||
assert.Equal(t, string(wantMeta), meta.Buf.String())
|
||||
|
||||
// summary
|
||||
wantSummary, err := os.ReadFile(filepath.Join(td, string(typ)+".summary"))
|
||||
require.NoError(t, err, "unable to find test data file")
|
||||
summary, ok := fs.Files[string(typ)+".summary"]
|
||||
require.True(t, ok, "container is missing summary file")
|
||||
assert.Equal(t, string(wantSummary), summary.Buf.String())
|
||||
|
||||
// SHA256SUM checksums
|
||||
wantChecksums, err := os.ReadFile(filepath.Join(td, "SHA256SUM"))
|
||||
require.NoError(t, err, "unable to find test data file")
|
||||
checksums, ok := fs.Files["SHA256SUM"]
|
||||
require.True(t, ok, "container is missing checksums file")
|
||||
assert.Equal(t, string(wantChecksums), checksums.Buf.String())
|
||||
|
||||
// SHA256SUM.sig signature file
|
||||
sig, ok := fs.Files["SHA256SUM.sig"]
|
||||
require.True(t, ok, "container is missing sig file")
|
||||
switch state {
|
||||
case "closed":
|
||||
want, err := keys.SignWithPrivKey(ctx, wantChecksums)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := &wrapping.SigInfo{}
|
||||
err = proto.Unmarshal(sig.Buf.Bytes(), got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t,
|
||||
cmp.Diff(
|
||||
want,
|
||||
got,
|
||||
cmpopts.IgnoreUnexported(wrapping.SigInfo{}, wrapping.KeyInfo{}),
|
||||
),
|
||||
)
|
||||
default:
|
||||
assert.Equal(t, "", sig.Buf.String())
|
||||
}
|
||||
|
||||
// journal
|
||||
wantJournal, err := os.ReadFile(filepath.Join(td, ".journal"))
|
||||
require.NoError(t, err, "unable to find test data file")
|
||||
journal, ok := fs.Files[".journal"]
|
||||
require.True(t, ok, "container is missing journal file")
|
||||
assert.Equal(t, string(wantJournal), journal.Buf.String())
|
||||
}
|
||||
|
||||
type connection struct {
|
||||
mem *fstest.MemContainer
|
||||
conn *bsr.Connection
|
||||
id string
|
||||
|
||||
channels []*channel
|
||||
files []*file
|
||||
}
|
||||
|
||||
type channel struct {
|
||||
mem *fstest.MemContainer
|
||||
channel *bsr.Channel
|
||||
id string
|
||||
|
||||
files []*file
|
||||
}
|
||||
|
||||
type file struct {
|
||||
mem *fstest.MemFile
|
||||
file io.Writer
|
||||
}
|
||||
|
||||
type createConn struct {
|
||||
id string
|
||||
channels []createChannel
|
||||
files []createFile
|
||||
}
|
||||
|
||||
type createChannel struct {
|
||||
id string
|
||||
files []createFile
|
||||
}
|
||||
|
||||
type createFile struct {
|
||||
typ string
|
||||
dir bsr.Direction
|
||||
}
|
||||
|
||||
func TestBsr(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
id string
|
||||
opts []bsr.Option
|
||||
c *fstest.MemFS
|
||||
keys *kms.Keys
|
||||
conns []createConn
|
||||
}{
|
||||
{
|
||||
"session_not_multiplexed",
|
||||
"session_123456789",
|
||||
[]bsr.Option{},
|
||||
fstest.NewMemFS(),
|
||||
func() *kms.Keys {
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789")
|
||||
require.NoError(t, err)
|
||||
return keys
|
||||
}(),
|
||||
[]createConn{
|
||||
{
|
||||
"conn_1",
|
||||
nil,
|
||||
[]createFile{
|
||||
{"messages", bsr.Inbound},
|
||||
{"messages", bsr.Outbound},
|
||||
{"requests", bsr.Inbound},
|
||||
{"requests", bsr.Outbound},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"session_multiplexed",
|
||||
"session_123456789",
|
||||
[]bsr.Option{bsr.WithSupportsMultiplex(true)},
|
||||
fstest.NewMemFS(),
|
||||
func() *kms.Keys {
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789")
|
||||
require.NoError(t, err)
|
||||
return keys
|
||||
}(),
|
||||
[]createConn{
|
||||
{
|
||||
"conn_1",
|
||||
[]createChannel{
|
||||
{
|
||||
"chan_1",
|
||||
[]createFile{
|
||||
{"messages", bsr.Inbound},
|
||||
{"requests", bsr.Inbound},
|
||||
{"messages", bsr.Outbound},
|
||||
{"requests", bsr.Outbound},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]createFile{
|
||||
{"requests", bsr.Inbound},
|
||||
{"requests", bsr.Outbound},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: tc.id}, tc.c, tc.keys, tc.opts...)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, s)
|
||||
|
||||
sContainer, ok := tc.c.Containers[tc.id+".bsr"]
|
||||
require.True(t, ok)
|
||||
|
||||
assertContainer(ctx, t, "", "opened", "session", sContainer, tc.keys)
|
||||
|
||||
createdConnections := make([]*connection, 0)
|
||||
|
||||
// create all the things
|
||||
for _, conn := range tc.conns {
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: conn.id})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, c)
|
||||
|
||||
cContainer, ok := sContainer.Sub[conn.id+".connection"]
|
||||
require.True(t, ok)
|
||||
|
||||
assertContainer(ctx, t, conn.id, "opened", "connection", cContainer, tc.keys)
|
||||
|
||||
ff := make([]*file, 0, len(conn.files))
|
||||
for _, f := range conn.files {
|
||||
var w io.Writer
|
||||
var err error
|
||||
switch f.typ {
|
||||
case "messages":
|
||||
w, err = c.NewMessagesWriter(ctx, f.dir)
|
||||
case "requests":
|
||||
w, err = c.NewRequestsWriter(ctx, f.dir)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String())
|
||||
memf, ok := cContainer.Files[fname]
|
||||
require.True(t, ok, "file %s not in container %s", fname, cContainer.Name)
|
||||
|
||||
require.NoError(t, err)
|
||||
ff = append(ff, &file{
|
||||
mem: memf,
|
||||
file: w,
|
||||
})
|
||||
}
|
||||
|
||||
createdChannels := make([]*channel, 0, len(conn.channels))
|
||||
for _, chann := range conn.channels {
|
||||
ch, err := c.NewChannel(ctx, &bsr.ChannelMeta{Id: chann.id})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ch)
|
||||
|
||||
chContainer, ok := cContainer.Sub[chann.id+".channel"]
|
||||
require.True(t, ok)
|
||||
|
||||
assertContainer(ctx, t, filepath.Join(conn.id, chann.id), "opened", "channel", chContainer, tc.keys)
|
||||
|
||||
ff := make([]*file, 0, len(chann.files))
|
||||
for _, f := range chann.files {
|
||||
var w io.Writer
|
||||
var err error
|
||||
switch f.typ {
|
||||
case "messages":
|
||||
w, err = ch.NewMessagesWriter(ctx, f.dir)
|
||||
case "requests":
|
||||
w, err = ch.NewRequestsWriter(ctx, f.dir)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String())
|
||||
memf, ok := chContainer.Files[fname]
|
||||
require.True(t, ok, "file %s not in container %s", fname, chContainer.Name)
|
||||
|
||||
require.NoError(t, err)
|
||||
ff = append(ff, &file{
|
||||
mem: memf,
|
||||
file: w,
|
||||
})
|
||||
}
|
||||
createdChannels = append(createdChannels, &channel{
|
||||
mem: chContainer,
|
||||
channel: ch,
|
||||
id: chann.id,
|
||||
files: ff,
|
||||
})
|
||||
}
|
||||
createdConnections = append(createdConnections, &connection{
|
||||
mem: cContainer,
|
||||
conn: c,
|
||||
id: conn.id,
|
||||
channels: createdChannels,
|
||||
files: ff,
|
||||
})
|
||||
}
|
||||
|
||||
// now close all the things that where created.
|
||||
for _, conn := range createdConnections {
|
||||
for _, channel := range conn.channels {
|
||||
for _, f := range channel.files {
|
||||
v, ok := f.file.(io.Closer)
|
||||
require.True(t, ok, "file is not a io.Closer")
|
||||
err = v.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = channel.channel.Close(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertContainer(ctx, t, filepath.Join(conn.id, channel.id), "closed", "channel", channel.mem, tc.keys)
|
||||
}
|
||||
|
||||
for _, f := range conn.files {
|
||||
v, ok := f.file.(io.Closer)
|
||||
require.True(t, ok, "file is not a io.Closer")
|
||||
err = v.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = conn.conn.Close(ctx)
|
||||
require.NoError(t, err)
|
||||
assertContainer(ctx, t, conn.id, "closed", "connection", conn.mem, tc.keys)
|
||||
}
|
||||
|
||||
err = s.Close(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertContainer(ctx, t, "", "closed", "session", sContainer, tc.keys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSessionErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
meta *bsr.SessionMeta
|
||||
f storage.FS
|
||||
keys *kms.Keys
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
"nil-meta",
|
||||
nil,
|
||||
&fstest.MemFS{},
|
||||
keys,
|
||||
errors.New("bsr.NewSession: missing session meta: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"empty-session-id",
|
||||
&bsr.SessionMeta{Id: ""},
|
||||
&fstest.MemFS{},
|
||||
keys,
|
||||
errors.New("bsr.NewSession: missing session id: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"nil-fs",
|
||||
&bsr.SessionMeta{Id: "session"},
|
||||
nil,
|
||||
keys,
|
||||
errors.New("bsr.NewSession: missing storage fs: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"nil-keys",
|
||||
&bsr.SessionMeta{Id: "session"},
|
||||
&fstest.MemFS{},
|
||||
nil,
|
||||
errors.New("bsr.NewSession: missing kms keys: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"fs-new-error",
|
||||
&bsr.SessionMeta{Id: "session"},
|
||||
fstest.NewMemFS(fstest.WithNewFunc(func(_ context.Context, _ string) (storage.Container, error) {
|
||||
return nil, errors.New("fs new error")
|
||||
})),
|
||||
keys,
|
||||
errors.New("fs new error"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := bsr.NewSession(ctx, tc.meta, tc.f, tc.keys)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.wantError.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConnectionErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
session *bsr.Session
|
||||
meta *bsr.ConnectionMeta
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
"nil-meta",
|
||||
func() *bsr.Session {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys)
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}(),
|
||||
nil,
|
||||
errors.New("bsr.(Session).NewConnection: missing connection meta: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"empty-connection-id",
|
||||
func() *bsr.Session {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys)
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}(),
|
||||
&bsr.ConnectionMeta{Id: ""},
|
||||
errors.New("bsr.(Session).NewConnection: missing connection id: invalid parameter"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.session.NewConnection(ctx, tc.meta)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.wantError.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewChannelErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
connection *bsr.Connection
|
||||
meta *bsr.ChannelMeta
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
"nil-meta",
|
||||
func() *bsr.Connection {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}(),
|
||||
nil,
|
||||
errors.New("bsr.(Connection).NewChannel: missing channel meta: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"empty-connection-id",
|
||||
func() *bsr.Connection {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}(),
|
||||
&bsr.ChannelMeta{Id: ""},
|
||||
errors.New("bsr.(Connection).NewChannel: missing channel id: invalid parameter"),
|
||||
},
|
||||
{
|
||||
"not-multiplexed",
|
||||
func() *bsr.Connection {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(false))
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}(),
|
||||
&bsr.ChannelMeta{Id: ""},
|
||||
errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"),
|
||||
},
|
||||
{
|
||||
"not-multiplexed-default",
|
||||
func() *bsr.Connection {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}(),
|
||||
&bsr.ChannelMeta{Id: ""},
|
||||
errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.connection.NewChannel(ctx, tc.meta)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.wantError.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelNewMessagesWriterErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
channel *bsr.Channel
|
||||
dir bsr.Direction
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
"invalid-dir",
|
||||
func() *bsr.Channel {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
|
||||
ch, err := c.NewChannel(ctx, &bsr.ChannelMeta{Id: "channel"})
|
||||
require.NoError(t, err)
|
||||
return ch
|
||||
}(),
|
||||
bsr.Direction(uint8(255)),
|
||||
errors.New("bsr.(Channel).NewMessagesWriter: invalid direction: invalid parameter"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.channel.NewMessagesWriter(ctx, tc.dir)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.wantError.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelNewRequestsWriterErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
channel *bsr.Channel
|
||||
dir bsr.Direction
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
"invalid-dir",
|
||||
func() *bsr.Channel {
|
||||
s, err := bsr.NewSession(ctx, &bsr.SessionMeta{Id: "session"}, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := s.NewConnection(ctx, &bsr.ConnectionMeta{Id: "connection"})
|
||||
require.NoError(t, err)
|
||||
|
||||
ch, err := c.NewChannel(ctx, &bsr.ChannelMeta{Id: "channel"})
|
||||
require.NoError(t, err)
|
||||
return ch
|
||||
}(),
|
||||
bsr.Direction(uint8(255)),
|
||||
errors.New("bsr.(Channel).NewRequestsWriter: invalid direction: invalid parameter"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.channel.NewRequestsWriter(ctx, tc.dir)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tc.wantError.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr
|
||||
|
||||
// ByteSource identifies whether bytes are flowing from the user to target (client)
|
||||
// or target to user (server)
|
||||
type ByteSource uint8
|
||||
|
||||
// ByteDirections
|
||||
const (
|
||||
UnknownByteSource ByteSource = iota
|
||||
Client
|
||||
Server
|
||||
)
|
||||
|
||||
func (d ByteSource) String() string {
|
||||
switch d {
|
||||
case Client:
|
||||
return "client"
|
||||
case Server:
|
||||
return "server"
|
||||
default:
|
||||
return "unknown bytesource"
|
||||
}
|
||||
}
|
||||
|
||||
// ValidByteSource checks if a given ByteSource is valid.
|
||||
func ValidByteSource(d ByteSource) bool {
|
||||
switch d {
|
||||
case Client, Server:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidByteSource(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in bsr.ByteSource
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
bsr.Client.String(),
|
||||
bsr.Client,
|
||||
true,
|
||||
},
|
||||
{
|
||||
bsr.Server.String(),
|
||||
bsr.Server,
|
||||
true,
|
||||
},
|
||||
{
|
||||
bsr.UnknownByteSource.String(),
|
||||
bsr.UnknownByteSource,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"something else",
|
||||
bsr.ByteSource(255),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := bsr.ValidByteSource(tc.in)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteSourceString(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in bsr.ByteSource
|
||||
want string
|
||||
}{
|
||||
{
|
||||
bsr.Client.String(),
|
||||
bsr.Client,
|
||||
"client",
|
||||
},
|
||||
{
|
||||
bsr.Server.String(),
|
||||
bsr.Server,
|
||||
"server",
|
||||
},
|
||||
{
|
||||
bsr.UnknownByteSource.String(),
|
||||
bsr.UnknownByteSource,
|
||||
"unknown bytesource",
|
||||
},
|
||||
{
|
||||
"something else",
|
||||
bsr.ByteSource(255),
|
||||
"unknown bytesource",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.in.String()
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/journal"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/sign"
|
||||
"github.com/hashicorp/boundary/internal/bsr/kms"
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
const (
|
||||
metaFile = "%s.meta"
|
||||
summaryFile = "%s.summary"
|
||||
checksumFile = "SHA256SUM"
|
||||
sigFile = "SHA256SUM.sig"
|
||||
journalFile = ".journal"
|
||||
)
|
||||
|
||||
// ContainerType defines the type of container.
|
||||
type containerType string
|
||||
|
||||
// Valid container types.
|
||||
const (
|
||||
sessionContainer containerType = "session"
|
||||
connectionContainer containerType = "connection"
|
||||
channelContainer containerType = "channel"
|
||||
)
|
||||
|
||||
// container contains a group of files in a BSR.
|
||||
// Each container has corresponding .meta, .summary, SHA256SUM, and SHA256SUM.sig files.
|
||||
type container struct {
|
||||
container storage.Container
|
||||
journal *journal.Journal
|
||||
|
||||
metaName string
|
||||
sumName string
|
||||
meta *checksum.File
|
||||
sum *checksum.File
|
||||
checksums *sign.File
|
||||
sigs storage.File
|
||||
|
||||
keys *kms.Keys
|
||||
}
|
||||
|
||||
// newContainer creates a container for the given type backed by the provide storage.Container.
|
||||
func newContainer(ctx context.Context, t containerType, c storage.Container, keys *kms.Keys) (*container, error) {
|
||||
j, err := c.OpenFile(ctx, journalFile,
|
||||
storage.WithCreateFile(),
|
||||
storage.WithFileAccessMode(storage.WriteOnly),
|
||||
storage.WithCloseSyncMode(storage.NoSync))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jj, err := journal.New(ctx, j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := &container{
|
||||
container: c,
|
||||
journal: jj,
|
||||
keys: keys,
|
||||
}
|
||||
|
||||
cc.sigs, err = cc.create(ctx, sigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs, err := cc.create(ctx, checksumFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc.checksums, err = sign.NewFile(ctx, cs, cc.sigs, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc.metaName = fmt.Sprintf(metaFile, t)
|
||||
meta, err := cc.create(ctx, cc.metaName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc.meta, err = checksum.NewFile(ctx, meta, cc.checksums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc.sumName = fmt.Sprintf(summaryFile, t)
|
||||
sum, err := cc.create(ctx, cc.sumName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc.sum, err = checksum.NewFile(ctx, sum, cc.checksums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// Create creates a new file in the container for writing.
|
||||
func (c *container) create(ctx context.Context, s string) (storage.File, error) {
|
||||
c.journal.Record("CREATING", s)
|
||||
f, err := c.container.Create(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jf, err := journal.NewFile(ctx, f, c.journal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer c.journal.Record("CREATED", s)
|
||||
return jf, nil
|
||||
}
|
||||
|
||||
// writeMetaString writes a string to the containers meta file.
|
||||
func (c *container) writeMetaString(_ context.Context, s string) (int, error) {
|
||||
return c.meta.WriteString(s)
|
||||
}
|
||||
|
||||
// writeMetaLine writes a new line terminated line to the container's meta file.
|
||||
func (c *container) writeMetaLine(_ context.Context, s string) (int, error) {
|
||||
return c.meta.WriteString(s + "\n")
|
||||
}
|
||||
|
||||
// WriteMeta writes a new line terminated key : value pair to the container's meta file
|
||||
func (c *container) WriteMeta(_ context.Context, k, v string) (int, error) {
|
||||
return c.meta.WriteString(fmt.Sprintf("%s: %s\n", k, v))
|
||||
}
|
||||
|
||||
// writeSummaryString writes a string to the container's summary file.
|
||||
func (c *container) writeSummaryString(_ context.Context, s string) (int, error) {
|
||||
return c.sum.WriteString(s)
|
||||
}
|
||||
|
||||
// WriteSummary writes a new line terminated key : value pair to the container's summary file
|
||||
func (c *container) WriteSummary(_ context.Context, k, v string) (int, error) {
|
||||
return c.sum.WriteString(fmt.Sprintf("%s: %s\n", k, v))
|
||||
}
|
||||
|
||||
// WriteSummaryLine writes a new line terminated string to the container's summary file.
|
||||
func (c *container) WriteSummaryLine(_ context.Context, s string) (int, error) {
|
||||
return c.sum.WriteString(s + "\n")
|
||||
}
|
||||
|
||||
// WriteBinaryChecksum writes a checksum for a binary file to the checksum file.
|
||||
func (c *container) WriteBinaryChecksum(_ context.Context, sum []byte, fname string) (int, error) {
|
||||
return c.checksums.WriteString(fmt.Sprintf("%x *%s\n", sum, fname))
|
||||
}
|
||||
|
||||
// close closes a container, closing the underlying files in a container.
|
||||
func (c *container) close(_ context.Context) error {
|
||||
const op = "bsr.(container).close"
|
||||
|
||||
var closeErrors *multierror.Error
|
||||
|
||||
if err := c.meta.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
if err := c.sum.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
if err := c.checksums.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
if err := c.sigs.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
if err := c.journal.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
if err := c.container.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrUnknown represents an unknown error
|
||||
var ErrUnknown = errors.New("unknown error")
|
||||
|
||||
// ErrInvalidParameter represents an invalid parameter error
|
||||
var ErrInvalidParameter = errors.New("invalid parameter")
|
||||
|
||||
// ErrNotSupported represents an operation that is not supported for a
|
||||
// particular protocol.
|
||||
var ErrNotSupported = errors.New("not supported by protocol")
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package checksum provides a wrapper to compute a checksum on a writable file
|
||||
// while it is being written to, and record the final checksum when the file is
|
||||
// closed.
|
||||
package checksum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
"github.com/hashicorp/go-kms-wrapping/v2/extras/crypto"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
const (
|
||||
// A line for a checksum that can be read by sha256sum.
|
||||
// Note that there are two spaces between the checksum and the file.
|
||||
checksumLine = "%s %s\n"
|
||||
)
|
||||
|
||||
var errInvalidParameter = errors.New("invalid parameter")
|
||||
|
||||
type writerFile interface {
|
||||
fs.File
|
||||
io.WriteCloser
|
||||
io.StringWriter
|
||||
}
|
||||
|
||||
// File is a writable file that will compute its checksum while it is written
|
||||
// to. When closed, it will write its checksum to the provided io.Writer.
|
||||
// The checksum is written in a format so that it can be verified by sha256sum.
|
||||
type File struct {
|
||||
// ctx is used to provide a context to other functions when implementing
|
||||
// std interfaces that to not take a context as an arg.
|
||||
ctx context.Context
|
||||
|
||||
// Performs the checksuming, provides implementations for Write and
|
||||
// WriteString
|
||||
*crypto.Sha256SumWriter
|
||||
|
||||
checksumWriter io.Writer
|
||||
|
||||
underlying fs.File
|
||||
}
|
||||
|
||||
// NewFile wraps the provided writerFile in a checksumed File. When the file is
|
||||
// closed, its checksum will be written to cs.
|
||||
func NewFile(ctx context.Context, f writerFile, cs io.Writer) (*File, error) {
|
||||
const op = "checksum.NewFile"
|
||||
|
||||
switch {
|
||||
case is.Nil(f):
|
||||
return nil, fmt.Errorf("%s: missing writable file: %w", op, errInvalidParameter)
|
||||
case is.Nil(cs):
|
||||
return nil, fmt.Errorf("%s: missing checksum writer: %w", op, errInvalidParameter)
|
||||
}
|
||||
|
||||
wrapped, err := crypto.NewSha256SumWriter(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
ctx: ctx,
|
||||
checksumWriter: cs,
|
||||
Sha256SumWriter: wrapped,
|
||||
underlying: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Stat returns the fs.FileInfo for the underlying fs.File.
|
||||
func (f *File) Stat() (fs.FileInfo, error) {
|
||||
return f.underlying.Stat()
|
||||
}
|
||||
|
||||
// Read reads from the underlying fs.File.
|
||||
func (f *File) Read(b []byte) (int, error) {
|
||||
return f.underlying.Read(b)
|
||||
}
|
||||
|
||||
// Close closes the Sha256SumWriter, computes the checksum and writes it to
|
||||
// f.checksumWriter
|
||||
func (f *File) Close() error {
|
||||
const op = "checksum.(File).Close"
|
||||
|
||||
var closeErrors *multierror.Error
|
||||
|
||||
// f.Sha256SumWriter will close f.underlying
|
||||
if err := f.Sha256SumWriter.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
sum, err := f.Sha256SumWriter.Sum(f.ctx, crypto.WithHexEncoding(true))
|
||||
if err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
if _, err := f.checksumWriter.Write([]byte(fmt.Sprintf(checksumLine, sum, s.Name()))); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
var _ writerFile = (*File)(nil)
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package checksum_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type bufWriter interface {
|
||||
io.Writer
|
||||
String() string
|
||||
}
|
||||
|
||||
type badWriter struct{}
|
||||
|
||||
func (*badWriter) Write(_ []byte) (int, error) {
|
||||
return 0, errors.New("write failed")
|
||||
}
|
||||
|
||||
func (*badWriter) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// testSum created via: echo -n "test-string" | sha256sum
|
||||
const (
|
||||
testSum = "ffe65f1d98fafedea3514adc956c8ada5980c6c5d2552fd61f48401aefd5c00e"
|
||||
testString = "test-string"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
f *fstest.MemFile
|
||||
cs bufWriter
|
||||
write string
|
||||
want string
|
||||
wantNewErr error
|
||||
wantCloseErr error
|
||||
expectCheckSumOnErr bool
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil-file",
|
||||
nil,
|
||||
bytes.NewBuffer([]byte{}),
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
errors.New("checksum.NewFile: missing writable file: invalid parameter"),
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil-checksum-writer",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
nil,
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
errors.New("checksum.NewFile: missing checksum writer: invalid parameter"),
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"close-error",
|
||||
fstest.NewMemFile("test", 0o664, fstest.WithCloseFunc(func() error { return errors.New("close error") })),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
nil,
|
||||
func() error {
|
||||
var wantErrors *multierror.Error
|
||||
wantErrors = multierror.Append(wantErrors, errors.New("checksum.(File).Close: crypto.(Sha256SumWriter).Close: close error"))
|
||||
return wantErrors.ErrorOrNil()
|
||||
}(),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"stat-error",
|
||||
fstest.NewMemFile("test", 0o664, fstest.WithStatFunc(func() (fs.FileInfo, error) { return nil, errors.New("stat error") })),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
nil,
|
||||
func() error {
|
||||
var wantErrors *multierror.Error
|
||||
wantErrors = multierror.Append(wantErrors, errors.New("checksum.(File).Close: stat error"))
|
||||
return wantErrors.ErrorOrNil()
|
||||
}(),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"write-error",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
&badWriter{},
|
||||
testString,
|
||||
testSum + " test\n",
|
||||
nil,
|
||||
func() error {
|
||||
var wantErrors *multierror.Error
|
||||
wantErrors = multierror.Append(wantErrors, errors.New("checksum.(File).Close: write failed"))
|
||||
return wantErrors.ErrorOrNil()
|
||||
}(),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, err := checksum.NewFile(ctx, tc.f, tc.cs)
|
||||
if tc.wantNewErr != nil {
|
||||
require.EqualError(t, tc.wantNewErr, err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.WriteString(tc.write)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.Close()
|
||||
if tc.wantCloseErr != nil {
|
||||
require.EqualError(t, err, tc.wantCloseErr.Error())
|
||||
|
||||
if tc.expectCheckSumOnErr {
|
||||
require.Equal(t, tc.want, tc.cs.String())
|
||||
} else {
|
||||
require.Equal(t, "", tc.cs.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, tc.f.Closed)
|
||||
require.Equal(t, tc.want, tc.cs.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package fs defines the interfaces bsr needs with a storage system.
|
||||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// FS is a filesystem for creating or reading BSRs.
|
||||
type FS interface {
|
||||
New(context.Context, string) (Container, error)
|
||||
}
|
||||
|
||||
// SyncMode is use to determine how a file is synced when closed.
|
||||
type SyncMode uint8
|
||||
|
||||
// Valid sync modes.
|
||||
const (
|
||||
Asynchronous SyncMode = iota
|
||||
Synchronous
|
||||
NoSync
|
||||
)
|
||||
|
||||
// GetOpts - iterate the inbound Options and return a struct.
|
||||
func GetOpts(opt ...Option) Options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func getDefaultOptions() Options {
|
||||
return Options{
|
||||
WithCloseSyncMode: Asynchronous,
|
||||
}
|
||||
}
|
||||
|
||||
// Options are fs options.
|
||||
type Options struct {
|
||||
WithCloseSyncMode SyncMode
|
||||
}
|
||||
|
||||
// Option is an fs option.
|
||||
type Option func(*Options)
|
||||
|
||||
// WithCloseSyncMode sets how a file is synced when closed.
|
||||
func WithCloseSyncMode(m SyncMode) Option {
|
||||
return func(o *Options) {
|
||||
o.WithCloseSyncMode = m
|
||||
}
|
||||
}
|
||||
|
||||
// A Container is a filesystem abstraction that can create files or other containers.
|
||||
type Container interface {
|
||||
io.Closer
|
||||
Create(context.Context, string) (WriterFile, error)
|
||||
OpenFile(context.Context, string, ...Option) (WriterFile, error)
|
||||
SubContainer(context.Context, string) (Container, error)
|
||||
}
|
||||
|
||||
// WriterFile is a file that can be written to.
|
||||
type WriterFile interface {
|
||||
fs.File
|
||||
io.Writer
|
||||
io.StringWriter
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package fstest provides test implementations of a the fs interfaces.
|
||||
package fstest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sfs "io/fs"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
)
|
||||
|
||||
// MemFS is a storage.FS that only resides in memory.
|
||||
type MemFS struct {
|
||||
Containers map[string]*MemContainer
|
||||
|
||||
newFunc NewFunc
|
||||
}
|
||||
|
||||
// NewMemFS creates a MemFS. It supports WithNewFunc.
|
||||
func NewMemFS(options ...Option) *MemFS {
|
||||
opts := getOpts(options...)
|
||||
|
||||
return &MemFS{
|
||||
Containers: make(map[string]*MemContainer),
|
||||
newFunc: opts.withNewFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a storage.Container in the MemFS.
|
||||
func (m *MemFS) New(ctx context.Context, n string) (storage.Container, error) {
|
||||
if m.newFunc != nil {
|
||||
return m.newFunc(ctx, n)
|
||||
}
|
||||
if m.Containers == nil {
|
||||
m.Containers = make(map[string]*MemContainer)
|
||||
}
|
||||
if _, exists := m.Containers[n]; exists {
|
||||
return nil, fmt.Errorf("container %s already exists", n)
|
||||
}
|
||||
c := &MemContainer{
|
||||
Name: n,
|
||||
Sub: make(map[string]*MemContainer),
|
||||
Files: make(map[string]*MemFile),
|
||||
}
|
||||
m.Containers[n] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// MemContainer is a storage.Container that resides in memory.
|
||||
type MemContainer struct {
|
||||
Name string
|
||||
|
||||
Sub map[string]*MemContainer
|
||||
Files map[string]*MemFile
|
||||
|
||||
Closed bool
|
||||
}
|
||||
|
||||
// Close closes the container.
|
||||
func (m *MemContainer) Close() error {
|
||||
m.Closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create makes a new storage.File in the container.
|
||||
func (m *MemContainer) Create(_ context.Context, n string) (storage.File, error) {
|
||||
if m.Closed {
|
||||
return nil, fmt.Errorf("create on closed container")
|
||||
}
|
||||
if _, exists := m.Files[n]; exists {
|
||||
return nil, fmt.Errorf("file %s already exists", n)
|
||||
}
|
||||
f := &MemFile{
|
||||
name: n,
|
||||
Buf: bytes.NewBuffer([]byte{}),
|
||||
mode: sfs.ModeAppend,
|
||||
}
|
||||
m.Files[n] = f
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// OpenFile creates a storage.File in the container using the provided options
|
||||
// It supports WithCloseSyncMode.
|
||||
func (m *MemContainer) OpenFile(_ context.Context, n string, option ...storage.Option) (storage.File, error) {
|
||||
opts := storage.GetOpts(option...)
|
||||
if m.Closed {
|
||||
return nil, fmt.Errorf("create on closed container")
|
||||
}
|
||||
if _, exists := m.Files[n]; exists {
|
||||
return nil, fmt.Errorf("file %s already exists", n)
|
||||
}
|
||||
|
||||
var mode sfs.FileMode
|
||||
|
||||
switch opts.WithCloseSyncMode {
|
||||
case storage.NoSync:
|
||||
mode = sfs.ModeAppend | sfs.ModeTemporary
|
||||
default:
|
||||
mode = sfs.ModeAppend
|
||||
}
|
||||
|
||||
f := &MemFile{
|
||||
name: n,
|
||||
Buf: bytes.NewBuffer([]byte{}),
|
||||
mode: mode,
|
||||
}
|
||||
m.Files[n] = f
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// SubContainer creates a new storage.Container in the container.
|
||||
func (m *MemContainer) SubContainer(_ context.Context, n string, _ ...storage.Option) (storage.Container, error) {
|
||||
if _, exists := m.Sub[n]; exists {
|
||||
return nil, fmt.Errorf("container %s already exists", n)
|
||||
}
|
||||
c := &MemContainer{
|
||||
Name: n,
|
||||
Sub: make(map[string]*MemContainer),
|
||||
Files: make(map[string]*MemFile),
|
||||
}
|
||||
m.Sub[n] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// memFileInfo implements storage.FileInfo
|
||||
type memFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode sfs.FileMode
|
||||
mod time.Time
|
||||
}
|
||||
|
||||
func (m *memFileInfo) Name() string { return m.name }
|
||||
func (m *memFileInfo) Size() int64 { return m.size }
|
||||
func (m *memFileInfo) Mode() sfs.FileMode { return m.mode }
|
||||
func (m *memFileInfo) ModTime() time.Time { return m.mod }
|
||||
func (m *memFileInfo) IsDir() bool { return false }
|
||||
func (m *memFileInfo) Sys() any { return nil }
|
||||
|
||||
// MemFile is a storage.File that resides in memory.
|
||||
type MemFile struct {
|
||||
name string
|
||||
Buf *bytes.Buffer
|
||||
mode sfs.FileMode
|
||||
modtime time.Time
|
||||
|
||||
statFunc StatFunc
|
||||
closeFunc CloseFunc
|
||||
|
||||
Closed bool
|
||||
}
|
||||
|
||||
// NewMemFile creates a MemFile. It supports WithCloseFunc and WithStatFunc
|
||||
// to customize the behavior of Close and Stat.
|
||||
func NewMemFile(n string, mode sfs.FileMode, options ...Option) *MemFile {
|
||||
opts := getOpts(options...)
|
||||
|
||||
return &MemFile{
|
||||
name: n,
|
||||
Buf: bytes.NewBuffer([]byte{}),
|
||||
mode: mode,
|
||||
statFunc: opts.withStatFunc,
|
||||
closeFunc: opts.withCloseFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// Stat returns the FileInfo for the file.
|
||||
func (m *MemFile) Stat() (sfs.FileInfo, error) {
|
||||
if m.statFunc != nil {
|
||||
return m.statFunc()
|
||||
}
|
||||
return &memFileInfo{
|
||||
name: m.name,
|
||||
size: int64(m.Buf.Len()),
|
||||
mode: m.mode,
|
||||
mod: m.modtime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MemFile) Read(p []byte) (int, error) {
|
||||
return m.Buf.Read(p)
|
||||
}
|
||||
|
||||
// Close closes the file.
|
||||
func (m *MemFile) Close() error {
|
||||
if m.closeFunc != nil {
|
||||
return m.closeFunc()
|
||||
}
|
||||
m.Closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteString implements io.StringWriter.
|
||||
func (m *MemFile) WriteString(s string) (n int, err error) {
|
||||
return m.Write([]byte(s))
|
||||
}
|
||||
|
||||
func (m *MemFile) Write(p []byte) (n int, err error) {
|
||||
if m.Closed {
|
||||
return 0, fmt.Errorf("write on closed file")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
m.modtime = time.Now()
|
||||
}()
|
||||
return m.Buf.Write(p)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package fstest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/storage"
|
||||
)
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments
|
||||
type Option func(*options)
|
||||
|
||||
// NewFunc is a function called in New.
|
||||
// It can be used to simulate an error during New.
|
||||
type NewFunc func(context.Context, string) (storage.Container, error)
|
||||
|
||||
// CloseFunc is a function called at close.
|
||||
// It can be used to simulate an error during Close.
|
||||
type CloseFunc func() error
|
||||
|
||||
// StatFunc is a function called for Stat.
|
||||
// It can be used to simulate an error on calls to Stat
|
||||
type StatFunc func() (fs.FileInfo, error)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withNewFunc NewFunc
|
||||
withCloseFunc CloseFunc
|
||||
withStatFunc StatFunc
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{
|
||||
withNewFunc: nil,
|
||||
withCloseFunc: nil,
|
||||
withStatFunc: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// WithNewFunc is used to provide a custom new function.
|
||||
func WithNewFunc(f NewFunc) Option {
|
||||
return func(o *options) {
|
||||
o.withNewFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// WithCloseFunc is used to provide a custom close function.
|
||||
func WithCloseFunc(f CloseFunc) Option {
|
||||
return func(o *options) {
|
||||
o.withCloseFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatFunc is used to provide a custom Stat function.
|
||||
func WithStatFunc(f StatFunc) Option {
|
||||
return func(o *options) {
|
||||
o.withStatFunc = f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package journal provides a writer that uses a journal file to aide in recovery.
|
||||
package journal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
)
|
||||
|
||||
var errInvalidParameter = errors.New("invalid parameter")
|
||||
|
||||
type writerFile interface {
|
||||
fs.File
|
||||
io.WriteCloser
|
||||
io.StringWriter
|
||||
}
|
||||
|
||||
// Journal is used to record meta data about the operations that will be and
|
||||
// have been performed on bsr containers and files. This can be used to aide in
|
||||
// recovery if a system crashes while writing a bsr.
|
||||
type Journal struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// New creates a Journal that is written to the provided io.Writer.
|
||||
func New(_ context.Context, w io.Writer) (*Journal, error) {
|
||||
const op = "journal.New"
|
||||
|
||||
switch {
|
||||
case is.Nil(w):
|
||||
return nil, fmt.Errorf("%s: missing writer: %w", op, errInvalidParameter)
|
||||
}
|
||||
return &Journal{
|
||||
Writer: w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close optionally closes the underlying io.Writer if it also implements
|
||||
// io.Closer.
|
||||
func (j *Journal) Close() error {
|
||||
var i interface{} = j.Writer
|
||||
v, ok := i.(io.WriteCloser)
|
||||
if ok {
|
||||
if err := v.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Record writes the entry to the journal.
|
||||
func (j *Journal) Record(op, f string) error {
|
||||
_, err := j.Write([]byte(fmt.Sprintf("%s %s\n", op, f)))
|
||||
return err
|
||||
}
|
||||
|
||||
// File is a writable file that will update a Journal as it closed.
|
||||
type File struct {
|
||||
j *Journal
|
||||
writerFile
|
||||
}
|
||||
|
||||
// NewFile wraps the provided writable file in a journaled File. When the file
|
||||
// is closed, the journal will be written to.
|
||||
func NewFile(_ context.Context, f writerFile, j *Journal) (*File, error) {
|
||||
const op = "journal.NewFile"
|
||||
switch {
|
||||
case is.Nil(f):
|
||||
return nil, fmt.Errorf("%s: missing writable file: %w", op, errInvalidParameter)
|
||||
case is.Nil(j):
|
||||
return nil, fmt.Errorf("%s: missing journal: %w", op, errInvalidParameter)
|
||||
}
|
||||
|
||||
return &File{
|
||||
j: j,
|
||||
writerFile: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying file, writing to the journal prior to and after
|
||||
// closing.
|
||||
func (f *File) Close() error {
|
||||
const op = "journal.(File).Close"
|
||||
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
if err := f.j.Record("CLOSING", s.Name()); err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
if err := f.writerFile.Close(); err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
return f.j.Record("CLOSED", s.Name())
|
||||
}
|
||||
|
||||
var _ writerFile = (*File)(nil)
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package journal_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/journal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type bufWriter interface {
|
||||
io.Writer
|
||||
String() string
|
||||
}
|
||||
|
||||
type badWriter struct{}
|
||||
|
||||
func (*badWriter) Write(_ []byte) (int, error) {
|
||||
return 0, errors.New("write failed")
|
||||
}
|
||||
|
||||
func (*badWriter) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
testString = "test-string"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
f *fstest.MemFile
|
||||
buf bufWriter
|
||||
newJournalFunc func(w bufWriter) *journal.Journal
|
||||
write string
|
||||
want string
|
||||
wantNewErr error
|
||||
wantCloseErr error
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
func(w bufWriter) *journal.Journal { ww, err := journal.New(ctx, w); require.NoError(t, err); return ww },
|
||||
testString,
|
||||
"CLOSING test\nCLOSED test\n",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"nil-file",
|
||||
nil,
|
||||
bytes.NewBuffer([]byte{}),
|
||||
func(w bufWriter) *journal.Journal { ww, err := journal.New(ctx, w); require.NoError(t, err); return ww },
|
||||
testString,
|
||||
"",
|
||||
errors.New("journal.NewFile: missing writable file: invalid parameter"),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"nil-journal-writer",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
nil,
|
||||
func(w bufWriter) *journal.Journal { return nil },
|
||||
testString,
|
||||
"",
|
||||
errors.New("journal.NewFile: missing journal: invalid parameter"),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"close-error",
|
||||
fstest.NewMemFile("test", 0o664, fstest.WithCloseFunc(func() error { return errors.New("close error") })),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
func(w bufWriter) *journal.Journal { ww, err := journal.New(ctx, w); require.NoError(t, err); return ww },
|
||||
testString,
|
||||
"CLOSING test\n",
|
||||
nil,
|
||||
errors.New("journal.(File).Close: close error"),
|
||||
},
|
||||
{
|
||||
"stat-error",
|
||||
fstest.NewMemFile("test", 0o664, fstest.WithStatFunc(func() (fs.FileInfo, error) { return nil, errors.New("stat error") })),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
func(w bufWriter) *journal.Journal { ww, err := journal.New(ctx, w); require.NoError(t, err); return ww },
|
||||
testString,
|
||||
"",
|
||||
nil,
|
||||
errors.New("journal.(File).Close: stat error"),
|
||||
},
|
||||
{
|
||||
"write-error",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
&badWriter{},
|
||||
func(w bufWriter) *journal.Journal { ww, err := journal.New(ctx, w); require.NoError(t, err); return ww },
|
||||
testString,
|
||||
"",
|
||||
nil,
|
||||
errors.New("journal.(File).Close: write failed"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
j := tc.newJournalFunc(tc.buf)
|
||||
|
||||
f, err := journal.NewFile(ctx, tc.f, j)
|
||||
if tc.wantNewErr != nil {
|
||||
require.EqualError(t, tc.wantNewErr, err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.WriteString(tc.write)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.Close()
|
||||
if tc.wantCloseErr != nil {
|
||||
require.EqualError(t, err, tc.wantCloseErr.Error())
|
||||
|
||||
require.Equal(t, tc.want, tc.buf.String())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, tc.f.Closed)
|
||||
require.Equal(t, tc.want, tc.buf.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package sign provides wrappers to compute a signature of data written to an
|
||||
// io.Writer
|
||||
package sign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
"github.com/hashicorp/boundary/internal/bsr/kms"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var errInvalidParameter = errors.New("invalid parameter")
|
||||
|
||||
// Writer is a wrapper that will compute a signature of all the data
|
||||
// written to its writer.
|
||||
type Writer struct {
|
||||
keys *kms.Keys
|
||||
|
||||
buf *bytes.Buffer
|
||||
w io.Writer
|
||||
tee io.Writer
|
||||
}
|
||||
|
||||
// NewWriter returns a Writer that wraps an io.Writer.
|
||||
func NewWriter(_ context.Context, w io.Writer, keys *kms.Keys) (*Writer, error) {
|
||||
const op = "sign.NewWriter"
|
||||
switch {
|
||||
case is.Nil(w):
|
||||
return nil, fmt.Errorf("%s: missing sign writer: %w", op, errInvalidParameter)
|
||||
case is.Nil(keys):
|
||||
return nil, fmt.Errorf("%s: missing keys: %w", op, errInvalidParameter)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
tee := io.MultiWriter(w, &buf)
|
||||
return &Writer{
|
||||
buf: &buf,
|
||||
tee: tee,
|
||||
keys: keys,
|
||||
w: w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Writer) Write(b []byte) (int, error) {
|
||||
return w.tee.Write(b)
|
||||
}
|
||||
|
||||
// WriteString implements the io.StringWriter WriteString method.
|
||||
func (w *Writer) WriteString(s string) (int, error) {
|
||||
return w.Write([]byte(s))
|
||||
}
|
||||
|
||||
// Close implements the io.Closer method.
|
||||
func (w *Writer) Close() error {
|
||||
const op = "sign.(Writer).Close"
|
||||
var i interface{} = w.w
|
||||
v, ok := i.(io.WriteCloser)
|
||||
if ok {
|
||||
if err := v.Close(); err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign returns the signature of the data written to the writer.
|
||||
func (w *Writer) Sign(ctx context.Context) (*wrapping.SigInfo, error) {
|
||||
sig, err := w.keys.SignWithPrivKey(ctx, w.buf.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
type writerFile interface {
|
||||
fs.File
|
||||
io.WriteCloser
|
||||
io.StringWriter
|
||||
}
|
||||
|
||||
// File is a writable file that will compute it's signature while it is written
|
||||
// to. When closed, it will write the signature to the provided io.Writer.
|
||||
type File struct {
|
||||
ctx context.Context
|
||||
signatureWriter io.Writer
|
||||
|
||||
// Performs the signing, and provides implementations for Write and
|
||||
// WriteString.
|
||||
*Writer
|
||||
|
||||
underlying fs.File
|
||||
}
|
||||
|
||||
// Stat returns the fs.FileInfo for the underlying fs.File.
|
||||
func (f *File) Stat() (fs.FileInfo, error) {
|
||||
return f.underlying.Stat()
|
||||
}
|
||||
|
||||
// Read reads from the underlying fs.File.
|
||||
func (f *File) Read(b []byte) (int, error) {
|
||||
return f.underlying.Read(b)
|
||||
}
|
||||
|
||||
// NewFile wraps the provided writerFile in a signed File. When the file is
|
||||
// closed, its signature is written to w.
|
||||
func NewFile(ctx context.Context, f writerFile, w io.Writer, keys *kms.Keys) (*File, error) {
|
||||
const op = "sign.NewFile"
|
||||
|
||||
switch {
|
||||
case is.Nil(f):
|
||||
return nil, fmt.Errorf("%s: missing writable file: %w", op, errInvalidParameter)
|
||||
case is.Nil(w):
|
||||
return nil, fmt.Errorf("%s: missing sign writer: %w", op, errInvalidParameter)
|
||||
case is.Nil(keys):
|
||||
return nil, fmt.Errorf("%s: missing keys: %w", op, errInvalidParameter)
|
||||
}
|
||||
|
||||
sw, err := NewWriter(ctx, f, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
ctx: ctx,
|
||||
signatureWriter: w,
|
||||
Writer: sw,
|
||||
underlying: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the wrapped writer and writes the SigInfo to f.signatureWriter.
|
||||
func (f *File) Close() error {
|
||||
const op = "sign.(File).Close"
|
||||
|
||||
var closeErrors *multierror.Error
|
||||
if err := f.Writer.Close(); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
sig, err := f.Writer.Sign(f.ctx)
|
||||
if err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
b, err := proto.Marshal(sig)
|
||||
if err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
if _, err := f.signatureWriter.Write(b); err != nil {
|
||||
closeErrors = multierror.Append(closeErrors, fmt.Errorf("%s: %w", op, err))
|
||||
}
|
||||
|
||||
return closeErrors.ErrorOrNil()
|
||||
}
|
||||
|
||||
var _ writerFile = (*File)(nil)
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package sign_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/sign"
|
||||
"github.com/hashicorp/boundary/internal/bsr/kms"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type bufWriter interface {
|
||||
io.Writer
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type badWriter struct{}
|
||||
|
||||
func (*badWriter) Write(_ []byte) (int, error) {
|
||||
return 0, errors.New("write failed")
|
||||
}
|
||||
|
||||
func (*badWriter) Bytes() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
|
||||
require.NoError(t, err)
|
||||
|
||||
testSig := []byte("")
|
||||
const (
|
||||
testString = "test-string"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
f *fstest.MemFile
|
||||
sig bufWriter
|
||||
keys *kms.Keys
|
||||
write string
|
||||
want []byte
|
||||
wantNewErr error
|
||||
wantCloseErr error
|
||||
expectSigInfoOnErr bool
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
keys,
|
||||
testString,
|
||||
testSig,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil-file",
|
||||
nil,
|
||||
bytes.NewBuffer([]byte{}),
|
||||
keys,
|
||||
testString,
|
||||
testSig,
|
||||
errors.New("sign.NewFile: missing writable file: invalid parameter"),
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil-sign-writer",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
nil,
|
||||
keys,
|
||||
testString,
|
||||
testSig,
|
||||
errors.New("sign.NewFile: missing sign writer: invalid parameter"),
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil-keys",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
nil,
|
||||
testString,
|
||||
testSig,
|
||||
errors.New("sign.NewFile: missing keys: invalid parameter"),
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"close-error",
|
||||
fstest.NewMemFile("test", 0o664, fstest.WithCloseFunc(func() error { return errors.New("close error") })),
|
||||
bytes.NewBuffer([]byte{}),
|
||||
keys,
|
||||
testString,
|
||||
testSig,
|
||||
nil,
|
||||
func() error {
|
||||
var wantErrors *multierror.Error
|
||||
wantErrors = multierror.Append(wantErrors, errors.New("sign.(File).Close: sign.(Writer).Close: close error"))
|
||||
return wantErrors.ErrorOrNil()
|
||||
}(),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"write-error",
|
||||
fstest.NewMemFile("test", 0o664),
|
||||
&badWriter{},
|
||||
keys,
|
||||
testString,
|
||||
testSig,
|
||||
nil,
|
||||
func() error {
|
||||
var wantErrors *multierror.Error
|
||||
wantErrors = multierror.Append(wantErrors, errors.New("sign.(File).Close: write failed"))
|
||||
return wantErrors.ErrorOrNil()
|
||||
}(),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, err := sign.NewFile(ctx, tc.f, tc.sig, tc.keys)
|
||||
if tc.wantNewErr != nil {
|
||||
require.EqualError(t, tc.wantNewErr, err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.WriteString(tc.write)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.Close()
|
||||
if tc.wantCloseErr != nil {
|
||||
require.EqualError(t, err, tc.wantCloseErr.Error())
|
||||
|
||||
if tc.expectSigInfoOnErr {
|
||||
sigInfo := &wrapping.SigInfo{}
|
||||
err = proto.Unmarshal(tc.sig.Bytes(), sigInfo)
|
||||
require.NoError(t, err)
|
||||
want, err := keys.SignWithPrivKey(ctx, []byte(tc.write))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t,
|
||||
cmp.Diff(
|
||||
want,
|
||||
sigInfo,
|
||||
cmpopts.IgnoreUnexported(wrapping.SigInfo{}, wrapping.KeyInfo{}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
require.Empty(t, tc.sig.Bytes())
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, tc.f.Closed)
|
||||
|
||||
sigInfo := &wrapping.SigInfo{}
|
||||
err = proto.Unmarshal(tc.sig.Bytes(), sigInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
want, err := keys.SignWithPrivKey(ctx, []byte(tc.write))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t,
|
||||
cmp.Diff(
|
||||
want,
|
||||
sigInfo,
|
||||
cmpopts.IgnoreUnexported(wrapping.SigInfo{}, wrapping.KeyInfo{}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package bsr
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withSupportsMultiplex bool
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{
|
||||
withSupportsMultiplex: false,
|
||||
}
|
||||
}
|
||||
|
||||
// WithSupportsMultiplex is used indicate that a protocol supports multiplexing
|
||||
// and therefore a BSR can contain Channels.
|
||||
func WithSupportsMultiplex(b bool) Option {
|
||||
return func(o *options) {
|
||||
o.withSupportsMultiplex = b
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING session.meta
|
||||
CREATED session.meta
|
||||
CREATING session.summary
|
||||
CREATED session.summary
|
||||
CLOSING session.meta
|
||||
CLOSED session.meta
|
||||
CLOSING session.summary
|
||||
CLOSED session.summary
|
||||
CLOSING SHA256SUM
|
||||
CLOSED SHA256SUM
|
||||
CLOSING SHA256SUM.sig
|
||||
CLOSED SHA256SUM.sig
|
||||
@@ -0,0 +1,2 @@
|
||||
ff2a532f46f3e2202e69b3aab760da95c290adf33a9a4b1c4a7e17e0a2671421 session.meta
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 session.summary
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING connection.meta
|
||||
CREATED connection.meta
|
||||
CREATING connection.summary
|
||||
CREATED connection.summary
|
||||
CREATING requests-inbound.data
|
||||
CREATED requests-inbound.data
|
||||
CREATING requests-outbound.data
|
||||
CREATED requests-outbound.data
|
||||
CLOSING requests-inbound.data
|
||||
CLOSED requests-inbound.data
|
||||
CLOSING requests-outbound.data
|
||||
CLOSED requests-outbound.data
|
||||
CLOSING connection.meta
|
||||
CLOSED connection.meta
|
||||
CLOSING connection.summary
|
||||
CLOSED connection.summary
|
||||
CLOSING SHA256SUM
|
||||
CLOSED SHA256SUM
|
||||
CLOSING SHA256SUM.sig
|
||||
CLOSED SHA256SUM.sig
|
||||
@@ -0,0 +1,4 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-inbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-outbound.data
|
||||
e4df3504aaa6fee4426f67ba998cdd90f84c579d98db979e589d8ca2ffd1e733 connection.meta
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connection.summary
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING channel.meta
|
||||
CREATED channel.meta
|
||||
CREATING channel.summary
|
||||
CREATED channel.summary
|
||||
CREATING messages-inbound.data
|
||||
CREATED messages-inbound.data
|
||||
CREATING requests-inbound.data
|
||||
CREATED requests-inbound.data
|
||||
CREATING messages-outbound.data
|
||||
CREATED messages-outbound.data
|
||||
CREATING requests-outbound.data
|
||||
CREATED requests-outbound.data
|
||||
CLOSING messages-inbound.data
|
||||
CLOSED messages-inbound.data
|
||||
CLOSING requests-inbound.data
|
||||
CLOSED requests-inbound.data
|
||||
CLOSING messages-outbound.data
|
||||
CLOSED messages-outbound.data
|
||||
CLOSING requests-outbound.data
|
||||
CLOSED requests-outbound.data
|
||||
CLOSING channel.meta
|
||||
CLOSED channel.meta
|
||||
CLOSING channel.summary
|
||||
CLOSED channel.summary
|
||||
CLOSING SHA256SUM
|
||||
CLOSED SHA256SUM
|
||||
CLOSING SHA256SUM.sig
|
||||
CLOSED SHA256SUM.sig
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 messages-inbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-inbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 messages-outbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-outbound.data
|
||||
e30a207b53f444c7bd2d1e8cc102abf910dfafd70aaef7f24db00298832b74d3 channel.meta
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 channel.summary
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
id: chan_1
|
||||
messages: inbound
|
||||
requests: inbound
|
||||
messages: outbound
|
||||
requests: outbound
|
||||
Whitespace-only changes.
+4
@@ -0,0 +1,4 @@
|
||||
id: conn_1
|
||||
requests: inbound
|
||||
requests: outbound
|
||||
channel: chan_1.channel
|
||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
id: session_123456789
|
||||
connection: conn_1.connection
|
||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING session.meta
|
||||
CREATED session.meta
|
||||
CREATING session.summary
|
||||
CREATED session.summary
|
||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING connection.meta
|
||||
CREATED connection.meta
|
||||
CREATING connection.summary
|
||||
CREATED connection.summary
|
||||
Whitespace-only changes.
+8
@@ -0,0 +1,8 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING channel.meta
|
||||
CREATED channel.meta
|
||||
CREATING channel.summary
|
||||
CREATED channel.summary
|
||||
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
id: chan_1
|
||||
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
id: conn_1
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
id: session_123456789
|
||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING session.meta
|
||||
CREATED session.meta
|
||||
CREATING session.summary
|
||||
CREATED session.summary
|
||||
CLOSING session.meta
|
||||
CLOSED session.meta
|
||||
CLOSING session.summary
|
||||
CLOSED session.summary
|
||||
CLOSING SHA256SUM
|
||||
CLOSED SHA256SUM
|
||||
CLOSING SHA256SUM.sig
|
||||
CLOSED SHA256SUM.sig
|
||||
@@ -0,0 +1,2 @@
|
||||
ff2a532f46f3e2202e69b3aab760da95c290adf33a9a4b1c4a7e17e0a2671421 session.meta
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 session.summary
|
||||
@@ -0,0 +1,32 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING connection.meta
|
||||
CREATED connection.meta
|
||||
CREATING connection.summary
|
||||
CREATED connection.summary
|
||||
CREATING messages-inbound.data
|
||||
CREATED messages-inbound.data
|
||||
CREATING messages-outbound.data
|
||||
CREATED messages-outbound.data
|
||||
CREATING requests-inbound.data
|
||||
CREATED requests-inbound.data
|
||||
CREATING requests-outbound.data
|
||||
CREATED requests-outbound.data
|
||||
CLOSING messages-inbound.data
|
||||
CLOSED messages-inbound.data
|
||||
CLOSING messages-outbound.data
|
||||
CLOSED messages-outbound.data
|
||||
CLOSING requests-inbound.data
|
||||
CLOSED requests-inbound.data
|
||||
CLOSING requests-outbound.data
|
||||
CLOSED requests-outbound.data
|
||||
CLOSING connection.meta
|
||||
CLOSED connection.meta
|
||||
CLOSING connection.summary
|
||||
CLOSED connection.summary
|
||||
CLOSING SHA256SUM
|
||||
CLOSED SHA256SUM
|
||||
CLOSING SHA256SUM.sig
|
||||
CLOSED SHA256SUM.sig
|
||||
@@ -0,0 +1,6 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 messages-inbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 messages-outbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-inbound.data
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests-outbound.data
|
||||
03edd8ed2a4d678ea79d14538e504b6bb9aaec51c7e3ebf3a4891083d97ac7a9 connection.meta
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connection.summary
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
id: conn_1
|
||||
messages: inbound
|
||||
messages: outbound
|
||||
requests: inbound
|
||||
requests: outbound
|
||||
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
id: session_123456789
|
||||
connection: conn_1.connection
|
||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING session.meta
|
||||
CREATED session.meta
|
||||
CREATING session.summary
|
||||
CREATED session.summary
|
||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
CREATING SHA256SUM.sig
|
||||
CREATED SHA256SUM.sig
|
||||
CREATING SHA256SUM
|
||||
CREATED SHA256SUM
|
||||
CREATING connection.meta
|
||||
CREATED connection.meta
|
||||
CREATING connection.summary
|
||||
CREATED connection.summary
|
||||
Whitespace-only changes.
+1
@@ -0,0 +1 @@
|
||||
id: conn_1
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
id: session_123456789
|
||||
Whitespace-only changes.
Reference in new issue
Block a user