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/portbase/log" "github.com/safing/portbase/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 }