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