mirror of
https://github.com/safing/portmaster
synced 2025-04-22 11:59:09 +00:00
272 lines
7.8 KiB
Go
272 lines
7.8 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/safing/jess/filesig"
|
|
"github.com/safing/jess/lhash"
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/base/utils"
|
|
)
|
|
|
|
// ScanStorage scans root within the storage dir and adds found
|
|
// resources to the registry. If an error occurred, it is logged
|
|
// and the last error is returned. Everything that was found
|
|
// despite errors is added to the registry anyway. Leave root
|
|
// empty to scan the full storage dir.
|
|
func (reg *ResourceRegistry) ScanStorage(root string) error {
|
|
var lastError error
|
|
|
|
// prep root
|
|
if root == "" {
|
|
root = reg.storageDir.Path
|
|
} else {
|
|
var err error
|
|
root, err = filepath.Abs(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(root, reg.storageDir.Path) {
|
|
return errors.New("supplied scan root path not within storage")
|
|
}
|
|
}
|
|
|
|
// walk fs
|
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
// skip tmp dir (including errors trying to read it)
|
|
if strings.HasPrefix(path, reg.tmpDir.Path) {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// handle walker error
|
|
if err != nil {
|
|
lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err)
|
|
log.Warning(lastError.Error())
|
|
return nil
|
|
}
|
|
|
|
// Ignore file signatures.
|
|
if strings.HasSuffix(path, filesig.Extension) {
|
|
return nil
|
|
}
|
|
|
|
// get relative path to storage
|
|
relativePath, err := filepath.Rel(reg.storageDir.Path, path)
|
|
if err != nil {
|
|
lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err)
|
|
log.Warning(lastError.Error())
|
|
return nil
|
|
}
|
|
|
|
// convert to identifier and version
|
|
relativePath = filepath.ToSlash(relativePath)
|
|
identifier, version, ok := GetIdentifierAndVersion(relativePath)
|
|
if !ok {
|
|
// file does not conform to format
|
|
return nil
|
|
}
|
|
|
|
// fully ignore directories that also have an identifier - these will be unpacked resources
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// save
|
|
err = reg.AddResource(identifier, version, nil, true, false, false)
|
|
if err != nil {
|
|
lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err)
|
|
log.Warning(lastError.Error())
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return lastError
|
|
}
|
|
|
|
// LoadIndexes loads the current release indexes from disk
|
|
// or will fetch a new version if not available and the
|
|
// registry is marked as online.
|
|
func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error {
|
|
var firstErr error
|
|
client := &http.Client{}
|
|
for _, idx := range reg.getIndexes() {
|
|
err := reg.loadIndexFile(idx)
|
|
if err == nil {
|
|
log.Debugf("%s: loaded index %s", reg.Name, idx.Path)
|
|
} else if reg.Online {
|
|
// try to download the index file if a local disk version
|
|
// does not exist or we don't have permission to read it.
|
|
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
|
|
err = reg.downloadIndex(ctx, client, idx)
|
|
}
|
|
}
|
|
|
|
if err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
|
|
return firstErr
|
|
}
|
|
|
|
// getIndexes returns a copy of the index.
|
|
// The indexes itself are references.
|
|
func (reg *ResourceRegistry) getIndexes() []*Index {
|
|
reg.RLock()
|
|
defer reg.RUnlock()
|
|
|
|
indexes := make([]*Index, len(reg.indexes))
|
|
copy(indexes, reg.indexes)
|
|
return indexes
|
|
}
|
|
|
|
func (reg *ResourceRegistry) loadIndexFile(idx *Index) error {
|
|
indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path))
|
|
indexData, err := os.ReadFile(indexPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read index file %s: %w", idx.Path, err)
|
|
}
|
|
|
|
// Verify signature, if enabled.
|
|
if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil {
|
|
// Load and check signature.
|
|
verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension)
|
|
if err != nil {
|
|
switch verifOpts.DiskLoadPolicy {
|
|
case SignaturePolicyRequire:
|
|
return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err)
|
|
case SignaturePolicyWarn:
|
|
log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
|
|
case SignaturePolicyDisable:
|
|
log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
|
|
}
|
|
}
|
|
|
|
// Check if signature checksum matches the index data.
|
|
if err == nil && !verifiedHash.Matches(indexData) {
|
|
switch verifOpts.DiskLoadPolicy {
|
|
case SignaturePolicyRequire:
|
|
return fmt.Errorf("index file %s does not match signature", idx.Path)
|
|
case SignaturePolicyWarn:
|
|
log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path)
|
|
case SignaturePolicyDisable:
|
|
log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the index file.
|
|
indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err)
|
|
}
|
|
|
|
// Update last seen release.
|
|
idx.LastRelease = indexFile.Published
|
|
|
|
// Warn if there aren't any releases in the index.
|
|
if len(indexFile.Releases) == 0 {
|
|
log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
|
|
return nil
|
|
}
|
|
|
|
// Add index releases to available resources.
|
|
err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease)
|
|
if err != nil {
|
|
log.Warningf("%s: failed to add resource: %s", reg.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) {
|
|
// Load signature file.
|
|
sigFileData, err := os.ReadFile(sigFilePath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read signature file: %w", err)
|
|
}
|
|
|
|
// Extract all signatures.
|
|
sigs, err := filesig.ParseSigFile(sigFileData)
|
|
switch {
|
|
case len(sigs) == 0 && err != nil:
|
|
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
|
|
case len(sigs) == 0:
|
|
return nil, nil, errors.New("no signatures found in signature file")
|
|
case err != nil:
|
|
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
|
|
}
|
|
|
|
// Verify all signatures.
|
|
var verifiedHash *lhash.LabeledHash
|
|
for _, sig := range sigs {
|
|
fd, err := filesig.VerifyFileData(
|
|
sig,
|
|
nil,
|
|
verifOpts.TrustStore,
|
|
)
|
|
if err != nil {
|
|
return nil, sigFileData, err
|
|
}
|
|
|
|
// Save or check verified hash.
|
|
if verifiedHash == nil {
|
|
verifiedHash = fd.FileHash()
|
|
} else if !fd.FileHash().Equal(verifiedHash) {
|
|
// Return an error if two valid hashes mismatch.
|
|
// For simplicity, all hash algorithms must be the same for now.
|
|
return nil, sigFileData, errors.New("file hashes from different signatures do not match")
|
|
}
|
|
}
|
|
|
|
return verifiedHash, sigFileData, nil
|
|
}
|
|
|
|
// CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list.
|
|
func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error {
|
|
err := os.RemoveAll(symlinkRoot.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to wipe symlink root: %w", err)
|
|
}
|
|
|
|
err = symlinkRoot.Ensure()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create symlink root: %w", err)
|
|
}
|
|
|
|
reg.RLock()
|
|
defer reg.RUnlock()
|
|
|
|
for _, res := range reg.resources {
|
|
if res.SelectedVersion == nil {
|
|
return fmt.Errorf("no selected version available for %s", res.Identifier)
|
|
}
|
|
|
|
targetPath := res.SelectedVersion.storagePath()
|
|
linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier))
|
|
linkPathDir := filepath.Dir(linkPath)
|
|
|
|
err = symlinkRoot.EnsureAbsPath(linkPathDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create dir for link: %w", err)
|
|
}
|
|
|
|
relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get relative target path: %w", err)
|
|
}
|
|
|
|
err = os.Symlink(relativeTargetPath, linkPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to link %s: %w", res.Identifier, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|