Files
threshold/internal/bsr/bsr.go
T

1035 lines
30 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package bsr
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"strconv"
"strings"
"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"
"github.com/hashicorp/go-kms-wrapping/v2/extras/crypto"
"google.golang.org/protobuf/proto"
)
const (
bsrFile = "%s.bsr"
connectionFile = "%s.connection"
channelFile = "%s.channel"
messagesFile = "messages-%s.data"
requestsFile = "requests-%s.data"
bsrPubKeyFile = "bsrKey.pub"
wrappedBsrKeyFile = "wrappedBsrKey"
wrappedPrivKeyFile = "wrappedPrivKey"
pubKeyBsrSignatureFile = "pubKeyBsrSignature.sign"
pubKeySelfSignatureFile = "pubKeySelfSignature.sign"
)
// decodeSessionMeta will populate a SessionMeta for an opened BSR Session
// TODO Unmarshal without brute force
func decodeSessionMeta(ctx context.Context, r io.Reader) (*SessionMeta, error) {
const op = "bsr.decodeSessionMeta"
switch {
case r == nil:
return nil, fmt.Errorf("%s: missing session meta file: %w", op, ErrInvalidParameter)
}
connections := make(map[string]bool)
s := &SessionMeta{}
user := &User{Scope: Scope{}}
target := &Target{Scope: Scope{}}
worker := &Worker{}
sHost := &StaticHost{Catalog: StaticHostCatalog{}}
dHost := &DynamicHost{Catalog: DynamicHostCatalog{}}
staticCredStore := make(map[string]StaticCredentialStore)
scStoreToJsonCreds := make(map[string]StaticJsonCredential)
scStoreToUPCreds := make(map[string]StaticUsernamePasswordCredential)
scStoreToSshCreds := make(map[string]StaticSshPrivateKeyCredential)
vaultCredStore := make(map[string]VaultCredentialStore)
vsStoreToVaultLib := make(map[string]VaultLibrary)
vsStoreToVaultSshCertLib := make(map[string]VaultSshCertLibrary)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
l := scanner.Text()
if len(l) == 0 {
continue
}
k, v, ok := strings.Cut(l, ":")
if !ok {
return nil, fmt.Errorf("%s: session meta file contains invalid entry; expecting k: v pair:%s", op, l)
}
v = strings.TrimSpace(v)
switch {
case k == "id":
s.Id = v
case k == "protocol":
s.Protocol = Protocol(v)
// User
case k == "user_publicId":
user.PublicId = v
case k == "user_name":
user.Name = v
case k == "user_description":
user.Description = v
case k == "user_scope_publicId":
user.Scope.PublicId = v
case k == "user_scope_name":
user.Scope.Name = v
case k == "user_scope_description":
user.Scope.Description = v
case k == "user_scope_type":
user.Scope.Type = v
case k == "user_scope_parentId":
user.Scope.ParentId = v
case k == "user_scope_primaryAuthMethod":
user.Scope.PrimaryAuthMethodId = v
// Target
case k == "target_publicId":
target.PublicId = v
case k == "target_name":
target.Name = v
case k == "target_description":
target.Description = v
case k == "target_defaultPort":
dp, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
target.DefaultPort = uint32(dp)
case k == "target_sessionMaxSeconds":
sms, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
target.SessionMaxSeconds = uint32(sms)
case k == "target_sessionConnectionLimit":
scl, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
target.SessionConnectionLimit = int32(scl)
case k == "target_workerFilter":
target.WorkerFilter = v
case k == "target_egressWorkerFilter":
target.EgressWorkerFilter = v
case k == "target_ingressWorkerFilter":
target.IngressWorkerFilter = v
case k == "target_scope_publicId":
target.Scope.PublicId = v
case k == "target_scope_name":
target.Scope.Name = v
case k == "target_scope_description":
target.Scope.Description = v
case k == "target_scope_type":
target.Scope.Type = v
case k == "target_scope_parentId":
target.Scope.ParentId = v
case k == "target_scope_primaryAuthMethod":
target.Scope.PrimaryAuthMethodId = v
// Worker
case k == "worker_publicId":
worker.PublicId = v
case k == "worker_version":
worker.Version = v
case k == "worker_sha":
worker.Sha = v
// Static Host
case k == "staticHost_publicId":
sHost.PublicId = v
case k == "staticHost_name":
sHost.Name = v
case k == "staticHost_description":
sHost.Description = v
case k == "staticHost_address":
sHost.Address = v
case k == "staticHostCatalog_publicId":
sHost.Catalog.PublicId = v
case k == "staticHostCatalog_projectId":
sHost.Catalog.ProjectId = v
case k == "staticHostCatalog_name":
sHost.Catalog.Name = v
case k == "staticHostCatalog_description":
sHost.Catalog.Description = v
// Dynamic Host
case k == "dynamicHost_publicId":
dHost.PublicId = v
case k == "dynamicHost_name":
dHost.Name = v
case k == "dynamicHost_description":
dHost.Description = v
case k == "dynamicHost_externalId":
dHost.ExternalId = v
case k == "dynamicHostCatalog_publicId":
dHost.Catalog.PublicId = v
case k == "dynamicHostCatalog_projectId":
dHost.Catalog.ProjectId = v
case k == "dynamicHostCatalog_name":
dHost.Catalog.Name = v
case k == "dynamicHostCatalog_description":
dHost.Catalog.Description = v
case k == "dynamicHostCatalog_pluginId":
dHost.Catalog.PluginId = v
case k == "dynamicHostCatalog_attributes":
dHost.Catalog.Attributes = v
// Static Credential Store
case strings.Contains(k, "staticCredentialStore_"):
idx := strings.Index(k, "_staticCredentialStore")
sId := k[:idx]
scs, ok := staticCredStore[sId]
if !ok {
scs = StaticCredentialStore{
PublicId: sId,
}
}
switch {
case strings.Contains(k, "staticJsonCredential"):
idxEnd := strings.Index(k, "_staticJsonCredential")
idxStart := strings.Index(k, "_credential_") + len("_credential_")
credId := k[idxStart:idxEnd]
// credstoreid - staticJsonCredentialid in the map
mapId := fmt.Sprintf("%s-%s", sId, credId)
c, ok := scStoreToJsonCreds[mapId]
if !ok {
c = StaticJsonCredential{
PublicId: credId,
}
}
switch {
case strings.Contains(k, "_projectId"):
c.ProjectId = v
case strings.Contains(k, "_name"):
c.Name = v
case strings.Contains(k, "_description"):
c.Description = v
case strings.Contains(k, "_objectHmac"):
c.ObjectHmac = []byte(v)
}
scStoreToJsonCreds[mapId] = c
case strings.Contains(k, "staticUsernamePasswordCredential"):
idxEnd := strings.Index(k, "_staticJUsernamePasswordCredential")
idxStart := strings.Index(k, "_credential_") + len("_credential_")
credId := k[idxStart:idxEnd]
// credstoreid - UsernamePasswordCredentialid in the map
mapId := fmt.Sprintf("%s-%s", sId, credId)
c, ok := scStoreToUPCreds[mapId]
if !ok {
c = StaticUsernamePasswordCredential{
PublicId: credId,
}
}
switch {
case strings.Contains(k, "_projectId"):
c.ProjectId = v
case strings.Contains(k, "_name"):
c.Name = v
case strings.Contains(k, "_description"):
c.Description = v
case strings.Contains(k, "_passwordHmac"):
c.PasswordHmac = []byte(v)
}
scStoreToUPCreds[mapId] = c
case strings.Contains(k, "staticSshPrivateKeyCredential"):
idxEnd := strings.Index(k, "_staticSshPrivateKeyCredential")
idxStart := strings.Index(k, "_credential_") + len("_credential_")
credId := k[idxStart:idxEnd]
// credstoreid - staticSshPrivateKeyid in the map
mapId := fmt.Sprintf("%s-%s", sId, credId)
c, ok := scStoreToSshCreds[mapId]
if !ok {
c = StaticSshPrivateKeyCredential{
PublicId: credId,
}
}
switch {
case strings.Contains(k, "_projectId"):
c.ProjectId = v
case strings.Contains(k, "_name"):
c.Name = v
case strings.Contains(k, "_description"):
c.Description = v
case strings.Contains(k, "_privateKeyHmac"):
c.PrivateKeyHmac = []byte(v)
case strings.Contains(k, "_privateKeyPassphraseHmac"):
c.PrivateKeyPassphraseHmac = []byte(v)
}
scStoreToSshCreds[mapId] = c
case strings.Contains(k, "_projectId"):
scs.ProjectId = v
case strings.Contains(k, "_name"):
scs.Name = v
case strings.Contains(k, "_description"):
scs.Description = v
}
staticCredStore[sId] = scs
// Vault Credential Store
case strings.Contains(k, "vaultCredentialStore_"):
idx := strings.Index(k, "_vaultCredentialStore")
vId := k[:idx]
vcs, ok := vaultCredStore[vId]
if !ok {
vcs = VaultCredentialStore{
PublicId: vId,
}
}
switch {
case strings.Contains(k, "vaultLibrary"):
idxEnd := strings.Index(k, "_vaultLibrary")
idxStart := strings.Index(k, "_credential_") + len("_credential_")
credId := k[idxStart:idxEnd]
// credstoreid - vaultLibraryid in the map
mapId := fmt.Sprintf("%s-%s", vId, credId)
c, ok := vsStoreToVaultLib[mapId]
if !ok {
c = VaultLibrary{
PublicId: credId,
}
}
switch {
case strings.Contains(k, "_projectId"):
c.ProjectId = v
case strings.Contains(k, "_name"):
c.Name = v
case strings.Contains(k, "_description"):
c.Description = v
case strings.Contains(k, "_vaultPath"):
c.VaultPath = v
case strings.Contains(k, "_httpMethod"):
c.HttpMethod = v
case strings.Contains(k, "_httpRequestBody"):
c.HttpRequestBody = []byte(v)
case strings.Contains(k, "_credentialType"):
c.CredentialType = v
}
vsStoreToVaultLib[mapId] = c
case strings.Contains(k, "vaultSshCertLibrary"):
idxEnd := strings.Index(k, "_vaultSshCertLibrary")
idxStart := strings.Index(k, "_credential_") + len("_credential_")
credId := k[idxStart:idxEnd]
// credstoreid - vaultSshCertLibraryid in the map
mapId := fmt.Sprintf("%s-%s", vId, credId)
c, ok := vsStoreToVaultSshCertLib[mapId]
if !ok {
c = VaultSshCertLibrary{
PublicId: credId,
}
}
switch {
case strings.Contains(k, "_projectId"):
c.ProjectId = v
case strings.Contains(k, "_username"):
c.Username = v
case strings.Contains(k, "_name"):
c.Name = v
case strings.Contains(k, "_description"):
c.Description = v
case strings.Contains(k, "_vaultPath"):
c.VaultPath = v
case strings.Contains(k, "_keyType"):
c.KeyType = v
case strings.Contains(k, "_keyBits"):
var err error
c.KeyBits, err = strconv.Atoi(v)
if err != nil {
return nil, err
}
case strings.Contains(k, "_ttl"):
c.Ttl = v
case strings.Contains(k, "_criticalOptions"):
c.CriticalOptions = []byte(v)
case strings.Contains(k, "_extensions"):
c.Extensions = []byte(v)
case strings.Contains(k, "_credentialType"):
c.CredentialType = v
}
vsStoreToVaultSshCertLib[mapId] = c
case strings.Contains(k, "_projectId"):
vcs.ProjectId = v
case strings.Contains(k, "_namespace"):
vcs.Namespace = v
case strings.Contains(k, "_name"):
vcs.Name = v
case strings.Contains(k, "_description"):
vcs.Description = v
case strings.Contains(k, "_vaultAddress"):
vcs.VaultAddress = v
case strings.Contains(k, "_tlsServerName"):
vcs.TlsServerName = v
case strings.Contains(k, "_tlsSkipVerify"):
var err error
vcs.TlsSkipVerify, err = strconv.ParseBool(v)
if err != nil {
return nil, err
}
case strings.Contains(k, "_workerFilter"):
vcs.WorkerFilter = v
}
vaultCredStore[vId] = vcs
// connections
case k == "connection":
connections[v] = true
}
}
s.Target = target
s.Worker = worker
s.User = user
if sHost.PublicId != "" {
s.StaticHost = sHost
}
if dHost.PublicId != "" {
s.DynamicHost = dHost
}
for k, v := range scStoreToJsonCreds {
scId, _, ok := strings.Cut(k, "-")
if ok {
staticCred := staticCredStore[scId]
staticCred.Credentials = append(staticCred.Credentials, v)
staticCredStore[scId] = staticCred
}
}
for k, v := range scStoreToUPCreds {
scId, _, ok := strings.Cut(k, "-")
if ok {
staticCred := staticCredStore[scId]
staticCred.Credentials = append(staticCred.Credentials, v)
staticCredStore[scId] = staticCred
}
}
for k, v := range scStoreToSshCreds {
scId, _, ok := strings.Cut(k, "-")
if ok {
staticCred := staticCredStore[scId]
staticCred.Credentials = append(staticCred.Credentials, v)
staticCredStore[scId] = staticCred
}
}
scStores := make([]StaticCredentialStore, 0)
for _, v := range staticCredStore {
scStores = append(scStores, v)
}
for k, v := range vsStoreToVaultSshCertLib {
vlId, _, ok := strings.Cut(k, "-")
if ok {
vaultLib := vaultCredStore[vlId]
vaultLib.CredentialLibraries = append(vaultLib.CredentialLibraries, v)
vaultCredStore[vlId] = vaultLib
}
}
for k, v := range vsStoreToVaultLib {
vlId, _, ok := strings.Cut(k, "-")
if ok {
vaultLib := vaultCredStore[vlId]
vaultLib.CredentialLibraries = append(vaultLib.CredentialLibraries, v)
vaultCredStore[vlId] = vaultLib
}
}
vcStores := make([]VaultCredentialStore, 0)
for _, v := range vaultCredStore {
vcStores = append(vcStores, v)
}
s.StaticCredentialStore = scStores
s.VaultCredentialStore = vcStores
s.connections = connections
return s, nil
}
// 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(meta.StaticHost) && is.Nil(meta.DynamicHost):
return nil, fmt.Errorf("%s: missing session host information: %w", op, ErrInvalidParameter)
case !is.Nil(meta.StaticHost) && !is.Nil(meta.DynamicHost):
return nil, fmt.Errorf("%s: meta cannot contain both static and dynamic host information: %w", op, ErrInvalidParameter)
case len(meta.StaticCredentialStore) == 0 && len(meta.VaultCredentialStore) == 0:
return nil, fmt.Errorf("%s: missing credential information: %w", op, ErrInvalidParameter)
case is.Nil(meta.User):
return nil, fmt.Errorf("%s: missing session user: %w", op, ErrInvalidParameter)
case is.Nil(meta.Target):
return nil, fmt.Errorf("%s: missing session target: %w", op, ErrInvalidParameter)
case is.Nil(meta.Worker):
return nil, fmt.Errorf("%s: missing session worker: %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
}
// Sync keys and signatures
err = persistBsrSessionKeys(ctx, keys, nc)
if err != nil {
return nil, errors.Join(err, fmt.Errorf("%s: %w", op, ErrBsrKeyPersistenceFailure))
}
err = meta.writeMeta(ctx, nc)
if err != nil {
return nil, err
}
return &Session{
container: nc,
multiplexed: opts.withSupportsMultiplex,
Meta: meta,
}, nil
}
// persistBsrSessionKeys will marshal, write, and close BSR keys locally before syncing to
// storage. Any error while syncing the key files should result in the termination of
// the session this recording is for.
// The key files synced are:
// * the BSR public key, bsrKey.pub
// * the wrapped BSR key, wrappedBsrKey
// * the wrapped private key, wrappedPrivKey
// * the public key BSR signature, pubKeyBsrSignature.sign
// * the public key self signature, pubKeySelfSignature.sign
func persistBsrSessionKeys(ctx context.Context, keys *kms.Keys, c *container) error {
const op = "bsr.persistBsrSessionKeys"
switch {
case keys.PubKey == nil:
return fmt.Errorf("%s: missing kms pub key: %w", op, ErrInvalidParameter)
case keys.WrappedBsrKey == nil:
return fmt.Errorf("%s: missing kms wrapped BSR key: %w", op, ErrInvalidParameter)
case keys.WrappedPrivKey == nil:
return fmt.Errorf("%s: missing kms wrapped priv key: %w", op, ErrInvalidParameter)
case keys.PubKeyBsrSignature == nil:
return fmt.Errorf("%s: missing kms pub key BSR signature: %w", op, ErrInvalidParameter)
case keys.PubKeySelfSignature == nil:
return fmt.Errorf("%s: missing kms pub key self signature: %w", op, ErrInvalidParameter)
}
keyFiles := map[string]proto.Message{
bsrPubKeyFile: keys.PubKey,
wrappedBsrKeyFile: keys.WrappedBsrKey,
wrappedPrivKeyFile: keys.WrappedPrivKey,
pubKeyBsrSignatureFile: keys.PubKeyBsrSignature,
pubKeySelfSignatureFile: keys.PubKeySelfSignature,
}
for f, k := range keyFiles {
b, err := proto.Marshal(k)
if err != nil {
return fmt.Errorf("%s: failed to marshal data for %s: %w", op, f, err)
}
err = c.syncBsrKey(ctx, f, b)
if err != nil {
return fmt.Errorf("%s: failed syncing bsr key %s: %w", op, f, err)
}
}
return nil
}
// OpenSession retrieves a BSR from storage using the sessionRecordingId and initializes it for reading.
// Encryption keys necessary for checking signed files will be unwrapped using the keyUnwrapFn
// Signature and checksum files will then be verified.
// Fields on the underlying container will be populated so that the returned Session can be used for BSR
// playback and conversion to formats such as asciinema
func OpenSession(ctx context.Context, sessionRecordingId string, f storage.FS, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (*Session, error) {
const op = "bsr.OpenSession"
switch {
case sessionRecordingId == "":
return nil, fmt.Errorf("%s: missing session recording id: %w", op, ErrInvalidParameter)
case f == nil:
return nil, fmt.Errorf("%s: missing storage: %w", op, ErrInvalidParameter)
case keyUnwrapFn == nil:
return nil, fmt.Errorf("%s: missing key unwrap function: %w", op, ErrInvalidParameter)
}
c, err := f.Open(ctx, fmt.Sprintf(bsrFile, sessionRecordingId))
if err != nil {
return nil, err
}
keyPopFn := func(c *container) (*kms.Keys, error) {
return c.loadKeys(ctx, keyUnwrapFn)
}
cc, err := openContainer(ctx, sessionContainer, c, keyPopFn)
if err != nil {
return nil, err
}
// Load and verify session metadata
sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
sm, err := decodeSessionMeta(ctx, sha256Reader)
if err != nil {
return nil, err
}
err = cc.verifyMetadata(ctx, sha256Reader)
if err != nil {
return nil, err
}
session := &Session{
container: cc,
Meta: sm,
}
return session, 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
channels map[string]bool
}
func (c ConnectionMeta) isValid() bool {
switch {
case c.Id == "":
return false
default:
return true
}
}
// 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
}
// decodeConnectionMeta will populate the ConnectionMeta for a BSR Connection
// TODO Unmarshal without brute force
func decodeConnectionMeta(ctx context.Context, r io.Reader) (*ConnectionMeta, error) {
const op = "bsr.decodeConnectionMeta"
switch {
case r == nil:
return nil, fmt.Errorf("%s: missing connection meta file: %w", op, ErrInvalidParameter)
}
c := &ConnectionMeta{}
channels := make(map[string]bool)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
l := scanner.Text()
if len(l) == 0 {
continue
}
k, v, ok := strings.Cut(l, ":")
if !ok {
return nil, fmt.Errorf("%s: connection meta file contains invalid entry; expecting k: v pair:%s", op, l)
}
v = strings.TrimSpace(v)
switch {
case k == "id":
c.Id = v
case k == "channel":
channels[v] = true
}
}
c.channels = channels
return c, 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, storage.WithCreateFile(), storage.WithFileAccessMode(storage.WriteOnly))
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
}
// OpenConnection will open and validate a BSR connection
func (s *Session) OpenConnection(ctx context.Context, connId string) (*Connection, error) {
const op = "bsr.OpenConnection"
switch {
case connId == "":
return nil, fmt.Errorf("%s: missing connection id: %w", op, ErrInvalidParameter)
case !s.Meta.connections[connId]:
return nil, fmt.Errorf("%s: connection id does not exist within this session: %w", op, ErrInvalidParameter)
}
c, err := s.container.container.SubContainer(ctx, connId)
if err != nil {
return nil, err
}
keyPopFn := func(c *container) (*kms.Keys, error) {
return s.keys, nil
}
cc, err := openContainer(ctx, connectionContainer, c, keyPopFn)
if err != nil {
return nil, err
}
// Load and verify connection metadata
sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
sm, err := decodeConnectionMeta(ctx, sha256Reader)
if err != nil {
return nil, err
}
err = cc.verifyMetadata(ctx, sha256Reader)
if err != nil {
return nil, err
}
connection := &Connection{
container: cc,
Meta: sm,
}
return connection, nil
}
// ChannelMeta contains metadata about a channel in a BSR.
type ChannelMeta struct {
Id string
Type string
}
func (c ChannelMeta) isValid() bool {
switch {
case c.Id == "":
return false
case c.Type == "":
return false
default:
return true
}
}
// decodeChannelMeta will populate the ChannelMeta for a BSR Channel
// TODO Unmarshal without brute force
func decodeChannelMeta(ctx context.Context, r io.Reader) (*ChannelMeta, error) {
const op = "bsr.decodeChannelMeta"
switch {
case r == nil:
return nil, fmt.Errorf("%s: missing channel meta file: %w", op, ErrInvalidParameter)
}
c := &ChannelMeta{}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
l := scanner.Text()
if len(l) == 0 {
continue
}
k, v, ok := strings.Cut(l, ":")
if !ok {
return nil, fmt.Errorf("%s: channel meta file contains invalid entry; expecting k: v pair:%s", op, l)
}
v = strings.TrimSpace(v)
switch {
case k == "id":
c.Id = v
case k == "channelType":
c.Type = v
}
}
return c, nil
}
// 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, storage.WithCreateFile(), storage.WithFileAccessMode(storage.WriteOnly))
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
}
if _, err := nc.WriteMeta(ctx, "channelType", meta.Type); err != nil {
return nil, err
}
return &Channel{
container: nc,
Meta: meta,
}, nil
}
// OpenChannel will open and validate a BSR channel
func (c *Connection) OpenChannel(ctx context.Context, chanId string) (*Channel, error) {
const op = "bsr.OpenChannel"
switch {
case chanId == "":
return nil, fmt.Errorf("%s: missing channel id: %w", op, ErrInvalidParameter)
case !c.Meta.channels[chanId]:
return nil, fmt.Errorf("%s: channel id does not exist within this connection: %w", op, ErrInvalidParameter)
}
con, err := c.container.container.SubContainer(ctx, chanId)
if err != nil {
return nil, err
}
keyPopFn := func(cn *container) (*kms.Keys, error) {
return c.keys, nil
}
cc, err := openContainer(ctx, channelContainer, con, keyPopFn)
if err != nil {
return nil, err
}
// Load and verify channel metadata
sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
sm, err := decodeChannelMeta(ctx, sha256Reader)
if err != nil {
return nil, err
}
err = cc.verifyMetadata(ctx, sha256Reader)
if err != nil {
return nil, err
}
channel := &Channel{
container: cc,
Meta: sm,
}
return channel, 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())
_, err := c.container.WriteMeta(ctx, "messages", dir.String())
if err != nil {
return nil, err
}
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())
_, err := c.container.WriteMeta(ctx, "requests", dir.String())
if err != nil {
return nil, err
}
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)
}
// 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())
_, err := c.container.WriteMeta(ctx, "messages", dir.String())
if err != nil {
return nil, err
}
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())
_, err := c.container.WriteMeta(ctx, "requests", dir.String())
if err != nil {
return nil, err
}
m, err := c.container.create(ctx, requestName)
if err != nil {
return nil, err
}
return checksum.NewFile(ctx, m, c.checksums)
}
// OpenMessageScanner opens a ChunkScanner for a channel's recorded messages.
func (c *Channel) OpenMessageScanner(ctx context.Context, dir Direction) (*ChunkScanner, error) {
const op = "bsr.(Channel).OpenMessageScanner"
messagesName := fmt.Sprintf(messagesFile, dir.String())
m, err := c.container.container.OpenFile(ctx, messagesName, storage.WithFileAccessMode(storage.ReadOnly))
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
expectedSum, err := c.shaSums.Sum(messagesName)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
return NewChunkScanner(ctx, m, WithSha256Sum(expectedSum))
}
// OpenRequestScanner opens a ChunkScanner for a channel's recorded requests.
func (c *Channel) OpenRequestScanner(ctx context.Context, dir Direction) (*ChunkScanner, error) {
const op = "bsr.(Channel).OpenRequestScanner"
requestName := fmt.Sprintf(requestsFile, dir.String())
m, err := c.container.container.OpenFile(ctx, requestName, storage.WithFileAccessMode(storage.ReadOnly))
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
expectedSum, err := c.shaSums.Sum(requestName)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
return NewChunkScanner(ctx, m, WithSha256Sum(expectedSum))
}