[WIP] New updater first working prototype

This commit is contained in:
Vladimir Stoilov
2024-08-16 16:05:01 +03:00
parent abf444630b
commit 9bae1afd73
46 changed files with 4107 additions and 4149 deletions

View File

@@ -1,15 +1,15 @@
package updater package updater
// Export exports the list of resources. // // Export exports the list of resources.
func (reg *ResourceRegistry) Export() map[string]*Resource { // func (reg *ResourceRegistry) Export() map[string]*Resource {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
// copy the map // // copy the map
copiedResources := make(map[string]*Resource) // copiedResources := make(map[string]*Resource)
for key, val := range reg.resources { // for key, val := range reg.resources {
copiedResources[key] = val.Export() // copiedResources[key] = val.Export()
} // }
return copiedResources // return copiedResources
} // }

View File

@@ -1,347 +1,347 @@
package updater package updater
import ( // import (
"bytes" // "bytes"
"context" // "context"
"errors" // "errors"
"fmt" // "fmt"
"hash" // "hash"
"io" // "io"
"net/http" // "net/http"
"net/url" // "net/url"
"os" // "os"
"path" // "path"
"path/filepath" // "path/filepath"
"time" // "time"
"github.com/safing/jess/filesig" // "github.com/safing/jess/filesig"
"github.com/safing/jess/lhash" // "github.com/safing/jess/lhash"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils/renameio" // "github.com/safing/portmaster/base/utils/renameio"
) // )
func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { // func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
// backoff when retrying // // backoff when retrying
if tries > 0 { // if tries > 0 {
select { // select {
case <-ctx.Done(): // case <-ctx.Done():
return nil // module is shutting down // return nil // module is shutting down
case <-time.After(time.Duration(tries*tries) * time.Second): // case <-time.After(time.Duration(tries*tries) * time.Second):
} // }
} // }
// check destination dir // // check destination dir
dirPath := filepath.Dir(rv.storagePath()) // dirPath := filepath.Dir(rv.storagePath())
err := reg.storageDir.EnsureAbsPath(dirPath) // err := reg.storageDir.EnsureAbsPath(dirPath)
if err != nil { // if err != nil {
return fmt.Errorf("could not create updates folder: %s", dirPath) // return fmt.Errorf("could not create updates folder: %s", dirPath)
} // }
// If verification is enabled, download signature first. // // If verification is enabled, download signature first.
var ( // var (
verifiedHash *lhash.LabeledHash // verifiedHash *lhash.LabeledHash
sigFileData []byte // sigFileData []byte
) // )
if rv.resource.VerificationOptions != nil { // if rv.resource.VerificationOptions != nil {
verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( // verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile(
ctx, client, // ctx, client,
rv.resource.VerificationOptions, // rv.resource.VerificationOptions,
rv.versionedSigPath(), rv.SigningMetadata(), // rv.versionedSigPath(), rv.SigningMetadata(),
tries, // tries,
) // )
if err != nil { // if err != nil {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("signature verification failed: %w", err) // return fmt.Errorf("signature verification failed: %w", err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) // log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) // log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
} // }
} // }
} // }
// open file for writing // // open file for writing
atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) // atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath())
if err != nil { // if err != nil {
return fmt.Errorf("could not create temp file for download: %w", err) // return fmt.Errorf("could not create temp file for download: %w", err)
} // }
defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway // defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
// start file download // // start file download
resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) // resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries)
if err != nil { // if err != nil {
return err // return err
} // }
defer func() { // defer func() {
_ = resp.Body.Close() // _ = resp.Body.Close()
}() // }()
// Write to the hasher at the same time, if needed. // // Write to the hasher at the same time, if needed.
var hasher hash.Hash // var hasher hash.Hash
var writeDst io.Writer = atomicFile // var writeDst io.Writer = atomicFile
if verifiedHash != nil { // if verifiedHash != nil {
hasher = verifiedHash.Algorithm().RawHasher() // hasher = verifiedHash.Algorithm().RawHasher()
writeDst = io.MultiWriter(hasher, atomicFile) // writeDst = io.MultiWriter(hasher, atomicFile)
} // }
// Download and write file. // // Download and write file.
n, err := io.Copy(writeDst, resp.Body) // n, err := io.Copy(writeDst, resp.Body)
if err != nil { // if err != nil {
return fmt.Errorf("failed to download %q: %w", downloadURL, err) // return fmt.Errorf("failed to download %q: %w", downloadURL, err)
} // }
if resp.ContentLength != n { // if resp.ContentLength != n {
return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) // return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
} // }
// Before file is finalized, check if hash, if available. // // Before file is finalized, check if hash, if available.
if hasher != nil { // if hasher != nil {
downloadDigest := hasher.Sum(nil) // downloadDigest := hasher.Sum(nil)
if verifiedHash.EqualRaw(downloadDigest) { // if verifiedHash.EqualRaw(downloadDigest) {
log.Infof("%s: verified signature of %s", reg.Name, downloadURL) // log.Infof("%s: verified signature of %s", reg.Name, downloadURL)
} else { // } else {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return errors.New("file does not match signed checksum") // return errors.New("file does not match signed checksum")
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) // log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) // log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL)
} // }
// Reset hasher to signal that the sig should not be written. // // Reset hasher to signal that the sig should not be written.
hasher = nil // hasher = nil
} // }
} // }
// Write signature file, if we have one and if verification succeeded. // // Write signature file, if we have one and if verification succeeded.
if len(sigFileData) > 0 && hasher != nil { // if len(sigFileData) > 0 && hasher != nil {
sigFilePath := rv.storagePath() + filesig.Extension // sigFilePath := rv.storagePath() + filesig.Extension
err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec // err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec
if err != nil { // if err != nil {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) // return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) // log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) // log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
} // }
} // }
} // }
// finalize file // // finalize file
err = atomicFile.CloseAtomicallyReplace() // err = atomicFile.CloseAtomicallyReplace()
if err != nil { // if err != nil {
return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) // return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
} // }
// set permissions // // set permissions
if !onWindows { // if !onWindows {
// TODO: only set executable files to 0755, set other to 0644 // // TODO: only set executable files to 0755, set other to 0644
err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. // err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
if err != nil { // if err != nil {
log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) // log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
} // }
} // }
log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) // log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath())
return nil // return nil
} // }
func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { // func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
// backoff when retrying // // backoff when retrying
if tries > 0 { // if tries > 0 {
select { // select {
case <-ctx.Done(): // case <-ctx.Done():
return nil // module is shutting down // return nil // module is shutting down
case <-time.After(time.Duration(tries*tries) * time.Second): // case <-time.After(time.Duration(tries*tries) * time.Second):
} // }
} // }
// Check destination dir. // // Check destination dir.
dirPath := filepath.Dir(rv.storagePath()) // dirPath := filepath.Dir(rv.storagePath())
err := reg.storageDir.EnsureAbsPath(dirPath) // err := reg.storageDir.EnsureAbsPath(dirPath)
if err != nil { // if err != nil {
return fmt.Errorf("could not create updates folder: %s", dirPath) // return fmt.Errorf("could not create updates folder: %s", dirPath)
} // }
// Download and verify the missing signature. // // Download and verify the missing signature.
verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( // verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile(
ctx, client, // ctx, client,
rv.resource.VerificationOptions, // rv.resource.VerificationOptions,
rv.versionedSigPath(), rv.SigningMetadata(), // rv.versionedSigPath(), rv.SigningMetadata(),
tries, // tries,
) // )
if err != nil { // if err != nil {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("signature verification failed: %w", err) // return fmt.Errorf("signature verification failed: %w", err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) // log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) // log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
} // }
return nil // return nil
} // }
// Check if the signature matches the resource file. // // Check if the signature matches the resource file.
ok, err := verifiedHash.MatchesFile(rv.storagePath()) // ok, err := verifiedHash.MatchesFile(rv.storagePath())
if err != nil { // if err != nil {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("error while verifying resource file: %w", err) // return fmt.Errorf("error while verifying resource file: %w", err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) // log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) // log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
} // }
return nil // return nil
} // }
if !ok { // if !ok {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return errors.New("resource file does not match signed checksum") // return errors.New("resource file does not match signed checksum")
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) // log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) // log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
} // }
return nil // return nil
} // }
// Write signature file. // // Write signature file.
err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec // err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec
if err != nil { // if err != nil {
switch rv.resource.VerificationOptions.DownloadPolicy { // switch rv.resource.VerificationOptions.DownloadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) // return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) // log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) // log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
} // }
} // }
log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) // log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath())
return nil // return nil
} // }
func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { // func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) {
// Download signature file. // // Download signature file.
resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) // resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries)
if err != nil { // if err != nil {
return nil, nil, err // return nil, nil, err
} // }
defer func() { // defer func() {
_ = resp.Body.Close() // _ = resp.Body.Close()
}() // }()
sigFileData, err := io.ReadAll(resp.Body) // sigFileData, err := io.ReadAll(resp.Body)
if err != nil { // if err != nil {
return nil, nil, err // return nil, nil, err
} // }
// Extract all signatures. // // Extract all signatures.
sigs, err := filesig.ParseSigFile(sigFileData) // sigs, err := filesig.ParseSigFile(sigFileData)
switch { // switch {
case len(sigs) == 0 && err != nil: // case len(sigs) == 0 && err != nil:
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) // return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
case len(sigs) == 0: // case len(sigs) == 0:
return nil, nil, errors.New("no signatures found in signature file") // return nil, nil, errors.New("no signatures found in signature file")
case err != nil: // case err != nil:
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) // return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
} // }
// Verify all signatures. // // Verify all signatures.
var verifiedHash *lhash.LabeledHash // var verifiedHash *lhash.LabeledHash
for _, sig := range sigs { // for _, sig := range sigs {
fd, err := filesig.VerifyFileData( // fd, err := filesig.VerifyFileData(
sig, // sig,
requiredMetadata, // requiredMetadata,
verifOpts.TrustStore, // verifOpts.TrustStore,
) // )
if err != nil { // if err != nil {
return nil, sigFileData, err // return nil, sigFileData, err
} // }
// Save or check verified hash. // // Save or check verified hash.
if verifiedHash == nil { // if verifiedHash == nil {
verifiedHash = fd.FileHash() // verifiedHash = fd.FileHash()
} else if !fd.FileHash().Equal(verifiedHash) { // } else if !fd.FileHash().Equal(verifiedHash) {
// Return an error if two valid hashes mismatch. // // Return an error if two valid hashes mismatch.
// For simplicity, all hash algorithms must be the same for now. // // 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 nil, sigFileData, errors.New("file hashes from different signatures do not match")
} // }
} // }
return verifiedHash, sigFileData, nil // return verifiedHash, sigFileData, nil
} // }
func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { // func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) {
// backoff when retrying // // backoff when retrying
if tries > 0 { // if tries > 0 {
select { // select {
case <-ctx.Done(): // case <-ctx.Done():
return nil, "", nil // module is shutting down // return nil, "", nil // module is shutting down
case <-time.After(time.Duration(tries*tries) * time.Second): // case <-time.After(time.Duration(tries*tries) * time.Second):
} // }
} // }
// start file download // // start file download
resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) // resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries)
if err != nil { // if err != nil {
return nil, downloadURL, err // return nil, downloadURL, err
} // }
defer func() { // defer func() {
_ = resp.Body.Close() // _ = resp.Body.Close()
}() // }()
// download and write file // // download and write file
buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) // buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
n, err := io.Copy(buf, resp.Body) // n, err := io.Copy(buf, resp.Body)
if err != nil { // if err != nil {
return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) // return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err)
} // }
if resp.ContentLength != n { // if resp.ContentLength != n {
return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) // return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
} // }
return buf.Bytes(), downloadURL, nil // return buf.Bytes(), downloadURL, nil
} // }
func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { // func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) {
// parse update URL // // parse update URL
updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] // updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)]
u, err := url.Parse(updateBaseURL) // u, err := url.Parse(updateBaseURL)
if err != nil { // if err != nil {
return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) // return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err)
} // }
// add download path // // add download path
u.Path = path.Join(u.Path, downloadPath) // u.Path = path.Join(u.Path, downloadPath)
// compile URL // // compile URL
downloadURL = u.String() // downloadURL = u.String()
// create request // // create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) // req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody)
if err != nil { // if err != nil {
return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) // return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err)
} // }
// set user agent // // set user agent
if reg.UserAgent != "" { // if reg.UserAgent != "" {
req.Header.Set("User-Agent", reg.UserAgent) // req.Header.Set("User-Agent", reg.UserAgent)
} // }
// start request // // start request
resp, err = client.Do(req) // resp, err = client.Do(req)
if err != nil { // if err != nil {
return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) // return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err)
} // }
// check return code // // check return code
if resp.StatusCode != http.StatusOK { // if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close() // _ = resp.Body.Close()
return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) // return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status)
} // }
return resp, downloadURL, err // return resp, downloadURL, err
} // }

View File

@@ -1,105 +1,97 @@
package updater package updater
import ( import (
"errors"
"io"
"io/fs"
"os"
"strings"
semver "github.com/hashicorp/go-version" // semver "github.com/hashicorp/go-version"
"github.com/safing/jess/filesig"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils"
) )
// File represents a file from the update system. // File represents a file from the update system.
type File struct { // type File struct {
resource *Resource // resource *Resource
version *ResourceVersion // version *ResourceVersion
notifier *notifier // notifier *notifier
versionedPath string // versionedPath string
storagePath string // storagePath string
} // }
// Identifier returns the identifier of the file. // // Identifier returns the identifier of the file.
func (file *File) Identifier() string { // func (file *File) Identifier() string {
return file.resource.Identifier // return file.resource.Identifier
} // }
// Version returns the version of the file. // // Version returns the version of the file.
func (file *File) Version() string { // func (file *File) Version() string {
return file.version.VersionNumber // return file.version.VersionNumber
} // }
// SemVer returns the semantic version of the file. // // SemVer returns the semantic version of the file.
func (file *File) SemVer() *semver.Version { // func (file *File) SemVer() *semver.Version {
return file.version.semVer // return file.version.semVer
} // }
// EqualsVersion normalizes the given version and checks equality with semver. // // EqualsVersion normalizes the given version and checks equality with semver.
func (file *File) EqualsVersion(version string) bool { // func (file *File) EqualsVersion(version string) bool {
return file.version.EqualsVersion(version) // return file.version.EqualsVersion(version)
} // }
// Path returns the absolute filepath of the file. // // Path returns the absolute filepath of the file.
func (file *File) Path() string { // func (file *File) Path() string {
return file.storagePath // return file.storagePath
} // }
// SigningMetadata returns the metadata to be included in signatures. // // SigningMetadata returns the metadata to be included in signatures.
func (file *File) SigningMetadata() map[string]string { // func (file *File) SigningMetadata() map[string]string {
return map[string]string{ // return map[string]string{
"id": file.Identifier(), // "id": file.Identifier(),
"version": file.Version(), // "version": file.Version(),
} // }
} // }
// Verify verifies the given file. // Verify verifies the given file.
func (file *File) Verify() ([]*filesig.FileData, error) { // func (file *File) Verify() ([]*filesig.FileData, error) {
// Check if verification is configured. // // Check if verification is configured.
if file.resource.VerificationOptions == nil { // if file.resource.VerificationOptions == nil {
return nil, ErrVerificationNotConfigured // return nil, ErrVerificationNotConfigured
} // }
// Verify file. // // Verify file.
fileData, err := filesig.VerifyFile( // fileData, err := filesig.VerifyFile(
file.storagePath, // file.storagePath,
file.storagePath+filesig.Extension, // file.storagePath+filesig.Extension,
file.SigningMetadata(), // file.SigningMetadata(),
file.resource.VerificationOptions.TrustStore, // file.resource.VerificationOptions.TrustStore,
) // )
if err != nil { // if err != nil {
switch file.resource.VerificationOptions.DiskLoadPolicy { // switch file.resource.VerificationOptions.DiskLoadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return nil, err // return nil, err
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) // log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) // log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err)
} // }
} // }
return fileData, nil // return fileData, nil
} // }
// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted. // Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted.
func (file *File) Blacklist() error { // func (file *File) Blacklist() error {
return file.resource.Blacklist(file.version.VersionNumber) // return file.resource.Blacklist(file.version.VersionNumber)
} // }
// markActiveWithLocking marks the file as active, locking the resource in the process. // markActiveWithLocking marks the file as active, locking the resource in the process.
func (file *File) markActiveWithLocking() { // func (file *File) markActiveWithLocking() {
file.resource.Lock() // file.resource.Lock()
defer file.resource.Unlock() // defer file.resource.Unlock()
// update last used version // // update last used version
if file.resource.ActiveVersion != file.version { // if file.resource.ActiveVersion != file.version {
log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) // log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber)
file.resource.ActiveVersion = file.version // file.resource.ActiveVersion = file.version
} // }
} // }
// Unpacker describes the function that is passed to // Unpacker describes the function that is passed to
// File.Unpack. It receives a reader to the compressed/packed // File.Unpack. It receives a reader to the compressed/packed
@@ -107,50 +99,50 @@ func (file *File) markActiveWithLocking() {
// unpacked file contents. If the returned reader implements // unpacked file contents. If the returned reader implements
// io.Closer it's close method is invoked when an error // io.Closer it's close method is invoked when an error
// or io.EOF is returned from Read(). // or io.EOF is returned from Read().
type Unpacker func(io.Reader) (io.Reader, error) // type Unpacker func(io.Reader) (io.Reader, error)
// Unpack returns the path to the unpacked version of file and // Unpack returns the path to the unpacked version of file and
// unpacks it on demand using unpacker. // unpacks it on demand using unpacker.
func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { // func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) {
path := strings.TrimSuffix(file.Path(), suffix) // path := strings.TrimSuffix(file.Path(), suffix)
if suffix == "" { // if suffix == "" {
path += "-unpacked" // path += "-unpacked"
} // }
_, err := os.Stat(path) // _, err := os.Stat(path)
if err == nil { // if err == nil {
return path, nil // return path, nil
} // }
if !errors.Is(err, fs.ErrNotExist) { // if !errors.Is(err, fs.ErrNotExist) {
return "", err // return "", err
} // }
f, err := os.Open(file.Path()) // f, err := os.Open(file.Path())
if err != nil { // if err != nil {
return "", err // return "", err
} // }
defer func() { // defer func() {
_ = f.Close() // _ = f.Close()
}() // }()
r, err := unpacker(f) // r, err := unpacker(f)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ // ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{
TempDir: file.resource.registry.TmpDir().Path, // TempDir: file.resource.registry.TmpDir().Path,
}) // })
if c, ok := r.(io.Closer); ok { // if c, ok := r.(io.Closer); ok {
if err := c.Close(); err != nil && ioErr == nil { // if err := c.Close(); err != nil && ioErr == nil {
// if ioErr is already set we ignore the error from // // if ioErr is already set we ignore the error from
// closing the unpacker. // // closing the unpacker.
ioErr = err // ioErr = err
} // }
} // }
return path, ioErr // return path, ioErr
} // }

View File

@@ -2,7 +2,6 @@ package updater
import ( import (
"errors" "errors"
"fmt"
) )
// Errors returned by the updater package. // Errors returned by the updater package.
@@ -14,75 +13,75 @@ var (
// GetFile returns the selected (mostly newest) file with the given // GetFile returns the selected (mostly newest) file with the given
// identifier or an error, if it fails. // identifier or an error, if it fails.
func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { // func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) {
return nil, fmt.Errorf("invalid file: %s", identifier) // return nil, fmt.Errorf("invalid file: %s", identifier)
// reg.RLock() // reg.RLock()
// res, ok := reg.resources[identifier] // res, ok := reg.resources[identifier]
// reg.RUnlock() // reg.RUnlock()
// if !ok { // if !ok {
// return nil, ErrNotFound // return nil, ErrNotFound
// } // }
// file := res.GetFile() // file := res.GetFile()
// // check if file is available locally // // check if file is available locally
// if file.version.Available { // if file.version.Available {
// file.markActiveWithLocking() // file.markActiveWithLocking()
// // Verify file, if configured. // // Verify file, if configured.
// _, err := file.Verify() // _, err := file.Verify()
// if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { // if err != nil && !errors.Is(err, ErrVerificationNotConfigured) {
// // TODO: If verification is required, try deleting the resource and downloading it again. // // TODO: If verification is required, try deleting the resource and downloading it again.
// return nil, fmt.Errorf("failed to verify file: %w", err) // return nil, fmt.Errorf("failed to verify file: %w", err)
// } // }
// return file, nil // return file, nil
// } // }
// // check if online // // check if online
// if !reg.Online { // if !reg.Online {
// return nil, ErrNotAvailableLocally // return nil, ErrNotAvailableLocally
// } // }
// // check download dir // // check download dir
// err := reg.tmpDir.Ensure() // err := reg.tmpDir.Ensure()
// if err != nil { // if err != nil {
// return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) // return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err)
// } // }
// // Start registry operation. // // Start registry operation.
// reg.state.StartOperation(StateFetching) // reg.state.StartOperation(StateFetching)
// defer reg.state.EndOperation() // defer reg.state.EndOperation()
// // download file // // download file
// log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) // log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath)
// client := &http.Client{} // client := &http.Client{}
// for tries := range 5 { // for tries := range 5 {
// err = reg.fetchFile(context.TODO(), client, file.version, tries) // err = reg.fetchFile(context.TODO(), client, file.version, tries)
// if err != nil { // if err != nil {
// log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) // log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1)
// } else { // } else {
// file.markActiveWithLocking() // file.markActiveWithLocking()
// // TODO: We just download the file - should we verify it again? // // TODO: We just download the file - should we verify it again?
// return file, nil // return file, nil
// } // }
// } // }
// log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) // log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err)
// return nil, err // return nil, err
} // }
// GetVersion returns the selected version of the given identifier. // GetVersion returns the selected version of the given identifier.
// The returned resource version may not be modified. // The returned resource version may not be modified.
func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { // func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) {
reg.RLock() // reg.RLock()
res, ok := reg.resources[identifier] // res, ok := reg.resources[identifier]
reg.RUnlock() // reg.RUnlock()
if !ok { // if !ok {
return nil, ErrNotFound // return nil, ErrNotFound
} // }
res.Lock() // res.Lock()
defer res.Unlock() // defer res.Unlock()
return res.SelectedVersion, nil // return res.SelectedVersion, nil
} // }

View File

@@ -1,33 +1,33 @@
package updater package updater
import ( // import (
"github.com/tevino/abool" // "github.com/tevino/abool"
) // )
type notifier struct { // type notifier struct {
upgradeAvailable *abool.AtomicBool // upgradeAvailable *abool.AtomicBool
notifyChannel chan struct{} // notifyChannel chan struct{}
} // }
func newNotifier() *notifier { // func newNotifier() *notifier {
return &notifier{ // return &notifier{
upgradeAvailable: abool.NewBool(false), // upgradeAvailable: abool.NewBool(false),
notifyChannel: make(chan struct{}), // notifyChannel: make(chan struct{}),
} // }
} // }
func (n *notifier) markAsUpgradeable() { // func (n *notifier) markAsUpgradeable() {
if n.upgradeAvailable.SetToIf(false, true) { // if n.upgradeAvailable.SetToIf(false, true) {
close(n.notifyChannel) // close(n.notifyChannel)
} // }
} // }
// UpgradeAvailable returns whether an upgrade is available for this file. // // UpgradeAvailable returns whether an upgrade is available for this file.
func (file *File) UpgradeAvailable() bool { // func (file *File) UpgradeAvailable() bool {
return file.notifier.upgradeAvailable.IsSet() // return file.notifier.upgradeAvailable.IsSet()
} // }
// WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. // // WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available.
func (file *File) WaitForAvailableUpgrade() <-chan struct{} { // func (file *File) WaitForAvailableUpgrade() <-chan struct{} {
return file.notifier.notifyChannel // return file.notifier.notifyChannel
} // }

View File

@@ -1,270 +1,270 @@
package updater package updater
import ( // import (
"errors" // "errors"
"fmt" // "fmt"
"os" // "os"
"path/filepath" // "path/filepath"
"runtime" // "runtime"
"strings" // "strings"
"sync" // "sync"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) // )
const ( // const (
onWindows = runtime.GOOS == "windows" // onWindows = runtime.GOOS == "windows"
) // )
// ResourceRegistry is a registry for managing update resources. // ResourceRegistry is a registry for managing update resources.
type ResourceRegistry struct { // type ResourceRegistry struct {
sync.RWMutex // sync.RWMutex
Name string // Name string
storageDir *utils.DirStructure // storageDir *utils.DirStructure
tmpDir *utils.DirStructure // tmpDir *utils.DirStructure
indexes []*Index // indexes []*Index
state *RegistryState // state *RegistryState
resources map[string]*Resource // resources map[string]*Resource
UpdateURLs []string // UpdateURLs []string
UserAgent string // UserAgent string
MandatoryUpdates []string // MandatoryUpdates []string
AutoUnpack []string // AutoUnpack []string
// Verification holds a map of VerificationOptions assigned to their // // Verification holds a map of VerificationOptions assigned to their
// applicable identifier path prefix. // // applicable identifier path prefix.
// Use an empty string to denote the default. // // Use an empty string to denote the default.
// Use empty options to disable verification for a path prefix. // // Use empty options to disable verification for a path prefix.
Verification map[string]*VerificationOptions // Verification map[string]*VerificationOptions
// UsePreReleases signifies that pre-releases should be used when selecting a // // UsePreReleases signifies that pre-releases should be used when selecting a
// version. Even if false, a pre-release version will still be used if it is // // version. Even if false, a pre-release version will still be used if it is
// defined as the current version by an index. // // defined as the current version by an index.
UsePreReleases bool // UsePreReleases bool
// DevMode specifies if a local 0.0.0 version should be always chosen, when available. // // DevMode specifies if a local 0.0.0 version should be always chosen, when available.
DevMode bool // DevMode bool
// Online specifies if resources may be downloaded if not available locally. // // Online specifies if resources may be downloaded if not available locally.
Online bool // Online bool
// StateNotifyFunc may be set to receive any changes to the registry state. // // StateNotifyFunc may be set to receive any changes to the registry state.
// The specified function may lock the state, but may not block or take a // // The specified function may lock the state, but may not block or take a
// lot of time. // // lot of time.
StateNotifyFunc func(*RegistryState) // StateNotifyFunc func(*RegistryState)
} // }
// AddIndex adds a new index to the resource registry. // // AddIndex adds a new index to the resource registry.
// The order is important, as indexes added later will override the current // // The order is important, as indexes added later will override the current
// release from earlier indexes. // // release from earlier indexes.
func (reg *ResourceRegistry) AddIndex(idx Index) { // func (reg *ResourceRegistry) AddIndex(idx Index) {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
// Get channel name from path. // // Get channel name from path.
idx.Channel = strings.TrimSuffix( // idx.Channel = strings.TrimSuffix(
filepath.Base(idx.Path), filepath.Ext(idx.Path), // filepath.Base(idx.Path), filepath.Ext(idx.Path),
) // )
reg.indexes = append(reg.indexes, &idx) // reg.indexes = append(reg.indexes, &idx)
} // }
// PreInitUpdateState sets the initial update state of the registry before initialization. // // PreInitUpdateState sets the initial update state of the registry before initialization.
func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { // func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error {
if reg.state != nil { // if reg.state != nil {
return errors.New("registry already initialized") // return errors.New("registry already initialized")
} // }
reg.state = &RegistryState{ // reg.state = &RegistryState{
Updates: s, // Updates: s,
} // }
return nil // return nil
} // }
// Initialize initializes a raw registry struct and makes it ready for usage. // // Initialize initializes a raw registry struct and makes it ready for usage.
func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { // func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error {
// check if storage dir is available // // check if storage dir is available
err := storageDir.Ensure() // err := storageDir.Ensure()
if err != nil { // if err != nil {
return err // return err
} // }
// set default name // // set default name
if reg.Name == "" { // if reg.Name == "" {
reg.Name = "updater" // reg.Name = "updater"
} // }
// initialize private attributes // // initialize private attributes
reg.storageDir = storageDir // reg.storageDir = storageDir
reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) // reg.tmpDir = storageDir.ChildDir("tmp", 0o0700)
reg.resources = make(map[string]*Resource) // reg.resources = make(map[string]*Resource)
if reg.state == nil { // if reg.state == nil {
reg.state = &RegistryState{} // reg.state = &RegistryState{}
} // }
reg.state.ID = StateReady // reg.state.ID = StateReady
reg.state.reg = reg // reg.state.reg = reg
// remove tmp dir to delete old entries // // remove tmp dir to delete old entries
err = reg.Cleanup() // err = reg.Cleanup()
if err != nil { // if err != nil {
log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) // log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err)
} // }
// (re-)create tmp dir // // (re-)create tmp dir
err = reg.tmpDir.Ensure() // err = reg.tmpDir.Ensure()
if err != nil { // if err != nil {
log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) // log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err)
} // }
// Check verification options. // // Check verification options.
if reg.Verification != nil { // if reg.Verification != nil {
for prefix, opts := range reg.Verification { // for prefix, opts := range reg.Verification {
// Check if verification is disable for this prefix. // // Check if verification is disable for this prefix.
if opts == nil { // if opts == nil {
continue // continue
} // }
// If enabled, a trust store is required. // // If enabled, a trust store is required.
if opts.TrustStore == nil { // if opts.TrustStore == nil {
return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) // return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix)
} // }
// DownloadPolicy must be equal or stricter than DiskLoadPolicy. // // DownloadPolicy must be equal or stricter than DiskLoadPolicy.
if opts.DiskLoadPolicy < opts.DownloadPolicy { // if opts.DiskLoadPolicy < opts.DownloadPolicy {
return errors.New("verification download policy must be equal or stricter than the disk load policy") // return errors.New("verification download policy must be equal or stricter than the disk load policy")
} // }
// Warn if all policies are disabled. // // Warn if all policies are disabled.
if opts.DownloadPolicy == SignaturePolicyDisable && // if opts.DownloadPolicy == SignaturePolicyDisable &&
opts.DiskLoadPolicy == SignaturePolicyDisable { // opts.DiskLoadPolicy == SignaturePolicyDisable {
log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) // log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix)
} // }
} // }
} // }
return nil // return nil
} // }
// StorageDir returns the main storage dir of the resource registry. // // StorageDir returns the main storage dir of the resource registry.
func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { // func (reg *ResourceRegistry) StorageDir() *utils.DirStructure {
return reg.storageDir // return reg.storageDir
} // }
// TmpDir returns the temporary working dir of the resource registry. // // TmpDir returns the temporary working dir of the resource registry.
func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { // func (reg *ResourceRegistry) TmpDir() *utils.DirStructure {
return reg.tmpDir // return reg.tmpDir
} // }
// SetDevMode sets the development mode flag. // // SetDevMode sets the development mode flag.
func (reg *ResourceRegistry) SetDevMode(on bool) { // func (reg *ResourceRegistry) SetDevMode(on bool) {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
reg.DevMode = on // reg.DevMode = on
} // }
// SetUsePreReleases sets the UsePreReleases flag. // // SetUsePreReleases sets the UsePreReleases flag.
func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { // func (reg *ResourceRegistry) SetUsePreReleases(yes bool) {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
reg.UsePreReleases = yes // reg.UsePreReleases = yes
} // }
// AddResource adds a resource to the registry. Does _not_ select new version. // // AddResource adds a resource to the registry. Does _not_ select new version.
func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { // func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) // err := reg.addResource(identifier, version, index, available, currentRelease, preRelease)
return err // return err
} // }
func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { // func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error {
res, ok := reg.resources[identifier] // res, ok := reg.resources[identifier]
if !ok { // if !ok {
res = reg.newResource(identifier) // res = reg.newResource(identifier)
reg.resources[identifier] = res // reg.resources[identifier] = res
} // }
res.Index = index // res.Index = index
return res.AddVersion(version, available, currentRelease, preRelease) // return res.AddVersion(version, available, currentRelease, preRelease)
} // }
// AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. // // AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions.
func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { // func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
// add versions and their flags to registry // // add versions and their flags to registry
var lastError error // var lastError error
for identifier, version := range versions { // for identifier, version := range versions {
lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) // lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease)
if lastError != nil { // if lastError != nil {
log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) // log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError)
} // }
} // }
return lastError // return lastError
} // }
// SelectVersions selects new resource versions depending on the current registry state. // // SelectVersions selects new resource versions depending on the current registry state.
func (reg *ResourceRegistry) SelectVersions() { // func (reg *ResourceRegistry) SelectVersions() {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
for _, res := range reg.resources { // for _, res := range reg.resources {
res.Lock() // res.Lock()
res.selectVersion() // res.selectVersion()
res.Unlock() // res.Unlock()
} // }
} // }
// GetSelectedVersions returns a list of the currently selected versions. // // GetSelectedVersions returns a list of the currently selected versions.
func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { // func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
for _, res := range reg.resources { // for _, res := range reg.resources {
res.Lock() // res.Lock()
versions[res.Identifier] = res.SelectedVersion.VersionNumber // versions[res.Identifier] = res.SelectedVersion.VersionNumber
res.Unlock() // res.Unlock()
} // }
return // return
} // }
// Purge deletes old updates, retaining a certain amount, specified by the keep // // Purge deletes old updates, retaining a certain amount, specified by the keep
// parameter. Will at least keep 2 updates per resource. // // parameter. Will at least keep 2 updates per resource.
func (reg *ResourceRegistry) Purge(keep int) { // func (reg *ResourceRegistry) Purge(keep int) {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
for _, res := range reg.resources { // for _, res := range reg.resources {
res.Purge(keep) // res.Purge(keep)
} // }
} // }
// ResetResources removes all resources from the registry. // // ResetResources removes all resources from the registry.
func (reg *ResourceRegistry) ResetResources() { // func (reg *ResourceRegistry) ResetResources() {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
reg.resources = make(map[string]*Resource) // reg.resources = make(map[string]*Resource)
} // }
// ResetIndexes removes all indexes from the registry. // // ResetIndexes removes all indexes from the registry.
func (reg *ResourceRegistry) ResetIndexes() { // func (reg *ResourceRegistry) ResetIndexes() {
reg.Lock() // reg.Lock()
defer reg.Unlock() // defer reg.Unlock()
reg.indexes = make([]*Index, 0, len(reg.indexes)) // reg.indexes = make([]*Index, 0, len(reg.indexes))
} // }
// Cleanup removes temporary files. // // Cleanup removes temporary files.
func (reg *ResourceRegistry) Cleanup() error { // func (reg *ResourceRegistry) Cleanup() error {
// delete download tmp dir // // delete download tmp dir
return os.RemoveAll(reg.tmpDir.Path) // return os.RemoveAll(reg.tmpDir.Path)
} // }

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,49 @@
package updater package updater
import ( // import (
"strings" // "strings"
"github.com/safing/jess" // "github.com/safing/jess"
) // )
// VerificationOptions holds options for verification of files. // // VerificationOptions holds options for verification of files.
type VerificationOptions struct { // type VerificationOptions struct {
TrustStore jess.TrustStore // TrustStore jess.TrustStore
DownloadPolicy SignaturePolicy // DownloadPolicy SignaturePolicy
DiskLoadPolicy SignaturePolicy // DiskLoadPolicy SignaturePolicy
} // }
// GetVerificationOptions returns the verification options for the given identifier. // // GetVerificationOptions returns the verification options for the given identifier.
func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { // func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions {
if reg.Verification == nil { // if reg.Verification == nil {
return nil // return nil
} // }
var ( // var (
longestPrefix = -1 // longestPrefix = -1
bestMatch *VerificationOptions // bestMatch *VerificationOptions
) // )
for prefix, opts := range reg.Verification { // for prefix, opts := range reg.Verification {
if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { // if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) {
longestPrefix = len(prefix) // longestPrefix = len(prefix)
bestMatch = opts // bestMatch = opts
} // }
} // }
return bestMatch // return bestMatch
} // }
// SignaturePolicy defines behavior in case of errors. // // SignaturePolicy defines behavior in case of errors.
type SignaturePolicy uint8 // type SignaturePolicy uint8
// Signature Policies. // // Signature Policies.
const ( // const (
// SignaturePolicyRequire fails on any error. // // SignaturePolicyRequire fails on any error.
SignaturePolicyRequire = iota // SignaturePolicyRequire = iota
// SignaturePolicyWarn only warns on errors. // // SignaturePolicyWarn only warns on errors.
SignaturePolicyWarn // SignaturePolicyWarn
// SignaturePolicyDisable only downloads signatures, but does not verify them. // // SignaturePolicyDisable only downloads signatures, but does not verify them.
SignaturePolicyDisable // SignaturePolicyDisable
) // )

View File

@@ -1,180 +1,180 @@
package updater package updater
import ( // import (
"sort" // "sort"
"sync" // "sync"
"time" // "time"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) // )
// Registry States. // // Registry States.
const ( // const (
StateReady = "ready" // Default idle state. // StateReady = "ready" // Default idle state.
StateChecking = "checking" // Downloading indexes. // StateChecking = "checking" // Downloading indexes.
StateDownloading = "downloading" // Downloading updates. // StateDownloading = "downloading" // Downloading updates.
StateFetching = "fetching" // Fetching a single file. // StateFetching = "fetching" // Fetching a single file.
) // )
// RegistryState describes the registry state. // // RegistryState describes the registry state.
type RegistryState struct { // type RegistryState struct {
sync.Mutex // sync.Mutex
reg *ResourceRegistry // reg *ResourceRegistry
// ID holds the ID of the state the registry is currently in. // // ID holds the ID of the state the registry is currently in.
ID string // ID string
// Details holds further information about the current state. // // Details holds further information about the current state.
Details any // Details any
// Updates holds generic information about the current status of pending // // Updates holds generic information about the current status of pending
// and recently downloaded updates. // // and recently downloaded updates.
Updates UpdateState // Updates UpdateState
// operationLock locks the operation of any state changing operation. // // operationLock locks the operation of any state changing operation.
// This is separate from the registry lock, which locks access to the // // This is separate from the registry lock, which locks access to the
// registry struct. // // registry struct.
operationLock sync.Mutex // operationLock sync.Mutex
} // }
// StateDownloadingDetails holds details of the downloading state. // // StateDownloadingDetails holds details of the downloading state.
type StateDownloadingDetails struct { // type StateDownloadingDetails struct {
// Resources holds the resource IDs that are being downloaded. // // Resources holds the resource IDs that are being downloaded.
Resources []string // Resources []string
// FinishedUpTo holds the index of Resources that is currently being // // FinishedUpTo holds the index of Resources that is currently being
// downloaded. Previous resources have finished downloading. // // downloaded. Previous resources have finished downloading.
FinishedUpTo int // FinishedUpTo int
} // }
// UpdateState holds generic information about the current status of pending // // UpdateState holds generic information about the current status of pending
// and recently downloaded updates. // // and recently downloaded updates.
type UpdateState struct { // type UpdateState struct {
// LastCheckAt holds the time of the last update check. // // LastCheckAt holds the time of the last update check.
LastCheckAt *time.Time // LastCheckAt *time.Time
// LastCheckError holds the error of the last check. // // LastCheckError holds the error of the last check.
LastCheckError error // LastCheckError error
// PendingDownload holds the resources that are pending download. // // PendingDownload holds the resources that are pending download.
PendingDownload []string // PendingDownload []string
// LastDownloadAt holds the time when resources were downloaded the last time. // // LastDownloadAt holds the time when resources were downloaded the last time.
LastDownloadAt *time.Time // LastDownloadAt *time.Time
// LastDownloadError holds the error of the last download. // // LastDownloadError holds the error of the last download.
LastDownloadError error // LastDownloadError error
// LastDownload holds the resources that we downloaded the last time updates // // LastDownload holds the resources that we downloaded the last time updates
// were downloaded. // // were downloaded.
LastDownload []string // LastDownload []string
// LastSuccessAt holds the time of the last successful update (check). // // LastSuccessAt holds the time of the last successful update (check).
LastSuccessAt *time.Time // LastSuccessAt *time.Time
} // }
// GetState returns the current registry state. // // GetState returns the current registry state.
// The returned data must not be modified. // // The returned data must not be modified.
func (reg *ResourceRegistry) GetState() RegistryState { // func (reg *ResourceRegistry) GetState() RegistryState {
reg.state.Lock() // reg.state.Lock()
defer reg.state.Unlock() // defer reg.state.Unlock()
return RegistryState{ // return RegistryState{
ID: reg.state.ID, // ID: reg.state.ID,
Details: reg.state.Details, // Details: reg.state.Details,
Updates: reg.state.Updates, // Updates: reg.state.Updates,
} // }
} // }
// StartOperation starts an operation. // // StartOperation starts an operation.
func (s *RegistryState) StartOperation(id string) bool { // func (s *RegistryState) StartOperation(id string) bool {
defer s.notify() // defer s.notify()
s.operationLock.Lock() // s.operationLock.Lock()
s.Lock() // s.Lock()
defer s.Unlock() // defer s.Unlock()
s.ID = id // s.ID = id
return true // return true
} // }
// UpdateOperationDetails updates the details of an operation. // // UpdateOperationDetails updates the details of an operation.
// The supplied struct should be a copy and must not be changed after calling // // The supplied struct should be a copy and must not be changed after calling
// this function. // // this function.
func (s *RegistryState) UpdateOperationDetails(details any) { // func (s *RegistryState) UpdateOperationDetails(details any) {
defer s.notify() // defer s.notify()
s.Lock() // s.Lock()
defer s.Unlock() // defer s.Unlock()
s.Details = details // s.Details = details
} // }
// EndOperation ends an operation. // // EndOperation ends an operation.
func (s *RegistryState) EndOperation() { // func (s *RegistryState) EndOperation() {
defer s.notify() // defer s.notify()
defer s.operationLock.Unlock() // defer s.operationLock.Unlock()
s.Lock() // s.Lock()
defer s.Unlock() // defer s.Unlock()
s.ID = StateReady // s.ID = StateReady
s.Details = nil // s.Details = nil
} // }
// ReportUpdateCheck reports an update check to the registry state. // // ReportUpdateCheck reports an update check to the registry state.
func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { // func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) {
defer s.notify() // defer s.notify()
sort.Strings(pendingDownload) // sort.Strings(pendingDownload)
s.Lock() // s.Lock()
defer s.Unlock() // defer s.Unlock()
now := time.Now() // now := time.Now()
s.Updates.LastCheckAt = &now // s.Updates.LastCheckAt = &now
s.Updates.LastCheckError = failed // s.Updates.LastCheckError = failed
s.Updates.PendingDownload = pendingDownload // s.Updates.PendingDownload = pendingDownload
if failed == nil { // if failed == nil {
s.Updates.LastSuccessAt = &now // s.Updates.LastSuccessAt = &now
} // }
} // }
// ReportDownloads reports downloaded updates to the registry state. // // ReportDownloads reports downloaded updates to the registry state.
func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { // func (s *RegistryState) ReportDownloads(downloaded []string, failed error) {
defer s.notify() // defer s.notify()
sort.Strings(downloaded) // sort.Strings(downloaded)
s.Lock() // s.Lock()
defer s.Unlock() // defer s.Unlock()
now := time.Now() // now := time.Now()
s.Updates.LastDownloadAt = &now // s.Updates.LastDownloadAt = &now
s.Updates.LastDownloadError = failed // s.Updates.LastDownloadError = failed
s.Updates.LastDownload = downloaded // s.Updates.LastDownload = downloaded
// Remove downloaded resources from the pending list. // // Remove downloaded resources from the pending list.
if len(s.Updates.PendingDownload) > 0 { // if len(s.Updates.PendingDownload) > 0 {
newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) // newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload))
for _, pending := range s.Updates.PendingDownload { // for _, pending := range s.Updates.PendingDownload {
if !utils.StringInSlice(downloaded, pending) { // if !utils.StringInSlice(downloaded, pending) {
newPendingDownload = append(newPendingDownload, pending) // newPendingDownload = append(newPendingDownload, pending)
} // }
} // }
s.Updates.PendingDownload = newPendingDownload // s.Updates.PendingDownload = newPendingDownload
} // }
if failed == nil { // if failed == nil {
s.Updates.LastSuccessAt = &now // s.Updates.LastSuccessAt = &now
} // }
} // }
func (s *RegistryState) notify() { // func (s *RegistryState) notify() {
switch { // switch {
case s.reg == nil: // case s.reg == nil:
return // return
case s.reg.StateNotifyFunc == nil: // case s.reg.StateNotifyFunc == nil:
return // return
} // }
s.reg.StateNotifyFunc(s) // s.reg.StateNotifyFunc(s)
} // }

View File

@@ -1,272 +1,272 @@
package updater package updater
import ( // import (
"context" // "context"
"errors" // "errors"
"fmt" // "fmt"
"io/fs" // "io/fs"
"net/http" // "net/http"
"os" // "os"
"path/filepath" // "path/filepath"
"strings" // "strings"
"github.com/safing/jess/filesig" // "github.com/safing/jess/filesig"
"github.com/safing/jess/lhash" // "github.com/safing/jess/lhash"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) // )
// ScanStorage scans root within the storage dir and adds found // // ScanStorage scans root within the storage dir and adds found
// resources to the registry. If an error occurred, it is logged // // resources to the registry. If an error occurred, it is logged
// and the last error is returned. Everything that was found // // and the last error is returned. Everything that was found
// despite errors is added to the registry anyway. Leave root // // despite errors is added to the registry anyway. Leave root
// empty to scan the full storage dir. // // empty to scan the full storage dir.
func (reg *ResourceRegistry) ScanStorage(root string) error { // func (reg *ResourceRegistry) ScanStorage(root string) error {
var lastError error // var lastError error
// prep root // // prep root
if root == "" { // if root == "" {
root = reg.storageDir.Path // root = reg.storageDir.Path
} else { // } else {
var err error // var err error
root, err = filepath.Abs(root) // root, err = filepath.Abs(root)
if err != nil { // if err != nil {
return err // return err
} // }
if !strings.HasPrefix(root, reg.storageDir.Path) { // if !strings.HasPrefix(root, reg.storageDir.Path) {
return errors.New("supplied scan root path not within storage") // return errors.New("supplied scan root path not within storage")
} // }
} // }
// walk fs // // walk fs
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { // _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
// skip tmp dir (including errors trying to read it) // // skip tmp dir (including errors trying to read it)
if strings.HasPrefix(path, reg.tmpDir.Path) { // if strings.HasPrefix(path, reg.tmpDir.Path) {
return filepath.SkipDir // return filepath.SkipDir
} // }
// handle walker error // // handle walker error
if err != nil { // if err != nil {
lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) // lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err)
log.Warning(lastError.Error()) // log.Warning(lastError.Error())
return nil // return nil
} // }
// Ignore file signatures. // // Ignore file signatures.
if strings.HasSuffix(path, filesig.Extension) { // if strings.HasSuffix(path, filesig.Extension) {
return nil // return nil
} // }
// get relative path to storage // // get relative path to storage
relativePath, err := filepath.Rel(reg.storageDir.Path, path) // relativePath, err := filepath.Rel(reg.storageDir.Path, path)
if err != nil { // if err != nil {
lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) // lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err)
log.Warning(lastError.Error()) // log.Warning(lastError.Error())
return nil // return nil
} // }
// convert to identifier and version // // convert to identifier and version
relativePath = filepath.ToSlash(relativePath) // relativePath = filepath.ToSlash(relativePath)
identifier, version, ok := GetIdentifierAndVersion(relativePath) // identifier, version, ok := GetIdentifierAndVersion(relativePath)
if !ok { // if !ok {
// file does not conform to format // // file does not conform to format
return nil // return nil
} // }
// fully ignore directories that also have an identifier - these will be unpacked resources // // fully ignore directories that also have an identifier - these will be unpacked resources
if info.IsDir() { // if info.IsDir() {
return filepath.SkipDir // return filepath.SkipDir
} // }
// save // // save
err = reg.AddResource(identifier, version, nil, true, false, false) // err = reg.AddResource(identifier, version, nil, true, false, false)
if err != nil { // if err != nil {
lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) // lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err)
log.Warning(lastError.Error()) // log.Warning(lastError.Error())
} // }
return nil // return nil
}) // })
return lastError // return lastError
} // }
// LoadIndexes loads the current release indexes from disk // // LoadIndexes loads the current release indexes from disk
// or will fetch a new version if not available and the // // or will fetch a new version if not available and the
// registry is marked as online. // // registry is marked as online.
func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { // func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error {
var firstErr error // var firstErr error
client := &http.Client{} // client := &http.Client{}
for _, idx := range reg.getIndexes() { // for _, idx := range reg.getIndexes() {
err := reg.loadIndexFile(idx) // err := reg.loadIndexFile(idx)
if err == nil { // if err == nil {
log.Debugf("%s: loaded index %s", reg.Name, idx.Path) // log.Debugf("%s: loaded index %s", reg.Name, idx.Path)
} else if reg.Online { // } else if reg.Online {
// try to download the index file if a local disk version // // try to download the index file if a local disk version
// does not exist or we don't have permission to read it. // // does not exist or we don't have permission to read it.
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { // if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
err = reg.downloadIndex(ctx, client, idx) // err = reg.downloadIndex(ctx, client, idx)
} // }
} // }
if err != nil && firstErr == nil { // if err != nil && firstErr == nil {
firstErr = err // firstErr = err
} // }
} // }
return firstErr // return firstErr
} // }
// getIndexes returns a copy of the index. // // getIndexes returns a copy of the index.
// The indexes itself are references. // // The indexes itself are references.
func (reg *ResourceRegistry) getIndexes() []*Index { // func (reg *ResourceRegistry) getIndexes() []*Index {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
indexes := make([]*Index, len(reg.indexes)) // indexes := make([]*Index, len(reg.indexes))
copy(indexes, reg.indexes) // copy(indexes, reg.indexes)
return indexes // return indexes
} // }
func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { // func (reg *ResourceRegistry) loadIndexFile(idx *Index) error {
indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) // indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path))
indexData, err := os.ReadFile(indexPath) // indexData, err := os.ReadFile(indexPath)
if err != nil { // if err != nil {
return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) // return fmt.Errorf("failed to read index file %s: %w", idx.Path, err)
} // }
// Verify signature, if enabled. // // Verify signature, if enabled.
if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { // if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil {
// Load and check signature. // // Load and check signature.
verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) // verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension)
if err != nil { // if err != nil {
switch verifOpts.DiskLoadPolicy { // switch verifOpts.DiskLoadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) // return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) // log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) // log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
} // }
} // }
// Check if signature checksum matches the index data. // // Check if signature checksum matches the index data.
if err == nil && !verifiedHash.Matches(indexData) { // if err == nil && !verifiedHash.Matches(indexData) {
switch verifOpts.DiskLoadPolicy { // switch verifOpts.DiskLoadPolicy {
case SignaturePolicyRequire: // case SignaturePolicyRequire:
return fmt.Errorf("index file %s does not match signature", idx.Path) // return fmt.Errorf("index file %s does not match signature", idx.Path)
case SignaturePolicyWarn: // case SignaturePolicyWarn:
log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) // log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path)
case SignaturePolicyDisable: // case SignaturePolicyDisable:
log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) // log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path)
} // }
} // }
} // }
// Parse the index file. // // Parse the index file.
indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) // indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
if err != nil { // if err != nil {
return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) // return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err)
} // }
// Update last seen release. // // Update last seen release.
idx.LastRelease = indexFile.Published // idx.LastRelease = indexFile.Published
// Warn if there aren't any releases in the index. // // Warn if there aren't any releases in the index.
if len(indexFile.Releases) == 0 { // if len(indexFile.Releases) == 0 {
log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) // log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
return nil // return nil
} // }
// Add index releases to available resources. // // Add index releases to available resources.
err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) // err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease)
if err != nil { // if err != nil {
log.Warningf("%s: failed to add resource: %s", reg.Name, err) // log.Warningf("%s: failed to add resource: %s", reg.Name, err)
} // }
return nil // return nil
} // }
func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { // func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) {
// Load signature file. // // Load signature file.
sigFileData, err := os.ReadFile(sigFilePath) // sigFileData, err := os.ReadFile(sigFilePath)
if err != nil { // if err != nil {
return nil, nil, fmt.Errorf("failed to read signature file: %w", err) // return nil, nil, fmt.Errorf("failed to read signature file: %w", err)
} // }
// Extract all signatures. // // Extract all signatures.
sigs, err := filesig.ParseSigFile(sigFileData) // sigs, err := filesig.ParseSigFile(sigFileData)
switch { // switch {
case len(sigs) == 0 && err != nil: // case len(sigs) == 0 && err != nil:
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) // return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
case len(sigs) == 0: // case len(sigs) == 0:
return nil, nil, errors.New("no signatures found in signature file") // return nil, nil, errors.New("no signatures found in signature file")
case err != nil: // case err != nil:
return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) // return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
} // }
// Verify all signatures. // // Verify all signatures.
var verifiedHash *lhash.LabeledHash // var verifiedHash *lhash.LabeledHash
for _, sig := range sigs { // for _, sig := range sigs {
fd, err := filesig.VerifyFileData( // fd, err := filesig.VerifyFileData(
sig, // sig,
nil, // nil,
verifOpts.TrustStore, // verifOpts.TrustStore,
) // )
if err != nil { // if err != nil {
return nil, sigFileData, err // return nil, sigFileData, err
} // }
// Save or check verified hash. // // Save or check verified hash.
if verifiedHash == nil { // if verifiedHash == nil {
verifiedHash = fd.FileHash() // verifiedHash = fd.FileHash()
} else if !fd.FileHash().Equal(verifiedHash) { // } else if !fd.FileHash().Equal(verifiedHash) {
// Return an error if two valid hashes mismatch. // // Return an error if two valid hashes mismatch.
// For simplicity, all hash algorithms must be the same for now. // // 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 nil, sigFileData, errors.New("file hashes from different signatures do not match")
} // }
} // }
return verifiedHash, sigFileData, nil // return verifiedHash, sigFileData, nil
} // }
// CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. // // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list.
func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { // func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error {
err := os.RemoveAll(symlinkRoot.Path) // err := os.RemoveAll(symlinkRoot.Path)
if err != nil { // if err != nil {
return fmt.Errorf("failed to wipe symlink root: %w", err) // return fmt.Errorf("failed to wipe symlink root: %w", err)
} // }
err = symlinkRoot.Ensure() // err = symlinkRoot.Ensure()
if err != nil { // if err != nil {
return fmt.Errorf("failed to create symlink root: %w", err) // return fmt.Errorf("failed to create symlink root: %w", err)
} // }
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
for _, res := range reg.resources { // for _, res := range reg.resources {
if res.SelectedVersion == nil { // if res.SelectedVersion == nil {
return fmt.Errorf("no selected version available for %s", res.Identifier) // return fmt.Errorf("no selected version available for %s", res.Identifier)
} // }
targetPath := res.SelectedVersion.storagePath() // targetPath := res.SelectedVersion.storagePath()
linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) // linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier))
linkPathDir := filepath.Dir(linkPath) // linkPathDir := filepath.Dir(linkPath)
err = symlinkRoot.EnsureAbsPath(linkPathDir) // err = symlinkRoot.EnsureAbsPath(linkPathDir)
if err != nil { // if err != nil {
return fmt.Errorf("failed to create dir for link: %w", err) // return fmt.Errorf("failed to create dir for link: %w", err)
} // }
relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) // relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
if err != nil { // if err != nil {
return fmt.Errorf("failed to get relative target path: %w", err) // return fmt.Errorf("failed to get relative target path: %w", err)
} // }
err = os.Symlink(relativeTargetPath, linkPath) // err = os.Symlink(relativeTargetPath, linkPath)
if err != nil { // if err != nil {
return fmt.Errorf("failed to link %s: %w", res.Identifier, err) // return fmt.Errorf("failed to link %s: %w", res.Identifier, err)
} // }
} // }
return nil // return nil
} // }

View File

@@ -1,195 +1,195 @@
package updater package updater
import ( // import (
"archive/zip" // "archive/zip"
"compress/gzip" // "compress/gzip"
"errors" // "errors"
"fmt" // "fmt"
"io" // "io"
"io/fs" // "io/fs"
"os" // "os"
"path" // "path"
"path/filepath" // "path/filepath"
"strings" // "strings"
"github.com/hashicorp/go-multierror" // "github.com/hashicorp/go-multierror"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) // )
// MaxUnpackSize specifies the maximum size that will be unpacked. // // MaxUnpackSize specifies the maximum size that will be unpacked.
const MaxUnpackSize = 1000000000 // 1GB // const MaxUnpackSize = 1000000000 // 1GB
// UnpackGZIP unpacks a GZIP compressed reader r // // UnpackGZIP unpacks a GZIP compressed reader r
// and returns a new reader. It's suitable to be // // and returns a new reader. It's suitable to be
// used with registry.GetPackedFile. // // used with registry.GetPackedFile.
func UnpackGZIP(r io.Reader) (io.Reader, error) { // func UnpackGZIP(r io.Reader) (io.Reader, error) {
return gzip.NewReader(r) // return gzip.NewReader(r)
} // }
// UnpackResources unpacks all resources defined in the AutoUnpack list. // // UnpackResources unpacks all resources defined in the AutoUnpack list.
func (reg *ResourceRegistry) UnpackResources() error { // func (reg *ResourceRegistry) UnpackResources() error {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
var multierr *multierror.Error // var multierr *multierror.Error
for _, res := range reg.resources { // for _, res := range reg.resources {
if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { // if utils.StringInSlice(reg.AutoUnpack, res.Identifier) {
err := res.UnpackArchive() // err := res.UnpackArchive()
if err != nil { // if err != nil {
multierr = multierror.Append( // multierr = multierror.Append(
multierr, // multierr,
fmt.Errorf("%s: %w", res.Identifier, err), // fmt.Errorf("%s: %w", res.Identifier, err),
) // )
} // }
} // }
} // }
return multierr.ErrorOrNil() // return multierr.ErrorOrNil()
} // }
const ( // const (
zipSuffix = ".zip" // zipSuffix = ".zip"
) // )
// UnpackArchive unpacks the archive the resource refers to. The contents are // // UnpackArchive unpacks the archive the resource refers to. The contents are
// unpacked into a directory with the same name as the file, excluding the // // unpacked into a directory with the same name as the file, excluding the
// suffix. If the destination folder already exists, it is assumed that the // // suffix. If the destination folder already exists, it is assumed that the
// contents have already been correctly unpacked. // // contents have already been correctly unpacked.
func (res *Resource) UnpackArchive() error { // func (res *Resource) UnpackArchive() error {
res.Lock() // res.Lock()
defer res.Unlock() // defer res.Unlock()
// Only unpack selected versions. // // Only unpack selected versions.
if res.SelectedVersion == nil { // if res.SelectedVersion == nil {
return nil // return nil
} // }
switch { // switch {
case strings.HasSuffix(res.Identifier, zipSuffix): // case strings.HasSuffix(res.Identifier, zipSuffix):
return res.unpackZipArchive() // return res.unpackZipArchive()
default: // default:
return fmt.Errorf("unsupported file type for unpacking") // return fmt.Errorf("unsupported file type for unpacking")
} // }
} // }
func (res *Resource) unpackZipArchive() error { // func (res *Resource) unpackZipArchive() error {
// Get file and directory paths. // // Get file and directory paths.
archiveFile := res.SelectedVersion.storagePath() // archiveFile := res.SelectedVersion.storagePath()
destDir := strings.TrimSuffix(archiveFile, zipSuffix) // destDir := strings.TrimSuffix(archiveFile, zipSuffix)
tmpDir := filepath.Join( // tmpDir := filepath.Join(
res.registry.tmpDir.Path, // res.registry.tmpDir.Path,
filepath.FromSlash(strings.TrimSuffix( // filepath.FromSlash(strings.TrimSuffix(
path.Base(res.SelectedVersion.versionedPath()), // path.Base(res.SelectedVersion.versionedPath()),
zipSuffix, // zipSuffix,
)), // )),
) // )
// Check status of destination. // // Check status of destination.
dstStat, err := os.Stat(destDir) // dstStat, err := os.Stat(destDir)
switch { // switch {
case errors.Is(err, fs.ErrNotExist): // case errors.Is(err, fs.ErrNotExist):
// The destination does not exist, continue with unpacking. // // The destination does not exist, continue with unpacking.
case err != nil: // case err != nil:
return fmt.Errorf("cannot access destination for unpacking: %w", err) // return fmt.Errorf("cannot access destination for unpacking: %w", err)
case !dstStat.IsDir(): // case !dstStat.IsDir():
return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) // return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name())
default: // default:
// Archive already seems to be unpacked. // // Archive already seems to be unpacked.
return nil // return nil
} // }
// Create the tmp directory for unpacking. // // Create the tmp directory for unpacking.
err = res.registry.tmpDir.EnsureAbsPath(tmpDir) // err = res.registry.tmpDir.EnsureAbsPath(tmpDir)
if err != nil { // if err != nil {
return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) // return fmt.Errorf("failed to create tmp dir for unpacking: %w", err)
} // }
// Defer clean up of directories. // // Defer clean up of directories.
defer func() { // defer func() {
// Always clean up the tmp dir. // // Always clean up the tmp dir.
_ = os.RemoveAll(tmpDir) // _ = os.RemoveAll(tmpDir)
// Cleanup the destination in case of an error. // // Cleanup the destination in case of an error.
if err != nil { // if err != nil {
_ = os.RemoveAll(destDir) // _ = os.RemoveAll(destDir)
} // }
}() // }()
// Open the archive for reading. // // Open the archive for reading.
var archiveReader *zip.ReadCloser // var archiveReader *zip.ReadCloser
archiveReader, err = zip.OpenReader(archiveFile) // archiveReader, err = zip.OpenReader(archiveFile)
if err != nil { // if err != nil {
return fmt.Errorf("failed to open zip reader: %w", err) // return fmt.Errorf("failed to open zip reader: %w", err)
} // }
defer func() { // defer func() {
_ = archiveReader.Close() // _ = archiveReader.Close()
}() // }()
// Save all files to the tmp dir. // // Save all files to the tmp dir.
for _, file := range archiveReader.File { // for _, file := range archiveReader.File {
err = copyFromZipArchive( // err = copyFromZipArchive(
file, // file,
filepath.Join(tmpDir, filepath.FromSlash(file.Name)), // filepath.Join(tmpDir, filepath.FromSlash(file.Name)),
) // )
if err != nil { // if err != nil {
return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) // return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err)
} // }
} // }
// Make the final move. // // Make the final move.
err = os.Rename(tmpDir, destDir) // err = os.Rename(tmpDir, destDir)
if err != nil { // if err != nil {
return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) // return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err)
} // }
// Fix permissions on the destination dir. // // Fix permissions on the destination dir.
err = res.registry.storageDir.EnsureAbsPath(destDir) // err = res.registry.storageDir.EnsureAbsPath(destDir)
if err != nil { // if err != nil {
return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) // return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err)
} // }
log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) // log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath())
return nil // return nil
} // }
func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { // func copyFromZipArchive(archiveFile *zip.File, dstPath string) error {
// If file is a directory, create it and continue. // // If file is a directory, create it and continue.
if archiveFile.FileInfo().IsDir() { // if archiveFile.FileInfo().IsDir() {
err := os.Mkdir(dstPath, archiveFile.Mode()) // err := os.Mkdir(dstPath, archiveFile.Mode())
if err != nil { // if err != nil {
return fmt.Errorf("failed to create directory %s: %w", dstPath, err) // return fmt.Errorf("failed to create directory %s: %w", dstPath, err)
} // }
return nil // return nil
} // }
// Open archived file for reading. // // Open archived file for reading.
fileReader, err := archiveFile.Open() // fileReader, err := archiveFile.Open()
if err != nil { // if err != nil {
return fmt.Errorf("failed to open file in archive: %w", err) // return fmt.Errorf("failed to open file in archive: %w", err)
} // }
defer func() { // defer func() {
_ = fileReader.Close() // _ = fileReader.Close()
}() // }()
// Open destination file for writing. // // Open destination file for writing.
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) // dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode())
if err != nil { // if err != nil {
return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) // return fmt.Errorf("failed to open destination file %s: %w", dstPath, err)
} // }
defer func() { // defer func() {
_ = dstFile.Close() // _ = dstFile.Close()
}() // }()
// Copy full file from archive to dst. // // Copy full file from archive to dst.
if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { // if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil {
// EOF is expected here as the archive is likely smaller // // EOF is expected here as the archive is likely smaller
// thane MaxUnpackSize // // thane MaxUnpackSize
if errors.Is(err, io.EOF) { // if errors.Is(err, io.EOF) {
return nil // return nil
} // }
return err // return err
} // }
return nil // return nil
} // }

View File

@@ -1,359 +1,359 @@
package updater package updater
import ( // import (
"context" // "context"
"fmt" // "fmt"
"net/http" // "net/http"
"os" // "os"
"path" // "path"
"path/filepath" // "path/filepath"
"strings" // "strings"
"golang.org/x/exp/slices" // "golang.org/x/exp/slices"
"github.com/safing/jess/filesig" // "github.com/safing/jess/filesig"
"github.com/safing/jess/lhash" // "github.com/safing/jess/lhash"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) // )
// UpdateIndexes downloads all indexes. An error is only returned when all // // UpdateIndexes downloads all indexes. An error is only returned when all
// indexes fail to update. // // indexes fail to update.
func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { // func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error {
var lastErr error // var lastErr error
var anySuccess bool // var anySuccess bool
// Start registry operation. // // Start registry operation.
reg.state.StartOperation(StateChecking) // reg.state.StartOperation(StateChecking)
defer reg.state.EndOperation() // defer reg.state.EndOperation()
client := &http.Client{} // client := &http.Client{}
for _, idx := range reg.getIndexes() { // for _, idx := range reg.getIndexes() {
if err := reg.downloadIndex(ctx, client, idx); err != nil { // if err := reg.downloadIndex(ctx, client, idx); err != nil {
lastErr = err // lastErr = err
log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) // log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err)
} else { // } else {
anySuccess = true // anySuccess = true
} // }
} // }
// If all indexes failed to update, fail. // // If all indexes failed to update, fail.
if !anySuccess { // if !anySuccess {
err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) // err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr)
reg.state.ReportUpdateCheck(nil, err) // reg.state.ReportUpdateCheck(nil, err)
return err // return err
} // }
// Get pending resources and update status. // // Get pending resources and update status.
pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) // pendingResourceVersions, _ := reg.GetPendingDownloads(true, false)
reg.state.ReportUpdateCheck( // reg.state.ReportUpdateCheck(
humanInfoFromResourceVersions(pendingResourceVersions), // humanInfoFromResourceVersions(pendingResourceVersions),
nil, // nil,
) // )
return nil // return nil
} // }
func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { // func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error {
var ( // var (
// Index. // // Index.
indexErr error // indexErr error
indexData []byte // indexData []byte
downloadURL string // downloadURL string
// Signature. // // Signature.
sigErr error // sigErr error
verifiedHash *lhash.LabeledHash // verifiedHash *lhash.LabeledHash
sigFileData []byte // sigFileData []byte
verifOpts = reg.GetVerificationOptions(idx.Path) // verifOpts = reg.GetVerificationOptions(idx.Path)
) // )
// Upgrade to v2 index if verification is enabled. // // Upgrade to v2 index if verification is enabled.
downloadIndexPath := idx.Path // downloadIndexPath := idx.Path
if verifOpts != nil { // if verifOpts != nil {
downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension // downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension
} // }
// Download new index and signature. // // Download new index and signature.
for tries := range 3 { // for tries := range 3 {
// Index and signature need to be fetched together, so that they are // // Index and signature need to be fetched together, so that they are
// fetched from the same source. One source should always have a matching // // fetched from the same source. One source should always have a matching
// index and signature. Backup sources may be behind a little. // // index and signature. Backup sources may be behind a little.
// If the signature verification fails, another source should be tried. // // If the signature verification fails, another source should be tried.
// Get index data. // // Get index data.
indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) // indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries)
if indexErr != nil { // if indexErr != nil {
log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) // log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr)
continue // continue
} // }
// Get signature and verify it. // // Get signature and verify it.
if verifOpts != nil { // if verifOpts != nil {
verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( // verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile(
ctx, client, // ctx, client,
verifOpts, downloadIndexPath+filesig.Extension, nil, // verifOpts, downloadIndexPath+filesig.Extension, nil,
tries, // tries,
) // )
if sigErr != nil { // if sigErr != nil {
log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) // log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr)
continue // continue
} // }
// Check if the index matches the verified hash. // // Check if the index matches the verified hash.
if verifiedHash.Matches(indexData) { // if verifiedHash.Matches(indexData) {
log.Infof("%s: verified signature of %s", reg.Name, downloadURL) // log.Infof("%s: verified signature of %s", reg.Name, downloadURL)
} else { // } else {
sigErr = ErrIndexChecksumMismatch // sigErr = ErrIndexChecksumMismatch
log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) // log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL)
continue // continue
} // }
} // }
break // break
} // }
if indexErr != nil { // if indexErr != nil {
return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) // return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr)
} // }
if sigErr != nil { // if sigErr != nil {
return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) // return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr)
} // }
// Parse the index file. // // Parse the index file.
indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) // indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
if err != nil { // if err != nil {
return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) // return fmt.Errorf("failed to parse index %s: %w", idx.Path, err)
} // }
// Add index data to registry. // // Add index data to registry.
if len(indexFile.Releases) > 0 { // if len(indexFile.Releases) > 0 {
// Check if all resources are within the indexes' authority. // // Check if all resources are within the indexes' authority.
authoritativePath := path.Dir(idx.Path) + "/" // authoritativePath := path.Dir(idx.Path) + "/"
if authoritativePath == "./" { // if authoritativePath == "./" {
// Fix path for indexes at the storage root. // // Fix path for indexes at the storage root.
authoritativePath = "" // authoritativePath = ""
} // }
cleanedData := make(map[string]string, len(indexFile.Releases)) // cleanedData := make(map[string]string, len(indexFile.Releases))
for key, version := range indexFile.Releases { // for key, version := range indexFile.Releases {
if strings.HasPrefix(key, authoritativePath) { // if strings.HasPrefix(key, authoritativePath) {
cleanedData[key] = version // cleanedData[key] = version
} else { // } else {
log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) // log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key)
} // }
} // }
// add resources to registry // // add resources to registry
err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) // err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease)
if err != nil { // if err != nil {
log.Warningf("%s: failed to add resources: %s", reg.Name, err) // log.Warningf("%s: failed to add resources: %s", reg.Name, err)
} // }
} else { // } else {
log.Debugf("%s: index %s is empty", reg.Name, idx.Path) // log.Debugf("%s: index %s is empty", reg.Name, idx.Path)
} // }
// Check if dest dir exists. // // Check if dest dir exists.
indexDir := filepath.FromSlash(path.Dir(idx.Path)) // indexDir := filepath.FromSlash(path.Dir(idx.Path))
err = reg.storageDir.EnsureRelPath(indexDir) // err = reg.storageDir.EnsureRelPath(indexDir)
if err != nil { // if err != nil {
log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) // log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err)
} // }
// Index files must be readable by portmaster-staert with user permissions in order to load the index. // // Index files must be readable by portmaster-staert with user permissions in order to load the index.
err = os.WriteFile( //nolint:gosec // err = os.WriteFile( //nolint:gosec
filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), // filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)),
indexData, 0o0644, // indexData, 0o0644,
) // )
if err != nil { // if err != nil {
log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) // log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err)
} // }
// Write signature file, if we have one. // // Write signature file, if we have one.
if len(sigFileData) > 0 { // if len(sigFileData) > 0 {
err = os.WriteFile( //nolint:gosec // err = os.WriteFile( //nolint:gosec
filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), // filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension),
sigFileData, 0o0644, // sigFileData, 0o0644,
) // )
if err != nil { // if err != nil {
log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) // log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err)
} // }
} // }
log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) // log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases))
return nil // return nil
} // }
// DownloadUpdates checks if updates are available and downloads updates of used components. // // DownloadUpdates checks if updates are available and downloads updates of used components.
func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { // func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error {
// Start registry operation. // // Start registry operation.
reg.state.StartOperation(StateDownloading) // reg.state.StartOperation(StateDownloading)
defer reg.state.EndOperation() // defer reg.state.EndOperation()
// Get pending updates. // // Get pending updates.
toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) // toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true)
downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) // downloadDetailsResources := humanInfoFromResourceVersions(toUpdate)
reg.state.UpdateOperationDetails(&StateDownloadingDetails{ // reg.state.UpdateOperationDetails(&StateDownloadingDetails{
Resources: downloadDetailsResources, // Resources: downloadDetailsResources,
}) // })
// nothing to update // // nothing to update
if len(toUpdate) == 0 && len(missingSigs) == 0 { // if len(toUpdate) == 0 && len(missingSigs) == 0 {
log.Infof("%s: everything up to date", reg.Name) // log.Infof("%s: everything up to date", reg.Name)
return nil // return nil
} // }
// check download dir // // check download dir
if err := reg.tmpDir.Ensure(); err != nil { // if err := reg.tmpDir.Ensure(); err != nil {
return fmt.Errorf("could not prepare tmp directory for download: %w", err) // return fmt.Errorf("could not prepare tmp directory for download: %w", err)
} // }
// download updates // // download updates
log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) // log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate))
client := &http.Client{} // client := &http.Client{}
var reportError error // var reportError error
for i, rv := range toUpdate { // for i, rv := range toUpdate {
log.Infof( // log.Infof(
"%s: downloading update [%d/%d]: %s version %s", // "%s: downloading update [%d/%d]: %s version %s",
reg.Name, // reg.Name,
i+1, len(toUpdate), // i+1, len(toUpdate),
rv.resource.Identifier, rv.VersionNumber, // rv.resource.Identifier, rv.VersionNumber,
) // )
var err error // var err error
for tries := range 3 { // for tries := range 3 {
err = reg.fetchFile(ctx, client, rv, tries) // err = reg.fetchFile(ctx, client, rv, tries)
if err == nil { // if err == nil {
// Update resource version state. // // Update resource version state.
rv.resource.Lock() // rv.resource.Lock()
rv.Available = true // rv.Available = true
if rv.resource.VerificationOptions != nil { // if rv.resource.VerificationOptions != nil {
rv.SigAvailable = true // rv.SigAvailable = true
} // }
rv.resource.Unlock() // rv.resource.Unlock()
break // break
} // }
} // }
if err != nil { // if err != nil {
reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) // reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err)
log.Warningf("%s: %s", reg.Name, reportError) // log.Warningf("%s: %s", reg.Name, reportError)
} // }
reg.state.UpdateOperationDetails(&StateDownloadingDetails{ // reg.state.UpdateOperationDetails(&StateDownloadingDetails{
Resources: downloadDetailsResources, // Resources: downloadDetailsResources,
FinishedUpTo: i + 1, // FinishedUpTo: i + 1,
}) // })
} // }
if len(missingSigs) > 0 { // if len(missingSigs) > 0 {
log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) // log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs))
for _, rv := range missingSigs { // for _, rv := range missingSigs {
var err error // var err error
for tries := range 3 { // for tries := range 3 {
err = reg.fetchMissingSig(ctx, client, rv, tries) // err = reg.fetchMissingSig(ctx, client, rv, tries)
if err == nil { // if err == nil {
// Update resource version state. // // Update resource version state.
rv.resource.Lock() // rv.resource.Lock()
rv.SigAvailable = true // rv.SigAvailable = true
rv.resource.Unlock() // rv.resource.Unlock()
break // break
} // }
} // }
if err != nil { // if err != nil {
reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) // reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err)
log.Warningf("%s: %s", reg.Name, reportError) // log.Warningf("%s: %s", reg.Name, reportError)
} // }
} // }
} // }
reg.state.ReportDownloads( // reg.state.ReportDownloads(
downloadDetailsResources, // downloadDetailsResources,
reportError, // reportError,
) // )
log.Infof("%s: finished downloading updates", reg.Name) // log.Infof("%s: finished downloading updates", reg.Name)
return nil // return nil
} // }
// DownloadUpdates checks if updates are available and downloads updates of used components. // // DownloadUpdates checks if updates are available and downloads updates of used components.
// GetPendingDownloads returns the list of pending downloads. // // GetPendingDownloads returns the list of pending downloads.
// If manual is set, indexes with AutoDownload=false will be checked. // // If manual is set, indexes with AutoDownload=false will be checked.
// If auto is set, indexes with AutoDownload=true will be checked. // // If auto is set, indexes with AutoDownload=true will be checked.
func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { // func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) {
reg.RLock() // reg.RLock()
defer reg.RUnlock() // defer reg.RUnlock()
// create list of downloads // // create list of downloads
var toUpdate []*ResourceVersion // var toUpdate []*ResourceVersion
var missingSigs []*ResourceVersion // var missingSigs []*ResourceVersion
for _, res := range reg.resources { // for _, res := range reg.resources {
func() { // func() {
res.Lock() // res.Lock()
defer res.Unlock() // defer res.Unlock()
// Skip resources without index or indexes that should not be reported // // Skip resources without index or indexes that should not be reported
// according to parameters. // // according to parameters.
switch { // switch {
case res.Index == nil: // case res.Index == nil:
// Cannot download if resource is not part of an index. // // Cannot download if resource is not part of an index.
return // return
case manual && !res.Index.AutoDownload: // case manual && !res.Index.AutoDownload:
// Manual update report and index is not auto-download. // // Manual update report and index is not auto-download.
case auto && res.Index.AutoDownload: // case auto && res.Index.AutoDownload:
// Auto update report and index is auto-download. // // Auto update report and index is auto-download.
default: // default:
// Resource should not be reported. // // Resource should not be reported.
return // return
} // }
// Skip resources we don't need. // // Skip resources we don't need.
switch { // switch {
case res.inUse(): // case res.inUse():
// Update if resource is in use. // // Update if resource is in use.
case res.available(): // case res.available():
// Update if resource is available locally, ie. was used in the past. // // Update if resource is available locally, ie. was used in the past.
case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): // case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier):
// Update is set as mandatory. // // Update is set as mandatory.
default: // default:
// Resource does not need to be updated. // // Resource does not need to be updated.
return // return
} // }
// Go through all versions until we find versions that need updating. // // Go through all versions until we find versions that need updating.
for _, rv := range res.Versions { // for _, rv := range res.Versions {
switch { // switch {
case !rv.CurrentRelease: // case !rv.CurrentRelease:
// We are not interested in older releases. // // We are not interested in older releases.
case !rv.Available: // case !rv.Available:
// File not available locally, download! // // File not available locally, download!
toUpdate = append(toUpdate, rv) // toUpdate = append(toUpdate, rv)
case !rv.SigAvailable && res.VerificationOptions != nil: // case !rv.SigAvailable && res.VerificationOptions != nil:
// File signature is not available and verification is enabled, download signature! // // File signature is not available and verification is enabled, download signature!
missingSigs = append(missingSigs, rv) // missingSigs = append(missingSigs, rv)
} // }
} // }
}() // }()
} // }
slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { // slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int {
return strings.Compare(a.resource.Identifier, b.resource.Identifier) // return strings.Compare(a.resource.Identifier, b.resource.Identifier)
}) // })
slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { // slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int {
return strings.Compare(a.resource.Identifier, b.resource.Identifier) // return strings.Compare(a.resource.Identifier, b.resource.Identifier)
}) // })
return toUpdate, missingSigs // return toUpdate, missingSigs
} // }
func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { // func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string {
identifiers := make([]string, len(resourceVersions)) // identifiers := make([]string, len(resourceVersions))
for i, rv := range resourceVersions { // for i, rv := range resourceVersions {
identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) // identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber)
} // }
return identifiers // return identifiers
} // }

View File

@@ -7,7 +7,6 @@ import (
"github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/service/intel/geoip" "github.com/safing/portmaster/service/intel/geoip"
"github.com/safing/portmaster/service/netenv" "github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/spn/access" "github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/access/account" "github.com/safing/portmaster/spn/access/account"
"github.com/safing/portmaster/spn/captain" "github.com/safing/portmaster/spn/captain"
@@ -18,18 +17,19 @@ var portmasterStarted = time.Now()
func collectData() interface{} { func collectData() interface{} {
data := make(map[string]interface{}) data := make(map[string]interface{})
// TODO(vladimir)
// Get data about versions. // Get data about versions.
versions := updates.GetSimpleVersions() // versions := updates.GetSimpleVersions()
data["Updates"] = versions // data["Updates"] = versions
data["Version"] = versions.Build.Version // data["Version"] = versions.Build.Version
numericVersion, err := MakeNumericVersion(versions.Build.Version) // numericVersion, err := MakeNumericVersion(versions.Build.Version)
if err != nil { // if err != nil {
data["NumericVersion"] = &DataError{ // data["NumericVersion"] = &DataError{
Error: err, // Error: err,
} // }
} else { // } else {
data["NumericVersion"] = numericVersion // data["NumericVersion"] = numericVersion
} // }
// Get data about install. // Get data about install.
installInfo, err := GetInstallInfo() installInfo, err := GetInstallInfo()

View File

@@ -8,6 +8,7 @@ import (
"github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
) )
type Broadcasts struct { type Broadcasts struct {
@@ -91,4 +92,6 @@ func New(instance instance) (*Broadcasts, error) {
return module, nil return module, nil
} }
type instance interface{} type instance interface {
Updates() *updates.Updates
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
) )
const ( const (
@@ -68,7 +67,7 @@ type BroadcastNotification struct {
func broadcastNotify(ctx *mgr.WorkerCtx) error { func broadcastNotify(ctx *mgr.WorkerCtx) error {
// Get broadcast notifications file, load it from disk and parse it. // Get broadcast notifications file, load it from disk and parse it.
broadcastsResource, err := updates.GetFile(broadcastsResourcePath) broadcastsResource, err := module.instance.Updates().GetFile(broadcastsResourcePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to get broadcast notifications update: %w", err) return fmt.Errorf("failed to get broadcast notifications update: %w", err)
} }

View File

@@ -149,7 +149,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
config.AddToDebugInfo(di) config.AddToDebugInfo(di)
// Detailed information. // Detailed information.
updates.AddToDebugInfo(di) // TODO(vladimir): updates.AddToDebugInfo(di)
compat.AddToDebugInfo(di) compat.AddToDebugInfo(di)
module.instance.AddWorkerInfoToDebugInfo(di) module.instance.AddWorkerInfoToDebugInfo(di)
di.AddGoroutineStack() di.AddGoroutineStack()

View File

@@ -14,15 +14,14 @@ import (
"github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
) )
const ( const (
baseListFilePath = "intel/lists/base.dsdl" baseListFilePath = "base.dsdl"
intermediateListFilePath = "intel/lists/intermediate.dsdl" intermediateListFilePath = "intermediate.dsdl"
urgentListFilePath = "intel/lists/urgent.dsdl" urgentListFilePath = "urgent.dsdl"
listIndexFilePath = "intel/lists/index.dsd" listIndexFilePath = "index.dsd"
) )
// default bloomfilter element sizes (estimated). // default bloomfilter element sizes (estimated).
@@ -40,9 +39,9 @@ var (
filterListLock sync.RWMutex filterListLock sync.RWMutex
// Updater files for tracking upgrades. // Updater files for tracking upgrades.
baseFile *updater.File baseFile *registry.File
intermediateFile *updater.File intermediateFile *registry.File
urgentFile *updater.File urgentFile *registry.File
filterListsLoaded chan struct{} filterListsLoaded chan struct{}
) )
@@ -56,11 +55,10 @@ var cache = database.NewInterface(&database.Options{
// getFileFunc is the function used to get a file from // getFileFunc is the function used to get a file from
// the updater. It's basically updates.GetFile and used // the updater. It's basically updates.GetFile and used
// for unit testing. // for unit testing.
type getFileFunc func(string) (*updater.File, error)
// getFile points to updates.GetFile but may be set to // getFile points to updates.GetFile but may be set to
// something different during unit testing. // something different during unit testing.
var getFile getFileFunc = updates.GetFile // var getFile getFileFunc = registry.GetFile
func init() { func init() {
filterListsLoaded = make(chan struct{}) filterListsLoaded = make(chan struct{})
@@ -79,7 +77,7 @@ func isLoaded() bool {
// processListFile opens the latest version of file and decodes it's DSDL // processListFile opens the latest version of file and decodes it's DSDL
// content. It calls processEntry for each decoded filterlists entry. // content. It calls processEntry for each decoded filterlists entry.
func processListFile(ctx context.Context, filter *scopedBloom, file *updater.File) error { func processListFile(ctx context.Context, filter *scopedBloom, file *registry.File) error {
f, err := os.Open(file.Path()) f, err := os.Open(file.Path())
if err != nil { if err != nil {
return err return err

View File

@@ -4,14 +4,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
"sync" "sync"
"github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/record" "github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater" "github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
"github.com/safing/structures/dsd" "github.com/safing/structures/dsd"
) )
@@ -164,7 +162,7 @@ func getListIndexFromCache() (*ListIndexFile, error) {
var ( var (
// listIndexUpdate must only be used by updateListIndex. // listIndexUpdate must only be used by updateListIndex.
listIndexUpdate *updater.File listIndexUpdate *registry.File
listIndexUpdateLock sync.Mutex listIndexUpdateLock sync.Mutex
) )
@@ -177,24 +175,24 @@ func updateListIndex() error {
case listIndexUpdate == nil: case listIndexUpdate == nil:
// This is the first time this function is run, get updater file for index. // This is the first time this function is run, get updater file for index.
var err error var err error
listIndexUpdate, err = updates.GetFile(listIndexFilePath) listIndexUpdate, err = module.instance.Updates().GetFile(listIndexFilePath)
if err != nil { if err != nil {
return err return err
} }
// Check if the version in the cache is current. // Check if the version in the cache is current.
index, err := getListIndexFromCache() _, err = getListIndexFromCache()
switch { switch {
case errors.Is(err, database.ErrNotFound): case errors.Is(err, database.ErrNotFound):
log.Info("filterlists: index not in cache, starting update") log.Info("filterlists: index not in cache, starting update")
case err != nil: case err != nil:
log.Warningf("filterlists: failed to load index from cache, starting update: %s", err) log.Warningf("filterlists: failed to load index from cache, starting update: %s", err)
case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")): // case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")):
log.Infof( // log.Infof(
"filterlists: index from cache is outdated, starting update (%s != %s)", // "filterlists: index from cache is outdated, starting update (%s != %s)",
strings.TrimPrefix(index.Version, "v"), // strings.TrimPrefix(index.Version, "v"),
listIndexUpdate.Version(), // listIndexUpdate.Version(),
) // )
default: default:
// List is in cache and current, there is nothing to do. // List is in cache and current, there is nothing to do.
log.Debug("filterlists: index is up to date") log.Debug("filterlists: index is up to date")
@@ -204,8 +202,8 @@ func updateListIndex() error {
return nil return nil
} }
case listIndexUpdate.UpgradeAvailable(): // case listIndexUpdate.UpgradeAvailable():
log.Info("filterlists: index update available, starting update") // log.Info("filterlists: index update available, starting update")
default: default:
// Index is loaded and no update is available, there is nothing to do. // Index is loaded and no update is available, there is nothing to do.
return nil return nil

View File

@@ -13,8 +13,8 @@ import (
"github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/query" "github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
) )
var updateInProgress = abool.New() var updateInProgress = abool.New()
@@ -174,51 +174,51 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
// getUpgradableFiles returns a slice of filterlists files // getUpgradableFiles returns a slice of filterlists files
// that should be updated. The files MUST be updated and // that should be updated. The files MUST be updated and
// processed in the returned order! // processed in the returned order!
func getUpgradableFiles() ([]*updater.File, error) { func getUpgradableFiles() ([]*registry.File, error) {
var updateOrder []*updater.File var updateOrder []*registry.File
cacheDBInUse := isLoaded() // cacheDBInUse := isLoaded()
if baseFile == nil || baseFile.UpgradeAvailable() || !cacheDBInUse { // if baseFile == nil || !cacheDBInUse { // TODO(vladimir): || baseFile.UpgradeAvailable()
var err error // var err error
baseFile, err = getFile(baseListFilePath) // baseFile, err = module.instance.Updates().GetFile(baseListFilePath)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version()) // log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version())
updateOrder = append(updateOrder, baseFile) // updateOrder = append(updateOrder, baseFile)
} // }
if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse { // if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse {
var err error // var err error
intermediateFile, err = getFile(intermediateListFilePath) // intermediateFile, err = getFile(intermediateListFilePath)
if err != nil && !errors.Is(err, updater.ErrNotFound) { // if err != nil && !errors.Is(err, updater.ErrNotFound) {
return nil, err // return nil, err
} // }
if err == nil { // if err == nil {
log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version()) // log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version())
updateOrder = append(updateOrder, intermediateFile) // updateOrder = append(updateOrder, intermediateFile)
} // }
} // }
if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse { // if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse {
var err error // var err error
urgentFile, err = getFile(urgentListFilePath) // urgentFile, err = getFile(urgentListFilePath)
if err != nil && !errors.Is(err, updater.ErrNotFound) { // if err != nil && !errors.Is(err, updater.ErrNotFound) {
return nil, err // return nil, err
} // }
if err == nil { // if err == nil {
log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version()) // log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version())
updateOrder = append(updateOrder, urgentFile) // updateOrder = append(updateOrder, urgentFile)
} // }
} // }
return resolveUpdateOrder(updateOrder) return resolveUpdateOrder(updateOrder)
} }
func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) { func resolveUpdateOrder(updateOrder []*registry.File) ([]*registry.File, error) {
// sort the update order by ascending version // sort the update order by ascending version
sort.Sort(byAscVersion(updateOrder)) sort.Sort(byAscVersion(updateOrder))
log.Tracef("intel/filterlists: order of updates: %v", updateOrder) log.Tracef("intel/filterlists: order of updates: %v", updateOrder)
@@ -258,7 +258,7 @@ func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) {
return updateOrder[startAtIdx:], nil return updateOrder[startAtIdx:], nil
} }
type byAscVersion []*updater.File type byAscVersion []*registry.File
func (fs byAscVersion) Len() int { return len(fs) } func (fs byAscVersion) Len() int { return len(fs) }

View File

@@ -8,9 +8,8 @@ import (
maxminddb "github.com/oschwald/maxminddb-golang" maxminddb "github.com/oschwald/maxminddb-golang"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/service/updates/registry"
) )
var worker *updateWorker var worker *updateWorker
@@ -22,13 +21,13 @@ func init() {
} }
const ( const (
v4MMDBResource = "intel/geoip/geoipv4.mmdb.gz" v4MMDBResource = "geoipv4.mmdb"
v6MMDBResource = "intel/geoip/geoipv6.mmdb.gz" v6MMDBResource = "geoipv6.mmdb"
) )
type geoIPDB struct { type geoIPDB struct {
*maxminddb.Reader *maxminddb.Reader
file *updater.File file *registry.File
} }
// updateBroadcaster stores a geoIPDB and provides synchronized // updateBroadcaster stores a geoIPDB and provides synchronized
@@ -47,7 +46,7 @@ func (ub *updateBroadcaster) NeedsUpdate() bool {
ub.rw.RLock() ub.rw.RLock()
defer ub.rw.RUnlock() defer ub.rw.RUnlock()
return ub.db == nil || ub.db.file.UpgradeAvailable() return ub.db == nil // TODO(vladimir) is this needed: || ub.db.file.UpgradeAvailable()
} }
// ReplaceDatabase replaces (or initially sets) the mmdb database. // ReplaceDatabase replaces (or initially sets) the mmdb database.
@@ -181,12 +180,12 @@ func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error {
func getGeoIPDB(resource string) (*geoIPDB, error) { func getGeoIPDB(resource string) (*geoIPDB, error) {
log.Debugf("geoip: opening database %s", resource) log.Debugf("geoip: opening database %s", resource)
file, unpackedPath, err := openAndUnpack(resource) file, err := open(resource)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reader, err := maxminddb.Open(unpackedPath) reader, err := maxminddb.Open(file.Path())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open: %w", err) return nil, fmt.Errorf("failed to open: %w", err)
} }
@@ -198,16 +197,16 @@ func getGeoIPDB(resource string) (*geoIPDB, error) {
}, nil }, nil
} }
func openAndUnpack(resource string) (*updater.File, string, error) { func open(resource string) (*registry.File, error) {
f, err := updates.GetFile(resource) f, err := module.instance.Updates().GetFile(resource)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("getting file: %w", err) return nil, fmt.Errorf("getting file: %w", err)
} }
unpacked, err := f.Unpack(".gz", updater.UnpackGZIP) // unpacked, err := f.Unpack(".gz", updater.UnpackGZIP)
if err != nil { // if err != nil {
return nil, "", fmt.Errorf("unpacking file: %w", err) // return nil, "", fmt.Errorf("unpacking file: %w", err)
} // }
return f, unpacked, nil return f, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
) )
// Event Names. // Event Names.
@@ -105,4 +106,6 @@ func New(instance instance) (*NetEnv, error) {
return module, nil return module, nil
} }
type instance interface{} type instance interface {
Updates() *updates.Updates
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network/netutils" "github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/updates"
) )
// OnlineStatus represent a state of connectivity to the Internet. // OnlineStatus represent a state of connectivity to the Internet.
@@ -221,7 +220,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
// Trigger update check when coming (semi) online. // Trigger update check when coming (semi) online.
if Online() { if Online() {
_ = updates.TriggerUpdate(false, false) module.instance.Updates().EventResourcesUpdated.Submit(struct{}{})
} }
} }
} }

View File

@@ -16,7 +16,6 @@ import (
"github.com/safing/portmaster/service/process" "github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver" "github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/service/status" "github.com/safing/portmaster/service/status"
"github.com/safing/portmaster/service/updates"
) )
func registerAPIEndpoints() error { func registerAPIEndpoints() error {
@@ -94,7 +93,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
config.AddToDebugInfo(di) config.AddToDebugInfo(di)
// Detailed information. // Detailed information.
updates.AddToDebugInfo(di) // TODO(vladimir): updates.AddToDebugInfo(di)
// compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs. // compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs.
di.AddGoroutineStack() di.AddGoroutineStack()

View File

@@ -8,6 +8,7 @@ import (
"github.com/safing/portmaster/base/dataroot" "github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
) )
func prep() error { func prep() error {
@@ -56,7 +57,10 @@ func (ui *UI) Stop() error {
return nil return nil
} }
var shimLoaded atomic.Bool var (
shimLoaded atomic.Bool
module *UI
)
// New returns a new UI module. // New returns a new UI module.
func New(instance instance) (*UI, error) { func New(instance instance) (*UI, error) {
@@ -64,7 +68,7 @@ func New(instance instance) (*UI, error) {
return nil, errors.New("only one instance allowed") return nil, errors.New("only one instance allowed")
} }
m := mgr.New("UI") m := mgr.New("UI")
module := &UI{ module = &UI{
mgr: m, mgr: m,
instance: instance, instance: instance,
} }
@@ -78,4 +82,5 @@ func New(instance instance) (*UI, error) {
type instance interface { type instance interface {
API() *api.API API() *api.API
Updates() *updates.Updates
} }

View File

@@ -15,9 +15,8 @@ import (
"github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/service/updates/registry"
) )
var ( var (
@@ -92,9 +91,9 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// get file from update system // get file from update system
zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName)) zipFile, err := module.instance.Updates().GetFile(fmt.Sprintf("%s.zip", moduleName))
if err != nil { if err != nil {
if errors.Is(err, updater.ErrNotFound) { if errors.Is(err, registry.ErrNotFound) {
log.Tracef("ui: requested module %s does not exist", moduleName) log.Tracef("ui: requested module %s does not exist", moduleName)
http.Error(w, err.Error(), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)
} else { } else {

View File

@@ -1,161 +1,161 @@
package updates package updates
import ( import (
"bytes" // "bytes"
"io" // "io"
"net/http" // "net/http"
"os" // "os"
"path/filepath" // "path/filepath"
"strings" // "strings"
"github.com/ghodss/yaml" // "github.com/ghodss/yaml"
"github.com/safing/portmaster/base/api" // "github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils" // "github.com/safing/portmaster/base/utils"
) )
const ( const (
apiPathCheckForUpdates = "updates/check" apiPathCheckForUpdates = "updates/check"
) )
func registerAPIEndpoints() error { // func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{ // if err := api.RegisterEndpoint(api.Endpoint{
Name: "Check for Updates", // Name: "Check for Updates",
Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", // Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.",
Parameters: []api.Parameter{{ // Parameters: []api.Parameter{{
Method: http.MethodPost, // Method: http.MethodPost,
Field: "download", // Field: "download",
Value: "", // Value: "",
Description: "Force downloading and applying of all updates, regardless of auto-update settings.", // Description: "Force downloading and applying of all updates, regardless of auto-update settings.",
}}, // }},
Path: apiPathCheckForUpdates, // Path: apiPathCheckForUpdates,
Write: api.PermitUser, // Write: api.PermitUser,
ActionFunc: func(r *api.Request) (msg string, err error) { // ActionFunc: func(r *api.Request) (msg string, err error) {
// Check if we should also download regardless of settings. // // Check if we should also download regardless of settings.
downloadAll := r.URL.Query().Has("download") // downloadAll := r.URL.Query().Has("download")
// Trigger update task. // // Trigger update task.
err = TriggerUpdate(true, downloadAll) // err = TriggerUpdate(true, downloadAll)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
// Report how we triggered. // // Report how we triggered.
if downloadAll { // if downloadAll {
return "downloading all updates...", nil // return "downloading all updates...", nil
} // }
return "checking for updates...", nil // return "checking for updates...", nil
}, // },
}); err != nil { // }); err != nil {
return err // return err
} // }
if err := api.RegisterEndpoint(api.Endpoint{ // if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Resource", // Name: "Get Resource",
Description: "Returns the requested resource from the udpate system", // Description: "Returns the requested resource from the udpate system",
Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, // Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`,
Read: api.PermitUser, // Read: api.PermitUser,
ReadMethod: http.MethodGet, // ReadMethod: http.MethodGet,
HandlerFunc: func(w http.ResponseWriter, r *http.Request) { // HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
// Get identifier from URL. // // Get identifier from URL.
var identifier string // var identifier string
if ar := api.GetAPIRequest(r); ar != nil { // if ar := api.GetAPIRequest(r); ar != nil {
identifier = ar.URLVars["identifier"] // identifier = ar.URLVars["identifier"]
} // }
if identifier == "" { // if identifier == "" {
http.Error(w, "no resource speicified", http.StatusBadRequest) // http.Error(w, "no resource speicified", http.StatusBadRequest)
return // return
} // }
// Get resource. // // Get resource.
resource, err := registry.GetFile(identifier) // resource, err := registry.GetFile(identifier)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) // http.Error(w, err.Error(), http.StatusNotFound)
return // return
} // }
// Open file for reading. // // Open file for reading.
file, err := os.Open(resource.Path()) // file, err := os.Open(resource.Path())
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
defer file.Close() //nolint:errcheck,gosec // defer file.Close() //nolint:errcheck,gosec
// Assign file to reader // // Assign file to reader
var reader io.Reader = file // var reader io.Reader = file
// Add version to header. // // Add version to header.
w.Header().Set("Resource-Version", resource.Version()) // w.Header().Set("Resource-Version", resource.Version())
// Set Content-Type. // // Set Content-Type.
contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) // contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path()))
w.Header().Set("Content-Type", contentType) // w.Header().Set("Content-Type", contentType)
// Check if the content type may be returned. // // Check if the content type may be returned.
accept := r.Header.Get("Accept") // accept := r.Header.Get("Accept")
if accept != "" { // if accept != "" {
mimeTypes := strings.Split(accept, ",") // mimeTypes := strings.Split(accept, ",")
// First, clean mime types. // // First, clean mime types.
for i, mimeType := range mimeTypes { // for i, mimeType := range mimeTypes {
mimeType = strings.TrimSpace(mimeType) // mimeType = strings.TrimSpace(mimeType)
mimeType, _, _ = strings.Cut(mimeType, ";") // mimeType, _, _ = strings.Cut(mimeType, ";")
mimeTypes[i] = mimeType // mimeTypes[i] = mimeType
} // }
// Second, check if we may return anything. // // Second, check if we may return anything.
var acceptsAny bool // var acceptsAny bool
for _, mimeType := range mimeTypes { // for _, mimeType := range mimeTypes {
switch mimeType { // switch mimeType {
case "*", "*/*": // case "*", "*/*":
acceptsAny = true // acceptsAny = true
} // }
} // }
// Third, check if we can convert. // // Third, check if we can convert.
if !acceptsAny { // if !acceptsAny {
var converted bool // var converted bool
sourceType, _, _ := strings.Cut(contentType, ";") // sourceType, _, _ := strings.Cut(contentType, ";")
findConvertiblePair: // findConvertiblePair:
for _, mimeType := range mimeTypes { // for _, mimeType := range mimeTypes {
switch { // switch {
case sourceType == "application/yaml" && mimeType == "application/json": // case sourceType == "application/yaml" && mimeType == "application/json":
yamlData, err := io.ReadAll(reader) // yamlData, err := io.ReadAll(reader)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
jsonData, err := yaml.YAMLToJSON(yamlData) // jsonData, err := yaml.YAMLToJSON(yamlData)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
reader = bytes.NewReader(jsonData) // reader = bytes.NewReader(jsonData)
converted = true // converted = true
break findConvertiblePair // break findConvertiblePair
} // }
} // }
// If we could not convert to acceptable format, return an error. // // If we could not convert to acceptable format, return an error.
if !converted { // if !converted {
http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) // http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable)
return // return
} // }
} // }
} // }
// Write file. // // Write file.
w.WriteHeader(http.StatusOK) // w.WriteHeader(http.StatusOK)
if r.Method != http.MethodHead { // if r.Method != http.MethodHead {
_, err = io.Copy(w, reader) // _, err = io.Copy(w, reader)
if err != nil { // if err != nil {
log.Errorf("updates: failed to serve resource file: %s", err) // log.Errorf("updates: failed to serve resource file: %s", err)
return // return
} // }
} // }
}, // },
}); err != nil { // }); err != nil {
return err // return err
} // }
return nil // return nil
} // }

View File

@@ -4,9 +4,9 @@ import (
"github.com/tevino/abool" "github.com/tevino/abool"
"github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr" // "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/helper" // "github.com/safing/portmaster/service/updates/helper"
) )
const cfgDevModeKey = "core/devMode" const cfgDevModeKey = "core/devMode"
@@ -27,152 +27,152 @@ var (
forceDownload = abool.New() forceDownload = abool.New()
) )
func registerConfig() error { // func registerConfig() error {
err := config.Register(&config.Option{ // err := config.Register(&config.Option{
Name: "Release Channel", // Name: "Release Channel",
Key: helper.ReleaseChannelKey, // Key: helper.ReleaseChannelKey,
Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, // Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`,
OptType: config.OptTypeString, // OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelExpert, // ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable, // ReleaseLevel: config.ReleaseLevelStable,
RequiresRestart: true, // RequiresRestart: true,
DefaultValue: helper.ReleaseChannelStable, // DefaultValue: helper.ReleaseChannelStable,
PossibleValues: []config.PossibleValue{ // PossibleValues: []config.PossibleValue{
{ // {
Name: "Stable", // Name: "Stable",
Description: "Production releases.", // Description: "Production releases.",
Value: helper.ReleaseChannelStable, // Value: helper.ReleaseChannelStable,
}, // },
{ // {
Name: "Beta", // Name: "Beta",
Description: "Production releases for testing new features that may break and cause interruption.", // Description: "Production releases for testing new features that may break and cause interruption.",
Value: helper.ReleaseChannelBeta, // Value: helper.ReleaseChannelBeta,
}, // },
{ // {
Name: "Support", // Name: "Support",
Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", // Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.",
Value: helper.ReleaseChannelSupport, // Value: helper.ReleaseChannelSupport,
}, // },
{ // {
Name: "Staging", // Name: "Staging",
Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", // Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.",
Value: helper.ReleaseChannelStaging, // Value: helper.ReleaseChannelStaging,
}, // },
}, // },
Annotations: config.Annotations{ // Annotations: config.Annotations{
config.DisplayOrderAnnotation: -4, // config.DisplayOrderAnnotation: -4,
config.DisplayHintAnnotation: config.DisplayHintOneOf, // config.DisplayHintAnnotation: config.DisplayHintOneOf,
config.CategoryAnnotation: "Updates", // config.CategoryAnnotation: "Updates",
}, // },
}) // })
if err != nil { // if err != nil {
return err // return err
} // }
err = config.Register(&config.Option{ // err = config.Register(&config.Option{
Name: "Automatic Software Updates", // Name: "Automatic Software Updates",
Key: enableSoftwareUpdatesKey, // Key: enableSoftwareUpdatesKey,
Description: "Automatically check for and download software updates. This does not include intelligence data updates.", // Description: "Automatically check for and download software updates. This does not include intelligence data updates.",
OptType: config.OptTypeBool, // OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert, // ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable, // ReleaseLevel: config.ReleaseLevelStable,
RequiresRestart: false, // RequiresRestart: false,
DefaultValue: true, // DefaultValue: true,
Annotations: config.Annotations{ // Annotations: config.Annotations{
config.DisplayOrderAnnotation: -12, // config.DisplayOrderAnnotation: -12,
config.CategoryAnnotation: "Updates", // config.CategoryAnnotation: "Updates",
}, // },
}) // })
if err != nil { // if err != nil {
return err // return err
} // }
err = config.Register(&config.Option{ // err = config.Register(&config.Option{
Name: "Automatic Intelligence Data Updates", // Name: "Automatic Intelligence Data Updates",
Key: enableIntelUpdatesKey, // Key: enableIntelUpdatesKey,
Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.", // Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.",
OptType: config.OptTypeBool, // OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert, // ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable, // ReleaseLevel: config.ReleaseLevelStable,
RequiresRestart: false, // RequiresRestart: false,
DefaultValue: true, // DefaultValue: true,
Annotations: config.Annotations{ // Annotations: config.Annotations{
config.DisplayOrderAnnotation: -11, // config.DisplayOrderAnnotation: -11,
config.CategoryAnnotation: "Updates", // config.CategoryAnnotation: "Updates",
}, // },
}) // })
if err != nil { // if err != nil {
return err // return err
} // }
return nil // return nil
} // }
func initConfig() { // func initConfig() {
releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) // releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable)
initialReleaseChannel = releaseChannel() // initialReleaseChannel = releaseChannel()
previousReleaseChannel = releaseChannel() // previousReleaseChannel = releaseChannel()
enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true) // enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true)
enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true) // enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true)
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() // softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
intelUpdatesCurrentlyEnabled = enableIntelUpdates() // intelUpdatesCurrentlyEnabled = enableIntelUpdates()
devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false) // devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false)
previousDevMode = devMode() // previousDevMode = devMode()
} // }
func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { // func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
changed := false // changed := false
if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { // if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled {
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() // softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
changed = true // changed = true
} // }
if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { // if enableIntelUpdates() != intelUpdatesCurrentlyEnabled {
intelUpdatesCurrentlyEnabled = enableIntelUpdates() // intelUpdatesCurrentlyEnabled = enableIntelUpdates()
changed = true // changed = true
} // }
if devMode() != previousDevMode { // if devMode() != previousDevMode {
registry.SetDevMode(devMode()) // registry.SetDevMode(devMode())
previousDevMode = devMode() // previousDevMode = devMode()
changed = true // changed = true
} // }
if releaseChannel() != previousReleaseChannel { // if releaseChannel() != previousReleaseChannel {
previousReleaseChannel = releaseChannel() // previousReleaseChannel = releaseChannel()
changed = true // changed = true
} // }
if changed { // if changed {
// Update indexes based on new settings. // // Update indexes based on new settings.
warning := helper.SetIndexes( // warning := helper.SetIndexes(
registry, // registry,
releaseChannel(), // releaseChannel(),
true, // true,
softwareUpdatesCurrentlyEnabled, // softwareUpdatesCurrentlyEnabled,
intelUpdatesCurrentlyEnabled, // intelUpdatesCurrentlyEnabled,
) // )
if warning != nil { // if warning != nil {
log.Warningf("updates: %s", warning) // log.Warningf("updates: %s", warning)
} // }
// Select versions depending on new indexes and modes. // // Select versions depending on new indexes and modes.
registry.SelectVersions() // registry.SelectVersions()
module.EventVersionsUpdated.Submit(struct{}{}) // module.EventVersionsUpdated.Submit(struct{}{})
if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { // if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled {
module.states.Clear() // module.states.Clear()
if err := TriggerUpdate(true, false); err != nil { // if err := TriggerUpdate(true, false); err != nil {
log.Warningf("updates: failed to trigger update: %s", err) // log.Warningf("updates: failed to trigger update: %s", err)
} // }
log.Infof("updates: automatic updates are now enabled") // log.Infof("updates: automatic updates are now enabled")
} else { // } else {
log.Warningf("updates: automatic updates are now completely disabled") // log.Warningf("updates: automatic updates are now completely disabled")
} // }
} // }
return false, nil // return false, nil
} // }

View File

@@ -1,238 +1,237 @@
package updates package updates
import ( // import (
"fmt" // "fmt"
"sort" // "sort"
"strings" // "sync"
"sync"
"github.com/safing/portmaster/base/database/record" // "github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/info" // "github.com/safing/portmaster/base/info"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/base/utils/debug" // "github.com/safing/portmaster/base/utils/debug"
"github.com/safing/portmaster/service/mgr" // "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/helper" // "github.com/safing/portmaster/service/updates/helper"
) // )
const ( // const (
// versionsDBKey is the database key for update version information. // // versionsDBKey is the database key for update version information.
versionsDBKey = "core:status/versions" // versionsDBKey = "core:status/versions"
// versionsDBKey is the database key for simple update version information. // // versionsDBKey is the database key for simple update version information.
simpleVersionsDBKey = "core:status/simple-versions" // simpleVersionsDBKey = "core:status/simple-versions"
// updateStatusDBKey is the database key for update status information. // // updateStatusDBKey is the database key for update status information.
updateStatusDBKey = "core:status/updates" // updateStatusDBKey = "core:status/updates"
) // )
// Versions holds update versions and status information. // // Versions holds update versions and status information.
type Versions struct { // type Versions struct {
record.Base // record.Base
sync.Mutex // sync.Mutex
Core *info.Info // Core *info.Info
Resources map[string]*updater.Resource // Resources map[string]*updater.Resource
Channel string // Channel string
Beta bool // Beta bool
Staging bool // Staging bool
} // }
// SimpleVersions holds simplified update versions and status information. // // SimpleVersions holds simplified update versions and status information.
type SimpleVersions struct { // type SimpleVersions struct {
record.Base // record.Base
sync.Mutex // sync.Mutex
Build *info.Info // Build *info.Info
Resources map[string]*SimplifiedResourceVersion // Resources map[string]*SimplifiedResourceVersion
Channel string // Channel string
} // }
// SimplifiedResourceVersion holds version information about one resource. // // SimplifiedResourceVersion holds version information about one resource.
type SimplifiedResourceVersion struct { // type SimplifiedResourceVersion struct {
Version string // Version string
} // }
// UpdateStateExport is a wrapper to export the updates state. // // UpdateStateExport is a wrapper to export the updates state.
type UpdateStateExport struct { // type UpdateStateExport struct {
record.Base // record.Base
sync.Mutex // sync.Mutex
*updater.UpdateState // *updater.UpdateState
} // }
// GetVersions returns the update versions and status information. // // GetVersions returns the update versions and status information.
// Resources must be locked when accessed. // // Resources must be locked when accessed.
func GetVersions() *Versions { // func GetVersions() *Versions {
return &Versions{ // return &Versions{
Core: info.GetInfo(), // Core: info.GetInfo(),
Resources: registry.Export(), // Resources: nil,
Channel: initialReleaseChannel, // Channel: initialReleaseChannel,
Beta: initialReleaseChannel == helper.ReleaseChannelBeta, // Beta: initialReleaseChannel == helper.ReleaseChannelBeta,
Staging: initialReleaseChannel == helper.ReleaseChannelStaging, // Staging: initialReleaseChannel == helper.ReleaseChannelStaging,
} // }
} // }
// GetSimpleVersions returns the simplified update versions and status information. // // GetSimpleVersions returns the simplified update versions and status information.
func GetSimpleVersions() *SimpleVersions { // func GetSimpleVersions() *SimpleVersions {
// Fill base info. // // Fill base info.
v := &SimpleVersions{ // v := &SimpleVersions{
Build: info.GetInfo(), // Build: info.GetInfo(),
Resources: make(map[string]*SimplifiedResourceVersion), // Resources: make(map[string]*SimplifiedResourceVersion),
Channel: initialReleaseChannel, // Channel: initialReleaseChannel,
} // }
// Iterate through all versions and add version info. // // Iterate through all versions and add version info.
for id, resource := range registry.Export() { // // for id, resource := range registry.Export() {
func() { // // func() {
resource.Lock() // // resource.Lock()
defer resource.Unlock() // // defer resource.Unlock()
// Get current in-used or selected version. // // // Get current in-used or selected version.
var rv *updater.ResourceVersion // // var rv *updater.ResourceVersion
switch { // // switch {
case resource.ActiveVersion != nil: // // case resource.ActiveVersion != nil:
rv = resource.ActiveVersion // // rv = resource.ActiveVersion
case resource.SelectedVersion != nil: // // case resource.SelectedVersion != nil:
rv = resource.SelectedVersion // // rv = resource.SelectedVersion
} // // }
// Get information from resource. // // // Get information from resource.
if rv != nil { // // if rv != nil {
v.Resources[id] = &SimplifiedResourceVersion{ // // v.Resources[id] = &SimplifiedResourceVersion{
Version: rv.VersionNumber, // // Version: rv.VersionNumber,
} // // }
} // // }
}() // // }()
} // // }
return v // return v
} // }
// GetStateExport gets the update state from the registry and returns it in an // // GetStateExport gets the update state from the registry and returns it in an
// exportable struct. // // exportable struct.
func GetStateExport() *UpdateStateExport { // func GetStateExport() *UpdateStateExport {
export := registry.GetState() // // export := registry.GetState()
return &UpdateStateExport{ // return &UpdateStateExport{
UpdateState: &export.Updates, // // UpdateState: &export.Updates,
} // }
} // }
// LoadStateExport loads the exported update state from the database. // // LoadStateExport loads the exported update state from the database.
func LoadStateExport() (*UpdateStateExport, error) { // func LoadStateExport() (*UpdateStateExport, error) {
r, err := db.Get(updateStatusDBKey) // r, err := db.Get(updateStatusDBKey)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
// unwrap // // unwrap
if r.IsWrapped() { // if r.IsWrapped() {
// only allocate a new struct, if we need it // // only allocate a new struct, if we need it
newRecord := &UpdateStateExport{} // newRecord := &UpdateStateExport{}
err = record.Unwrap(r, newRecord) // err = record.Unwrap(r, newRecord)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return newRecord, nil // return newRecord, nil
} // }
// or adjust type // // or adjust type
newRecord, ok := r.(*UpdateStateExport) // newRecord, ok := r.(*UpdateStateExport)
if !ok { // if !ok {
return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r) // return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r)
} // }
return newRecord, nil // return newRecord, nil
} // }
func initVersionExport() (err error) { // func initVersionExport() (err error) {
if err := GetVersions().save(); err != nil { // if err := GetVersions().save(); err != nil {
log.Warningf("updates: failed to export version information: %s", err) // log.Warningf("updates: failed to export version information: %s", err)
} // }
if err := GetSimpleVersions().save(); err != nil { // if err := GetSimpleVersions().save(); err != nil {
log.Warningf("updates: failed to export version information: %s", err) // log.Warningf("updates: failed to export version information: %s", err)
} // }
module.EventVersionsUpdated.AddCallback("export version status", export) // // module.EventVersionsUpdated.AddCallback("export version status", export)
return nil // return nil
} // }
func (v *Versions) save() error { // func (v *Versions) save() error {
if !v.KeyIsSet() { // if !v.KeyIsSet() {
v.SetKey(versionsDBKey) // v.SetKey(versionsDBKey)
} // }
return db.Put(v) // return db.Put(v)
} // }
func (v *SimpleVersions) save() error { // func (v *SimpleVersions) save() error {
if !v.KeyIsSet() { // if !v.KeyIsSet() {
v.SetKey(simpleVersionsDBKey) // v.SetKey(simpleVersionsDBKey)
} // }
return db.Put(v) // return db.Put(v)
} // }
func (s *UpdateStateExport) save() error { // func (s *UpdateStateExport) save() error {
if !s.KeyIsSet() { // if !s.KeyIsSet() {
s.SetKey(updateStatusDBKey) // s.SetKey(updateStatusDBKey)
} // }
return db.Put(s) // return db.Put(s)
} // }
// export is an event hook. // // export is an event hook.
func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { // func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
// Export versions. // // Export versions.
if err := GetVersions().save(); err != nil { // if err := GetVersions().save(); err != nil {
return false, err // return false, err
} // }
if err := GetSimpleVersions().save(); err != nil { // if err := GetSimpleVersions().save(); err != nil {
return false, err // return false, err
} // }
// Export udpate state. // // Export udpate state.
if err := GetStateExport().save(); err != nil { // if err := GetStateExport().save(); err != nil {
return false, err // return false, err
} // }
return false, nil // return false, nil
} // }
// AddToDebugInfo adds the update system status to the given debug.Info. // // AddToDebugInfo adds the update system status to the given debug.Info.
func AddToDebugInfo(di *debug.Info) { // func AddToDebugInfo(di *debug.Info) {
// Get resources from registry. // // Get resources from registry.
resources := registry.Export() // // resources := registry.Export()
platformPrefix := helper.PlatformIdentifier("") // // platformPrefix := helper.PlatformIdentifier("")
// Collect data for debug info. // // Collect data for debug info.
var active, selected []string // var active, selected []string
var activeCnt, totalCnt int // var activeCnt, totalCnt int
for id, r := range resources { // // for id, r := range resources {
// Ignore resources for other platforms. // // // Ignore resources for other platforms.
if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { // // if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) {
continue // // continue
} // // }
totalCnt++ // // totalCnt++
if r.ActiveVersion != nil { // // if r.ActiveVersion != nil {
activeCnt++ // // activeCnt++
active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) // // active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber))
} // // }
if r.SelectedVersion != nil { // // if r.SelectedVersion != nil {
selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) // // selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber))
} // // }
} // // }
sort.Strings(active) // sort.Strings(active)
sort.Strings(selected) // sort.Strings(selected)
// Compile to one list. // // Compile to one list.
lines := make([]string, 0, len(active)+len(selected)+3) // lines := make([]string, 0, len(active)+len(selected)+3)
lines = append(lines, "Active:") // lines = append(lines, "Active:")
lines = append(lines, active...) // lines = append(lines, active...)
lines = append(lines, "") // lines = append(lines, "")
lines = append(lines, "Selected:") // lines = append(lines, "Selected:")
lines = append(lines, selected...) // lines = append(lines, selected...)
// Add section. // // Add section.
di.AddSection( // di.AddSection(
fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), // fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt),
debug.UseCodeSection|debug.AddContentLineBreaks, // debug.UseCodeSection|debug.AddContentLineBreaks,
lines..., // lines...,
) // )
} // }

View File

@@ -1,72 +1,65 @@
package updates package updates
import (
"path"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/updates/helper"
)
// GetPlatformFile returns the latest platform specific file identified by the given identifier. // GetPlatformFile returns the latest platform specific file identified by the given identifier.
func GetPlatformFile(identifier string) (*updater.File, error) { // func GetPlatformFile(identifier string) (*updater.File, error) {
identifier = helper.PlatformIdentifier(identifier) // identifier = helper.PlatformIdentifier(identifier)
file, err := registry.GetFile(identifier) // file, err := registry.GetFile(identifier)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
module.EventVersionsUpdated.Submit(struct{}{}) // module.EventVersionsUpdated.Submit(struct{}{})
return file, nil // return file, nil
} // }
// GetFile returns the latest generic file identified by the given identifier. // GetFile returns the latest generic file identified by the given identifier.
func GetFile(identifier string) (*updater.File, error) { // func GetFile(identifier string) (*updater.File, error) {
identifier = path.Join("all", identifier) // identifier = path.Join("all", identifier)
file, err := registry.GetFile(identifier) // file, err := registry.GetFile(identifier)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
module.EventVersionsUpdated.Submit(struct{}{}) // module.EventVersionsUpdated.Submit(struct{}{})
return file, nil // return file, nil
} // }
// GetPlatformVersion returns the selected platform specific version of the // GetPlatformVersion returns the selected platform specific version of the
// given identifier. // given identifier.
// The returned resource version may not be modified. // The returned resource version may not be modified.
func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) { // func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) {
identifier = helper.PlatformIdentifier(identifier) // identifier = helper.PlatformIdentifier(identifier)
rv, err := registry.GetVersion(identifier) // rv, err := registry.GetVersion(identifier)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return rv, nil // return rv, nil
} // }
// GetVersion returns the selected generic version of the given identifier. // GetVersion returns the selected generic version of the given identifier.
// The returned resource version may not be modified. // The returned resource version may not be modified.
func GetVersion(identifier string) (*updater.ResourceVersion, error) { // func GetVersion(identifier string) (*updater.ResourceVersion, error) {
identifier = path.Join("all", identifier) // identifier = path.Join("all", identifier)
rv, err := registry.GetVersion(identifier) // rv, err := registry.GetVersion(identifier)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return rv, nil // return rv, nil
} // }
// GetVersionWithFullID returns the selected generic version of the given full identifier. // GetVersionWithFullID returns the selected generic version of the given full identifier.
// The returned resource version may not be modified. // The returned resource version may not be modified.
func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { // func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) {
rv, err := registry.GetVersion(identifier) // rv, err := registry.GetVersion(identifier)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return rv, nil // return rv, nil
} // }

View File

@@ -1,57 +1,58 @@
package helper package helper
import ( // import (
"errors" // "errors"
"fmt" // "fmt"
"os" // "os"
"path/filepath" // "path/filepath"
"runtime" // "runtime"
"strings" // "strings"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
) // "github.com/safing/portmaster/service/updates/registry"
// )
var pmElectronUpdate *updater.File // var pmElectronUpdate *registry.File
const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` // const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s`
// EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed // // EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed
// by our app-electron package has the SUID bit set on systems that do not // // by our app-electron package has the SUID bit set on systems that do not
// allow unprivileged CLONE_NEWUSER (clone(3)). // // allow unprivileged CLONE_NEWUSER (clone(3)).
// On non-linux systems or systems that have kernel.unprivileged_userns_clone // // On non-linux systems or systems that have kernel.unprivileged_userns_clone
// set to 1 EnsureChromeSandboPermissions is a NO-OP. // // set to 1 EnsureChromeSandboPermissions is a NO-OP.
func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { // func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error {
if runtime.GOOS != "linux" { // if runtime.GOOS != "linux" {
return nil // return nil
} // }
if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { // if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() {
return nil // return nil
} // }
identifier := PlatformIdentifier("app/portmaster-app.zip") // identifier := PlatformIdentifier("app/portmaster-app.zip")
var err error // var err error
pmElectronUpdate, err = reg.GetFile(identifier) // pmElectronUpdate, err = reg.GetFile(identifier)
if err != nil { // if err != nil {
if errors.Is(err, updater.ErrNotAvailableLocally) { // if errors.Is(err, updater.ErrNotAvailableLocally) {
return nil // return nil
} // }
return fmt.Errorf("failed to get file: %w", err) // return fmt.Errorf("failed to get file: %w", err)
} // }
unpackedPath := strings.TrimSuffix( // unpackedPath := strings.TrimSuffix(
pmElectronUpdate.Path(), // pmElectronUpdate.Path(),
filepath.Ext(pmElectronUpdate.Path()), // filepath.Ext(pmElectronUpdate.Path()),
) // )
sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") // sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox")
if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { // if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil {
log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) // log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile)
return fmt.Errorf("failed to chmod: %w", err) // return fmt.Errorf("failed to chmod: %w", err)
} // }
log.Debugf("updates: fixed SUID permission for chrome-sandbox") // log.Debugf("updates: fixed SUID permission for chrome-sandbox")
return nil // return nil
} // }

View File

@@ -1,136 +1,136 @@
package helper package helper
import ( // import (
"errors" // "errors"
"fmt" // "fmt"
"io/fs" // "io/fs"
"os" // "os"
"path/filepath" // "path/filepath"
"github.com/safing/jess/filesig" // "github.com/safing/jess/filesig"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
) // )
// Release Channel Configuration Keys. // // Release Channel Configuration Keys.
const ( // const (
ReleaseChannelKey = "core/releaseChannel" // ReleaseChannelKey = "core/releaseChannel"
ReleaseChannelJSONKey = "core.releaseChannel" // ReleaseChannelJSONKey = "core.releaseChannel"
) // )
// Release Channels. // // Release Channels.
const ( // const (
ReleaseChannelStable = "stable" // ReleaseChannelStable = "stable"
ReleaseChannelBeta = "beta" // ReleaseChannelBeta = "beta"
ReleaseChannelStaging = "staging" // ReleaseChannelStaging = "staging"
ReleaseChannelSupport = "support" // ReleaseChannelSupport = "support"
) // )
const jsonSuffix = ".json" // const jsonSuffix = ".json"
// SetIndexes sets the update registry indexes and also configures the registry // // SetIndexes sets the update registry indexes and also configures the registry
// to use pre-releases based on the channel. // // to use pre-releases based on the channel.
func SetIndexes( // func SetIndexes(
registry *updater.ResourceRegistry, // registry *updater.ResourceRegistry,
releaseChannel string, // releaseChannel string,
deleteUnusedIndexes bool, // deleteUnusedIndexes bool,
autoDownload bool, // autoDownload bool,
autoDownloadIntel bool, // autoDownloadIntel bool,
) (warning error) { // ) (warning error) {
usePreReleases := false // usePreReleases := false
// Be reminded that the order is important, as indexes added later will // // Be reminded that the order is important, as indexes added later will
// override the current release from earlier indexes. // // override the current release from earlier indexes.
// Reset indexes before adding them (again). // // Reset indexes before adding them (again).
registry.ResetIndexes() // registry.ResetIndexes()
// Add the intel index first, in order to be able to override it with the // // Add the intel index first, in order to be able to override it with the
// other indexes when needed. // // other indexes when needed.
registry.AddIndex(updater.Index{ // registry.AddIndex(updater.Index{
Path: "all/intel/intel.json", // Path: "all/intel/intel.json",
AutoDownload: autoDownloadIntel, // AutoDownload: autoDownloadIntel,
}) // })
// Always add the stable index as a base. // // Always add the stable index as a base.
registry.AddIndex(updater.Index{ // registry.AddIndex(updater.Index{
Path: ReleaseChannelStable + jsonSuffix, // Path: ReleaseChannelStable + jsonSuffix,
AutoDownload: autoDownload, // AutoDownload: autoDownload,
}) // })
// Add beta index if in beta or staging channel. // // Add beta index if in beta or staging channel.
indexPath := ReleaseChannelBeta + jsonSuffix // indexPath := ReleaseChannelBeta + jsonSuffix
if releaseChannel == ReleaseChannelBeta || // if releaseChannel == ReleaseChannelBeta ||
releaseChannel == ReleaseChannelStaging || // releaseChannel == ReleaseChannelStaging ||
(releaseChannel == "" && indexExists(registry, indexPath)) { // (releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{ // registry.AddIndex(updater.Index{
Path: indexPath, // Path: indexPath,
PreRelease: true, // PreRelease: true,
AutoDownload: autoDownload, // AutoDownload: autoDownload,
}) // })
usePreReleases = true // usePreReleases = true
} else if deleteUnusedIndexes { // } else if deleteUnusedIndexes {
err := deleteIndex(registry, indexPath) // err := deleteIndex(registry, indexPath)
if err != nil { // if err != nil {
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) // warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
} // }
} // }
// Add staging index if in staging channel. // // Add staging index if in staging channel.
indexPath = ReleaseChannelStaging + jsonSuffix // indexPath = ReleaseChannelStaging + jsonSuffix
if releaseChannel == ReleaseChannelStaging || // if releaseChannel == ReleaseChannelStaging ||
(releaseChannel == "" && indexExists(registry, indexPath)) { // (releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{ // registry.AddIndex(updater.Index{
Path: indexPath, // Path: indexPath,
PreRelease: true, // PreRelease: true,
AutoDownload: autoDownload, // AutoDownload: autoDownload,
}) // })
usePreReleases = true // usePreReleases = true
} else if deleteUnusedIndexes { // } else if deleteUnusedIndexes {
err := deleteIndex(registry, indexPath) // err := deleteIndex(registry, indexPath)
if err != nil { // if err != nil {
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) // warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
} // }
} // }
// Add support index if in support channel. // // Add support index if in support channel.
indexPath = ReleaseChannelSupport + jsonSuffix // indexPath = ReleaseChannelSupport + jsonSuffix
if releaseChannel == ReleaseChannelSupport || // if releaseChannel == ReleaseChannelSupport ||
(releaseChannel == "" && indexExists(registry, indexPath)) { // (releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{ // registry.AddIndex(updater.Index{
Path: indexPath, // Path: indexPath,
AutoDownload: autoDownload, // AutoDownload: autoDownload,
}) // })
usePreReleases = true // usePreReleases = true
} else if deleteUnusedIndexes { // } else if deleteUnusedIndexes {
err := deleteIndex(registry, indexPath) // err := deleteIndex(registry, indexPath)
if err != nil { // if err != nil {
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) // warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
} // }
} // }
// Set pre-release usage. // // Set pre-release usage.
registry.SetUsePreReleases(usePreReleases) // registry.SetUsePreReleases(usePreReleases)
return warning // return warning
} // }
func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { // func indexExists(registry *updater.ResourceRegistry, indexPath string) bool {
_, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) // _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath))
return err == nil // return err == nil
} // }
func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { // func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error {
// Remove index itself. // // Remove index itself.
err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) // err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath))
if err != nil && !errors.Is(err, fs.ErrNotExist) { // if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err // return err
} // }
// Remove any accompanying signature. // // Remove any accompanying signature.
err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) // err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension))
if err != nil && !errors.Is(err, fs.ErrNotExist) { // if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err // return err
} // }
return nil // return nil
} // }

View File

@@ -1,42 +1,42 @@
package helper package helper
import ( // import (
"github.com/safing/jess" // "github.com/safing/jess"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
) // )
var ( // var (
// VerificationConfig holds the complete verification configuration for the registry. // // VerificationConfig holds the complete verification configuration for the registry.
VerificationConfig = map[string]*updater.VerificationOptions{ // VerificationConfig = map[string]*updater.VerificationOptions{
"": { // Default. // "": { // Default.
TrustStore: BinarySigningTrustStore, // TrustStore: BinarySigningTrustStore,
DownloadPolicy: updater.SignaturePolicyRequire, // DownloadPolicy: updater.SignaturePolicyRequire,
DiskLoadPolicy: updater.SignaturePolicyWarn, // DiskLoadPolicy: updater.SignaturePolicyWarn,
}, // },
"all/intel/": nil, // Disable until IntelHub supports signing. // "all/intel/": nil, // Disable until IntelHub supports signing.
} // }
// BinarySigningKeys holds the signing keys in text format. // // BinarySigningKeys holds the signing keys in text format.
BinarySigningKeys = []string{ // BinarySigningKeys = []string{
// Safing Code Signing Key #1 // // Safing Code Signing Key #1
"recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", // "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD",
// Safing Code Signing Key #2 // // Safing Code Signing Key #2
"recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", // "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3",
} // }
// BinarySigningTrustStore is an in-memory trust store with the signing keys. // // BinarySigningTrustStore is an in-memory trust store with the signing keys.
BinarySigningTrustStore = jess.NewMemTrustStore() // BinarySigningTrustStore = jess.NewMemTrustStore()
) // )
func init() { // func init() {
for _, signingKey := range BinarySigningKeys { // for _, signingKey := range BinarySigningKeys {
rcpt, err := jess.RecipientFromTextFormat(signingKey) // rcpt, err := jess.RecipientFromTextFormat(signingKey)
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
err = BinarySigningTrustStore.StoreSignet(rcpt) // err = BinarySigningTrustStore.StoreSignet(rcpt)
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
} // }
} // }

View File

@@ -1,95 +1,95 @@
package helper package helper
import ( // import (
"fmt" // "fmt"
"runtime" // "runtime"
"github.com/tevino/abool" // "github.com/tevino/abool"
) // )
const onWindows = runtime.GOOS == "windows" // const onWindows = runtime.GOOS == "windows"
var intelOnly = abool.New() // var intelOnly = abool.New()
// IntelOnly specifies that only intel data is mandatory. // // IntelOnly specifies that only intel data is mandatory.
func IntelOnly() { // func IntelOnly() {
intelOnly.Set() // intelOnly.Set()
} // }
// PlatformIdentifier converts identifier for the current platform. // // PlatformIdentifier converts identifier for the current platform.
func PlatformIdentifier(identifier string) string { // func PlatformIdentifier(identifier string) string {
// From https://golang.org/pkg/runtime/#GOARCH // // From https://golang.org/pkg/runtime/#GOARCH
// GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. // // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on.
// GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. // // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on.
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) // return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
} // }
// MandatoryUpdates returns mandatory updates that should be loaded on install // // MandatoryUpdates returns mandatory updates that should be loaded on install
// or reset. // // or reset.
func MandatoryUpdates() (identifiers []string) { // func MandatoryUpdates() (identifiers []string) {
// Intel // // Intel
identifiers = append( // identifiers = append(
identifiers, // identifiers,
// Filter lists data // // Filter lists data
"all/intel/lists/index.dsd", // "all/intel/lists/index.dsd",
"all/intel/lists/base.dsdl", // "all/intel/lists/base.dsdl",
"all/intel/lists/intermediate.dsdl", // "all/intel/lists/intermediate.dsdl",
"all/intel/lists/urgent.dsdl", // "all/intel/lists/urgent.dsdl",
// Geo IP data // // Geo IP data
"all/intel/geoip/geoipv4.mmdb.gz", // "all/intel/geoip/geoipv4.mmdb.gz",
"all/intel/geoip/geoipv6.mmdb.gz", // "all/intel/geoip/geoipv6.mmdb.gz",
) // )
// Stop here if we only want intel data. // // Stop here if we only want intel data.
if intelOnly.IsSet() { // if intelOnly.IsSet() {
return identifiers // return identifiers
} // }
// Binaries // // Binaries
if onWindows { // if onWindows {
identifiers = append( // identifiers = append(
identifiers, // identifiers,
PlatformIdentifier("core/portmaster-core.exe"), // PlatformIdentifier("core/portmaster-core.exe"),
PlatformIdentifier("kext/portmaster-kext.sys"), // PlatformIdentifier("kext/portmaster-kext.sys"),
PlatformIdentifier("kext/portmaster-kext.pdb"), // PlatformIdentifier("kext/portmaster-kext.pdb"),
PlatformIdentifier("start/portmaster-start.exe"), // PlatformIdentifier("start/portmaster-start.exe"),
PlatformIdentifier("notifier/portmaster-notifier.exe"), // PlatformIdentifier("notifier/portmaster-notifier.exe"),
PlatformIdentifier("notifier/portmaster-wintoast.dll"), // PlatformIdentifier("notifier/portmaster-wintoast.dll"),
PlatformIdentifier("app2/portmaster-app.zip"), // PlatformIdentifier("app2/portmaster-app.zip"),
) // )
} else { // } else {
identifiers = append( // identifiers = append(
identifiers, // identifiers,
PlatformIdentifier("core/portmaster-core"), // PlatformIdentifier("core/portmaster-core"),
PlatformIdentifier("start/portmaster-start"), // PlatformIdentifier("start/portmaster-start"),
PlatformIdentifier("notifier/portmaster-notifier"), // PlatformIdentifier("notifier/portmaster-notifier"),
PlatformIdentifier("app2/portmaster-app"), // PlatformIdentifier("app2/portmaster-app"),
) // )
} // }
// Components, Assets and Data // // Components, Assets and Data
identifiers = append( // identifiers = append(
identifiers, // identifiers,
// User interface components // // User interface components
PlatformIdentifier("app/portmaster-app.zip"), // PlatformIdentifier("app/portmaster-app.zip"),
"all/ui/modules/portmaster.zip", // "all/ui/modules/portmaster.zip",
"all/ui/modules/assets.zip", // "all/ui/modules/assets.zip",
) // )
return identifiers // return identifiers
} // }
// AutoUnpackUpdates returns assets that need unpacking. // // AutoUnpackUpdates returns assets that need unpacking.
func AutoUnpackUpdates() []string { // func AutoUnpackUpdates() []string {
if intelOnly.IsSet() { // if intelOnly.IsSet() {
return []string{} // return []string{}
} // }
return []string{ // return []string{
PlatformIdentifier("app/portmaster-app.zip"), // PlatformIdentifier("app/portmaster-app.zip"),
PlatformIdentifier("app2/portmaster-app.zip"), // PlatformIdentifier("app2/portmaster-app.zip"),
} // }
} // }

View File

@@ -1,110 +0,0 @@
package updates
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/safing/portmaster/base/log"
)
type UpdateIndex struct {
Directory string
DownloadDirectory string
Ignore []string
IndexURLs []string
IndexFile string
AutoApply bool
}
func (ui *UpdateIndex) downloadIndexFile() (err error) {
_ = os.MkdirAll(ui.Directory, defaultDirMode)
_ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode)
for _, url := range ui.IndexURLs {
err = ui.downloadIndexFileFromURL(url)
if err != nil {
log.Warningf("updates: %s", err)
continue
}
// Downloading was successful.
err = nil
break
}
return
}
func (ui *UpdateIndex) checkForUpdates() (bool, error) {
err := ui.downloadIndexFile()
if err != nil {
return false, err
}
currentBundle, err := ui.GetInstallBundle()
if err != nil {
return true, err // Current installed bundle not found, act as there is update.
}
updateBundle, err := ui.GetUpdateBundle()
if err != nil {
return false, err
}
return currentBundle.Version != updateBundle.Version, nil
}
func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error {
client := http.Client{}
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("failed a get request to %s: %w", url, err)
}
defer func() { _ = resp.Body.Close() }()
filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile)
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}
func (ui *UpdateIndex) GetInstallBundle() (*Bundle, error) {
indexFile := fmt.Sprintf("%s/%s", ui.Directory, ui.IndexFile)
return ui.GetBundle(indexFile)
}
func (ui *UpdateIndex) GetUpdateBundle() (*Bundle, error) {
indexFile := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile)
return ui.GetBundle(indexFile)
}
func (ui *UpdateIndex) GetBundle(indexFile string) (*Bundle, error) {
// Check if the file exists.
file, err := os.Open(indexFile)
if err != nil {
return nil, fmt.Errorf("failed to open index file: %w", err)
}
defer func() { _ = file.Close() }()
// Read
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
// Parse
var bundle Bundle
err = json.Unmarshal(content, &bundle)
if err != nil {
return nil, err
}
return &bundle, nil
}

View File

@@ -6,9 +6,6 @@ import (
"time" "time"
"github.com/safing/portmaster/base/database" "github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/mgr"
) )
const ( const (
@@ -17,10 +14,6 @@ const (
enableSoftwareUpdatesKey = "core/automaticUpdates" enableSoftwareUpdatesKey = "core/automaticUpdates"
enableIntelUpdatesKey = "core/automaticIntelUpdates" enableIntelUpdatesKey = "core/automaticIntelUpdates"
// ModuleName is the name of the update module
// and can be used when declaring module dependencies.
ModuleName = "updates"
// VersionUpdateEvent is emitted every time a new // VersionUpdateEvent is emitted every time a new
// version of a monitored resource is selected. // version of a monitored resource is selected.
// During module initialization VersionUpdateEvent // During module initialization VersionUpdateEvent
@@ -37,8 +30,6 @@ const (
) )
var ( var (
registry *updater.ResourceRegistry
userAgentFromFlag string userAgentFromFlag string
updateServerFromFlag string updateServerFromFlag string
@@ -57,205 +48,13 @@ const (
updateTaskRepeatDuration = 1 * time.Hour updateTaskRepeatDuration = 1 * time.Hour
) )
func start() error {
// module.restartWorkerMgr.Repeat(10 * time.Minute)
// module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig)
// // create registry
// registry = &updater.ResourceRegistry{
// Name: ModuleName,
// UpdateURLs: DefaultUpdateURLs,
// UserAgent: UserAgent,
// MandatoryUpdates: helper.MandatoryUpdates(),
// AutoUnpack: helper.AutoUnpackUpdates(),
// Verification: helper.VerificationConfig,
// DevMode: devMode(),
// Online: true,
// }
// // Override values from flags.
// if userAgentFromFlag != "" {
// registry.UserAgent = userAgentFromFlag
// }
// if updateServerFromFlag != "" {
// registry.UpdateURLs = []string{updateServerFromFlag}
// }
// // pre-init state
// updateStateExport, err := LoadStateExport()
// if err != nil {
// log.Debugf("updates: failed to load exported update state: %s", err)
// } else if updateStateExport.UpdateState != nil {
// err := registry.PreInitUpdateState(*updateStateExport.UpdateState)
// if err != nil {
// return err
// }
// }
// initialize
// err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755))
// if err != nil {
// return err
// }
// // register state provider
// err = registerRegistryStateProvider()
// if err != nil {
// return err
// }
// registry.StateNotifyFunc = pushRegistryState
// // Set indexes based on the release channel.
// warning := helper.SetIndexes(
// registry,
// initialReleaseChannel,
// true,
// enableSoftwareUpdates() && !DisableSoftwareAutoUpdate,
// enableIntelUpdates(),
// )
// if warning != nil {
// log.Warningf("updates: %s", warning)
// }
// err = registry.LoadIndexes(module.m.Ctx())
// if err != nil {
// log.Warningf("updates: failed to load indexes: %s", err)
// }
// err = registry.ScanStorage("")
// if err != nil {
// log.Warningf("updates: error during storage scan: %s", err)
// }
// registry.SelectVersions()
// module.EventVersionsUpdated.Submit(struct{}{})
// // Initialize the version export - this requires the registry to be set up.
// err = initVersionExport()
// if err != nil {
// return err
// }
// // start updater task
// if !disableTaskSchedule {
// _ = module.updateWorkerMgr.Repeat(30 * time.Minute)
// }
// if updateASAP {
// module.updateWorkerMgr.Go()
// }
// // react to upgrades
// if err := initUpgrader(); err != nil {
// return err
// }
// warnOnIncorrectParentPath()
return nil
}
// TriggerUpdate queues the update task to execute ASAP.
func TriggerUpdate(forceIndexCheck, downloadAll bool) error {
// switch {
// case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates():
// return errors.New("automatic updating is disabled")
// default:
// if forceIndexCheck {
// forceCheck.Set()
// }
// if downloadAll {
// forceDownload.Set()
// }
// // If index check if forced, start quicker.
// module.updateWorkerMgr.Go()
// }
log.Debugf("updates: triggering update to run as soon as possible")
return nil
}
// DisableUpdateSchedule disables the update schedule.
// If called, updates are only checked when TriggerUpdate()
// is called.
func DisableUpdateSchedule() error {
// TODO: Updater state should be always on
// switch module.Status() {
// case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping:
// return errors.New("module already online")
// }
return nil
}
func checkForUpdates(ctx *mgr.WorkerCtx) (err error) {
// Set correct error if context was canceled.
// defer func() {
// select {
// case <-ctx.Done():
// err = context.Canceled
// default:
// }
// }()
// // Get flags.
// forceIndexCheck := forceCheck.SetToIf(true, false)
// downloadAll := forceDownload.SetToIf(true, false)
// // Check again if downloading updates is enabled, or forced.
// if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() {
// log.Warningf("updates: automatic updates are disabled")
// return nil
// }
// defer func() {
// // Resolve any error and send success notification.
// if err == nil {
// log.Infof("updates: successfully checked for updates")
// notifyUpdateSuccess(forceIndexCheck)
// return
// }
// // Log and notify error.
// log.Errorf("updates: check failed: %s", err)
// notifyUpdateCheckFailed(forceIndexCheck, err)
// }()
// if err = registry.UpdateIndexes(ctx.Ctx()); err != nil {
// err = fmt.Errorf("failed to update indexes: %w", err)
// return //nolint:nakedret // TODO: Would "return err" work with the defer?
// }
// err = registry.DownloadUpdates(ctx.Ctx(), downloadAll)
// if err != nil {
// err = fmt.Errorf("failed to download updates: %w", err)
// return //nolint:nakedret // TODO: Would "return err" work with the defer?
// }
// registry.SelectVersions()
// // Unpack selected resources.
// err = registry.UnpackResources()
// if err != nil {
// err = fmt.Errorf("failed to unpack updates: %w", err)
// return //nolint:nakedret // TODO: Would "return err" work with the defer?
// }
// // Purge old resources
// registry.Purge(2)
// module.EventResourcesUpdated.Submit(struct{}{})
return nil
}
func stop() error { func stop() error {
if registry != nil { // if registry != nil {
err := registry.Cleanup() // err := registry.Cleanup()
if err != nil { // if err != nil {
log.Warningf("updates: failed to clean up registry: %s", err) // log.Warningf("updates: failed to clean up registry: %s", err)
} // }
} // }
return nil return nil
} }

View File

@@ -2,10 +2,8 @@ package updates
import ( import (
"errors" "errors"
"flag"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
"sync/atomic" "sync/atomic"
"github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/api"
@@ -13,34 +11,33 @@ import (
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
) )
const ( var applyUpdates bool
defaultFileMode = os.FileMode(0o0644)
defaultDirMode = os.FileMode(0o0755) func init() {
) flag.BoolVar(&applyUpdates, "update", false, "apply downloaded updates")
}
// Updates provides access to released artifacts. // Updates provides access to released artifacts.
type Updates struct { type Updates struct {
m *mgr.Manager m *mgr.Manager
states *mgr.StateMgr states *mgr.StateMgr
updateWorkerMgr *mgr.WorkerMgr updateBinaryWorkerMgr *mgr.WorkerMgr
restartWorkerMgr *mgr.WorkerMgr updateIntelWorkerMgr *mgr.WorkerMgr
restartWorkerMgr *mgr.WorkerMgr
EventResourcesUpdated *mgr.EventMgr[struct{}] EventResourcesUpdated *mgr.EventMgr[struct{}]
EventVersionsUpdated *mgr.EventMgr[struct{}] EventVersionsUpdated *mgr.EventMgr[struct{}]
binUpdates UpdateIndex registry registry.Registry
intelUpdates UpdateIndex
instance instance instance instance
} }
var ( var shimLoaded atomic.Bool
module *Updates
shimLoaded atomic.Bool
)
// New returns a new UI module. // New returns a new UI module.
func New(instance instance) (*Updates, error) { func New(instance instance) (*Updates, error) {
@@ -49,20 +46,22 @@ func New(instance instance) (*Updates, error) {
} }
m := mgr.New("Updates") m := mgr.New("Updates")
module = &Updates{ module := &Updates{
m: m, m: m,
states: m.NewStateMgr(), states: m.NewStateMgr(),
EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m), EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m),
EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m), EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m),
instance: instance,
instance: instance,
} }
// Events // Events
module.updateWorkerMgr = m.NewWorkerMgr("updater", module.checkForUpdates, nil) module.updateBinaryWorkerMgr = m.NewWorkerMgr("binary updater", module.checkForBinaryUpdates, nil)
module.updateIntelWorkerMgr = m.NewWorkerMgr("intel updater", module.checkForIntelUpdates, nil)
module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil) module.restartWorkerMgr = m.NewWorkerMgr("automatic restart", automaticRestart, nil)
module.binUpdates = UpdateIndex{ binIndex := registry.UpdateIndex{
Directory: "/usr/lib/portmaster", Directory: "/usr/lib/portmaster",
DownloadDirectory: "/var/portmaster/new_bin", DownloadDirectory: "/var/portmaster/new_bin",
Ignore: []string{"databases", "intel", "config.json"}, Ignore: []string{"databases", "intel", "config.json"},
@@ -71,62 +70,48 @@ func New(instance instance) (*Updates, error) {
AutoApply: false, AutoApply: false,
} }
module.intelUpdates = UpdateIndex{ intelIndex := registry.UpdateIndex{
Directory: "/var/portmaster/intel", Directory: "/var/portmaster/intel",
DownloadDirectory: "/var/portmaster/new_intel", DownloadDirectory: "/var/portmaster/new_intel",
IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexURLs: []string{"http://localhost:8000/test-intel.json"},
IndexFile: "intel-index.json", IndexFile: "intel-index.json",
AutoApply: true, AutoApply: true,
} }
module.registry = registry.New(binIndex, intelIndex)
return module, nil return module, nil
} }
func deleteUnfinishedDownloads(rootDir string) error { func (u *Updates) checkForBinaryUpdates(_ *mgr.WorkerCtx) error {
return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { hasUpdates, err := u.registry.CheckForBinaryUpdates()
if err != nil {
log.Errorf("updates: failed to check for binary updates: %s", err)
}
if hasUpdates {
log.Infof("updates: there is updates available in the binary bundle")
err = u.registry.DownloadBinaryUpdates()
if err != nil { if err != nil {
return err log.Errorf("updates: failed to download bundle: %s", err)
} }
} else {
// Check if the current file has the specified extension log.Infof("updates: no new binary updates")
if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") { }
log.Warningf("updates deleting unfinished: %s\n", path) return nil
err := os.Remove(path)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", path, err)
}
}
return nil
})
} }
func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error { func (u *Updates) checkForIntelUpdates(_ *mgr.WorkerCtx) error {
_ = deleteUnfinishedDownloads(u.binUpdates.DownloadDirectory) hasUpdates, err := u.registry.CheckForIntelUpdates()
hasUpdate, err := u.binUpdates.checkForUpdates()
if err != nil { if err != nil {
log.Warningf("failed to get binary index file: %s", err) log.Errorf("updates: failed to check for intel updates: %s", err)
} }
if hasUpdate { if hasUpdates {
binBundle, err := u.binUpdates.GetUpdateBundle() log.Infof("updates: there is updates available in the intel bundle")
if err == nil { err = u.registry.DownloadIntelUpdates()
log.Debugf("Bin Bundle: %+v", binBundle) if err != nil {
_ = os.MkdirAll(u.binUpdates.DownloadDirectory, defaultDirMode) log.Errorf("updates: failed to download bundle: %s", err)
binBundle.downloadAndVerify(u.binUpdates.DownloadDirectory)
}
}
_ = deleteUnfinishedDownloads(u.intelUpdates.DownloadDirectory)
hasUpdate, err = u.intelUpdates.checkForUpdates()
if err != nil {
log.Warningf("failed to get intel index file: %s", err)
}
if hasUpdate {
intelBundle, err := u.intelUpdates.GetUpdateBundle()
if err == nil {
log.Debugf("Intel Bundle: %+v", intelBundle)
_ = os.MkdirAll(u.intelUpdates.DownloadDirectory, defaultDirMode)
intelBundle.downloadAndVerify(u.intelUpdates.DownloadDirectory)
} }
} else {
log.Infof("updates: no new intel data updates")
} }
return nil return nil
} }
@@ -143,38 +128,36 @@ func (u *Updates) Manager() *mgr.Manager {
// Start starts the module. // Start starts the module.
func (u *Updates) Start() error { func (u *Updates) Start() error {
initConfig() // initConfig()
u.m.Go("check for updates", func(w *mgr.WorkerCtx) error {
binBundle, err := u.binUpdates.GetInstallBundle()
if err != nil {
log.Warningf("failed to get binary bundle: %s", err)
} else {
err = binBundle.Verify(u.binUpdates.Directory)
if err != nil {
log.Warningf("binary bundle is not valid: %s", err)
} else {
log.Infof("binary bundle is valid")
}
}
intelBundle, err := u.intelUpdates.GetInstallBundle() if applyUpdates {
err := u.registry.ApplyBinaryUpdates()
if err != nil { if err != nil {
log.Warningf("failed to get intel bundle: %s", err) log.Errorf("updates: failed to apply binary updates: %s", err)
} else {
err = intelBundle.Verify(u.intelUpdates.Directory)
if err != nil {
log.Warningf("intel bundle is not valid: %s", err)
} else {
log.Infof("intel bundle is valid")
}
} }
err = u.registry.ApplyIntelUpdates()
if err != nil {
log.Errorf("updates: failed to apply intel updates: %s", err)
}
u.instance.Restart()
return nil return nil
}) }
u.updateWorkerMgr.Go()
err := u.registry.Initialize()
if err != nil {
// TODO(vladimir): Find a better way to handle this error. The service will stop if parsing of the bundle files fails.
return fmt.Errorf("failed to initialize registry: %w", err)
}
u.updateBinaryWorkerMgr.Go()
u.updateIntelWorkerMgr.Go()
return nil return nil
} }
func (u *Updates) GetFile(id string) (*registry.File, error) {
return u.registry.GetFile(id)
}
// Stop stops the module. // Stop stops the module.
func (u *Updates) Stop() error { func (u *Updates) Stop() error {
return stop() return stop()

View File

@@ -1,12 +1,8 @@
package updates package updates
import ( import (
"fmt"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/safing/portmaster/base/notifications"
) )
const ( const (
@@ -25,109 +21,109 @@ func (u *Updates) notificationsEnabled() bool {
return u.instance.Notifications() != nil return u.instance.Notifications() != nil
} }
func notifyUpdateSuccess(force bool) { // func notifyUpdateSuccess(force bool) {
if !module.notificationsEnabled() { // if !module.notificationsEnabled() {
return // return
} // }
updateFailedCnt.Store(0) // updateFailedCnt.Store(0)
module.states.Clear() // module.states.Clear()
updateState := registry.GetState().Updates // updateState := registry.GetState().Updates
flavor := updateSuccess // flavor := updateSuccess
switch { // switch {
case len(updateState.PendingDownload) > 0: // case len(updateState.PendingDownload) > 0:
// Show notification if there are pending downloads. // // Show notification if there are pending downloads.
flavor = updateSuccessPending // flavor = updateSuccessPending
case updateState.LastDownloadAt != nil && // case updateState.LastDownloadAt != nil &&
time.Since(*updateState.LastDownloadAt) < 5*time.Second: // time.Since(*updateState.LastDownloadAt) < 5*time.Second:
// Show notification if we downloaded something within the last minute. // // Show notification if we downloaded something within the last minute.
flavor = updateSuccessDownloaded // flavor = updateSuccessDownloaded
case force: // case force:
// Always show notification if update was manually triggered. // // Always show notification if update was manually triggered.
default: // default:
// Otherwise, the update was uneventful. Do not show notification. // // Otherwise, the update was uneventful. Do not show notification.
return // return
} // }
switch flavor { // switch flavor {
case updateSuccess: // case updateSuccess:
notifications.Notify(&notifications.Notification{ // notifications.Notify(&notifications.Notification{
EventID: updateSuccess, // EventID: updateSuccess,
Type: notifications.Info, // Type: notifications.Info,
Title: "Portmaster Is Up-To-Date", // Title: "Portmaster Is Up-To-Date",
Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), // Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(),
Expires: time.Now().Add(1 * time.Minute).Unix(), // Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{ // AvailableActions: []*notifications.Action{
{ // {
ID: "ack", // ID: "ack",
Text: "OK", // Text: "OK",
}, // },
}, // },
}) // })
case updateSuccessPending: // case updateSuccessPending:
msg := fmt.Sprintf( // msg := fmt.Sprintf(
`%d updates are available for download: // `%d updates are available for download:
- %s // - %s
Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, // Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`,
len(updateState.PendingDownload), // len(updateState.PendingDownload),
strings.Join(updateState.PendingDownload, "\n- "), // strings.Join(updateState.PendingDownload, "\n- "),
) // )
notifications.Notify(&notifications.Notification{ // notifications.Notify(&notifications.Notification{
EventID: updateSuccess, // EventID: updateSuccess,
Type: notifications.Info, // Type: notifications.Info,
Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), // Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)),
Message: msg, // Message: msg,
AvailableActions: []*notifications.Action{ // AvailableActions: []*notifications.Action{
{ // {
ID: "ack", // ID: "ack",
Text: "OK", // Text: "OK",
}, // },
{ // {
ID: "download", // ID: "download",
Text: "Download Now", // Text: "Download Now",
Type: notifications.ActionTypeWebhook, // Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{ // Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates + "?download", // URL: apiPathCheckForUpdates + "?download",
ResultAction: "display", // ResultAction: "display",
}, // },
}, // },
}, // },
}) // })
case updateSuccessDownloaded: // case updateSuccessDownloaded:
msg := fmt.Sprintf( // msg := fmt.Sprintf(
`%d updates were downloaded and applied: // `%d updates were downloaded and applied:
- %s // - %s
%s // %s
`, // `,
len(updateState.LastDownload), // len(updateState.LastDownload),
strings.Join(updateState.LastDownload, "\n- "), // strings.Join(updateState.LastDownload, "\n- "),
getUpdatingInfoMsg(), // getUpdatingInfoMsg(),
) // )
notifications.Notify(&notifications.Notification{ // notifications.Notify(&notifications.Notification{
EventID: updateSuccess, // EventID: updateSuccess,
Type: notifications.Info, // Type: notifications.Info,
Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), // Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)),
Message: msg, // Message: msg,
Expires: time.Now().Add(1 * time.Minute).Unix(), // Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{ // AvailableActions: []*notifications.Action{
{ // {
ID: "ack", // ID: "ack",
Text: "OK", // Text: "OK",
}, // },
}, // },
}) // })
} // }
} // }
func getUpdatingInfoMsg() string { func getUpdatingInfoMsg() string {
switch { switch {
@@ -140,41 +136,41 @@ func getUpdatingInfoMsg() string {
} }
} }
func notifyUpdateCheckFailed(force bool, err error) { // func notifyUpdateCheckFailed(force bool, err error) {
if !module.notificationsEnabled() { // if !module.notificationsEnabled() {
return // return
} // }
failedCnt := updateFailedCnt.Add(1) // failedCnt := updateFailedCnt.Add(1)
lastSuccess := registry.GetState().Updates.LastSuccessAt // lastSuccess := registry.GetState().Updates.LastSuccessAt
switch { // switch {
case force: // case force:
// Always show notification if update was manually triggered. // // Always show notification if update was manually triggered.
case failedCnt < failedUpdateNotifyCountThreshold: // case failedCnt < failedUpdateNotifyCountThreshold:
// Not failed often enough for notification. // // Not failed often enough for notification.
return // return
case lastSuccess == nil: // case lastSuccess == nil:
// No recorded successful update. // // No recorded successful update.
case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess): // case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess):
// Failed too recently for notification. // // Failed too recently for notification.
return // return
} // }
notifications.NotifyWarn( // notifications.NotifyWarn(
updateFailed, // updateFailed,
"Update Check Failed", // "Update Check Failed",
fmt.Sprintf( // fmt.Sprintf(
"Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s", // "Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s",
err, // err,
), // ),
notifications.Action{ // notifications.Action{
Text: "Try Again Now", // Text: "Try Again Now",
Type: notifications.ActionTypeWebhook, // Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{ // Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates, // URL: apiPathCheckForUpdates,
ResultAction: "display", // ResultAction: "display",
}, // },
}, // },
).SyncWithState(module.states) // ).SyncWithState(module.states)
} // }

View File

@@ -1,204 +1,201 @@
package updates package updates
import ( // import (
"bytes" // "crypto/sha256"
"crypto/sha256" // _ "embed"
_ "embed" // "encoding/hex"
"encoding/hex" // "errors"
"errors" // "fmt"
"fmt" // "io/fs"
"io" // "os"
"io/fs" // "path/filepath"
"os"
"path/filepath"
"github.com/tevino/abool" // "github.com/tevino/abool"
"golang.org/x/exp/slices" // "golang.org/x/exp/slices"
"github.com/safing/portmaster/base/dataroot" // "github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils/renameio" // )
)
var ( // var (
portmasterCoreServiceFilePath = "portmaster.service" // portmasterCoreServiceFilePath = "portmaster.service"
portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" // portmasterNotifierServiceFilePath = "portmaster_notifier.desktop"
backupExtension = ".backup" // backupExtension = ".backup"
//go:embed assets/portmaster.service // //go:embed assets/portmaster.service
currentPortmasterCoreServiceFile []byte // currentPortmasterCoreServiceFile []byte
checkedSystemIntegration = abool.New() // checkedSystemIntegration = abool.New()
// ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. // // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade.
ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") // ErrRequiresManualUpgrade = errors.New("requires a manual upgrade")
) // )
func upgradeSystemIntegration() { // func upgradeSystemIntegration() {
// Check if we already checked the system integration. // // Check if we already checked the system integration.
if !checkedSystemIntegration.SetToIf(false, true) { // if !checkedSystemIntegration.SetToIf(false, true) {
return // return
} // }
// Upgrade portmaster core systemd service. // // Upgrade portmaster core systemd service.
err := upgradeSystemIntegrationFile( // err := upgradeSystemIntegrationFile(
"portmaster core systemd service", // "portmaster core systemd service",
filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), // filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath),
0o0600, // 0o0600,
currentPortmasterCoreServiceFile, // currentPortmasterCoreServiceFile,
[]string{ // []string{
"bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 // "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25
"cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 // "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24
"d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 // "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07
}, // },
) // )
if err != nil { // if err != nil {
log.Warningf("updates: %s", err) // log.Warningf("updates: %s", err)
return // return
} // }
// Upgrade portmaster notifier systemd user service. // // Upgrade portmaster notifier systemd user service.
// Permissions only! // // Permissions only!
err = upgradeSystemIntegrationFile( // err = upgradeSystemIntegrationFile(
"portmaster notifier systemd user service", // "portmaster notifier systemd user service",
filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), // filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath),
0o0644, // 0o0644,
nil, // Do not update contents. // nil, // Do not update contents.
nil, // Do not update contents. // nil, // Do not update contents.
) // )
if err != nil { // if err != nil {
log.Warningf("updates: %s", err) // log.Warningf("updates: %s", err)
return // return
} // }
} // }
// upgradeSystemIntegrationFile upgrades the file contents and permissions. // // upgradeSystemIntegrationFile upgrades the file contents and permissions.
// System integration files are not necessarily present and may also be // // System integration files are not necessarily present and may also be
// edited by third parties, such as the OS itself or other installers. // // edited by third parties, such as the OS itself or other installers.
// The supplied hashes must be sha256 hex-encoded. // // The supplied hashes must be sha256 hex-encoded.
func upgradeSystemIntegrationFile( // func upgradeSystemIntegrationFile(
name string, // name string,
filePath string, // filePath string,
fileMode fs.FileMode, // fileMode fs.FileMode,
fileData []byte, // fileData []byte,
permittedUpgradeHashes []string, // permittedUpgradeHashes []string,
) error { // ) error {
// Upgrade file contents. // // Upgrade file contents.
if len(fileData) > 0 { // if len(fileData) > 0 {
if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { // if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil {
return err // return err
} // }
} // }
// Upgrade file permissions. // // Upgrade file permissions.
if fileMode != 0 { // if fileMode != 0 {
if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { // if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil {
return err // return err
} // }
} // }
return nil // return nil
} // }
// upgradeSystemIntegrationFileContents upgrades the file contents. // // upgradeSystemIntegrationFileContents upgrades the file contents.
// System integration files are not necessarily present and may also be // // System integration files are not necessarily present and may also be
// edited by third parties, such as the OS itself or other installers. // // edited by third parties, such as the OS itself or other installers.
// The supplied hashes must be sha256 hex-encoded. // // The supplied hashes must be sha256 hex-encoded.
func upgradeSystemIntegrationFileContents( // func upgradeSystemIntegrationFileContents(
name string, // name string,
filePath string, // filePath string,
fileData []byte, // fileData []byte,
permittedUpgradeHashes []string, // permittedUpgradeHashes []string,
) error { // ) error {
// Read existing file. // // Read existing file.
existingFileData, err := os.ReadFile(filePath) // existingFileData, err := os.ReadFile(filePath)
if err != nil { // if err != nil {
if errors.Is(err, os.ErrNotExist) { // if errors.Is(err, os.ErrNotExist) {
return nil // return nil
} // }
return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) // return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err)
} // }
// Check if file is already the current version. // // Check if file is already the current version.
existingSum := sha256.Sum256(existingFileData) // existingSum := sha256.Sum256(existingFileData)
existingHexSum := hex.EncodeToString(existingSum[:]) // existingHexSum := hex.EncodeToString(existingSum[:])
currentSum := sha256.Sum256(fileData) // currentSum := sha256.Sum256(fileData)
currentHexSum := hex.EncodeToString(currentSum[:]) // currentHexSum := hex.EncodeToString(currentSum[:])
if existingHexSum == currentHexSum { // if existingHexSum == currentHexSum {
log.Debugf("updates: %s at %s is up to date", name, filePath) // log.Debugf("updates: %s at %s is up to date", name, filePath)
return nil // return nil
} // }
// Check if we are allowed to upgrade from the existing file. // // Check if we are allowed to upgrade from the existing file.
if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { // if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) {
return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) // return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade)
} // }
// Start with upgrade! // // Start with upgrade!
// Make backup of existing file. // // Make backup of existing file.
err = CopyFile(filePath, filePath+backupExtension) // err = CopyFile(filePath, filePath+backupExtension)
if err != nil { // if err != nil {
return fmt.Errorf( // return fmt.Errorf(
"failed to create backup of %s from %s to %s: %w", // "failed to create backup of %s from %s to %s: %w",
name, // name,
filePath, // filePath,
filePath+backupExtension, // filePath+backupExtension,
err, // err,
) // )
} // }
// Open destination file for writing. // // Open destination file for writing.
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) // // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath)
if err != nil { // // if err != nil {
return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) // // return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err)
} // // }
defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway // // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
// Write file. // // // Write file.
_, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) // // _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData))
if err != nil { // // if err != nil {
return err // // return err
} // // }
// Finalize file. // // // Finalize file.
err = atomicDstFile.CloseAtomicallyReplace() // // err = atomicDstFile.CloseAtomicallyReplace()
if err != nil { // // if err != nil {
return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) // // return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err)
} // // }
log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) // log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum)
return nil // return nil
} // }
// upgradeSystemIntegrationFilePermissions upgrades the file permissions. // // upgradeSystemIntegrationFilePermissions upgrades the file permissions.
// System integration files are not necessarily present and may also be // // System integration files are not necessarily present and may also be
// edited by third parties, such as the OS itself or other installers. // // edited by third parties, such as the OS itself or other installers.
func upgradeSystemIntegrationFilePermissions( // func upgradeSystemIntegrationFilePermissions(
name string, // name string,
filePath string, // filePath string,
fileMode fs.FileMode, // fileMode fs.FileMode,
) error { // ) error {
// Get current file permissions. // // Get current file permissions.
stat, err := os.Stat(filePath) // stat, err := os.Stat(filePath)
if err != nil { // if err != nil {
if errors.Is(err, os.ErrNotExist) { // if errors.Is(err, os.ErrNotExist) {
return nil // return nil
} // }
return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) // return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err)
} // }
// If permissions are as expected, do nothing. // // If permissions are as expected, do nothing.
if stat.Mode().Perm() == fileMode { // if stat.Mode().Perm() == fileMode {
return nil // return nil
} // }
// Otherwise, set correct permissions. // // Otherwise, set correct permissions.
err = os.Chmod(filePath, fileMode) // err = os.Chmod(filePath, fileMode)
if err != nil { // if err != nil {
return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) // return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err)
} // }
log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) // log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode)
return nil // return nil
} // }

View File

@@ -1 +0,0 @@
package updates

View File

@@ -1,4 +1,4 @@
package updates package registry
import ( import (
"archive/zip" "archive/zip"
@@ -17,6 +17,12 @@ import (
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
) )
const (
defaultFileMode = os.FileMode(0o0644)
executableFileMode = os.FileMode(0o0744)
defaultDirMode = os.FileMode(0o0755)
)
const MaxUnpackSize = 1 << 30 // 2^30 == 1GB const MaxUnpackSize = 1 << 30 // 2^30 == 1GB
type Artifact struct { type Artifact struct {
@@ -29,40 +35,40 @@ type Artifact struct {
} }
type Bundle struct { type Bundle struct {
dir string
Name string `json:"Bundle"` Name string `json:"Bundle"`
Version string `json:"Version"` Version string `json:"Version"`
Published time.Time `json:"Published"` Published time.Time `json:"Published"`
Artifacts []Artifact `json:"Artifacts"` Artifacts []Artifact `json:"Artifacts"`
} }
func (bundle Bundle) downloadAndVerify(dataDir string) { func (bundle Bundle) downloadAndVerify() {
client := http.Client{} client := http.Client{}
for _, artifact := range bundle.Artifacts { for _, artifact := range bundle.Artifacts {
filePath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) filePath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename)
// TODO(vladimir): is this needed? // TODO(vladimir): is this needed?
_ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) _ = os.MkdirAll(filepath.Dir(filePath), defaultDirMode)
// Check file is already downloaded and valid. // Check file is already downloaded and valid.
exists, err := checkIfFileIsValid(filePath, artifact) exists, _ := checkIfFileIsValid(filePath, artifact)
if exists { if exists {
log.Debugf("file already download: %s", filePath) log.Debugf("updates: file already downloaded: %s", filePath)
continue continue
} else if err != nil {
log.Errorf("error while checking old download: %s", err)
} }
// Download artifact // Download artifact
err = processArtifact(&client, artifact, filePath) err := processArtifact(&client, artifact, filePath)
if err != nil { if err != nil {
log.Errorf("updates: %s", err) log.Errorf("updates: %s", err)
} }
} }
} }
func (bundle Bundle) Verify(dataDir string) error { // Verify checks if the files are present int the dataDir and have the correct hash.
func (bundle Bundle) Verify() error {
for _, artifact := range bundle.Artifacts { for _, artifact := range bundle.Artifacts {
artifactPath := fmt.Sprintf("%s/%s", dataDir, artifact.Filename) artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename)
file, err := os.Open(artifactPath) file, err := os.Open(artifactPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to open file %s: %w", artifactPath, err) return fmt.Errorf("failed to open file %s: %w", artifactPath, err)
@@ -86,8 +92,7 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) {
// Check if file already exists // Check if file already exists
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
//nolint:nilerr return false, err
return false, nil
} }
defer func() { _ = file.Close() }() defer func() { _ = file.Close() }()
@@ -131,7 +136,7 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er
// Verify // Verify
hash := sha256.Sum256(content) hash := sha256.Sum256(content)
if !bytes.Equal(providedHash, hash[:]) { if !bytes.Equal(providedHash, hash[:]) {
// FIXME(vladimir): just for testing. Make it an error before commit. // FIXME(vladimir): just for testing. Make it an error.
err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename) err = fmt.Errorf("failed to verify artifact: %s", artifact.Filename)
log.Debugf("updates: %s", err) log.Debugf("updates: %s", err)
} }
@@ -142,6 +147,11 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er
if err != nil { if err != nil {
return fmt.Errorf("failed to create file: %w", err) return fmt.Errorf("failed to create file: %w", err)
} }
if artifact.Platform == "" {
_ = file.Chmod(defaultFileMode)
} else {
_ = file.Chmod(executableFileMode)
}
_, err = file.Write(content) _, err = file.Write(content)
if err != nil { if err != nil {
return fmt.Errorf("failed to write to file: %w", err) return fmt.Errorf("failed to write to file: %w", err)

View File

@@ -0,0 +1,56 @@
package registry
import (
"fmt"
"io"
"net/http"
"os"
"github.com/safing/portmaster/base/log"
)
type UpdateIndex struct {
Directory string
DownloadDirectory string
Ignore []string
IndexURLs []string
IndexFile string
AutoApply bool
}
func (ui *UpdateIndex) downloadIndexFile() (err error) {
_ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode)
for _, url := range ui.IndexURLs {
err = ui.downloadIndexFileFromURL(url)
if err != nil {
log.Warningf("updates: %s", err)
continue
}
// Downloading was successful.
err = nil
break
}
return
}
func (ui *UpdateIndex) downloadIndexFileFromURL(url string) error {
client := http.Client{}
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("failed a get request to %s: %w", url, err)
}
defer func() { _ = resp.Body.Close() }()
filePath := fmt.Sprintf("%s/%s", ui.DownloadDirectory, ui.IndexFile)
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, defaultFileMode)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,245 @@
package registry
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/safing/portmaster/base/log"
)
var ErrNotFound error = errors.New("file not found")
type File struct {
id string
path string
}
func (f *File) Identifier() string {
return f.id
}
func (f *File) Path() string {
return f.path
}
func (f *File) Version() string {
return ""
}
type Registry struct {
binaryUpdateIndex UpdateIndex
intelUpdateIndex UpdateIndex
binaryBundle *Bundle
intelBundle *Bundle
binaryUpdateBundle *Bundle
intelUpdateBundle *Bundle
files map[string]File
}
// New create new Registry.
func New(binIndex UpdateIndex, intelIndex UpdateIndex) Registry {
return Registry{
binaryUpdateIndex: binIndex,
intelUpdateIndex: intelIndex,
files: make(map[string]File),
}
}
// Initialize parses and initializes currently installed bundles.
func (reg *Registry) Initialize() error {
var err error
// Parse current installed binary bundle.
reg.binaryBundle, err = parseBundle(reg.binaryUpdateIndex.Directory, reg.binaryUpdateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed to parse binary bundle: %w", err)
}
// Parse current installed intel bundle.
reg.intelBundle, err = parseBundle(reg.intelUpdateIndex.Directory, reg.intelUpdateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed to parse intel bundle: %w", err)
}
// Add bundle artifacts to registry.
reg.processBundle(reg.binaryBundle)
reg.processBundle(reg.intelBundle)
return nil
}
func (reg *Registry) processBundle(bundle *Bundle) {
for _, artifact := range bundle.Artifacts {
artifactPath := fmt.Sprintf("%s/%s", bundle.dir, artifact.Filename)
reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath}
}
}
// GetFile returns the object of a artifact by id.
func (reg *Registry) GetFile(id string) (*File, error) {
file, ok := reg.files[id]
if ok {
return &file, nil
} else {
log.Errorf("updates: requested file id not found: %s", id)
return nil, ErrNotFound
}
}
// CheckForBinaryUpdates checks if there is a new binary bundle updates.
func (reg *Registry) CheckForBinaryUpdates() (bool, error) {
err := reg.binaryUpdateIndex.downloadIndexFile()
if err != nil {
return false, err
}
reg.binaryUpdateBundle, err = parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile)
if err != nil {
return false, fmt.Errorf("failed to parse bundle file: %w", err)
}
// TODO(vladimir): Make a better check.
if reg.binaryBundle.Version != reg.binaryUpdateBundle.Version {
return true, nil
}
return false, nil
}
// DownloadBinaryUpdates downloads available binary updates.
func (reg *Registry) DownloadBinaryUpdates() error {
if reg.binaryUpdateBundle == nil {
// CheckForBinaryUpdates needs to be called before this.
return fmt.Errorf("no valid update bundle found")
}
_ = deleteUnfinishedDownloads(reg.binaryBundle.dir)
reg.binaryUpdateBundle.downloadAndVerify()
return nil
}
// CheckForIntelUpdates checks if there is a new intel data bundle updates.
func (reg *Registry) CheckForIntelUpdates() (bool, error) {
err := reg.intelUpdateIndex.downloadIndexFile()
if err != nil {
return false, err
}
reg.intelUpdateBundle, err = parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile)
if err != nil {
return false, fmt.Errorf("failed to parse bundle file: %w", err)
}
// TODO(vladimir): Make a better check.
if reg.intelBundle.Version != reg.intelUpdateBundle.Version {
return true, nil
}
return false, nil
}
// DownloadIntelUpdates downloads available intel data updates.
func (reg *Registry) DownloadIntelUpdates() error {
if reg.intelUpdateBundle == nil {
// CheckForIntelUpdates needs to be called before this.
return fmt.Errorf("no valid update bundle found")
}
_ = deleteUnfinishedDownloads(reg.intelBundle.dir)
reg.intelUpdateBundle.downloadAndVerify()
return nil
}
// ApplyBinaryUpdates removes the current binary folder and replaces it with the downloaded one.
func (reg *Registry) ApplyBinaryUpdates() error {
bundle, err := parseBundle(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed to parse index file: %w", err)
}
err = bundle.Verify()
if err != nil {
return fmt.Errorf("binary bundle is not valid: %w", err)
}
err = os.RemoveAll(reg.binaryUpdateIndex.Directory)
if err != nil {
return fmt.Errorf("failed to remove dir: %w", err)
}
err = os.Rename(reg.binaryUpdateIndex.DownloadDirectory, reg.binaryUpdateIndex.Directory)
if err != nil {
return fmt.Errorf("failed to move dir: %w", err)
}
return nil
}
// ApplyIntelUpdates removes the current intel folder and replaces it with the downloaded one.
func (reg *Registry) ApplyIntelUpdates() error {
bundle, err := parseBundle(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed to parse index file: %w", err)
}
err = bundle.Verify()
if err != nil {
return fmt.Errorf("binary bundle is not valid: %w", err)
}
err = os.RemoveAll(reg.intelUpdateIndex.Directory)
if err != nil {
return fmt.Errorf("failed to remove dir: %w", err)
}
err = os.Rename(reg.intelUpdateIndex.DownloadDirectory, reg.intelUpdateIndex.Directory)
if err != nil {
return fmt.Errorf("failed to move dir: %w", err)
}
return nil
}
func parseBundle(dir string, indexFile string) (*Bundle, error) {
filepath := fmt.Sprintf("%s/%s", dir, indexFile)
// Check if the file exists.
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open index file: %w", err)
}
defer func() { _ = file.Close() }()
// Read
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
// Parse
var bundle Bundle
err = json.Unmarshal(content, &bundle)
if err != nil {
return nil, err
}
bundle.dir = dir
return &bundle, nil
}
func deleteUnfinishedDownloads(rootDir string) error {
return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if the current file has the specified extension
if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") {
log.Warningf("updates deleting unfinished: %s\n", path)
err := os.Remove(path)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", path, err)
}
}
return nil
})
}

View File

@@ -54,7 +54,7 @@ func DelayedRestart(delay time.Duration) {
// Schedule the restart task. // Schedule the restart task.
log.Warningf("updates: restart triggered, will execute in %s", delay) log.Warningf("updates: restart triggered, will execute in %s", delay)
restartAt := time.Now().Add(delay) restartAt := time.Now().Add(delay)
module.restartWorkerMgr.Delay(delay) // module.restartWorkerMgr.Delay(delay)
// Set restartTime. // Set restartTime.
restartTimeLock.Lock() restartTimeLock.Lock()
@@ -68,23 +68,23 @@ func AbortRestart() {
log.Warningf("updates: restart aborted") log.Warningf("updates: restart aborted")
// Cancel schedule. // Cancel schedule.
module.restartWorkerMgr.Delay(0) // module.restartWorkerMgr.Delay(0)
} }
} }
// TriggerRestartIfPending triggers an automatic restart, if one is pending. // TriggerRestartIfPending triggers an automatic restart, if one is pending.
// This can be used to prepone a scheduled restart if the conditions are preferable. // This can be used to prepone a scheduled restart if the conditions are preferable.
func TriggerRestartIfPending() { func TriggerRestartIfPending() {
if restartPending.IsSet() { // if restartPending.IsSet() {
module.restartWorkerMgr.Go() // module.restartWorkerMgr.Go()
} // }
} }
// RestartNow immediately executes a restart. // RestartNow immediately executes a restart.
// This only works if the process is managed by portmaster-start. // This only works if the process is managed by portmaster-start.
func RestartNow() { func RestartNow() {
restartPending.Set() restartPending.Set()
module.restartWorkerMgr.Go() // module.restartWorkerMgr.Go()
} }
func automaticRestart(w *mgr.WorkerCtx) error { func automaticRestart(w *mgr.WorkerCtx) error {
@@ -108,11 +108,11 @@ func automaticRestart(w *mgr.WorkerCtx) error {
} }
// Set restart exit code. // Set restart exit code.
if !rebooting { // if !rebooting {
module.instance.Restart() // module.instance.Restart()
} else { // } else {
module.instance.Shutdown() // module.instance.Shutdown()
} // }
} }
return nil return nil

View File

@@ -1,49 +1,49 @@
package updates package updates
import ( // import (
"github.com/safing/portmaster/base/database/record" // "github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/runtime" // "github.com/safing/portmaster/base/runtime"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
) // )
var pushRegistryStatusUpdate runtime.PushFunc // var pushRegistryStatusUpdate runtime.PushFunc
// RegistryStateExport is a wrapper to export the registry state. // // RegistryStateExport is a wrapper to export the registry state.
type RegistryStateExport struct { // type RegistryStateExport struct {
record.Base // record.Base
*updater.RegistryState // *updater.RegistryState
} // }
func exportRegistryState(s *updater.RegistryState) *RegistryStateExport { // func exportRegistryState(s *updater.RegistryState) *RegistryStateExport {
if s == nil { // // if s == nil {
state := registry.GetState() // // state := registry.GetState()
s = &state // // s = &state
} // // }
export := &RegistryStateExport{ // export := &RegistryStateExport{
RegistryState: s, // RegistryState: s,
} // }
export.CreateMeta() // export.CreateMeta()
export.SetKey("runtime:core/updates/state") // export.SetKey("runtime:core/updates/state")
return export // return export
} // }
func pushRegistryState(s *updater.RegistryState) { // func pushRegistryState(s *updater.RegistryState) {
export := exportRegistryState(s) // export := exportRegistryState(s)
pushRegistryStatusUpdate(export) // pushRegistryStatusUpdate(export)
} // }
func registerRegistryStateProvider() (err error) { // func registerRegistryStateProvider() (err error) {
registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { // registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) {
return []record.Record{exportRegistryState(nil)}, nil // return []record.Record{exportRegistryState(nil)}, nil
}) // })
pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider) // pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider)
if err != nil { // if err != nil {
return err // return err
} // }
return nil // return nil
} // }

View File

@@ -1,406 +1,403 @@
package updates package updates
import ( // import (
"context" // "context"
"fmt" // "fmt"
"io" // "os"
"os" // "os/exec"
"os/exec" // "path/filepath"
"path/filepath" // "regexp"
"regexp" // "strings"
"strings" // "time"
"time"
processInfo "github.com/shirou/gopsutil/process" // processInfo "github.com/shirou/gopsutil/process"
"github.com/tevino/abool" // "github.com/tevino/abool"
"github.com/safing/portmaster/base/dataroot" // "github.com/safing/portmaster/base/dataroot"
"github.com/safing/portmaster/base/info" // "github.com/safing/portmaster/base/info"
"github.com/safing/portmaster/base/log" // "github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications" // "github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/base/rng" // "github.com/safing/portmaster/base/rng"
"github.com/safing/portmaster/base/updater" // "github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/base/utils/renameio" // "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/mgr" // )
"github.com/safing/portmaster/service/updates/helper"
)
const ( // const (
upgradedSuffix = "-upgraded" // upgradedSuffix = "-upgraded"
exeExt = ".exe" // exeExt = ".exe"
) // )
var ( // var (
upgraderActive = abool.NewBool(false) // upgraderActive = abool.NewBool(false)
pmCtrlUpdate *updater.File // pmCtrlUpdate *updater.File
pmCoreUpdate *updater.File // pmCoreUpdate *updater.File
spnHubUpdate *updater.File // spnHubUpdate *updater.File
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) // rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
) // )
func initUpgrader() error { // func initUpgrader() error {
module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) // // module.EventResourcesUpdated.AddCallback("run upgrades", upgrader)
return nil // return nil
} // }
func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { // func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
// Lock runs, but discard additional runs. // // Lock runs, but discard additional runs.
if !upgraderActive.SetToIf(false, true) { // if !upgraderActive.SetToIf(false, true) {
return false, nil // return false, nil
} // }
defer upgraderActive.SetTo(false) // defer upgraderActive.SetTo(false)
// Upgrade portmaster-start. // // Upgrade portmaster-start.
err = upgradePortmasterStart() // err = upgradePortmasterStart()
if err != nil { // if err != nil {
log.Warningf("updates: failed to upgrade portmaster-start: %s", err) // log.Warningf("updates: failed to upgrade portmaster-start: %s", err)
} // }
// Upgrade based on binary. // // Upgrade based on binary.
binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] // binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
switch binBaseName { // switch binBaseName {
case "portmaster-core": // case "portmaster-core":
// Notify about upgrade. // // Notify about upgrade.
if err := upgradeCoreNotify(); err != nil { // if err := upgradeCoreNotify(); err != nil {
log.Warningf("updates: failed to notify about core upgrade: %s", err) // log.Warningf("updates: failed to notify about core upgrade: %s", err)
} // }
// Fix chrome sandbox permissions. // // Fix chrome sandbox permissions.
if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { // // if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
log.Warningf("updates: failed to handle electron upgrade: %s", err) // // log.Warningf("updates: failed to handle electron upgrade: %s", err)
} // // }
// Upgrade system integration. // // Upgrade system integration.
upgradeSystemIntegration() // upgradeSystemIntegration()
case "spn-hub": // case "spn-hub":
// Trigger upgrade procedure. // // Trigger upgrade procedure.
if err := upgradeHub(); err != nil { // if err := upgradeHub(); err != nil {
log.Warningf("updates: failed to initiate hub upgrade: %s", err) // log.Warningf("updates: failed to initiate hub upgrade: %s", err)
} // }
} // }
return false, nil // return false, nil
} // }
func upgradeCoreNotify() error { // func upgradeCoreNotify() error {
if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { // if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() {
return nil // return nil
} // }
// make identifier // // make identifier
identifier := "core/portmaster-core" // identifier, use forward slash! // identifier := "core/portmaster-core" // identifier, use forward slash!
if onWindows { // if onWindows {
identifier += exeExt // identifier += exeExt
} // }
// get newest portmaster-core // // get newest portmaster-core
newFile, err := GetPlatformFile(identifier) // // newFile, err := GetPlatformFile(identifier)
if err != nil { // // if err != nil {
return err // // return err
} // // }
pmCoreUpdate = newFile // // pmCoreUpdate = newFile
// check for new version // // check for new version
if info.VersionNumber() != pmCoreUpdate.Version() { // if info.VersionNumber() != pmCoreUpdate.Version() {
n := notifications.Notify(&notifications.Notification{ // n := notifications.Notify(&notifications.Notification{
EventID: "updates:core-update-available", // EventID: "updates:core-update-available",
Type: notifications.Info, // Type: notifications.Info,
Title: fmt.Sprintf( // Title: fmt.Sprintf(
"Portmaster Update v%s Is Ready!", // "Portmaster Update v%s Is Ready!",
pmCoreUpdate.Version(), // pmCoreUpdate.Version(),
), // ),
Category: "Core", // Category: "Core",
Message: fmt.Sprintf( // Message: fmt.Sprintf(
`A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, // `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`,
pmCoreUpdate.Version(), // pmCoreUpdate.Version(),
), // ),
ShowOnSystem: true, // ShowOnSystem: true,
AvailableActions: []*notifications.Action{ // AvailableActions: []*notifications.Action{
// TODO: Use special UI action in order to reload UI on restart. // // TODO: Use special UI action in order to reload UI on restart.
{ // {
ID: "restart", // ID: "restart",
Text: "Restart", // Text: "Restart",
}, // },
{ // {
ID: "later", // ID: "later",
Text: "Not now", // Text: "Not now",
}, // },
}, // },
}) // })
n.SetActionFunction(upgradeCoreNotifyActionHandler) // n.SetActionFunction(upgradeCoreNotifyActionHandler)
log.Debugf("updates: new portmaster version available, sending notification to user") // log.Debugf("updates: new portmaster version available, sending notification to user")
} // }
return nil // return nil
} // }
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { // func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
switch n.SelectedActionID { // switch n.SelectedActionID {
case "restart": // case "restart":
log.Infof("updates: user triggered restart via core update notification") // log.Infof("updates: user triggered restart via core update notification")
RestartNow() // RestartNow()
case "later": // case "later":
n.Delete() // n.Delete()
} // }
return nil // return nil
} // }
func upgradeHub() error { // func upgradeHub() error {
if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { // if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() {
return nil // return nil
} // }
// Make identifier for getting file from updater. // // Make identifier for getting file from updater.
identifier := "hub/spn-hub" // identifier, use forward slash! // identifier := "hub/spn-hub" // identifier, use forward slash!
if onWindows { // if onWindows {
identifier += exeExt // identifier += exeExt
} // }
// Get newest spn-hub file. // // Get newest spn-hub file.
newFile, err := GetPlatformFile(identifier) // // newFile, err := GetPlatformFile(identifier)
if err != nil { // // if err != nil {
return err // // return err
} // // }
spnHubUpdate = newFile // // spnHubUpdate = newFile
// Check if the new version is different. // // Check if the new version is different.
if info.GetInfo().Version != spnHubUpdate.Version() { // if info.GetInfo().Version != spnHubUpdate.Version() {
// Get random delay with up to three hours. // // Get random delay with up to three hours.
delayMinutes, err := rng.Number(3 * 60) // delayMinutes, err := rng.Number(3 * 60)
if err != nil { // if err != nil {
return err // return err
} // }
// Delay restart for at least one hour for preparations. // // Delay restart for at least one hour for preparations.
DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) // DelayedRestart(time.Duration(delayMinutes+60) * time.Minute)
// Increase update checks in order to detect aborts better. // // Increase update checks in order to detect aborts better.
// if !disableTaskSchedule { // // if !disableTaskSchedule {
module.updateWorkerMgr.Repeat(10 * time.Minute) // // module.updateBinaryWorkerMgr.Repeat(10 * time.Minute)
// } // // }
} else { // } else {
AbortRestart() // AbortRestart()
// Set update task schedule back to normal. // // Set update task schedule back to normal.
// if !disableTaskSchedule { // // if !disableTaskSchedule {
module.updateWorkerMgr.Repeat(updateTaskRepeatDuration) // // module.updateBinaryWorkerMgr.Repeat(updateTaskRepeatDuration)
// } // // }
} // }
return nil // return nil
} // }
func upgradePortmasterStart() error { // func upgradePortmasterStart() error {
filename := "portmaster-start" // filename := "portmaster-start"
if onWindows { // if onWindows {
filename += exeExt // filename += exeExt
} // }
// check if we can upgrade // // check if we can upgrade
if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { // if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() {
// get newest portmaster-start // // get newest portmaster-start
newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! // // newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash!
if err != nil { // // if err != nil {
return err // // return err
} // // }
pmCtrlUpdate = newFile // // pmCtrlUpdate = newFile
} else { // } else {
return nil // return nil
} // }
// update portmaster-start in data root // // update portmaster-start in data root
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) // rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) // err := upgradeBinary(rootPmStartPath, pmCtrlUpdate)
if err != nil { // if err != nil {
return err // return err
} // }
return nil // return nil
} // }
func warnOnIncorrectParentPath() { // func warnOnIncorrectParentPath() {
expectedFileName := "portmaster-start" // expectedFileName := "portmaster-start"
if onWindows { // if onWindows {
expectedFileName += exeExt // expectedFileName += exeExt
} // }
// upgrade parent process, if it's portmaster-start // // upgrade parent process, if it's portmaster-start
parent, err := processInfo.NewProcess(int32(os.Getppid())) // parent, err := processInfo.NewProcess(int32(os.Getppid()))
if err != nil { // if err != nil {
log.Tracef("could not get parent process: %s", err) // log.Tracef("could not get parent process: %s", err)
return // return
} // }
parentName, err := parent.Name() // parentName, err := parent.Name()
if err != nil { // if err != nil {
log.Tracef("could not get parent process name: %s", err) // log.Tracef("could not get parent process name: %s", err)
return // return
} // }
if parentName != expectedFileName { // if parentName != expectedFileName {
// Only warn about this if not in dev mode. // // Only warn about this if not in dev mode.
if !devMode() { // if !devMode() {
log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) // log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName)
} // }
// TODO(ppacher): once we released a new installer and folks had time // // TODO(ppacher): once we released a new installer and folks had time
// to update we should send a module warning/hint to the // // to update we should send a module warning/hint to the
// UI notifying the user that he's still using portmaster-control. // // UI notifying the user that he's still using portmaster-control.
return // return
} // }
parentPath, err := parent.Exe() // // parentPath, err := parent.Exe()
if err != nil { // // if err != nil {
log.Tracef("could not get parent process path: %s", err) // // log.Tracef("could not get parent process path: %s", err)
return // // return
} // // }
absPath, err := filepath.Abs(parentPath) // // absPath, err := filepath.Abs(parentPath)
if err != nil { // // if err != nil {
log.Tracef("could not get absolut parent process path: %s", err) // // log.Tracef("could not get absolut parent process path: %s", err)
return // // return
} // // }
root := filepath.Dir(registry.StorageDir().Path) // // root := filepath.Dir(registry.StorageDir().Path)
if !strings.HasPrefix(absPath, root) { // // if !strings.HasPrefix(absPath, root) {
log.Warningf("detected unexpected path %s for portmaster-start", absPath) // // log.Warningf("detected unexpected path %s for portmaster-start", absPath)
notifications.NotifyWarn( // // notifications.NotifyWarn(
"updates:unsupported-parent", // // "updates:unsupported-parent",
"Unsupported Launcher", // // "Unsupported Launcher",
fmt.Sprintf( // // fmt.Sprintf(
"The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", // // "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
expectedFileName, // // expectedFileName,
absPath, // // absPath,
filepath.Join(root, expectedFileName), // // filepath.Join(root, expectedFileName),
), // // ),
) // // )
} // // }
} // }
func upgradeBinary(fileToUpgrade string, file *updater.File) error { // func upgradeBinary(fileToUpgrade string, file *updater.File) error {
fileExists := false // fileExists := false
_, err := os.Stat(fileToUpgrade) // _, err := os.Stat(fileToUpgrade)
if err == nil { // if err == nil {
// file exists and is accessible // // file exists and is accessible
fileExists = true // fileExists = true
} // }
if fileExists { // if fileExists {
// get current version // // get current version
var currentVersion string // var currentVersion string
cmd := exec.Command(fileToUpgrade, "version", "--short") // cmd := exec.Command(fileToUpgrade, "version", "--short")
out, err := cmd.Output() // out, err := cmd.Output()
if err == nil { // if err == nil {
// abort if version matches // // abort if version matches
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") // currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
if currentVersion == file.Version() { // if currentVersion == file.Version() {
log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) // log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version())
// already up to date! // // already up to date!
return nil // return nil
} // }
} else { // } else {
log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) // log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err)
currentVersion = "0.0.0" // currentVersion = "0.0.0"
} // }
// test currentVersion for sanity // // test currentVersion for sanity
if !rawVersionRegex.MatchString(currentVersion) { // if !rawVersionRegex.MatchString(currentVersion) {
log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) // log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
} // }
// try removing old version // // try removing old version
err = os.Remove(fileToUpgrade) // err = os.Remove(fileToUpgrade)
if err != nil { // if err != nil {
// ensure tmp dir is here // // ensure tmp dir is here
err = registry.TmpDir().Ensure() // // err = registry.TmpDir().Ensure()
if err != nil { // // if err != nil {
return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) // // return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err)
} // // }
// maybe we're on windows and it's in use, try moving // // maybe we're on windows and it's in use, try moving
err = os.Rename(fileToUpgrade, filepath.Join( // // err = os.Rename(fileToUpgrade, filepath.Join(
registry.TmpDir().Path, // // registry.TmpDir().Path,
fmt.Sprintf( // // fmt.Sprintf(
"%s-%d%s", // // "%s-%d%s",
filepath.Base(fileToUpgrade), // // filepath.Base(fileToUpgrade),
time.Now().UTC().Unix(), // // time.Now().UTC().Unix(),
upgradedSuffix, // // upgradedSuffix,
), // // ),
)) // // ))
if err != nil { // // if err != nil {
return fmt.Errorf("unable to move file that needs upgrade: %w", err) // // return fmt.Errorf("unable to move file that needs upgrade: %w", err)
} // // }
} // }
} // }
// copy upgrade // // copy upgrade
err = CopyFile(file.Path(), fileToUpgrade) // err = CopyFile(file.Path(), fileToUpgrade)
if err != nil { // if err != nil {
// try again // // try again
time.Sleep(1 * time.Second) // time.Sleep(1 * time.Second)
err = CopyFile(file.Path(), fileToUpgrade) // err = CopyFile(file.Path(), fileToUpgrade)
if err != nil { // if err != nil {
return err // return err
} // }
} // }
// check permissions // // check permissions
if !onWindows { // if !onWindows {
info, err := os.Stat(fileToUpgrade) // info, err := os.Stat(fileToUpgrade)
if err != nil { // if err != nil {
return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) // return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)
} // }
if info.Mode() != 0o0755 { // if info.Mode() != 0o0755 {
err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. // err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions.
if err != nil { // if err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) // return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
} // }
} // }
} // }
log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) // log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version())
return nil // return nil
} // }
// CopyFile atomically copies a file using the update registry's tmp dir. // // CopyFile atomically copies a file using the update registry's tmp dir.
func CopyFile(srcPath, dstPath string) error { // func CopyFile(srcPath, dstPath string) error {
// check tmp dir // // check tmp dir
err := registry.TmpDir().Ensure() // // err := registry.TmpDir().Ensure()
if err != nil { // // if err != nil {
return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) // // return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
} // // }
// open file for writing // // open file for writing
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) // // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath)
if err != nil { // // if err != nil {
return fmt.Errorf("could not create temp file for atomic copy: %w", err) // // return fmt.Errorf("could not create temp file for atomic copy: %w", err)
} // // }
defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway // // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
// open source // // // open source
srcFile, err := os.Open(srcPath) // // srcFile, err := os.Open(srcPath)
if err != nil { // // if err != nil {
return err // // return err
} // // }
defer func() { // // defer func() {
_ = srcFile.Close() // // _ = srcFile.Close()
}() // // }()
// copy data // // // copy data
_, err = io.Copy(atomicDstFile, srcFile) // // _, err = io.Copy(atomicDstFile, srcFile)
if err != nil { // // if err != nil {
return err // // return err
} // // }
// finalize file // // // finalize file
err = atomicDstFile.CloseAtomicallyReplace() // // err = atomicDstFile.CloseAtomicallyReplace()
if err != nil { // // if err != nil {
return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) // // return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err)
} // // }
return nil // return nil
} // }

View File

@@ -6,9 +6,8 @@ import (
"os" "os"
"sync" "sync"
"github.com/safing/portmaster/base/updater"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates" "github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/spn/conf" "github.com/safing/portmaster/spn/conf"
"github.com/safing/portmaster/spn/hub" "github.com/safing/portmaster/spn/hub"
"github.com/safing/portmaster/spn/navigator" "github.com/safing/portmaster/spn/navigator"
@@ -16,7 +15,7 @@ import (
) )
var ( var (
intelResource *updater.File intelResource *registry.File
intelResourcePath = "intel/spn/main-intel.yaml" intelResourcePath = "intel/spn/main-intel.yaml"
intelResourceMapName = "main" intelResourceMapName = "main"
intelResourceUpdateLock sync.Mutex intelResourceUpdateLock sync.Mutex
@@ -44,12 +43,13 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) {
} }
// Check if there is something to do. // Check if there is something to do.
if intelResource != nil && !intelResource.UpgradeAvailable() { // TODO(vladimir): is update check needed
if intelResource != nil { //&& !intelResource.UpgradeAvailable() {
return nil return nil
} }
// Get intel file and load it from disk. // Get intel file and load it from disk.
intelResource, err = updates.GetFile(intelResourcePath) intelResource, err = module.instance.Updates().GetFile(intelResourcePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to get SPN intel update: %w", err) return fmt.Errorf("failed to get SPN intel update: %w", err)
} }