github.com/safing/portbase@v0.19.5/updater/storage.go (about)

     1  package updater
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/safing/jess/filesig"
    14  	"github.com/safing/jess/lhash"
    15  	"github.com/safing/portbase/log"
    16  	"github.com/safing/portbase/utils"
    17  )
    18  
    19  // ScanStorage scans root within the storage dir and adds found
    20  // resources to the registry. If an error occurred, it is logged
    21  // and the last error is returned. Everything that was found
    22  // despite errors is added to the registry anyway. Leave root
    23  // empty to scan the full storage dir.
    24  func (reg *ResourceRegistry) ScanStorage(root string) error {
    25  	var lastError error
    26  
    27  	// prep root
    28  	if root == "" {
    29  		root = reg.storageDir.Path
    30  	} else {
    31  		var err error
    32  		root, err = filepath.Abs(root)
    33  		if err != nil {
    34  			return err
    35  		}
    36  		if !strings.HasPrefix(root, reg.storageDir.Path) {
    37  			return errors.New("supplied scan root path not within storage")
    38  		}
    39  	}
    40  
    41  	// walk fs
    42  	_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
    43  		// skip tmp dir (including errors trying to read it)
    44  		if strings.HasPrefix(path, reg.tmpDir.Path) {
    45  			return filepath.SkipDir
    46  		}
    47  
    48  		// handle walker error
    49  		if err != nil {
    50  			lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err)
    51  			log.Warning(lastError.Error())
    52  			return nil
    53  		}
    54  
    55  		// Ignore file signatures.
    56  		if strings.HasSuffix(path, filesig.Extension) {
    57  			return nil
    58  		}
    59  
    60  		// get relative path to storage
    61  		relativePath, err := filepath.Rel(reg.storageDir.Path, path)
    62  		if err != nil {
    63  			lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err)
    64  			log.Warning(lastError.Error())
    65  			return nil
    66  		}
    67  
    68  		// convert to identifier and version
    69  		relativePath = filepath.ToSlash(relativePath)
    70  		identifier, version, ok := GetIdentifierAndVersion(relativePath)
    71  		if !ok {
    72  			// file does not conform to format
    73  			return nil
    74  		}
    75  
    76  		// fully ignore directories that also have an identifier - these will be unpacked resources
    77  		if info.IsDir() {
    78  			return filepath.SkipDir
    79  		}
    80  
    81  		// save
    82  		err = reg.AddResource(identifier, version, nil, true, false, false)
    83  		if err != nil {
    84  			lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err)
    85  			log.Warning(lastError.Error())
    86  		}
    87  		return nil
    88  	})
    89  
    90  	return lastError
    91  }
    92  
    93  // LoadIndexes loads the current release indexes from disk
    94  // or will fetch a new version if not available and the
    95  // registry is marked as online.
    96  func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error {
    97  	var firstErr error
    98  	client := &http.Client{}
    99  	for _, idx := range reg.getIndexes() {
   100  		err := reg.loadIndexFile(idx)
   101  		if err == nil {
   102  			log.Debugf("%s: loaded index %s", reg.Name, idx.Path)
   103  		} else if reg.Online {
   104  			// try to download the index file if a local disk version
   105  			// does not exist or we don't have permission to read it.
   106  			if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
   107  				err = reg.downloadIndex(ctx, client, idx)
   108  			}
   109  		}
   110  
   111  		if err != nil && firstErr == nil {
   112  			firstErr = err
   113  		}
   114  	}
   115  
   116  	return firstErr
   117  }
   118  
   119  // getIndexes returns a copy of the index.
   120  // The indexes itself are references.
   121  func (reg *ResourceRegistry) getIndexes() []*Index {
   122  	reg.RLock()
   123  	defer reg.RUnlock()
   124  
   125  	indexes := make([]*Index, len(reg.indexes))
   126  	copy(indexes, reg.indexes)
   127  	return indexes
   128  }
   129  
   130  func (reg *ResourceRegistry) loadIndexFile(idx *Index) error {
   131  	indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path))
   132  	indexData, err := os.ReadFile(indexPath)
   133  	if err != nil {
   134  		return fmt.Errorf("failed to read index file %s: %w", idx.Path, err)
   135  	}
   136  
   137  	// Verify signature, if enabled.
   138  	if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil {
   139  		// Load and check signature.
   140  		verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension)
   141  		if err != nil {
   142  			switch verifOpts.DiskLoadPolicy {
   143  			case SignaturePolicyRequire:
   144  				return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err)
   145  			case SignaturePolicyWarn:
   146  				log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
   147  			case SignaturePolicyDisable:
   148  				log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
   149  			}
   150  		}
   151  
   152  		// Check if signature checksum matches the index data.
   153  		if err == nil && !verifiedHash.Matches(indexData) {
   154  			switch verifOpts.DiskLoadPolicy {
   155  			case SignaturePolicyRequire:
   156  				return fmt.Errorf("index file %s does not match signature", idx.Path)
   157  			case SignaturePolicyWarn:
   158  				log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path)
   159  			case SignaturePolicyDisable:
   160  				log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path)
   161  			}
   162  		}
   163  	}
   164  
   165  	// Parse the index file.
   166  	indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
   167  	if err != nil {
   168  		return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err)
   169  	}
   170  
   171  	// Update last seen release.
   172  	idx.LastRelease = indexFile.Published
   173  
   174  	// Warn if there aren't any releases in the index.
   175  	if len(indexFile.Releases) == 0 {
   176  		log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
   177  		return nil
   178  	}
   179  
   180  	// Add index releases to available resources.
   181  	err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease)
   182  	if err != nil {
   183  		log.Warningf("%s: failed to add resource: %s", reg.Name, err)
   184  	}
   185  	return nil
   186  }
   187  
   188  func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) {
   189  	// Load signature file.
   190  	sigFileData, err := os.ReadFile(sigFilePath)
   191  	if err != nil {
   192  		return nil, nil, fmt.Errorf("failed to read signature file: %w", err)
   193  	}
   194  
   195  	// Extract all signatures.
   196  	sigs, err := filesig.ParseSigFile(sigFileData)
   197  	switch {
   198  	case len(sigs) == 0 && err != nil:
   199  		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
   200  	case len(sigs) == 0:
   201  		return nil, nil, errors.New("no signatures found in signature file")
   202  	case err != nil:
   203  		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
   204  	}
   205  
   206  	// Verify all signatures.
   207  	var verifiedHash *lhash.LabeledHash
   208  	for _, sig := range sigs {
   209  		fd, err := filesig.VerifyFileData(
   210  			sig,
   211  			nil,
   212  			verifOpts.TrustStore,
   213  		)
   214  		if err != nil {
   215  			return nil, sigFileData, err
   216  		}
   217  
   218  		// Save or check verified hash.
   219  		if verifiedHash == nil {
   220  			verifiedHash = fd.FileHash()
   221  		} else if !fd.FileHash().Equal(verifiedHash) {
   222  			// Return an error if two valid hashes mismatch.
   223  			// For simplicity, all hash algorithms must be the same for now.
   224  			return nil, sigFileData, errors.New("file hashes from different signatures do not match")
   225  		}
   226  	}
   227  
   228  	return verifiedHash, sigFileData, nil
   229  }
   230  
   231  // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list.
   232  func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error {
   233  	err := os.RemoveAll(symlinkRoot.Path)
   234  	if err != nil {
   235  		return fmt.Errorf("failed to wipe symlink root: %w", err)
   236  	}
   237  
   238  	err = symlinkRoot.Ensure()
   239  	if err != nil {
   240  		return fmt.Errorf("failed to create symlink root: %w", err)
   241  	}
   242  
   243  	reg.RLock()
   244  	defer reg.RUnlock()
   245  
   246  	for _, res := range reg.resources {
   247  		if res.SelectedVersion == nil {
   248  			return fmt.Errorf("no selected version available for %s", res.Identifier)
   249  		}
   250  
   251  		targetPath := res.SelectedVersion.storagePath()
   252  		linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier))
   253  		linkPathDir := filepath.Dir(linkPath)
   254  
   255  		err = symlinkRoot.EnsureAbsPath(linkPathDir)
   256  		if err != nil {
   257  			return fmt.Errorf("failed to create dir for link: %w", err)
   258  		}
   259  
   260  		relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
   261  		if err != nil {
   262  			return fmt.Errorf("failed to get relative target path: %w", err)
   263  		}
   264  
   265  		err = os.Symlink(relativeTargetPath, linkPath)
   266  		if err != nil {
   267  			return fmt.Errorf("failed to link %s: %w", res.Identifier, err)
   268  		}
   269  	}
   270  
   271  	return nil
   272  }