github.com/safing/portbase@v0.19.5/updater/updating.go (about) 1 package updater 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 12 "golang.org/x/exp/slices" 13 14 "github.com/safing/jess/filesig" 15 "github.com/safing/jess/lhash" 16 "github.com/safing/portbase/log" 17 "github.com/safing/portbase/utils" 18 ) 19 20 // UpdateIndexes downloads all indexes. An error is only returned when all 21 // indexes fail to update. 22 func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { 23 var lastErr error 24 var anySuccess bool 25 26 // Start registry operation. 27 reg.state.StartOperation(StateChecking) 28 defer reg.state.EndOperation() 29 30 client := &http.Client{} 31 for _, idx := range reg.getIndexes() { 32 if err := reg.downloadIndex(ctx, client, idx); err != nil { 33 lastErr = err 34 log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) 35 } else { 36 anySuccess = true 37 } 38 } 39 40 // If all indexes failed to update, fail. 41 if !anySuccess { 42 err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) 43 reg.state.ReportUpdateCheck(nil, err) 44 return err 45 } 46 47 // Get pending resources and update status. 48 pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) 49 reg.state.ReportUpdateCheck( 50 humanInfoFromResourceVersions(pendingResourceVersions), 51 nil, 52 ) 53 54 return nil 55 } 56 57 func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { 58 var ( 59 // Index. 60 indexErr error 61 indexData []byte 62 downloadURL string 63 64 // Signature. 65 sigErr error 66 verifiedHash *lhash.LabeledHash 67 sigFileData []byte 68 verifOpts = reg.GetVerificationOptions(idx.Path) 69 ) 70 71 // Upgrade to v2 index if verification is enabled. 72 downloadIndexPath := idx.Path 73 if verifOpts != nil { 74 downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension 75 } 76 77 // Download new index and signature. 78 for tries := 0; tries < 3; tries++ { 79 // Index and signature need to be fetched together, so that they are 80 // fetched from the same source. One source should always have a matching 81 // index and signature. Backup sources may be behind a little. 82 // If the signature verification fails, another source should be tried. 83 84 // Get index data. 85 indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) 86 if indexErr != nil { 87 log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) 88 continue 89 } 90 91 // Get signature and verify it. 92 if verifOpts != nil { 93 verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( 94 ctx, client, 95 verifOpts, downloadIndexPath+filesig.Extension, nil, 96 tries, 97 ) 98 if sigErr != nil { 99 log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) 100 continue 101 } 102 103 // Check if the index matches the verified hash. 104 if verifiedHash.Matches(indexData) { 105 log.Infof("%s: verified signature of %s", reg.Name, downloadURL) 106 } else { 107 sigErr = ErrIndexChecksumMismatch 108 log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) 109 continue 110 } 111 } 112 113 break 114 } 115 if indexErr != nil { 116 return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) 117 } 118 if sigErr != nil { 119 return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) 120 } 121 122 // Parse the index file. 123 indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) 124 if err != nil { 125 return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) 126 } 127 128 // Add index data to registry. 129 if len(indexFile.Releases) > 0 { 130 // Check if all resources are within the indexes' authority. 131 authoritativePath := path.Dir(idx.Path) + "/" 132 if authoritativePath == "./" { 133 // Fix path for indexes at the storage root. 134 authoritativePath = "" 135 } 136 cleanedData := make(map[string]string, len(indexFile.Releases)) 137 for key, version := range indexFile.Releases { 138 if strings.HasPrefix(key, authoritativePath) { 139 cleanedData[key] = version 140 } else { 141 log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) 142 } 143 } 144 145 // add resources to registry 146 err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) 147 if err != nil { 148 log.Warningf("%s: failed to add resources: %s", reg.Name, err) 149 } 150 } else { 151 log.Debugf("%s: index %s is empty", reg.Name, idx.Path) 152 } 153 154 // Check if dest dir exists. 155 indexDir := filepath.FromSlash(path.Dir(idx.Path)) 156 err = reg.storageDir.EnsureRelPath(indexDir) 157 if err != nil { 158 log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) 159 } 160 161 // Index files must be readable by portmaster-staert with user permissions in order to load the index. 162 err = os.WriteFile( //nolint:gosec 163 filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), 164 indexData, 0o0644, 165 ) 166 if err != nil { 167 log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) 168 } 169 170 // Write signature file, if we have one. 171 if len(sigFileData) > 0 { 172 err = os.WriteFile( //nolint:gosec 173 filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), 174 sigFileData, 0o0644, 175 ) 176 if err != nil { 177 log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) 178 } 179 } 180 181 log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) 182 return nil 183 } 184 185 // DownloadUpdates checks if updates are available and downloads updates of used components. 186 func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { 187 // Start registry operation. 188 reg.state.StartOperation(StateDownloading) 189 defer reg.state.EndOperation() 190 191 // Get pending updates. 192 toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) 193 downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) 194 reg.state.UpdateOperationDetails(&StateDownloadingDetails{ 195 Resources: downloadDetailsResources, 196 }) 197 198 // nothing to update 199 if len(toUpdate) == 0 && len(missingSigs) == 0 { 200 log.Infof("%s: everything up to date", reg.Name) 201 return nil 202 } 203 204 // check download dir 205 if err := reg.tmpDir.Ensure(); err != nil { 206 return fmt.Errorf("could not prepare tmp directory for download: %w", err) 207 } 208 209 // download updates 210 log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) 211 client := &http.Client{} 212 var reportError error 213 214 for i, rv := range toUpdate { 215 log.Infof( 216 "%s: downloading update [%d/%d]: %s version %s", 217 reg.Name, 218 i+1, len(toUpdate), 219 rv.resource.Identifier, rv.VersionNumber, 220 ) 221 var err error 222 for tries := 0; tries < 3; tries++ { 223 err = reg.fetchFile(ctx, client, rv, tries) 224 if err == nil { 225 // Update resource version state. 226 rv.resource.Lock() 227 rv.Available = true 228 if rv.resource.VerificationOptions != nil { 229 rv.SigAvailable = true 230 } 231 rv.resource.Unlock() 232 233 break 234 } 235 } 236 if err != nil { 237 reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) 238 log.Warningf("%s: %s", reg.Name, reportError) 239 } 240 241 reg.state.UpdateOperationDetails(&StateDownloadingDetails{ 242 Resources: downloadDetailsResources, 243 FinishedUpTo: i + 1, 244 }) 245 } 246 247 if len(missingSigs) > 0 { 248 log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) 249 250 for _, rv := range missingSigs { 251 var err error 252 for tries := 0; tries < 3; tries++ { 253 err = reg.fetchMissingSig(ctx, client, rv, tries) 254 if err == nil { 255 // Update resource version state. 256 rv.resource.Lock() 257 rv.SigAvailable = true 258 rv.resource.Unlock() 259 260 break 261 } 262 } 263 if err != nil { 264 reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) 265 log.Warningf("%s: %s", reg.Name, reportError) 266 } 267 } 268 } 269 270 reg.state.ReportDownloads( 271 downloadDetailsResources, 272 reportError, 273 ) 274 log.Infof("%s: finished downloading updates", reg.Name) 275 276 return nil 277 } 278 279 // DownloadUpdates checks if updates are available and downloads updates of used components. 280 281 // GetPendingDownloads returns the list of pending downloads. 282 // If manual is set, indexes with AutoDownload=false will be checked. 283 // If auto is set, indexes with AutoDownload=true will be checked. 284 func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { 285 reg.RLock() 286 defer reg.RUnlock() 287 288 // create list of downloads 289 var toUpdate []*ResourceVersion 290 var missingSigs []*ResourceVersion 291 292 for _, res := range reg.resources { 293 func() { 294 res.Lock() 295 defer res.Unlock() 296 297 // Skip resources without index or indexes that should not be reported 298 // according to parameters. 299 switch { 300 case res.Index == nil: 301 // Cannot download if resource is not part of an index. 302 return 303 case manual && !res.Index.AutoDownload: 304 // Manual update report and index is not auto-download. 305 case auto && res.Index.AutoDownload: 306 // Auto update report and index is auto-download. 307 default: 308 // Resource should not be reported. 309 return 310 } 311 312 // Skip resources we don't need. 313 switch { 314 case res.inUse(): 315 // Update if resource is in use. 316 case res.available(): 317 // Update if resource is available locally, ie. was used in the past. 318 case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): 319 // Update is set as mandatory. 320 default: 321 // Resource does not need to be updated. 322 return 323 } 324 325 // Go through all versions until we find versions that need updating. 326 for _, rv := range res.Versions { 327 switch { 328 case !rv.CurrentRelease: 329 // We are not interested in older releases. 330 case !rv.Available: 331 // File not available locally, download! 332 toUpdate = append(toUpdate, rv) 333 case !rv.SigAvailable && res.VerificationOptions != nil: 334 // File signature is not available and verification is enabled, download signature! 335 missingSigs = append(missingSigs, rv) 336 } 337 } 338 }() 339 } 340 341 slices.SortFunc[[]*ResourceVersion, *ResourceVersion](toUpdate, func(a, b *ResourceVersion) int { 342 return strings.Compare(a.resource.Identifier, b.resource.Identifier) 343 }) 344 slices.SortFunc[[]*ResourceVersion, *ResourceVersion](missingSigs, func(a, b *ResourceVersion) int { 345 return strings.Compare(a.resource.Identifier, b.resource.Identifier) 346 }) 347 348 return toUpdate, missingSigs 349 } 350 351 func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { 352 identifiers := make([]string, len(resourceVersions)) 353 354 for i, rv := range resourceVersions { 355 identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) 356 } 357 358 return identifiers 359 }