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  }