Skip to content

Commit

Permalink
introduce FileIO abstraction instead of hardcoded os.* function calls (
Browse files Browse the repository at this point in the history
  • Loading branch information
seborama authored Jul 25, 2023
1 parent 1532f02 commit a938be1
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

test:
name: Test
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ vcr := govcr.NewVCR(

### Recipe: Cassette decryption

**govcr** provides a CLI utility to decrypt existing cassette files, should we want to.
**govcr** provides a CLI utility to decrypt existing cassette files, should this be wanted.

The command is located in the `cmd/govcr` folder, to install it:

Expand All @@ -447,7 +447,7 @@ Example usage:
govcr decrypt -cassette-file my.cassette.json -key-file my.key
```

`decrypt` will cowardly refuse to write to a file to avoid errors or lingering decrypted files. It will write to the standard output.
`decrypt` writes to the standard output to avoid errors or lingering decrypted files.

[(toc)](#table-of-content)

Expand Down
90 changes: 69 additions & 21 deletions cassette/cassette.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/seborama/govcr/v13/compression"
cryptoerr "github.com/seborama/govcr/v13/encryption/errors"
govcrerr "github.com/seborama/govcr/v13/errors"
"github.com/seborama/govcr/v13/fileio"
"github.com/seborama/govcr/v13/stats"
)

Expand All @@ -30,6 +31,14 @@ type Cassette struct {
trackSliceMutex sync.RWMutex
tracksLoaded int32
crypter Crypter
store FileIO
}

type FileIO interface {
MkdirAll(path string, perm os.FileMode) error
ReadFile(name string) ([]byte, error)
WriteFile(name string, data []byte, perm os.FileMode) error
IsNotExist(err error) bool
}

const (
Expand Down Expand Up @@ -59,7 +68,19 @@ func WithCrypter(crypter Crypter) Option {
}
}

// WithStore provides a dedicated storage engine for the cassette data.
func WithStore(crypter Crypter) Option {
return func(k7 *Cassette) {
if k7.crypter != nil {
log.Println("notice: setting a crypter but another one had already been registered - this is incorrect usage")
}

k7.crypter = crypter
}
}

// NewCassette creates a ready to use new cassette.
// When no storage backend (store) is provided, the default OSFile storage is used.
func NewCassette(name string, opts ...Option) *Cassette {
k7 := Cassette{
name: name,
Expand All @@ -70,6 +91,10 @@ func NewCassette(name string, opts ...Option) *Cassette {
option(&k7)
}

if k7.store == nil {
k7.store = &fileio.OSFile{}
}

return &k7
}

Expand Down Expand Up @@ -152,11 +177,15 @@ func (k7 *Cassette) wantEncrypted() bool {
return k7.crypter != nil
}

// saveCassette writes a cassette to file.
// saveCassette writes a cassette to storage.
func (k7 *Cassette) save() error {
k7.trackSliceMutex.Lock()
defer k7.trackSliceMutex.Unlock()

if k7.store == nil {
k7.store = &fileio.OSFile{}
}

data, err := json.MarshalIndent(k7, "", " ")
if err != nil {
return errors.WithStack(err)
Expand All @@ -174,11 +203,11 @@ func (k7 *Cassette) save() error {
}

path := filepath.Dir(k7.name)
if err = os.MkdirAll(path, 0o750); err != nil {
if err = k7.store.MkdirAll(path, 0o750); err != nil {
return errors.Wrap(err, path)
}

err = os.WriteFile(k7.name, eData, 0o600)
err = k7.store.WriteFile(k7.name, eData, 0o600)
return errors.Wrap(err, k7.name)
}

Expand Down Expand Up @@ -276,37 +305,35 @@ func (k7 *Cassette) Name() string {
return k7.name
}

// readCassetteFile reads the cassette file, if present or
// returns a blank cassette.
func (k7 *Cassette) readCassetteFile(cassetteName string) error {
// readCassette reads the cassette source, if present or else nil data.
func (k7 *Cassette) readCassette(cassetteName string) ([]byte, error) {
if cassetteName == "" {
return errors.New("a cassette name is required")
return nil, errors.New("a cassette name is required")
}

if k7.store == nil {
k7.store = &fileio.OSFile{}
}

data, err := os.ReadFile(cassetteName) // nolint:gosec
data, err := k7.store.ReadFile(cassetteName)
if err != nil {
if os.IsNotExist(err) {
return nil
if k7.store.IsNotExist(err) {
return nil, nil // not found, return nil data
}
return errors.Wrap(err, "failed to read cassette data from file")
return nil, errors.Wrap(err, "failed to read cassette data from source")
}

dData, err := k7.DecryptionFilter(data)
if err != nil {
return errors.WithStack(err)
return nil, errors.WithStack(err)
}

gData, err := k7.GunzipFilter(dData)
if err != nil {
return errors.WithStack(err)
return nil, errors.WithStack(err)
}

// NOTE: Properties which are of type 'interface{} / any' are not handled very well
if err = json.Unmarshal(gData, k7); err != nil {
return errors.Wrap(err, "failed to interpret cassette data in file")
}

return nil
return gData, nil
}

func getEncryptionMarker(data []byte) string {
Expand Down Expand Up @@ -396,19 +423,40 @@ func AddTrackToCassette(cassette *Cassette, trk *track.Track) error {
return cassette.save()
}

// LoadCassette loads a cassette from file and initialises its associated stats.
// LoadCassette loads a cassette from source and initialises its associated stats.
// It panics when a cassette exists but cannot be loaded because that indicates
// corruption (or a severe bug).
func LoadCassette(cassetteName string, opts ...Option) *Cassette {
k7 := NewCassette(cassetteName, opts...)

err := k7.readCassetteFile(cassetteName)
data, err := k7.readCassette(cassetteName)
if err != nil {
panic(fmt.Sprintf("unable to invalid / load corrupted cassette '%s': %+v", cassetteName, err))
}

if data != nil {
// NOTE: Properties which are of type 'interface{} / any' are not handled very well
if err = json.Unmarshal(data, k7); err != nil {
panic(fmt.Sprintf("failed to interpret cassette data in source '%s': %+v", cassetteName, err))
}
}

// initial stats
atomic.StoreInt32(&k7.tracksLoaded, k7.NumberOfTracks())

return k7
}

// DumpCassette loads a cassette from source and returns its (decrypted) contents.
// It panics when a cassette exists but cannot be loaded because that indicates
// corruption (or a severe bug).
func DumpCassette(cassetteName string, opts ...Option) []byte {
k7 := NewCassette(cassetteName, opts...)

data, err := k7.readCassette(cassetteName)
if err != nil {
panic(fmt.Sprintf("unable to invalid / load corrupted cassette '%s': %+v", cassetteName, err))
}

return data
}
13 changes: 3 additions & 10 deletions cmd/govcr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func decryptCommand(cassetteFile, keyFile string) error {
return nil
}

// TODO: offer ability to supply the key via an environment variable in base64 format.
func decryptCassette(cassetteFile, keyFile string) (string, error) {
key, err := os.ReadFile(keyFile)
if err != nil {
Expand All @@ -74,15 +75,7 @@ func decryptCassette(cassetteFile, keyFile string) (string, error) {
return "", errors.Wrap(err, "cryptographer")
}

k7RawData, err := os.ReadFile(cassetteFile)
if err != nil {
return "", errors.Wrap(err, "cassette file")
}

k7Data, err := cassette.Decrypt(k7RawData, crypter)
if err != nil {
return "", errors.Wrap(err, "decryption")
}
data := cassette.DumpCassette(cassetteFile, cassette.WithCrypter(crypter))

return string(k7Data), nil
return string(data), nil
}
2 changes: 2 additions & 0 deletions encryption/.study/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
)

// nolint: deadcode
// TODO: offer ability to supply the key via an environment variable in base64 format.
func readSSHRSAPrivateKeyFile(privKeyFile, passphrase string) (rsaPrivKey *rsa.PrivateKey, sshSigner ssh.Signer, rsaPubKey *rsa.PublicKey, err error) {
keyData, err := os.ReadFile(privKeyFile)
if err != nil {
Expand Down Expand Up @@ -61,6 +62,7 @@ func readSSHRSAPrivateKeyFile(privKeyFile, passphrase string) (rsaPrivKey *rsa.P
}

// nolint: deadcode
// TODO: offer ability to supply the key via an environment variable in base64 format.
func readSSHRSAPublicKeyFile(pubKeyFile string) (*rsa.PublicKey, error) {
keyData, err := os.ReadFile(pubKeyFile)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions fileio/os.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fileio

import "os"

// OSFile provides a storage based on Go's standard "os" package for filesystem support.
type OSFile struct{}

func (*OSFile) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}

func (*OSFile) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}

func (*OSFile) WriteFile(name string, data []byte, perm os.FileMode) error {
return os.WriteFile(name, data, perm)
}

func (*OSFile) IsNotExist(err error) bool {
return os.IsNotExist(err)
}
1 change: 1 addition & 0 deletions govcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (cb *CassetteLoader) load() *cassette.Cassette {
return cassette.LoadCassette(cb.cassetteName, cb.opts...)
}

// TODO: offer ability to supply the key via an environment variable in base64 format.
func makeCrypter(crypterNonce CrypterNonceProvider, keyFile string, nonceGenerator encryption.NonceGenerator) (*encryption.Crypter, error) {
if crypterNonce == nil {
return nil, errors.New("a cipher must be supplied for encryption, `nil` is not permitted")
Expand Down

0 comments on commit a938be1

Please sign in to comment.