145 lines
3.4 KiB
Go
145 lines
3.4 KiB
Go
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
var ErrNoValidCatalog = errors.New("no valid catalog available")
|
|
|
|
// Fetcher obtains a signed Catalog document from a remote or test source.
|
|
type Fetcher interface {
|
|
Fetch(context.Context) ([]byte, error)
|
|
}
|
|
|
|
// FetchFunc adapts a function to Fetcher.
|
|
type FetchFunc func(context.Context) ([]byte, error)
|
|
|
|
func (function FetchFunc) Fetch(ctx context.Context) ([]byte, error) {
|
|
return function(ctx)
|
|
}
|
|
|
|
// Cache stores the last verified signed document.
|
|
type Cache interface {
|
|
Load() ([]byte, error)
|
|
Store([]byte) error
|
|
}
|
|
|
|
// DocumentValidator rejects signed documents that this client cannot consume.
|
|
// Validators run before a remote document can replace the last valid cache.
|
|
type DocumentValidator interface {
|
|
Validate(VerifiedDocument) error
|
|
}
|
|
|
|
// DocumentValidatorFunc adapts a function to DocumentValidator.
|
|
type DocumentValidatorFunc func(VerifiedDocument) error
|
|
|
|
func (function DocumentValidatorFunc) Validate(document VerifiedDocument) error {
|
|
return function(document)
|
|
}
|
|
|
|
// LoadSource describes where a verified result came from.
|
|
type LoadSource string
|
|
|
|
const (
|
|
SourceRemote LoadSource = "remote"
|
|
SourceCache LoadSource = "cache"
|
|
)
|
|
|
|
// LoadResult returns verified bytes and a non-fatal refresh/cache warning.
|
|
type LoadResult struct {
|
|
Document VerifiedDocument
|
|
Source LoadSource
|
|
Warning error
|
|
}
|
|
|
|
// LoadError preserves both the refresh and cache failure.
|
|
type LoadError struct {
|
|
Refresh error
|
|
Cache error
|
|
}
|
|
|
|
func (err *LoadError) Error() string {
|
|
return fmt.Sprintf("%s: refresh=%v; cache=%v", ErrNoValidCatalog, err.Refresh, err.Cache)
|
|
}
|
|
|
|
func (err *LoadError) Unwrap() error {
|
|
return ErrNoValidCatalog
|
|
}
|
|
|
|
// Loader verifies remote data before storing it and re-verifies cache fallback.
|
|
type Loader struct {
|
|
verifier Verifier
|
|
fetcher Fetcher
|
|
cache Cache
|
|
validators []DocumentValidator
|
|
}
|
|
|
|
func NewLoader(
|
|
verifier Verifier,
|
|
fetcher Fetcher,
|
|
cache Cache,
|
|
validators ...DocumentValidator,
|
|
) *Loader {
|
|
return &Loader{
|
|
verifier: verifier,
|
|
fetcher: fetcher,
|
|
cache: cache,
|
|
validators: append([]DocumentValidator(nil), validators...),
|
|
}
|
|
}
|
|
|
|
// Load prefers a verified remote document and falls back to verified cache.
|
|
func (loader *Loader) Load(ctx context.Context) (LoadResult, error) {
|
|
remoteBytes, refreshErr := loader.fetcher.Fetch(ctx)
|
|
if refreshErr == nil {
|
|
verified, verifyErr := loader.verifier.Verify(remoteBytes)
|
|
if verifyErr == nil {
|
|
verifyErr = loader.validate(verified)
|
|
if verifyErr == nil {
|
|
storeErr := loader.cache.Store(verified.Bytes)
|
|
return LoadResult{
|
|
Document: verified,
|
|
Source: SourceRemote,
|
|
Warning: storeErr,
|
|
}, nil
|
|
}
|
|
}
|
|
refreshErr = verifyErr
|
|
}
|
|
|
|
cachedBytes, cacheErr := loader.cache.Load()
|
|
if cacheErr == nil {
|
|
var verified VerifiedDocument
|
|
verified, cacheErr = loader.verifier.Verify(cachedBytes)
|
|
if cacheErr == nil {
|
|
cacheErr = loader.validate(verified)
|
|
if cacheErr == nil {
|
|
return LoadResult{
|
|
Document: verified,
|
|
Source: SourceCache,
|
|
Warning: refreshErr,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return LoadResult{}, &LoadError{
|
|
Refresh: refreshErr,
|
|
Cache: cacheErr,
|
|
}
|
|
}
|
|
|
|
func (loader *Loader) validate(document VerifiedDocument) error {
|
|
for _, validator := range loader.validators {
|
|
if validator == nil {
|
|
continue
|
|
}
|
|
if err := validator.Validate(document); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|