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 }