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

     1  package updater
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"hash"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"time"
    16  
    17  	"github.com/safing/jess/filesig"
    18  	"github.com/safing/jess/lhash"
    19  	"github.com/safing/portbase/log"
    20  	"github.com/safing/portbase/utils/renameio"
    21  )
    22  
    23  func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
    24  	// backoff when retrying
    25  	if tries > 0 {
    26  		select {
    27  		case <-ctx.Done():
    28  			return nil // module is shutting down
    29  		case <-time.After(time.Duration(tries*tries) * time.Second):
    30  		}
    31  	}
    32  
    33  	// check destination dir
    34  	dirPath := filepath.Dir(rv.storagePath())
    35  	err := reg.storageDir.EnsureAbsPath(dirPath)
    36  	if err != nil {
    37  		return fmt.Errorf("could not create updates folder: %s", dirPath)
    38  	}
    39  
    40  	// If verification is enabled, download signature first.
    41  	var (
    42  		verifiedHash *lhash.LabeledHash
    43  		sigFileData  []byte
    44  	)
    45  	if rv.resource.VerificationOptions != nil {
    46  		verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile(
    47  			ctx, client,
    48  			rv.resource.VerificationOptions,
    49  			rv.versionedSigPath(), rv.SigningMetadata(),
    50  			tries,
    51  		)
    52  
    53  		if err != nil {
    54  			switch rv.resource.VerificationOptions.DownloadPolicy {
    55  			case SignaturePolicyRequire:
    56  				return fmt.Errorf("signature verification failed: %w", err)
    57  			case SignaturePolicyWarn:
    58  				log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
    59  			case SignaturePolicyDisable:
    60  				log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
    61  			}
    62  		}
    63  	}
    64  
    65  	// open file for writing
    66  	atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath())
    67  	if err != nil {
    68  		return fmt.Errorf("could not create temp file for download: %w", err)
    69  	}
    70  	defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
    71  
    72  	// start file download
    73  	resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer func() {
    78  		_ = resp.Body.Close()
    79  	}()
    80  
    81  	// Write to the hasher at the same time, if needed.
    82  	var hasher hash.Hash
    83  	var writeDst io.Writer = atomicFile
    84  	if verifiedHash != nil {
    85  		hasher = verifiedHash.Algorithm().RawHasher()
    86  		writeDst = io.MultiWriter(hasher, atomicFile)
    87  	}
    88  
    89  	// Download and write file.
    90  	n, err := io.Copy(writeDst, resp.Body)
    91  	if err != nil {
    92  		return fmt.Errorf("failed to download %q: %w", downloadURL, err)
    93  	}
    94  	if resp.ContentLength != n {
    95  		return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
    96  	}
    97  
    98  	// Before file is finalized, check if hash, if available.
    99  	if hasher != nil {
   100  		downloadDigest := hasher.Sum(nil)
   101  		if verifiedHash.EqualRaw(downloadDigest) {
   102  			log.Infof("%s: verified signature of %s", reg.Name, downloadURL)
   103  		} else {
   104  			switch rv.resource.VerificationOptions.DownloadPolicy {
   105  			case SignaturePolicyRequire:
   106  				return errors.New("file does not match signed checksum")
   107  			case SignaturePolicyWarn:
   108  				log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL)
   109  			case SignaturePolicyDisable:
   110  				log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL)
   111  			}
   112  
   113  			// Reset hasher to signal that the sig should not be written.
   114  			hasher = nil
   115  		}
   116  	}
   117  
   118  	// Write signature file, if we have one and if verification succeeded.
   119  	if len(sigFileData) > 0 && hasher != nil {
   120  		sigFilePath := rv.storagePath() + filesig.Extension
   121  		err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec
   122  		if err != nil {
   123  			switch rv.resource.VerificationOptions.DownloadPolicy {
   124  			case SignaturePolicyRequire:
   125  				return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err)
   126  			case SignaturePolicyWarn:
   127  				log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
   128  			case SignaturePolicyDisable:
   129  				log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
   130  			}
   131  		}
   132  	}
   133  
   134  	// finalize file
   135  	err = atomicFile.CloseAtomicallyReplace()
   136  	if err != nil {
   137  		return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
   138  	}
   139  	// set permissions
   140  	if !onWindows {
   141  		// TODO: only set executable files to 0755, set other to 0644
   142  		err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
   143  		if err != nil {
   144  			log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
   145  		}
   146  	}
   147  
   148  	log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath())
   149  	return nil
   150  }
   151  
   152  func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
   153  	// backoff when retrying
   154  	if tries > 0 {
   155  		select {
   156  		case <-ctx.Done():
   157  			return nil // module is shutting down
   158  		case <-time.After(time.Duration(tries*tries) * time.Second):
   159  		}
   160  	}
   161  
   162  	// Check destination dir.
   163  	dirPath := filepath.Dir(rv.storagePath())
   164  	err := reg.storageDir.EnsureAbsPath(dirPath)
   165  	if err != nil {
   166  		return fmt.Errorf("could not create updates folder: %s", dirPath)
   167  	}
   168  
   169  	// Download and verify the missing signature.
   170  	verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile(
   171  		ctx, client,
   172  		rv.resource.VerificationOptions,
   173  		rv.versionedSigPath(), rv.SigningMetadata(),
   174  		tries,
   175  	)
   176  	if err != nil {
   177  		switch rv.resource.VerificationOptions.DownloadPolicy {
   178  		case SignaturePolicyRequire:
   179  			return fmt.Errorf("signature verification failed: %w", err)
   180  		case SignaturePolicyWarn:
   181  			log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
   182  		case SignaturePolicyDisable:
   183  			log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
   184  		}
   185  		return nil
   186  	}
   187  
   188  	// Check if the signature matches the resource file.
   189  	ok, err := verifiedHash.MatchesFile(rv.storagePath())
   190  	if err != nil {
   191  		switch rv.resource.VerificationOptions.DownloadPolicy {
   192  		case SignaturePolicyRequire:
   193  			return fmt.Errorf("error while verifying resource file: %w", err)
   194  		case SignaturePolicyWarn:
   195  			log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
   196  		case SignaturePolicyDisable:
   197  			log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
   198  		}
   199  		return nil
   200  	}
   201  	if !ok {
   202  		switch rv.resource.VerificationOptions.DownloadPolicy {
   203  		case SignaturePolicyRequire:
   204  			return errors.New("resource file does not match signed checksum")
   205  		case SignaturePolicyWarn:
   206  			log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
   207  		case SignaturePolicyDisable:
   208  			log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
   209  		}
   210  		return nil
   211  	}
   212  
   213  	// Write signature file.
   214  	err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec
   215  	if err != nil {
   216  		switch rv.resource.VerificationOptions.DownloadPolicy {
   217  		case SignaturePolicyRequire:
   218  			return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err)
   219  		case SignaturePolicyWarn:
   220  			log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
   221  		case SignaturePolicyDisable:
   222  			log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
   223  		}
   224  	}
   225  
   226  	log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath())
   227  	return nil
   228  }
   229  
   230  func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) {
   231  	// Download signature file.
   232  	resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries)
   233  	if err != nil {
   234  		return nil, nil, err
   235  	}
   236  	defer func() {
   237  		_ = resp.Body.Close()
   238  	}()
   239  	sigFileData, err := io.ReadAll(resp.Body)
   240  	if err != nil {
   241  		return nil, nil, err
   242  	}
   243  
   244  	// Extract all signatures.
   245  	sigs, err := filesig.ParseSigFile(sigFileData)
   246  	switch {
   247  	case len(sigs) == 0 && err != nil:
   248  		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
   249  	case len(sigs) == 0:
   250  		return nil, nil, errors.New("no signatures found in signature file")
   251  	case err != nil:
   252  		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
   253  	}
   254  
   255  	// Verify all signatures.
   256  	var verifiedHash *lhash.LabeledHash
   257  	for _, sig := range sigs {
   258  		fd, err := filesig.VerifyFileData(
   259  			sig,
   260  			requiredMetadata,
   261  			verifOpts.TrustStore,
   262  		)
   263  		if err != nil {
   264  			return nil, sigFileData, err
   265  		}
   266  
   267  		// Save or check verified hash.
   268  		if verifiedHash == nil {
   269  			verifiedHash = fd.FileHash()
   270  		} else if !fd.FileHash().Equal(verifiedHash) {
   271  			// Return an error if two valid hashes mismatch.
   272  			// For simplicity, all hash algorithms must be the same for now.
   273  			return nil, sigFileData, errors.New("file hashes from different signatures do not match")
   274  		}
   275  	}
   276  
   277  	return verifiedHash, sigFileData, nil
   278  }
   279  
   280  func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) {
   281  	// backoff when retrying
   282  	if tries > 0 {
   283  		select {
   284  		case <-ctx.Done():
   285  			return nil, "", nil // module is shutting down
   286  		case <-time.After(time.Duration(tries*tries) * time.Second):
   287  		}
   288  	}
   289  
   290  	// start file download
   291  	resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries)
   292  	if err != nil {
   293  		return nil, downloadURL, err
   294  	}
   295  	defer func() {
   296  		_ = resp.Body.Close()
   297  	}()
   298  
   299  	// download and write file
   300  	buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
   301  	n, err := io.Copy(buf, resp.Body)
   302  	if err != nil {
   303  		return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err)
   304  	}
   305  	if resp.ContentLength != n {
   306  		return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
   307  	}
   308  
   309  	return buf.Bytes(), downloadURL, nil
   310  }
   311  
   312  func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) {
   313  	// parse update URL
   314  	updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)]
   315  	u, err := url.Parse(updateBaseURL)
   316  	if err != nil {
   317  		return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err)
   318  	}
   319  	// add download path
   320  	u.Path = path.Join(u.Path, downloadPath)
   321  	// compile URL
   322  	downloadURL = u.String()
   323  
   324  	// create request
   325  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody)
   326  	if err != nil {
   327  		return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err)
   328  	}
   329  
   330  	// set user agent
   331  	if reg.UserAgent != "" {
   332  		req.Header.Set("User-Agent", reg.UserAgent)
   333  	}
   334  
   335  	// start request
   336  	resp, err = client.Do(req)
   337  	if err != nil {
   338  		return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err)
   339  	}
   340  
   341  	// check return code
   342  	if resp.StatusCode != http.StatusOK {
   343  		_ = resp.Body.Close()
   344  		return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status)
   345  	}
   346  
   347  	return resp, downloadURL, err
   348  }