Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce FileIO abstraction instead of hardcoded os.* function calls #107

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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