wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
163
service/updates/api.go
Normal file
163
service/updates/api.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
apiPathCheckForUpdates = "updates/check"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Check for Updates",
|
||||
Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.",
|
||||
Parameters: []api.Parameter{{
|
||||
Method: http.MethodPost,
|
||||
Field: "download",
|
||||
Value: "",
|
||||
Description: "Force downloading and applying of all updates, regardless of auto-update settings.",
|
||||
}},
|
||||
Path: apiPathCheckForUpdates,
|
||||
Write: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
ActionFunc: func(r *api.Request) (msg string, err error) {
|
||||
// Check if we should also download regardless of settings.
|
||||
downloadAll := r.URL.Query().Has("download")
|
||||
|
||||
// Trigger update task.
|
||||
err = TriggerUpdate(true, downloadAll)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Report how we triggered.
|
||||
if downloadAll {
|
||||
return "downloading all updates...", nil
|
||||
}
|
||||
return "checking for updates...", nil
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Get Resource",
|
||||
Description: "Returns the requested resource from the udpate system",
|
||||
Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`,
|
||||
Read: api.PermitUser,
|
||||
ReadMethod: http.MethodGet,
|
||||
BelongsTo: module,
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get identifier from URL.
|
||||
var identifier string
|
||||
if ar := api.GetAPIRequest(r); ar != nil {
|
||||
identifier = ar.URLVars["identifier"]
|
||||
}
|
||||
if identifier == "" {
|
||||
http.Error(w, "no resource speicified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get resource.
|
||||
resource, err := registry.GetFile(identifier)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Open file for reading.
|
||||
file, err := os.Open(resource.Path())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close() //nolint:errcheck,gosec
|
||||
|
||||
// Assign file to reader
|
||||
var reader io.Reader = file
|
||||
|
||||
// Add version to header.
|
||||
w.Header().Set("Resource-Version", resource.Version())
|
||||
|
||||
// Set Content-Type.
|
||||
contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path()))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
// Check if the content type may be returned.
|
||||
accept := r.Header.Get("Accept")
|
||||
if accept != "" {
|
||||
mimeTypes := strings.Split(accept, ",")
|
||||
// First, clean mime types.
|
||||
for i, mimeType := range mimeTypes {
|
||||
mimeType = strings.TrimSpace(mimeType)
|
||||
mimeType, _, _ = strings.Cut(mimeType, ";")
|
||||
mimeTypes[i] = mimeType
|
||||
}
|
||||
// Second, check if we may return anything.
|
||||
var acceptsAny bool
|
||||
for _, mimeType := range mimeTypes {
|
||||
switch mimeType {
|
||||
case "*", "*/*":
|
||||
acceptsAny = true
|
||||
}
|
||||
}
|
||||
// Third, check if we can convert.
|
||||
if !acceptsAny {
|
||||
var converted bool
|
||||
sourceType, _, _ := strings.Cut(contentType, ";")
|
||||
findConvertiblePair:
|
||||
for _, mimeType := range mimeTypes {
|
||||
switch {
|
||||
case sourceType == "application/yaml" && mimeType == "application/json":
|
||||
yamlData, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonData, err := yaml.YAMLToJSON(yamlData)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
reader = bytes.NewReader(jsonData)
|
||||
converted = true
|
||||
break findConvertiblePair
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not convert to acceptable format, return an error.
|
||||
if !converted {
|
||||
http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write file.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method != http.MethodHead {
|
||||
_, err = io.Copy(w, reader)
|
||||
if err != nil {
|
||||
log.Errorf("updates: failed to serve resource file: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
44
service/updates/assets/portmaster.service
Normal file
44
service/updates/assets/portmaster.service
Normal file
@@ -0,0 +1,44 @@
|
||||
[Unit]
|
||||
Description=Portmaster by Safing
|
||||
Documentation=https://safing.io
|
||||
Documentation=https://docs.safing.io
|
||||
Before=nss-lookup.target network.target shutdown.target
|
||||
After=systemd-networkd.service
|
||||
Conflicts=shutdown.target
|
||||
Conflicts=firewalld.service
|
||||
Wants=nss-lookup.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PIDFile=/opt/safing/portmaster/core-lock.pid
|
||||
Environment=LOGLEVEL=info
|
||||
Environment=PORTMASTER_ARGS=
|
||||
EnvironmentFile=-/etc/default/portmaster
|
||||
ProtectSystem=true
|
||||
#ReadWritePaths=/var/lib/portmaster
|
||||
#ReadWritePaths=/run/xtables.lock
|
||||
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
# In future version portmaster will require access to user home
|
||||
# directories to verify application permissions.
|
||||
ProtectHome=read-only
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectControlGroups=yes
|
||||
PrivateDevices=yes
|
||||
AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
||||
CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
||||
# SystemCallArchitectures=native
|
||||
# SystemCallFilter=@system-service @module
|
||||
# SystemCallErrorNumber=EPERM
|
||||
ExecStart=/opt/safing/portmaster/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
|
||||
ExecStopPost=-/opt/safing/portmaster/portmaster-start recover-iptables
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
179
service/updates/config.go
Normal file
179
service/updates/config.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
const cfgDevModeKey = "core/devMode"
|
||||
|
||||
var (
|
||||
releaseChannel config.StringOption
|
||||
devMode config.BoolOption
|
||||
enableSoftwareUpdates config.BoolOption
|
||||
enableIntelUpdates config.BoolOption
|
||||
|
||||
initialReleaseChannel string
|
||||
previousReleaseChannel string
|
||||
|
||||
softwareUpdatesCurrentlyEnabled bool
|
||||
intelUpdatesCurrentlyEnabled bool
|
||||
previousDevMode bool
|
||||
forceCheck = abool.New()
|
||||
forceDownload = abool.New()
|
||||
)
|
||||
|
||||
func registerConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Release Channel",
|
||||
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.`,
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: helper.ReleaseChannelStable,
|
||||
PossibleValues: []config.PossibleValue{
|
||||
{
|
||||
Name: "Stable",
|
||||
Description: "Production releases.",
|
||||
Value: helper.ReleaseChannelStable,
|
||||
},
|
||||
{
|
||||
Name: "Beta",
|
||||
Description: "Production releases for testing new features that may break and cause interruption.",
|
||||
Value: helper.ReleaseChannelBeta,
|
||||
},
|
||||
{
|
||||
Name: "Support",
|
||||
Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.",
|
||||
Value: helper.ReleaseChannelSupport,
|
||||
},
|
||||
{
|
||||
Name: "Staging",
|
||||
Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.",
|
||||
Value: helper.ReleaseChannelStaging,
|
||||
},
|
||||
},
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -4,
|
||||
config.DisplayHintAnnotation: config.DisplayHintOneOf,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Automatic Software Updates",
|
||||
Key: enableSoftwareUpdatesKey,
|
||||
Description: "Automatically check for and download software updates. This does not include intelligence data updates.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: false,
|
||||
DefaultValue: true,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -12,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Automatic Intelligence Data Updates",
|
||||
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.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: false,
|
||||
DefaultValue: true,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -11,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable)
|
||||
initialReleaseChannel = releaseChannel()
|
||||
previousReleaseChannel = releaseChannel()
|
||||
|
||||
enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true)
|
||||
enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true)
|
||||
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
|
||||
intelUpdatesCurrentlyEnabled = enableIntelUpdates()
|
||||
|
||||
devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false)
|
||||
previousDevMode = devMode()
|
||||
}
|
||||
|
||||
func updateRegistryConfig(_ context.Context, _ interface{}) error {
|
||||
changed := false
|
||||
|
||||
if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled {
|
||||
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if enableIntelUpdates() != intelUpdatesCurrentlyEnabled {
|
||||
intelUpdatesCurrentlyEnabled = enableIntelUpdates()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if devMode() != previousDevMode {
|
||||
registry.SetDevMode(devMode())
|
||||
previousDevMode = devMode()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if releaseChannel() != previousReleaseChannel {
|
||||
previousReleaseChannel = releaseChannel()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
// Update indexes based on new settings.
|
||||
warning := helper.SetIndexes(
|
||||
registry,
|
||||
releaseChannel(),
|
||||
true,
|
||||
softwareUpdatesCurrentlyEnabled,
|
||||
intelUpdatesCurrentlyEnabled,
|
||||
)
|
||||
if warning != nil {
|
||||
log.Warningf("updates: %s", warning)
|
||||
}
|
||||
|
||||
// Select versions depending on new indexes and modes.
|
||||
registry.SelectVersions()
|
||||
module.TriggerEvent(VersionUpdateEvent, nil)
|
||||
|
||||
if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled {
|
||||
module.Resolve("")
|
||||
if err := TriggerUpdate(true, false); err != nil {
|
||||
log.Warningf("updates: failed to trigger update: %s", err)
|
||||
}
|
||||
log.Infof("updates: automatic updates are now enabled")
|
||||
} else {
|
||||
log.Warningf("updates: automatic updates are now completely disabled")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
242
service/updates/export.go
Normal file
242
service/updates/export.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
const (
|
||||
// versionsDBKey is the database key for update version information.
|
||||
versionsDBKey = "core:status/versions"
|
||||
|
||||
// versionsDBKey is the database key for simple update version information.
|
||||
simpleVersionsDBKey = "core:status/simple-versions"
|
||||
|
||||
// updateStatusDBKey is the database key for update status information.
|
||||
updateStatusDBKey = "core:status/updates"
|
||||
)
|
||||
|
||||
// Versions holds update versions and status information.
|
||||
type Versions struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
Core *info.Info
|
||||
Resources map[string]*updater.Resource
|
||||
Channel string
|
||||
Beta bool
|
||||
Staging bool
|
||||
}
|
||||
|
||||
// SimpleVersions holds simplified update versions and status information.
|
||||
type SimpleVersions struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
Build *info.Info
|
||||
Resources map[string]*SimplifiedResourceVersion
|
||||
Channel string
|
||||
}
|
||||
|
||||
// SimplifiedResourceVersion holds version information about one resource.
|
||||
type SimplifiedResourceVersion struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// UpdateStateExport is a wrapper to export the updates state.
|
||||
type UpdateStateExport struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
*updater.UpdateState
|
||||
}
|
||||
|
||||
// GetVersions returns the update versions and status information.
|
||||
// Resources must be locked when accessed.
|
||||
func GetVersions() *Versions {
|
||||
return &Versions{
|
||||
Core: info.GetInfo(),
|
||||
Resources: registry.Export(),
|
||||
Channel: initialReleaseChannel,
|
||||
Beta: initialReleaseChannel == helper.ReleaseChannelBeta,
|
||||
Staging: initialReleaseChannel == helper.ReleaseChannelStaging,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSimpleVersions returns the simplified update versions and status information.
|
||||
func GetSimpleVersions() *SimpleVersions {
|
||||
// Fill base info.
|
||||
v := &SimpleVersions{
|
||||
Build: info.GetInfo(),
|
||||
Resources: make(map[string]*SimplifiedResourceVersion),
|
||||
Channel: initialReleaseChannel,
|
||||
}
|
||||
|
||||
// Iterate through all versions and add version info.
|
||||
for id, resource := range registry.Export() {
|
||||
func() {
|
||||
resource.Lock()
|
||||
defer resource.Unlock()
|
||||
|
||||
// Get current in-used or selected version.
|
||||
var rv *updater.ResourceVersion
|
||||
switch {
|
||||
case resource.ActiveVersion != nil:
|
||||
rv = resource.ActiveVersion
|
||||
case resource.SelectedVersion != nil:
|
||||
rv = resource.SelectedVersion
|
||||
}
|
||||
|
||||
// Get information from resource.
|
||||
if rv != nil {
|
||||
v.Resources[id] = &SimplifiedResourceVersion{
|
||||
Version: rv.VersionNumber,
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// GetStateExport gets the update state from the registry and returns it in an
|
||||
// exportable struct.
|
||||
func GetStateExport() *UpdateStateExport {
|
||||
export := registry.GetState()
|
||||
return &UpdateStateExport{
|
||||
UpdateState: &export.Updates,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadStateExport loads the exported update state from the database.
|
||||
func LoadStateExport() (*UpdateStateExport, error) {
|
||||
r, err := db.Get(updateStatusDBKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
newRecord := &UpdateStateExport{}
|
||||
err = record.Unwrap(r, newRecord)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newRecord, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
newRecord, ok := r.(*UpdateStateExport)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r)
|
||||
}
|
||||
return newRecord, nil
|
||||
}
|
||||
|
||||
func initVersionExport() (err error) {
|
||||
if err := GetVersions().save(); err != nil {
|
||||
log.Warningf("updates: failed to export version information: %s", err)
|
||||
}
|
||||
if err := GetSimpleVersions().save(); err != nil {
|
||||
log.Warningf("updates: failed to export version information: %s", err)
|
||||
}
|
||||
|
||||
return module.RegisterEventHook(
|
||||
ModuleName,
|
||||
VersionUpdateEvent,
|
||||
"export version status",
|
||||
export,
|
||||
)
|
||||
}
|
||||
|
||||
func (v *Versions) save() error {
|
||||
if !v.KeyIsSet() {
|
||||
v.SetKey(versionsDBKey)
|
||||
}
|
||||
return db.Put(v)
|
||||
}
|
||||
|
||||
func (v *SimpleVersions) save() error {
|
||||
if !v.KeyIsSet() {
|
||||
v.SetKey(simpleVersionsDBKey)
|
||||
}
|
||||
return db.Put(v)
|
||||
}
|
||||
|
||||
func (s *UpdateStateExport) save() error {
|
||||
if !s.KeyIsSet() {
|
||||
s.SetKey(updateStatusDBKey)
|
||||
}
|
||||
return db.Put(s)
|
||||
}
|
||||
|
||||
// export is an event hook.
|
||||
func export(_ context.Context, _ interface{}) error {
|
||||
// Export versions.
|
||||
if err := GetVersions().save(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GetSimpleVersions().save(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Export udpate state.
|
||||
if err := GetStateExport().save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddToDebugInfo adds the update system status to the given debug.Info.
|
||||
func AddToDebugInfo(di *debug.Info) {
|
||||
// Get resources from registry.
|
||||
resources := registry.Export()
|
||||
platformPrefix := helper.PlatformIdentifier("")
|
||||
|
||||
// Collect data for debug info.
|
||||
var active, selected []string
|
||||
var activeCnt, totalCnt int
|
||||
for id, r := range resources {
|
||||
// Ignore resources for other platforms.
|
||||
if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
totalCnt++
|
||||
if r.ActiveVersion != nil {
|
||||
activeCnt++
|
||||
active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber))
|
||||
}
|
||||
if r.SelectedVersion != nil {
|
||||
selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber))
|
||||
}
|
||||
}
|
||||
sort.Strings(active)
|
||||
sort.Strings(selected)
|
||||
|
||||
// Compile to one list.
|
||||
lines := make([]string, 0, len(active)+len(selected)+3)
|
||||
lines = append(lines, "Active:")
|
||||
lines = append(lines, active...)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "Selected:")
|
||||
lines = append(lines, selected...)
|
||||
|
||||
// Add section.
|
||||
di.AddSection(
|
||||
fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt),
|
||||
debug.UseCodeSection|debug.AddContentLineBreaks,
|
||||
lines...,
|
||||
)
|
||||
}
|
||||
72
service/updates/get.go
Normal file
72
service/updates/get.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
// GetPlatformFile returns the latest platform specific file identified by the given identifier.
|
||||
func GetPlatformFile(identifier string) (*updater.File, error) {
|
||||
identifier = helper.PlatformIdentifier(identifier)
|
||||
|
||||
file, err := registry.GetFile(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.TriggerEvent(VersionUpdateEvent, nil)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// GetFile returns the latest generic file identified by the given identifier.
|
||||
func GetFile(identifier string) (*updater.File, error) {
|
||||
identifier = path.Join("all", identifier)
|
||||
|
||||
file, err := registry.GetFile(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.TriggerEvent(VersionUpdateEvent, nil)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// GetPlatformVersion returns the selected platform specific version of the
|
||||
// given identifier.
|
||||
// The returned resource version may not be modified.
|
||||
func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) {
|
||||
identifier = helper.PlatformIdentifier(identifier)
|
||||
|
||||
rv, err := registry.GetVersion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// GetVersion returns the selected generic version of the given identifier.
|
||||
// The returned resource version may not be modified.
|
||||
func GetVersion(identifier string) (*updater.ResourceVersion, error) {
|
||||
identifier = path.Join("all", identifier)
|
||||
|
||||
rv, err := registry.GetVersion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// GetVersionWithFullID returns the selected generic version of the given full identifier.
|
||||
// The returned resource version may not be modified.
|
||||
func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) {
|
||||
rv, err := registry.GetVersion(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rv, nil
|
||||
}
|
||||
57
service/updates/helper/electron.go
Normal file
57
service/updates/helper/electron.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
var pmElectronUpdate *updater.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`
|
||||
|
||||
// EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed
|
||||
// by our app-electron package has the SUID bit set on systems that do not
|
||||
// allow unprivileged CLONE_NEWUSER (clone(3)).
|
||||
// On non-linux systems or systems that have kernel.unprivileged_userns_clone
|
||||
// set to 1 EnsureChromeSandboPermissions is a NO-OP.
|
||||
func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
identifier := PlatformIdentifier("app/portmaster-app.zip")
|
||||
|
||||
var err error
|
||||
pmElectronUpdate, err = reg.GetFile(identifier)
|
||||
if err != nil {
|
||||
if errors.Is(err, updater.ErrNotAvailableLocally) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
unpackedPath := strings.TrimSuffix(
|
||||
pmElectronUpdate.Path(),
|
||||
filepath.Ext(pmElectronUpdate.Path()),
|
||||
)
|
||||
sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox")
|
||||
if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil {
|
||||
log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile)
|
||||
|
||||
return fmt.Errorf("failed to chmod: %w", err)
|
||||
}
|
||||
log.Debugf("updates: fixed SUID permission for chrome-sandbox")
|
||||
|
||||
return nil
|
||||
}
|
||||
134
service/updates/helper/indexes.go
Normal file
134
service/updates/helper/indexes.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/safing/jess/filesig"
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
// Release Channel Configuration Keys.
|
||||
const (
|
||||
ReleaseChannelKey = "core/releaseChannel"
|
||||
ReleaseChannelJSONKey = "core.releaseChannel"
|
||||
)
|
||||
|
||||
// Release Channels.
|
||||
const (
|
||||
ReleaseChannelStable = "stable"
|
||||
ReleaseChannelBeta = "beta"
|
||||
ReleaseChannelStaging = "staging"
|
||||
ReleaseChannelSupport = "support"
|
||||
)
|
||||
|
||||
// SetIndexes sets the update registry indexes and also configures the registry
|
||||
// to use pre-releases based on the channel.
|
||||
func SetIndexes(
|
||||
registry *updater.ResourceRegistry,
|
||||
releaseChannel string,
|
||||
deleteUnusedIndexes bool,
|
||||
autoDownload bool,
|
||||
autoDownloadIntel bool,
|
||||
) (warning error) {
|
||||
usePreReleases := false
|
||||
|
||||
// Be reminded that the order is important, as indexes added later will
|
||||
// override the current release from earlier indexes.
|
||||
|
||||
// Reset indexes before adding them (again).
|
||||
registry.ResetIndexes()
|
||||
|
||||
// Add the intel index first, in order to be able to override it with the
|
||||
// other indexes when needed.
|
||||
registry.AddIndex(updater.Index{
|
||||
Path: "all/intel/intel.json",
|
||||
AutoDownload: autoDownloadIntel,
|
||||
})
|
||||
|
||||
// Always add the stable index as a base.
|
||||
registry.AddIndex(updater.Index{
|
||||
Path: ReleaseChannelStable + ".json",
|
||||
AutoDownload: autoDownload,
|
||||
})
|
||||
|
||||
// Add beta index if in beta or staging channel.
|
||||
indexPath := ReleaseChannelBeta + ".json"
|
||||
if releaseChannel == ReleaseChannelBeta ||
|
||||
releaseChannel == ReleaseChannelStaging ||
|
||||
(releaseChannel == "" && indexExists(registry, indexPath)) {
|
||||
registry.AddIndex(updater.Index{
|
||||
Path: indexPath,
|
||||
PreRelease: true,
|
||||
AutoDownload: autoDownload,
|
||||
})
|
||||
usePreReleases = true
|
||||
} else if deleteUnusedIndexes {
|
||||
err := deleteIndex(registry, indexPath)
|
||||
if err != nil {
|
||||
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add staging index if in staging channel.
|
||||
indexPath = ReleaseChannelStaging + ".json"
|
||||
if releaseChannel == ReleaseChannelStaging ||
|
||||
(releaseChannel == "" && indexExists(registry, indexPath)) {
|
||||
registry.AddIndex(updater.Index{
|
||||
Path: indexPath,
|
||||
PreRelease: true,
|
||||
AutoDownload: autoDownload,
|
||||
})
|
||||
usePreReleases = true
|
||||
} else if deleteUnusedIndexes {
|
||||
err := deleteIndex(registry, indexPath)
|
||||
if err != nil {
|
||||
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add support index if in support channel.
|
||||
indexPath = ReleaseChannelSupport + ".json"
|
||||
if releaseChannel == ReleaseChannelSupport ||
|
||||
(releaseChannel == "" && indexExists(registry, indexPath)) {
|
||||
registry.AddIndex(updater.Index{
|
||||
Path: indexPath,
|
||||
AutoDownload: autoDownload,
|
||||
})
|
||||
usePreReleases = true
|
||||
} else if deleteUnusedIndexes {
|
||||
err := deleteIndex(registry, indexPath)
|
||||
if err != nil {
|
||||
warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set pre-release usage.
|
||||
registry.SetUsePreReleases(usePreReleases)
|
||||
|
||||
return warning
|
||||
}
|
||||
|
||||
func indexExists(registry *updater.ResourceRegistry, indexPath string) bool {
|
||||
_, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error {
|
||||
// Remove index itself.
|
||||
err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath))
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove any accompanying signature.
|
||||
err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension))
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
service/updates/helper/signing.go
Normal file
42
service/updates/helper/signing.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
var (
|
||||
// VerificationConfig holds the complete verification configuration for the registry.
|
||||
VerificationConfig = map[string]*updater.VerificationOptions{
|
||||
"": { // Default.
|
||||
TrustStore: BinarySigningTrustStore,
|
||||
DownloadPolicy: updater.SignaturePolicyRequire,
|
||||
DiskLoadPolicy: updater.SignaturePolicyWarn,
|
||||
},
|
||||
"all/intel/": nil, // Disable until IntelHub supports signing.
|
||||
}
|
||||
|
||||
// BinarySigningKeys holds the signing keys in text format.
|
||||
BinarySigningKeys = []string{
|
||||
// Safing Code Signing Key #1
|
||||
"recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD",
|
||||
// Safing Code Signing Key #2
|
||||
"recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3",
|
||||
}
|
||||
|
||||
// BinarySigningTrustStore is an in-memory trust store with the signing keys.
|
||||
BinarySigningTrustStore = jess.NewMemTrustStore()
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, signingKey := range BinarySigningKeys {
|
||||
rcpt, err := jess.RecipientFromTextFormat(signingKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = BinarySigningTrustStore.StoreSignet(rcpt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
service/updates/helper/updates.go
Normal file
92
service/updates/helper/updates.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
const onWindows = runtime.GOOS == "windows"
|
||||
|
||||
var intelOnly = abool.New()
|
||||
|
||||
// IntelOnly specifies that only intel data is mandatory.
|
||||
func IntelOnly() {
|
||||
intelOnly.Set()
|
||||
}
|
||||
|
||||
// PlatformIdentifier converts identifier for the current platform.
|
||||
func PlatformIdentifier(identifier string) string {
|
||||
// From https://golang.org/pkg/runtime/#GOARCH
|
||||
// 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.
|
||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
||||
}
|
||||
|
||||
// MandatoryUpdates returns mandatory updates that should be loaded on install
|
||||
// or reset.
|
||||
func MandatoryUpdates() (identifiers []string) {
|
||||
// Intel
|
||||
identifiers = append(
|
||||
identifiers,
|
||||
|
||||
// Filter lists data
|
||||
"all/intel/lists/index.dsd",
|
||||
"all/intel/lists/base.dsdl",
|
||||
"all/intel/lists/intermediate.dsdl",
|
||||
"all/intel/lists/urgent.dsdl",
|
||||
|
||||
// Geo IP data
|
||||
"all/intel/geoip/geoipv4.mmdb.gz",
|
||||
"all/intel/geoip/geoipv6.mmdb.gz",
|
||||
)
|
||||
|
||||
// Stop here if we only want intel data.
|
||||
if intelOnly.IsSet() {
|
||||
return identifiers
|
||||
}
|
||||
|
||||
// Binaries
|
||||
if onWindows {
|
||||
identifiers = append(
|
||||
identifiers,
|
||||
PlatformIdentifier("core/portmaster-core.exe"),
|
||||
PlatformIdentifier("kext/portmaster-kext.sys"),
|
||||
PlatformIdentifier("kext/portmaster-kext.pdb"),
|
||||
PlatformIdentifier("start/portmaster-start.exe"),
|
||||
PlatformIdentifier("notifier/portmaster-notifier.exe"),
|
||||
PlatformIdentifier("notifier/portmaster-wintoast.dll"),
|
||||
)
|
||||
} else {
|
||||
identifiers = append(
|
||||
identifiers,
|
||||
PlatformIdentifier("core/portmaster-core"),
|
||||
PlatformIdentifier("start/portmaster-start"),
|
||||
PlatformIdentifier("notifier/portmaster-notifier"),
|
||||
)
|
||||
}
|
||||
|
||||
// Components, Assets and Data
|
||||
identifiers = append(
|
||||
identifiers,
|
||||
|
||||
// User interface components
|
||||
PlatformIdentifier("app/portmaster-app.zip"),
|
||||
"all/ui/modules/portmaster.zip",
|
||||
"all/ui/modules/assets.zip",
|
||||
)
|
||||
|
||||
return identifiers
|
||||
}
|
||||
|
||||
// AutoUnpackUpdates returns assets that need unpacking.
|
||||
func AutoUnpackUpdates() []string {
|
||||
if intelOnly.IsSet() {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return []string{
|
||||
PlatformIdentifier("app/portmaster-app.zip"),
|
||||
}
|
||||
}
|
||||
343
service/updates/main.go
Normal file
343
service/updates/main.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
const (
|
||||
onWindows = runtime.GOOS == "windows"
|
||||
|
||||
enableSoftwareUpdatesKey = "core/automaticUpdates"
|
||||
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
|
||||
// version of a monitored resource is selected.
|
||||
// During module initialization VersionUpdateEvent
|
||||
// is also emitted.
|
||||
VersionUpdateEvent = "active version update"
|
||||
|
||||
// ResourceUpdateEvent is emitted every time the
|
||||
// updater successfully performed a resource update.
|
||||
// ResourceUpdateEvent is emitted even if no new
|
||||
// versions are available. Subscribers are expected
|
||||
// to check if new versions of their resources are
|
||||
// available by checking File.UpgradeAvailable().
|
||||
ResourceUpdateEvent = "resource update"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
registry *updater.ResourceRegistry
|
||||
|
||||
userAgentFromFlag string
|
||||
updateServerFromFlag string
|
||||
|
||||
updateTask *modules.Task
|
||||
updateASAP bool
|
||||
disableTaskSchedule bool
|
||||
|
||||
db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
})
|
||||
|
||||
// UserAgent is an HTTP User-Agent that is used to add
|
||||
// more context to requests made by the registry when
|
||||
// fetching resources from the update server.
|
||||
UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
// DefaultUpdateURLs defines the default base URLs of the update server.
|
||||
DefaultUpdateURLs = []string{
|
||||
"https://updates.safing.io",
|
||||
}
|
||||
|
||||
// DisableSoftwareAutoUpdate specifies whether software updates should be disabled.
|
||||
// This is used on Android, as it will never require binary updates.
|
||||
DisableSoftwareAutoUpdate = false
|
||||
)
|
||||
|
||||
const (
|
||||
updatesDirName = "updates"
|
||||
|
||||
updateTaskRepeatDuration = 1 * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register(ModuleName, prep, start, stop, "base")
|
||||
module.RegisterEvent(VersionUpdateEvent, true)
|
||||
module.RegisterEvent(ResourceUpdateEvent, true)
|
||||
|
||||
flag.StringVar(&updateServerFromFlag, "update-server", "", "set an alternative update server (full URL)")
|
||||
flag.StringVar(&userAgentFromFlag, "update-agent", "", "set an alternative user agent for requests to the update server")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
if err := registerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if update server URL supplied via flag is a valid URL.
|
||||
if updateServerFromFlag != "" {
|
||||
u, err := url.Parse(updateServerFromFlag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("supplied update server URL is invalid: %w", err)
|
||||
}
|
||||
if u.Scheme != "https" {
|
||||
return errors.New("supplied update server URL must use HTTPS")
|
||||
}
|
||||
}
|
||||
|
||||
return registerAPIEndpoints()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
initConfig()
|
||||
|
||||
restartTask = module.NewTask("automatic restart", automaticRestart).MaxDelay(10 * time.Minute)
|
||||
|
||||
if err := module.RegisterEventHook(
|
||||
"config",
|
||||
"config change",
|
||||
"update registry config",
|
||||
updateRegistryConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.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.TriggerEvent(VersionUpdateEvent, nil)
|
||||
|
||||
// Initialize the version export - this requires the registry to be set up.
|
||||
err = initVersionExport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start updater task
|
||||
updateTask = module.NewTask("updater", func(ctx context.Context, task *modules.Task) error {
|
||||
return checkForUpdates(ctx)
|
||||
})
|
||||
|
||||
if !disableTaskSchedule {
|
||||
updateTask.
|
||||
Repeat(updateTaskRepeatDuration).
|
||||
MaxDelay(30 * time.Minute)
|
||||
}
|
||||
|
||||
if updateASAP {
|
||||
updateTask.StartASAP()
|
||||
}
|
||||
|
||||
// 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 !module.Online():
|
||||
updateASAP = true
|
||||
|
||||
case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates():
|
||||
return fmt.Errorf("automatic updating is disabled")
|
||||
|
||||
default:
|
||||
if forceIndexCheck {
|
||||
forceCheck.Set()
|
||||
}
|
||||
if downloadAll {
|
||||
forceDownload.Set()
|
||||
}
|
||||
|
||||
// If index check if forced, start quicker.
|
||||
if forceIndexCheck {
|
||||
updateTask.StartASAP()
|
||||
} else {
|
||||
updateTask.Queue()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
switch module.Status() {
|
||||
case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping:
|
||||
return fmt.Errorf("module already online")
|
||||
}
|
||||
|
||||
disableTaskSchedule = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForUpdates(ctx context.Context) (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); 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, 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.TriggerEvent(ResourceUpdateEvent, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
if registry != nil {
|
||||
err := registry.Cleanup()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to clean up registry: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RootPath returns the root path used for storing updates.
|
||||
func RootPath() string {
|
||||
if !module.Online() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return registry.StorageDir().Path
|
||||
}
|
||||
168
service/updates/notify.go
Normal file
168
service/updates/notify.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/notifications"
|
||||
)
|
||||
|
||||
const (
|
||||
updateFailed = "updates:failed"
|
||||
updateSuccess = "updates:success"
|
||||
updateSuccessPending = "updates:success-pending"
|
||||
updateSuccessDownloaded = "updates:success-downloaded"
|
||||
|
||||
failedUpdateNotifyDurationThreshold = 24 * time.Hour
|
||||
failedUpdateNotifyCountThreshold = 3
|
||||
)
|
||||
|
||||
var updateFailedCnt = new(atomic.Int32)
|
||||
|
||||
func notifyUpdateSuccess(force bool) {
|
||||
updateFailedCnt.Store(0)
|
||||
module.Resolve(updateFailed)
|
||||
updateState := registry.GetState().Updates
|
||||
|
||||
flavor := updateSuccess
|
||||
switch {
|
||||
case len(updateState.PendingDownload) > 0:
|
||||
// Show notification if there are pending downloads.
|
||||
flavor = updateSuccessPending
|
||||
case updateState.LastDownloadAt != nil &&
|
||||
time.Since(*updateState.LastDownloadAt) < 5*time.Second:
|
||||
// Show notification if we downloaded something within the last minute.
|
||||
flavor = updateSuccessDownloaded
|
||||
case force:
|
||||
// Always show notification if update was manually triggered.
|
||||
default:
|
||||
// Otherwise, the update was uneventful. Do not show notification.
|
||||
return
|
||||
}
|
||||
|
||||
switch flavor {
|
||||
case updateSuccess:
|
||||
notifications.Notify(¬ifications.Notification{
|
||||
EventID: updateSuccess,
|
||||
Type: notifications.Info,
|
||||
Title: "Portmaster Is Up-To-Date",
|
||||
Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(),
|
||||
Expires: time.Now().Add(1 * time.Minute).Unix(),
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
ID: "ack",
|
||||
Text: "OK",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
case updateSuccessPending:
|
||||
msg := fmt.Sprintf(
|
||||
`%d updates are available for download:
|
||||
|
||||
- %s
|
||||
|
||||
Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`,
|
||||
len(updateState.PendingDownload),
|
||||
strings.Join(updateState.PendingDownload, "\n- "),
|
||||
)
|
||||
|
||||
notifications.Notify(¬ifications.Notification{
|
||||
EventID: updateSuccess,
|
||||
Type: notifications.Info,
|
||||
Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)),
|
||||
Message: msg,
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
ID: "ack",
|
||||
Text: "OK",
|
||||
},
|
||||
{
|
||||
ID: "download",
|
||||
Text: "Download Now",
|
||||
Type: notifications.ActionTypeWebhook,
|
||||
Payload: ¬ifications.ActionTypeWebhookPayload{
|
||||
URL: apiPathCheckForUpdates + "?download",
|
||||
ResultAction: "display",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
case updateSuccessDownloaded:
|
||||
msg := fmt.Sprintf(
|
||||
`%d updates were downloaded and applied:
|
||||
|
||||
- %s
|
||||
|
||||
%s
|
||||
`,
|
||||
len(updateState.LastDownload),
|
||||
strings.Join(updateState.LastDownload, "\n- "),
|
||||
getUpdatingInfoMsg(),
|
||||
)
|
||||
|
||||
notifications.Notify(¬ifications.Notification{
|
||||
EventID: updateSuccess,
|
||||
Type: notifications.Info,
|
||||
Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)),
|
||||
Message: msg,
|
||||
Expires: time.Now().Add(1 * time.Minute).Unix(),
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
ID: "ack",
|
||||
Text: "OK",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func getUpdatingInfoMsg() string {
|
||||
switch {
|
||||
case enableSoftwareUpdates() && enableIntelUpdates():
|
||||
return "You will be notified of important updates that need restarting."
|
||||
case enableIntelUpdates():
|
||||
return "Automatic software updates are disabled, but you will be notified when a new software update is ready to be downloaded and applied."
|
||||
default:
|
||||
return "Automatic software updates are disabled. Please check for updates regularly yourself."
|
||||
}
|
||||
}
|
||||
|
||||
func notifyUpdateCheckFailed(force bool, err error) {
|
||||
failedCnt := updateFailedCnt.Add(1)
|
||||
lastSuccess := registry.GetState().Updates.LastSuccessAt
|
||||
|
||||
switch {
|
||||
case force:
|
||||
// Always show notification if update was manually triggered.
|
||||
case failedCnt < failedUpdateNotifyCountThreshold:
|
||||
// Not failed often enough for notification.
|
||||
return
|
||||
case lastSuccess == nil:
|
||||
// No recorded successful update.
|
||||
case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess):
|
||||
// Failed too recently for notification.
|
||||
return
|
||||
}
|
||||
|
||||
notifications.NotifyWarn(
|
||||
updateFailed,
|
||||
"Update Check Failed",
|
||||
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",
|
||||
err,
|
||||
),
|
||||
notifications.Action{
|
||||
Text: "Try Again Now",
|
||||
Type: notifications.ActionTypeWebhook,
|
||||
Payload: ¬ifications.ActionTypeWebhookPayload{
|
||||
URL: apiPathCheckForUpdates,
|
||||
ResultAction: "display",
|
||||
},
|
||||
},
|
||||
).AttachToModule(module)
|
||||
}
|
||||
8
service/updates/os_integration_default.go
Normal file
8
service/updates/os_integration_default.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package updates
|
||||
|
||||
func upgradeSystemIntegration() error {
|
||||
return nil
|
||||
}
|
||||
204
service/updates/os_integration_linux.go
Normal file
204
service/updates/os_integration_linux.go
Normal file
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
143
service/updates/restart.go
Normal file
143
service/updates/restart.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
const (
|
||||
// RestartExitCode will instruct portmaster-start to restart the process immediately, potentially with a new version.
|
||||
RestartExitCode = 23
|
||||
)
|
||||
|
||||
var (
|
||||
// RebootOnRestart defines whether the whole system, not just the service,
|
||||
// should be restarted automatically when triggering a restart internally.
|
||||
RebootOnRestart bool
|
||||
|
||||
restartTask *modules.Task
|
||||
restartPending = abool.New()
|
||||
restartTriggered = abool.New()
|
||||
|
||||
restartTime time.Time
|
||||
restartTimeLock sync.Mutex
|
||||
)
|
||||
|
||||
// IsRestarting returns whether a restart has been triggered.
|
||||
func IsRestarting() bool {
|
||||
return restartTriggered.IsSet()
|
||||
}
|
||||
|
||||
// RestartIsPending returns whether a restart is pending.
|
||||
func RestartIsPending() (pending bool, restartAt time.Time) {
|
||||
if restartPending.IsNotSet() {
|
||||
return false, time.Time{}
|
||||
}
|
||||
|
||||
restartTimeLock.Lock()
|
||||
defer restartTimeLock.Unlock()
|
||||
|
||||
return true, restartTime
|
||||
}
|
||||
|
||||
// DelayedRestart triggers a restart of the application by shutting down the
|
||||
// module system gracefully and returning with RestartExitCode. The restart
|
||||
// may be further delayed by up to 10 minutes by the internal task scheduling
|
||||
// system. This only works if the process is managed by portmaster-start.
|
||||
func DelayedRestart(delay time.Duration) {
|
||||
// Check if restart is already pending.
|
||||
if !restartPending.SetToIf(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
// Schedule the restart task.
|
||||
log.Warningf("updates: restart triggered, will execute in %s", delay)
|
||||
restartAt := time.Now().Add(delay)
|
||||
restartTask.Schedule(restartAt)
|
||||
|
||||
// Set restartTime.
|
||||
restartTimeLock.Lock()
|
||||
defer restartTimeLock.Unlock()
|
||||
restartTime = restartAt
|
||||
}
|
||||
|
||||
// AbortRestart aborts a (delayed) restart.
|
||||
func AbortRestart() {
|
||||
if restartPending.SetToIf(true, false) {
|
||||
log.Warningf("updates: restart aborted")
|
||||
|
||||
// Cancel schedule.
|
||||
restartTask.Schedule(time.Time{})
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerRestartIfPending triggers an automatic restart, if one is pending.
|
||||
// This can be used to prepone a scheduled restart if the conditions are preferable.
|
||||
func TriggerRestartIfPending() {
|
||||
if restartPending.IsSet() {
|
||||
restartTask.StartASAP()
|
||||
}
|
||||
}
|
||||
|
||||
// RestartNow immediately executes a restart.
|
||||
// This only works if the process is managed by portmaster-start.
|
||||
func RestartNow() {
|
||||
restartPending.Set()
|
||||
restartTask.StartASAP()
|
||||
}
|
||||
|
||||
func automaticRestart(_ context.Context, _ *modules.Task) error {
|
||||
// Check if the restart is still scheduled.
|
||||
if restartPending.IsNotSet() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trigger restart.
|
||||
if restartTriggered.SetToIf(false, true) {
|
||||
log.Warning("updates: initiating (automatic) restart")
|
||||
|
||||
// Check if we should reboot instead.
|
||||
var rebooting bool
|
||||
if RebootOnRestart {
|
||||
// Trigger system reboot and record success.
|
||||
rebooting = triggerSystemReboot()
|
||||
if !rebooting {
|
||||
log.Warningf("updates: rebooting failed, only restarting service instead")
|
||||
}
|
||||
}
|
||||
|
||||
// Set restart exit code.
|
||||
if !rebooting {
|
||||
modules.SetExitStatusCode(RestartExitCode)
|
||||
}
|
||||
|
||||
// Do not use a worker, as this would block itself here.
|
||||
go modules.Shutdown() //nolint:errcheck
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func triggerSystemReboot() (success bool) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err := exec.Command("systemctl", "reboot").Run()
|
||||
if err != nil {
|
||||
log.Errorf("updates: triggering reboot with systemctl failed: %s", err)
|
||||
return false
|
||||
}
|
||||
default:
|
||||
log.Warningf("updates: rebooting is not support on %s", runtime.GOOS)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
49
service/updates/state.go
Normal file
49
service/updates/state.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/runtime"
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
var pushRegistryStatusUpdate runtime.PushFunc
|
||||
|
||||
// RegistryStateExport is a wrapper to export the registry state.
|
||||
type RegistryStateExport struct {
|
||||
record.Base
|
||||
*updater.RegistryState
|
||||
}
|
||||
|
||||
func exportRegistryState(s *updater.RegistryState) *RegistryStateExport {
|
||||
if s == nil {
|
||||
state := registry.GetState()
|
||||
s = &state
|
||||
}
|
||||
|
||||
export := &RegistryStateExport{
|
||||
RegistryState: s,
|
||||
}
|
||||
|
||||
export.CreateMeta()
|
||||
export.SetKey("runtime:core/updates/state")
|
||||
|
||||
return export
|
||||
}
|
||||
|
||||
func pushRegistryState(s *updater.RegistryState) {
|
||||
export := exportRegistryState(s)
|
||||
pushRegistryStatusUpdate(export)
|
||||
}
|
||||
|
||||
func registerRegistryStateProvider() (err error) {
|
||||
registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) {
|
||||
return []record.Record{exportRegistryState(nil)}, nil
|
||||
})
|
||||
|
||||
pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
409
service/updates/upgrader.go
Normal file
409
service/updates/upgrader.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portbase/utils/renameio"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
)
|
||||
|
||||
const (
|
||||
upgradedSuffix = "-upgraded"
|
||||
exeExt = ".exe"
|
||||
)
|
||||
|
||||
var (
|
||||
upgraderActive = abool.NewBool(false)
|
||||
|
||||
pmCtrlUpdate *updater.File
|
||||
pmCoreUpdate *updater.File
|
||||
|
||||
spnHubUpdate *updater.File
|
||||
|
||||
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
|
||||
)
|
||||
|
||||
func initUpgrader() error {
|
||||
return module.RegisterEventHook(
|
||||
ModuleName,
|
||||
ResourceUpdateEvent,
|
||||
"run upgrades",
|
||||
upgrader,
|
||||
)
|
||||
}
|
||||
|
||||
func upgrader(_ context.Context, _ interface{}) error {
|
||||
// Lock runs, but discard additional runs.
|
||||
if !upgraderActive.SetToIf(false, true) {
|
||||
return nil
|
||||
}
|
||||
defer upgraderActive.SetTo(false)
|
||||
|
||||
// Upgrade portmaster-start.
|
||||
err := upgradePortmasterStart()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to upgrade portmaster-start: %s", err)
|
||||
}
|
||||
|
||||
// Upgrade based on binary.
|
||||
binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
|
||||
switch binBaseName {
|
||||
case "portmaster-core":
|
||||
// Notify about upgrade.
|
||||
if err := upgradeCoreNotify(); err != nil {
|
||||
log.Warningf("updates: failed to notify about core upgrade: %s", err)
|
||||
}
|
||||
|
||||
// Fix chrome sandbox permissions.
|
||||
if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
|
||||
log.Warningf("updates: failed to handle electron upgrade: %s", err)
|
||||
}
|
||||
|
||||
// Upgrade system integration.
|
||||
upgradeSystemIntegration()
|
||||
|
||||
case "spn-hub":
|
||||
// Trigger upgrade procedure.
|
||||
if err := upgradeHub(); err != nil {
|
||||
log.Warningf("updates: failed to initiate hub upgrade: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeCoreNotify() error {
|
||||
if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// make identifier
|
||||
identifier := "core/portmaster-core" // identifier, use forward slash!
|
||||
if onWindows {
|
||||
identifier += exeExt
|
||||
}
|
||||
|
||||
// get newest portmaster-core
|
||||
newFile, err := GetPlatformFile(identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pmCoreUpdate = newFile
|
||||
|
||||
// check for new version
|
||||
if info.GetInfo().Version != pmCoreUpdate.Version() {
|
||||
n := notifications.Notify(¬ifications.Notification{
|
||||
EventID: "updates:core-update-available",
|
||||
Type: notifications.Info,
|
||||
Title: fmt.Sprintf(
|
||||
"Portmaster Update v%s Is Ready!",
|
||||
pmCoreUpdate.Version(),
|
||||
),
|
||||
Category: "Core",
|
||||
Message: fmt.Sprintf(
|
||||
`A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`,
|
||||
pmCoreUpdate.Version(),
|
||||
),
|
||||
ShowOnSystem: true,
|
||||
AvailableActions: []*notifications.Action{
|
||||
// TODO: Use special UI action in order to reload UI on restart.
|
||||
{
|
||||
ID: "restart",
|
||||
Text: "Restart",
|
||||
},
|
||||
{
|
||||
ID: "later",
|
||||
Text: "Not now",
|
||||
},
|
||||
},
|
||||
})
|
||||
n.SetActionFunction(upgradeCoreNotifyActionHandler)
|
||||
|
||||
log.Debugf("updates: new portmaster version available, sending notification to user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
|
||||
switch n.SelectedActionID {
|
||||
case "restart":
|
||||
log.Infof("updates: user triggered restart via core update notification")
|
||||
RestartNow()
|
||||
case "later":
|
||||
n.Delete()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeHub() error {
|
||||
if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make identifier for getting file from updater.
|
||||
identifier := "hub/spn-hub" // identifier, use forward slash!
|
||||
if onWindows {
|
||||
identifier += exeExt
|
||||
}
|
||||
|
||||
// Get newest spn-hub file.
|
||||
newFile, err := GetPlatformFile(identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spnHubUpdate = newFile
|
||||
|
||||
// Check if the new version is different.
|
||||
if info.GetInfo().Version != spnHubUpdate.Version() {
|
||||
// Get random delay with up to three hours.
|
||||
delayMinutes, err := rng.Number(3 * 60)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delay restart for at least one hour for preparations.
|
||||
DelayedRestart(time.Duration(delayMinutes+60) * time.Minute)
|
||||
|
||||
// Increase update checks in order to detect aborts better.
|
||||
if !disableTaskSchedule {
|
||||
updateTask.Repeat(10 * time.Minute)
|
||||
}
|
||||
} else {
|
||||
AbortRestart()
|
||||
|
||||
// Set update task schedule back to normal.
|
||||
if !disableTaskSchedule {
|
||||
updateTask.Repeat(updateTaskRepeatDuration)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradePortmasterStart() error {
|
||||
filename := "portmaster-start"
|
||||
if onWindows {
|
||||
filename += exeExt
|
||||
}
|
||||
|
||||
// check if we can upgrade
|
||||
if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() {
|
||||
// get newest portmaster-start
|
||||
newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pmCtrlUpdate = newFile
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// update portmaster-start in data root
|
||||
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
|
||||
err := upgradeBinary(rootPmStartPath, pmCtrlUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func warnOnIncorrectParentPath() {
|
||||
expectedFileName := "portmaster-start"
|
||||
if onWindows {
|
||||
expectedFileName += exeExt
|
||||
}
|
||||
|
||||
// upgrade parent process, if it's portmaster-start
|
||||
parent, err := processInfo.NewProcess(int32(os.Getppid()))
|
||||
if err != nil {
|
||||
log.Tracef("could not get parent process: %s", err)
|
||||
return
|
||||
}
|
||||
parentName, err := parent.Name()
|
||||
if err != nil {
|
||||
log.Tracef("could not get parent process name: %s", err)
|
||||
return
|
||||
}
|
||||
if parentName != expectedFileName {
|
||||
// Only warn about this if not in dev mode.
|
||||
if !devMode() {
|
||||
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
|
||||
// to update we should send a module warning/hint to the
|
||||
// UI notifying the user that he's still using portmaster-control.
|
||||
return
|
||||
}
|
||||
|
||||
parentPath, err := parent.Exe()
|
||||
if err != nil {
|
||||
log.Tracef("could not get parent process path: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(parentPath)
|
||||
if err != nil {
|
||||
log.Tracef("could not get absolut parent process path: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
root := filepath.Dir(registry.StorageDir().Path)
|
||||
if !strings.HasPrefix(absPath, root) {
|
||||
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
|
||||
notifications.NotifyWarn(
|
||||
"updates:unsupported-parent",
|
||||
"Unsupported Launcher",
|
||||
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.",
|
||||
expectedFileName,
|
||||
absPath,
|
||||
filepath.Join(root, expectedFileName),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeBinary(fileToUpgrade string, file *updater.File) error {
|
||||
fileExists := false
|
||||
_, err := os.Stat(fileToUpgrade)
|
||||
if err == nil {
|
||||
// file exists and is accessible
|
||||
fileExists = true
|
||||
}
|
||||
|
||||
if fileExists {
|
||||
// get current version
|
||||
var currentVersion string
|
||||
cmd := exec.Command(fileToUpgrade, "version", "--short")
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
// abort if version matches
|
||||
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
||||
if currentVersion == file.Version() {
|
||||
log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version())
|
||||
// already up to date!
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err)
|
||||
currentVersion = "0.0.0"
|
||||
}
|
||||
|
||||
// test currentVersion for sanity
|
||||
if !rawVersionRegex.MatchString(currentVersion) {
|
||||
log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
|
||||
}
|
||||
|
||||
// try removing old version
|
||||
err = os.Remove(fileToUpgrade)
|
||||
if err != nil {
|
||||
// ensure tmp dir is here
|
||||
err = registry.TmpDir().Ensure()
|
||||
if err != nil {
|
||||
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
|
||||
err = os.Rename(fileToUpgrade, filepath.Join(
|
||||
registry.TmpDir().Path,
|
||||
fmt.Sprintf(
|
||||
"%s-%d%s",
|
||||
filepath.Base(fileToUpgrade),
|
||||
time.Now().UTC().Unix(),
|
||||
upgradedSuffix,
|
||||
),
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to move file that needs upgrade: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy upgrade
|
||||
err = CopyFile(file.Path(), fileToUpgrade)
|
||||
if err != nil {
|
||||
// try again
|
||||
time.Sleep(1 * time.Second)
|
||||
err = CopyFile(file.Path(), fileToUpgrade)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// check permissions
|
||||
if !onWindows {
|
||||
info, err := os.Stat(fileToUpgrade)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)
|
||||
}
|
||||
if info.Mode() != 0o0755 {
|
||||
err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions.
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version())
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile atomically copies a file using the update registry's tmp dir.
|
||||
func CopyFile(srcPath, dstPath string) error {
|
||||
// check tmp dir
|
||||
err := registry.TmpDir().Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
|
||||
}
|
||||
|
||||
// open file for writing
|
||||
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
// open source
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = srcFile.Close()
|
||||
}()
|
||||
|
||||
// copy data
|
||||
_, err = io.Copy(atomicDstFile, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// finalize file
|
||||
err = atomicDstFile.CloseAtomicallyReplace()
|
||||
if err != nil {
|
||||
return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user