From a874ec9412fd0da931e1f172a1c416fd2835e8cb Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Tue, 8 Oct 2024 14:13:08 +0300 Subject: [PATCH] [WIP] Fix unit tests --- cmds/hub/main.go | 5 +- cmds/notifier/.gitignore | 34 -- cmds/notifier/README.md | 5 - cmds/notifier/http_api.go | 63 --- cmds/notifier/icons.go | 25 - cmds/notifier/main.go | 287 ----------- cmds/notifier/notification.go | 35 -- cmds/notifier/notify.go | 102 ---- cmds/notifier/notify_linux.go | 160 ------ cmds/notifier/notify_windows.go | 184 ------- cmds/notifier/shutdown.go | 50 -- cmds/notifier/snoretoast-guid.patch | 15 - cmds/notifier/spn.go | 104 ---- cmds/notifier/subsystems.go | 121 ----- cmds/notifier/tray.go | 217 -------- .../notifier/wintoast/notification_builder.go | 90 ---- cmds/notifier/wintoast/wintoast.go | 217 -------- cmds/observation-hub/main.go | 2 - cmds/portmaster-start/.gitignore | 6 - cmds/portmaster-start/build | 77 --- cmds/portmaster-start/console_default.go | 11 - cmds/portmaster-start/console_windows.go | 150 ------ cmds/portmaster-start/dirs.go | 42 -- cmds/portmaster-start/install_windows.go | 180 ------- cmds/portmaster-start/lock.go | 109 ---- cmds/portmaster-start/logs.go | 127 ----- cmds/portmaster-start/main.go | 257 --------- cmds/portmaster-start/pack | 123 ----- cmds/portmaster-start/recover_linux.go | 82 --- cmds/portmaster-start/run.go | 486 ------------------ cmds/portmaster-start/service_windows.go | 134 ----- cmds/portmaster-start/show.go | 45 -- cmds/portmaster-start/shutdown.go | 49 -- cmds/portmaster-start/update.go | 158 ------ cmds/portmaster-start/verify.go | 179 ------- cmds/portmaster-start/version.go | 81 --- service/intel/geoip/init_test.go | 16 +- service/netenv/init_test.go | 16 +- service/profile/endpoints/endpoints_test.go | 17 +- service/resolver/main_test.go | 19 +- service/updates/assets/portmaster.service | 44 -- service/updates/bundlegeneration.go | 48 ++ service/updates/downloader.go | 16 +- service/updates/module.go | 4 - service/updates/updates_test.go | 89 ++++ spn/hub/hub_test.go | 17 +- spn/instance.go | 39 +- spn/navigator/module_test.go | 15 +- 48 files changed, 264 insertions(+), 4088 deletions(-) delete mode 100644 cmds/notifier/.gitignore delete mode 100644 cmds/notifier/README.md delete mode 100644 cmds/notifier/http_api.go delete mode 100644 cmds/notifier/icons.go delete mode 100644 cmds/notifier/main.go delete mode 100644 cmds/notifier/notification.go delete mode 100644 cmds/notifier/notify.go delete mode 100644 cmds/notifier/notify_linux.go delete mode 100644 cmds/notifier/notify_windows.go delete mode 100644 cmds/notifier/shutdown.go delete mode 100644 cmds/notifier/snoretoast-guid.patch delete mode 100644 cmds/notifier/spn.go delete mode 100644 cmds/notifier/subsystems.go delete mode 100644 cmds/notifier/tray.go delete mode 100644 cmds/notifier/wintoast/notification_builder.go delete mode 100644 cmds/notifier/wintoast/wintoast.go delete mode 100644 cmds/portmaster-start/.gitignore delete mode 100755 cmds/portmaster-start/build delete mode 100644 cmds/portmaster-start/console_default.go delete mode 100644 cmds/portmaster-start/console_windows.go delete mode 100644 cmds/portmaster-start/dirs.go delete mode 100644 cmds/portmaster-start/install_windows.go delete mode 100644 cmds/portmaster-start/lock.go delete mode 100644 cmds/portmaster-start/logs.go delete mode 100644 cmds/portmaster-start/main.go delete mode 100755 cmds/portmaster-start/pack delete mode 100644 cmds/portmaster-start/recover_linux.go delete mode 100644 cmds/portmaster-start/run.go delete mode 100644 cmds/portmaster-start/service_windows.go delete mode 100644 cmds/portmaster-start/show.go delete mode 100644 cmds/portmaster-start/shutdown.go delete mode 100644 cmds/portmaster-start/update.go delete mode 100644 cmds/portmaster-start/verify.go delete mode 100644 cmds/portmaster-start/version.go delete mode 100644 service/updates/assets/portmaster.service create mode 100644 service/updates/updates_test.go diff --git a/cmds/hub/main.go b/cmds/hub/main.go index 3db002b3..b0b6e1d1 100644 --- a/cmds/hub/main.go +++ b/cmds/hub/main.go @@ -18,13 +18,12 @@ import ( "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/service/updates/helper" "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/conf" ) func init() { - flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") + // flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") } var sigUSR1 = syscall.Signal(0xa) @@ -40,7 +39,7 @@ func main() { // Configure user agent and updates. updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH) - helper.IntelOnly() + // helper.IntelOnly() // Set SPN public hub mode. conf.EnablePublicHub(true) diff --git a/cmds/notifier/.gitignore b/cmds/notifier/.gitignore deleted file mode 100644 index 602ad23c..00000000 --- a/cmds/notifier/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Compiled binaries -notifier -notifier.exe - -# Go vendor -vendor - -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out diff --git a/cmds/notifier/README.md b/cmds/notifier/README.md deleted file mode 100644 index bdfcece8..00000000 --- a/cmds/notifier/README.md +++ /dev/null @@ -1,5 +0,0 @@ -### Development Dependencies - -sudo apt install libgtk-3-dev libayatana-appindicator3-dev libwebkitgtk-3.0-dev libgl1-mesa-dev libglu1-mesa-dev libnotify-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev - -sudo pacman -S libappindicator-gtk3 diff --git a/cmds/notifier/http_api.go b/cmds/notifier/http_api.go deleted file mode 100644 index 7a68349a..00000000 --- a/cmds/notifier/http_api.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "strings" - "time" - - "github.com/safing/portmaster/base/log" -) - -const ( - apiBaseURL = "http://127.0.0.1:817/api/v1/" - apiShutdownEndpoint = "core/shutdown" -) - -var httpAPIClient *http.Client - -func init() { - // Make cookie jar. - jar, err := cookiejar.New(nil) - if err != nil { - log.Warningf("http-api: failed to create cookie jar: %s", err) - jar = nil - } - - // Create client. - httpAPIClient = &http.Client{ - Jar: jar, - Timeout: 3 * time.Second, - } -} - -func httpAPIAction(endpoint string) (response string, err error) { - // Make action request. - resp, err := httpAPIClient.Post(apiBaseURL+endpoint, "", nil) - if err != nil { - return "", fmt.Errorf("request failed: %w", err) - } - - // Read the response body. - defer func() { _ = resp.Body.Close() }() - respData, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read data: %w", err) - } - response = strings.TrimSpace(string(respData)) - - // Check if the request was successful on the server. - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return response, fmt.Errorf("server failed with %s: %s", resp.Status, response) - } - - return response, nil -} - -// TriggerShutdown triggers a shutdown via the APi. -func TriggerShutdown() error { - _, err := httpAPIAction(apiShutdownEndpoint) - return err -} diff --git a/cmds/notifier/icons.go b/cmds/notifier/icons.go deleted file mode 100644 index b3690a3f..00000000 --- a/cmds/notifier/icons.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "sync" - - icons "github.com/safing/portmaster/assets" -) - -var ( - appIconEnsureOnce sync.Once - appIconPath string -) - -func ensureAppIcon() (location string, err error) { - appIconEnsureOnce.Do(func() { - if appIconPath == "" { - appIconPath = filepath.Join(dataDir, "exec", "portmaster.png") - } - err = os.WriteFile(appIconPath, icons.PNG, 0o0644) // nolint:gosec - }) - - return appIconPath, err -} diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go deleted file mode 100644 index e40487bb..00000000 --- a/cmds/notifier/main.go +++ /dev/null @@ -1,287 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - "os" - "os/signal" - "path/filepath" - "runtime" - "runtime/pprof" - "strings" - "sync" - "syscall" - "time" - - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - dataDir string - printStackOnExit bool - showVersion bool - - apiClient = client.NewClient("127.0.0.1:817") - connected = abool.New() - shuttingDown = abool.New() - restarting = abool.New() - - mainCtx, cancelMainCtx = context.WithCancel(context.Background()) - mainWg = &sync.WaitGroup{} - - dataRoot *utils.DirStructure - // Create registry. - registry = &updater.ResourceRegistry{ - Name: "updates", - UpdateURLs: []string{ - "https://updates.safing.io", - }, - DevMode: false, - Online: false, // disable download of resources (this is job for the core). - } -) - -const query = "query " - -func init() { - flag.StringVar(&dataDir, "data", "", "set data directory") - flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") - flag.BoolVar(&showVersion, "version", false, "show version and exit") - - runtime.GOMAXPROCS(2) -} - -func main() { - // parse flags - flag.Parse() - - // set meta info - info.Set("Portmaster Notifier", "0.3.6", "GPLv3") - - // check if meta info is ok - err := info.CheckVersion() - if err != nil { - fmt.Println("compile error: please compile using the provided build script") - os.Exit(1) - } - - // print help - // if modules.HelpFlag { - // flag.Usage() - // os.Exit(0) - // } - - if showVersion { - fmt.Println(info.FullVersion()) - os.Exit(0) - } - - // auto detect - if dataDir == "" { - dataDir = detectDataDir() - } - - // check data dir - if dataDir == "" { - fmt.Fprintln(os.Stderr, "please set the data directory using --data=/path/to/data/dir") - os.Exit(1) - } - - // switch to safe exec dir - err = os.Chdir(filepath.Join(dataDir, "exec")) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to switch to safe exec dir: %s\n", err) - } - - // start log writer - err = log.Start() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to start logging: %s\n", err) - os.Exit(1) - } - - // load registry - err = configureRegistry(true) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to load registry: %s\n", err) - os.Exit(1) - } - - // connect to API - go apiClient.StayConnected() - go apiStatusMonitor() - - // start subsystems - go tray() - go subsystemsClient() - go spnStatusClient() - go notifClient() - go startShutdownEventListener() - - // Shutdown - // catch interrupt for clean shutdown - signalCh := make(chan os.Signal, 1) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // wait for shutdown - select { - case <-signalCh: - fmt.Println(" ") - log.Warning("program was interrupted, shutting down") - case <-mainCtx.Done(): - log.Warning("program is shutting down") - } - - if printStackOnExit { - fmt.Println("=== PRINTING STACK ===") - _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) - fmt.Println("=== END STACK ===") - } - go func() { - time.Sleep(10 * time.Second) - fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====") - _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) - os.Exit(1) - }() - - // clear all notifications - clearNotifications() - - // shutdown - cancelMainCtx() - mainWg.Wait() - - apiClient.Shutdown() - exitTray() - log.Shutdown() - - os.Exit(0) -} - -func apiStatusMonitor() { - for { - // Wait for connection. - <-apiClient.Online() - connected.Set() - triggerTrayUpdate() - - // Wait for lost connection. - <-apiClient.Offline() - connected.UnSet() - triggerTrayUpdate() - } -} - -func detectDataDir() string { - // get path of executable - binPath, err := os.Executable() - if err != nil { - return "" - } - // get directory - binDir := filepath.Dir(binPath) - // check if we in the updates directory - identifierDir := filepath.Join("updates", runtime.GOOS+"_"+runtime.GOARCH, "notifier") - // check if there is a match and return data dir - if strings.HasSuffix(binDir, identifierDir) { - return filepath.Clean(strings.TrimSuffix(binDir, identifierDir)) - } - return "" -} - -func configureRegistry(mustLoadIndex bool) error { - // If dataDir is not set, check the environment variable. - if dataDir == "" { - dataDir = os.Getenv("PORTMASTER_DATA") - } - - // If it's still empty, try to auto-detect it. - if dataDir == "" { - dataDir = detectInstallationDir() - } - - // Finally, if it's still empty, the user must provide it. - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // Remove left over quotes. - dataDir = strings.Trim(dataDir, `\"`) - // Initialize data root. - err := dataroot.Initialize(dataDir, 0o0755) - if err != nil { - return fmt.Errorf("failed to initialize data root: %w", err) - } - dataRoot = dataroot.Root() - - // Initialize registry. - err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755)) - if err != nil { - return err - } - - return updateRegistryIndex(mustLoadIndex) -} - -func detectInstallationDir() string { - exePath, err := filepath.Abs(os.Args[0]) - if err != nil { - return "" - } - - parent := filepath.Dir(exePath) // parent should be "...\updates\windows_amd64\notifier" - stableJSONFile := filepath.Join(parent, "..", "..", "stable.json") // "...\updates\stable.json" - stat, err := os.Stat(stableJSONFile) - if err != nil { - return "" - } - - if stat.IsDir() { - return "" - } - - return parent -} - -func updateRegistryIndex(mustLoadIndex bool) error { - // Set indexes based on the release channel. - warning := helper.SetIndexes(registry, "", false, false, false) - if warning != nil { - log.Warningf("%q", warning) - } - - // Load indexes from disk or network, if needed and desired. - err := registry.LoadIndexes(context.Background()) - if err != nil { - log.Warningf("error loading indexes %q", warning) - if mustLoadIndex { - return err - } - } - - // Load versions from disk to know which others we have and which are available. - err = registry.ScanStorage("") - if err != nil { - log.Warningf("error during storage scan: %q\n", err) - } - - registry.SelectVersions() - return nil -} diff --git a/cmds/notifier/notification.go b/cmds/notifier/notification.go deleted file mode 100644 index 0f6ded8d..00000000 --- a/cmds/notifier/notification.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - - pbnotify "github.com/safing/portmaster/base/notifications" -) - -// Notification represents a notification that is to be delivered to the user. -type Notification struct { - pbnotify.Notification - - // systemID holds the ID returned by the dbus interface on Linux or by WinToast library on Windows. - systemID NotificationID -} - -// IsSupportedAction returns whether the action is supported on this system. -func IsSupportedAction(a pbnotify.Action) bool { - switch a.Type { - case pbnotify.ActionTypeNone: - return true - default: - return false - } -} - -// SelectAction sends an action back to the portmaster. -func (n *Notification) SelectAction(action string) { - upd := &pbnotify.Notification{ - EventID: n.EventID, - SelectedActionID: action, - } - - _ = apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, upd.EventID), upd, nil) -} diff --git a/cmds/notifier/notify.go b/cmds/notifier/notify.go deleted file mode 100644 index 48e117c0..00000000 --- a/cmds/notifier/notify.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - pbnotify "github.com/safing/portmaster/base/notifications" - "github.com/safing/structures/dsd" -) - -const ( - dbNotifBasePath = "notifications:all/" -) - -var ( - notifications = make(map[string]*Notification) - notificationsLock sync.Mutex -) - -func notifClient() { - notifOp := apiClient.Qsub(fmt.Sprintf("query %s where ShowOnSystem is true", dbNotifBasePath), handleNotification) - notifOp.EnableResuscitation() - - // start the action listener and block - // until it's closed. - actionListener() -} - -func handleNotification(m *client.Message) { - notificationsLock.Lock() - defer notificationsLock.Unlock() - - log.Tracef("received %s msg: %s", m.Type, m.Key) - - switch m.Type { - case client.MsgError: - case client.MsgDone: - case client.MsgSuccess: - case client.MsgOk, client.MsgUpdate, client.MsgNew: - - n := &Notification{} - _, err := dsd.Load(m.RawValue, &n.Notification) - if err != nil { - log.Warningf("notify: failed to parse new notification: %s", err) - return - } - - // copy existing system values - existing, ok := notifications[n.EventID] - if ok { - existing.Lock() - n.systemID = existing.systemID - existing.Unlock() - } - - // save - notifications[n.EventID] = n - - // Handle notification. - switch { - case existing != nil: - // Cancel existing notification if not active, else ignore. - if n.State != pbnotify.Active { - existing.Cancel() - } - return - case n.State == pbnotify.Active: - // Show new notifications that are active. - n.Show() - default: - // Ignore new notifications that are not active. - } - - case client.MsgDelete: - - n, ok := notifications[strings.TrimPrefix(m.Key, dbNotifBasePath)] - if ok { - n.Cancel() - delete(notifications, n.EventID) - } - - case client.MsgWarning: - case client.MsgOffline: - } -} - -func clearNotifications() { - notificationsLock.Lock() - defer notificationsLock.Unlock() - - for _, n := range notifications { - n.Cancel() - } - - // Wait for goroutines that cancel notifications. - // TODO: Revamp to use a waitgroup. - time.Sleep(1 * time.Second) -} diff --git a/cmds/notifier/notify_linux.go b/cmds/notifier/notify_linux.go deleted file mode 100644 index 80cc8e15..00000000 --- a/cmds/notifier/notify_linux.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "context" - "errors" - "sync" - - notify "github.com/dhaavi/go-notify" - - "github.com/safing/portmaster/base/log" -) - -type NotificationID uint32 - -var ( - capabilities notify.Capabilities - notifsByID sync.Map -) - -func init() { - var err error - capabilities, err = notify.GetCapabilities() - if err != nil { - log.Errorf("failed to get notification system capabilities: %s", err) - } -} - -func handleActions(ctx context.Context, actions chan notify.Signal) { - mainWg.Add(1) - defer mainWg.Done() - -listenForNotifications: - for { - select { - case <-ctx.Done(): - return - case sig := <-actions: - if sig.Name != "org.freedesktop.Notifications.ActionInvoked" { - // we don't care for anything else (dismissed, closed) - continue listenForNotifications - } - - // get notification by system ID - n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID)) - - if !ok { - continue listenForNotifications - } - - notification, ok := n.(*Notification) - if !ok { - log.Errorf("received invalid notification type %T", n) - - continue listenForNotifications - } - - log.Tracef("notify: received signal: %+v", sig) - if sig.ActionKey != "" { - // send action - if ok { - notification.Lock() - notification.SelectAction(sig.ActionKey) - notification.Unlock() - } - } else { - log.Tracef("notify: notification clicked: %+v", sig) - // Global action invoked, start the app - launchApp() - } - } - } -} - -func actionListener() { - actions := make(chan notify.Signal, 100) - - go handleActions(mainCtx, actions) - - err := notify.SignalNotify(mainCtx, actions) - if err != nil && errors.Is(err, context.Canceled) { - log.Errorf("notify: signal listener failed: %s", err) - } -} - -// Show shows the notification. -func (n *Notification) Show() { - sysN := notify.NewNotification("Portmaster", n.Message) - // see https://developer.gnome.org/notification-spec/ - - // The optional name of the application sending the notification. - // Can be blank. - sysN.AppName = "Portmaster" - - // The optional notification ID that this notification replaces. - sysN.ReplacesID = uint32(n.systemID) - - // The optional program icon of the calling application. - // sysN.AppIcon string - - // The summary text briefly describing the notification. - // Summary string (arg 1) - - // The optional detailed body text. - // Body string (arg 2) - - // The actions send a request message back to the notification client - // when invoked. - // sysN.Actions []string - if capabilities.Actions { - sysN.Actions = make([]string, 0, len(n.AvailableActions)*2) - for _, action := range n.AvailableActions { - if IsSupportedAction(*action) { - sysN.Actions = append(sysN.Actions, action.ID) - sysN.Actions = append(sysN.Actions, action.Text) - } - } - } - - // Set Portmaster icon. - iconLocation, err := ensureAppIcon() - if err != nil { - log.Warningf("notify: failed to write icon: %s", err) - } - sysN.AppIcon = iconLocation - - // TODO: Use hints to display icon of affected app. - // Hints are a way to provide extra data to a notification server. - // sysN.Hints = make(map[string]interface{}) - - // The timeout time in milliseconds since the display of the - // notification at which the notification should automatically close. - // sysN.Timeout int32 - - newID, err := sysN.Show() - if err != nil { - log.Warningf("notify: failed to show notification %s", n.EventID) - return - } - - notifsByID.Store(NotificationID(newID), n) - - n.Lock() - defer n.Unlock() - n.systemID = NotificationID(newID) -} - -// Cancel cancels the notification. -func (n *Notification) Cancel() { - n.Lock() - defer n.Unlock() - - // TODO: could a ID of 0 be valid? - if n.systemID != 0 { - err := notify.CloseNotification(uint32(n.systemID)) - if err != nil { - log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID) - } - notifsByID.Delete(n.systemID) - } -} diff --git a/cmds/notifier/notify_windows.go b/cmds/notifier/notify_windows.go deleted file mode 100644 index 98cf987a..00000000 --- a/cmds/notifier/notify_windows.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "fmt" - "sync" - - "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/cmds/notifier/wintoast" - "github.com/safing/portmaster/service/updates/helper" -) - -type NotificationID int64 - -const ( - appName = "Portmaster" - appUserModelID = "io.safing.portmaster.2" - originalShortcutPath = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Portmaster\\Portmaster.lnk" -) - -const ( - SoundDefault = 0 - SoundSilent = 1 - SoundLoop = 2 -) - -const ( - SoundPathDefault = 0 - // see notification_glue.h if you need more types -) - -var ( - initOnce sync.Once - lib *wintoast.WinToast - notificationsByIDs sync.Map -) - -func getLib() *wintoast.WinToast { - initOnce.Do(func() { - dllPath, err := getDllPath() - if err != nil { - log.Errorf("notify: failed to get dll path: %s", err) - return - } - // Load dll and all the functions - newLib, err := wintoast.New(dllPath) - if err != nil { - log.Errorf("notify: failed to load library: %s", err) - return - } - - // Initialize. This will create or update application shortcut. C:\Users\\AppData\Roaming\Microsoft\Windows\Start Menu\Programs - // and it will be of the originalShortcutPath with no CLSID and different AUMI - err = newLib.Initialize(appName, appUserModelID, originalShortcutPath) - if err != nil { - log.Errorf("notify: failed to load library: %s", err) - return - } - - // library was initialized successfully - lib = newLib - - // Set callbacks - - err = lib.SetCallbacks(notificationActivatedCallback, notificationDismissedCallback, notificationDismissedCallback) - if err != nil { - log.Warningf("notify: failed to set callbacks: %s", err) - return - } - }) - - return lib -} - -// Show shows the notification. -func (n *Notification) Show() { - // Lock notification - n.Lock() - defer n.Unlock() - - // Create new notification object - builder, err := getLib().NewNotification(n.Title, n.Message) - if err != nil { - log.Errorf("notify: failed to create notification: %s", err) - return - } - // Make sure memory is freed when done - defer builder.Delete() - - // if needed set notification icon - // _ = builder.SetImage(iconLocation) - - // Leaving the default value for the sound - // _ = builder.SetSound(SoundDefault, SoundPathDefault) - - // Set all the required actions. - for _, action := range n.AvailableActions { - err = builder.AddButton(action.Text) - if err != nil { - log.Warningf("notify: failed to add button: %s", err) - } - } - - // Show notification. - id, err := builder.Show() - if err != nil { - log.Errorf("notify: failed to show notification: %s", err) - return - } - n.systemID = NotificationID(id) - - // Link system id to the notification object - notificationsByIDs.Store(NotificationID(id), n) - - log.Debugf("notify: showing notification %q: %d", n.Title, n.systemID) -} - -// Cancel cancels the notification. -func (n *Notification) Cancel() { - // Lock notification - n.Lock() - defer n.Unlock() - - // No need to check for errors. If it fails it is probably already dismissed - _ = getLib().HideNotification(int64(n.systemID)) - - notificationsByIDs.Delete(n.systemID) - log.Debugf("notify: notification canceled %q: %d", n.Title, n.systemID) -} - -func notificationActivatedCallback(id int64, actionIndex int32) { - if actionIndex == -1 { - // The user clicked on the notification (not a button), open the portmaster and delete - launchApp() - notificationsByIDs.Delete(NotificationID(id)) - log.Debugf("notify: notification clicked %d", id) - return - } - - // The user click one of the buttons - - // Get notified object - n, ok := notificationsByIDs.LoadAndDelete(NotificationID(id)) - if !ok { - return - } - - notification := n.(*Notification) - - notification.Lock() - defer notification.Unlock() - - // Set selected action - actionID := notification.AvailableActions[actionIndex].ID - notification.SelectAction(actionID) - - log.Debugf("notify: notification button cliecked %d button id: %d", id, actionIndex) -} - -func notificationDismissedCallback(id int64, reason int32) { - // Failure or user dismissed the notification - if reason == 0 { - notificationsByIDs.Delete(NotificationID(id)) - log.Debugf("notify: notification dissmissed %d", id) - } -} - -func getDllPath() (string, error) { - if dataDir == "" { - return "", fmt.Errorf("dataDir is empty") - } - - // Aks the registry for the dll path - identifier := helper.PlatformIdentifier("notifier/portmaster-wintoast.dll") - file, err := registry.GetFile(identifier) - if err != nil { - return "", err - } - return file.Path(), nil -} - -func actionListener() { - // initialize the library - _ = getLib() -} diff --git a/cmds/notifier/shutdown.go b/cmds/notifier/shutdown.go deleted file mode 100644 index 70b2e6d8..00000000 --- a/cmds/notifier/shutdown.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" -) - -func startShutdownEventListener() { - shutdownNotifOp := apiClient.Sub("query runtime:modules/core/event/shutdown", handleShutdownEvent) - shutdownNotifOp.EnableResuscitation() - - restartNotifOp := apiClient.Sub("query runtime:modules/core/event/restart", handleRestartEvent) - restartNotifOp.EnableResuscitation() -} - -func handleShutdownEvent(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - shuttingDown.Set() - triggerTrayUpdate() - - log.Warningf("shutdown: received shutdown event, shutting down now") - - // wait for the API client connection to die - <-apiClient.Offline() - shuttingDown.UnSet() - - cancelMainCtx() - - case client.MsgWarning, client.MsgError: - log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) - } -} - -func handleRestartEvent(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - restarting.Set() - triggerTrayUpdate() - - log.Warningf("restart: received restart event") - - // wait for the API client connection to die - <-apiClient.Offline() - restarting.UnSet() - triggerTrayUpdate() - case client.MsgWarning, client.MsgError: - log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) - } -} diff --git a/cmds/notifier/snoretoast-guid.patch b/cmds/notifier/snoretoast-guid.patch deleted file mode 100644 index 1a050e5f..00000000 --- a/cmds/notifier/snoretoast-guid.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 498226a..446ba5e 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.4) - - project(snoretoast VERSION 0.6.0) - # Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID --set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303) -+#We keep it fixed! -+set(SNORETOAST_CALLBACK_GUID 7F00FB48-65D5-4BA8-A35B-F194DA7E1A51) -+ - - set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/) - diff --git a/cmds/notifier/spn.go b/cmds/notifier/spn.go deleted file mode 100644 index 0b49d63c..00000000 --- a/cmds/notifier/spn.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "sync" - "time" - - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - "github.com/safing/structures/dsd" -) - -const ( - spnModuleKey = "config:spn/enable" - spnStatusKey = "runtime:spn/status" -) - -var ( - spnEnabled = abool.New() - - spnStatusCache *SPNStatus - spnStatusCacheLock sync.Mutex -) - -// SPNStatus holds SPN status information. -type SPNStatus struct { - Status string - HomeHubID string - HomeHubName string - ConnectedIP string - ConnectedTransport string - ConnectedSince *time.Time -} - -// GetSPNStatus returns the SPN status. -func GetSPNStatus() *SPNStatus { - spnStatusCacheLock.Lock() - defer spnStatusCacheLock.Unlock() - - return spnStatusCache -} - -func updateSPNStatus(s *SPNStatus) { - spnStatusCacheLock.Lock() - defer spnStatusCacheLock.Unlock() - - spnStatusCache = s -} - -func spnStatusClient() { - moduleQueryOp := apiClient.Qsub(query+spnModuleKey, handleSPNModuleUpdate) - moduleQueryOp.EnableResuscitation() - - statusQueryOp := apiClient.Qsub(query+spnStatusKey, handleSPNStatusUpdate) - statusQueryOp.EnableResuscitation() -} - -func handleSPNModuleUpdate(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - var cfg struct { - Value bool `json:"Value"` - } - _, err := dsd.Load(m.RawValue, &cfg) - if err != nil { - log.Warningf("config: failed to parse config: %s", err) - return - } - log.Infof("config: received update to SPN module: enabled=%v", cfg.Value) - - spnEnabled.SetTo(cfg.Value) - triggerTrayUpdate() - - default: - } -} - -func handleSPNStatusUpdate(m *client.Message) { - switch m.Type { - case client.MsgOk, client.MsgUpdate, client.MsgNew: - newStatus := &SPNStatus{} - _, err := dsd.Load(m.RawValue, newStatus) - if err != nil { - log.Warningf("config: failed to parse config: %s", err) - return - } - log.Infof("config: received update to SPN status: %+v", newStatus) - - updateSPNStatus(newStatus) - triggerTrayUpdate() - - default: - } -} - -func ToggleSPN() { - var cfg struct { - Value bool `json:"Value"` - } - cfg.Value = !spnEnabled.IsSet() - - apiClient.Update(spnModuleKey, &cfg, nil) -} diff --git a/cmds/notifier/subsystems.go b/cmds/notifier/subsystems.go deleted file mode 100644 index 587d6d84..00000000 --- a/cmds/notifier/subsystems.go +++ /dev/null @@ -1,121 +0,0 @@ -package main - -import ( - "sync" - - "github.com/safing/portmaster/base/api/client" - "github.com/safing/portmaster/base/log" - "github.com/safing/structures/dsd" -) - -const ( - subsystemsKeySpace = "runtime:subsystems/" - - // Module Failure Status Values - // FailureNone = 0 // unused - // FailureHint = 1 // unused. - FailureWarning = 2 - FailureError = 3 -) - -var ( - subsystems = make(map[string]*Subsystem) - subsystemsLock sync.Mutex -) - -// Subsystem describes a subset of modules that represent a part of a -// service or program to the user. Subsystems can be (de-)activated causing -// all related modules to be brought down or up. -type Subsystem struct { //nolint:maligned // not worth the effort - // ID is a unique identifier for the subsystem. - ID string - - // Name holds a human readable name of the subsystem. - Name string - - // Description may holds an optional description of - // the subsystem's purpose. - Description string - - // Modules contains all modules that are related to the subsystem. - // Note that this slice also contains a reference to the subsystem - // module itself. - Modules []*ModuleStatus - - // FailureStatus is the worst failure status that is currently - // set in one of the subsystem's dependencies. - FailureStatus uint8 -} - -// ModuleStatus describes the status of a module. -type ModuleStatus struct { - Name string - Enabled bool - Status uint8 - FailureStatus uint8 - FailureID string - FailureMsg string -} - -// GetFailure returns the worst of all subsystem failures. -func GetFailure() (failureStatus uint8, failureMsg string) { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - for _, subsystem := range subsystems { - for _, module := range subsystem.Modules { - if failureStatus < module.FailureStatus { - failureStatus = module.FailureStatus - failureMsg = module.FailureMsg - } - } - } - - return -} - -func updateSubsystem(s *Subsystem) { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - subsystems[s.ID] = s -} - -func clearSubsystems() { - subsystemsLock.Lock() - defer subsystemsLock.Unlock() - - for key := range subsystems { - delete(subsystems, key) - } -} - -func subsystemsClient() { - subsystemsOp := apiClient.Qsub("query "+subsystemsKeySpace, handleSubsystem) - subsystemsOp.EnableResuscitation() -} - -func handleSubsystem(m *client.Message) { - switch m.Type { - case client.MsgError: - case client.MsgDone: - case client.MsgSuccess: - case client.MsgOk, client.MsgUpdate, client.MsgNew: - - newSubsystem := &Subsystem{} - _, err := dsd.Load(m.RawValue, newSubsystem) - if err != nil { - log.Warningf("subsystems: failed to parse new subsystem: %s", err) - return - } - updateSubsystem(newSubsystem) - triggerTrayUpdate() - - case client.MsgDelete: - case client.MsgWarning: - case client.MsgOffline: - - clearSubsystems() - - } -} diff --git a/cmds/notifier/tray.go b/cmds/notifier/tray.go deleted file mode 100644 index abdf48d5..00000000 --- a/cmds/notifier/tray.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "flag" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "fyne.io/systray" - - icons "github.com/safing/portmaster/assets" - "github.com/safing/portmaster/base/log" -) - -const ( - shortenStatusMsgTo = 40 -) - -var ( - trayLock sync.Mutex - - scaleColoredIconsTo int - - activeIconID int = -1 - activeStatusMsg = "" - activeSPNStatus = "" - activeSPNSwitch = "" - - menuItemStatusMsg *systray.MenuItem - menuItemSPNStatus *systray.MenuItem - menuItemSPNSwitch *systray.MenuItem -) - -func init() { - flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels") - - // lock until ready - trayLock.Lock() -} - -func tray() { - if scaleColoredIconsTo > 0 { - icons.ScaleColoredIconsTo(scaleColoredIconsTo) - } - - systray.Run(onReady, onExit) -} - -func exitTray() { - systray.Quit() -} - -func onReady() { - // unlock when ready - defer trayLock.Unlock() - - // icon - systray.SetIcon(icons.ColoredIcons[icons.RedID]) - if runtime.GOOS == "windows" { - // systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu) - systray.SetTooltip("Portmaster Notifier") - } - - // menu: open app - if dataDir != "" { - menuItemOpenApp := systray.AddMenuItem("Open App", "") - go clickListener(menuItemOpenApp, launchApp) - systray.AddSeparator() - } - - // menu: status - - menuItemStatusMsg = systray.AddMenuItem("Loading...", "") - menuItemStatusMsg.Disable() - systray.AddSeparator() - - // menu: SPN - - menuItemSPNStatus = systray.AddMenuItem("Loading...", "") - menuItemSPNStatus.Disable() - menuItemSPNSwitch = systray.AddMenuItem("Loading...", "") - go clickListener(menuItemSPNSwitch, func() { - ToggleSPN() - }) - systray.AddSeparator() - - // menu: quit - systray.AddSeparator() - closeTray := systray.AddMenuItem("Close Tray Notifier", "") - go clickListener(closeTray, func() { - cancelMainCtx() - }) - shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "") - go clickListener(shutdownPortmaster, func() { - _ = TriggerShutdown() - time.Sleep(1 * time.Second) - cancelMainCtx() - }) -} - -func onExit() { -} - -func triggerTrayUpdate() { - // TODO: Deduplicate triggers. - go updateTray() -} - -// updateTray update the state of the tray depending on the currently available information. -func updateTray() { - // Get current information. - spnStatus := GetSPNStatus() - failureID, failureMsg := GetFailure() - - trayLock.Lock() - defer trayLock.Unlock() - - // Select icon and status message to show. - newIconID := icons.GreenID - newStatusMsg := "Secure" - switch { - case shuttingDown.IsSet(): - newIconID = icons.RedID - newStatusMsg = "Shutting Down Portmaster" - - case restarting.IsSet(): - newIconID = icons.YellowID - newStatusMsg = "Restarting Portmaster" - - case !connected.IsSet(): - newIconID = icons.RedID - newStatusMsg = "Waiting for Portmaster Core Service" - - case failureID == FailureError: - newIconID = icons.RedID - newStatusMsg = failureMsg - - case failureID == FailureWarning: - newIconID = icons.YellowID - newStatusMsg = failureMsg - - case spnEnabled.IsSet(): - newIconID = icons.BlueID - } - - // Set icon if changed. - if newIconID != activeIconID { - activeIconID = newIconID - systray.SetIcon(icons.ColoredIcons[activeIconID]) - } - - // Set message if changed. - if newStatusMsg != activeStatusMsg { - activeStatusMsg = newStatusMsg - - // Shorten message if too long. - shortenedMsg := activeStatusMsg - if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") { - shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0] - } - if len(shortenedMsg) > shortenStatusMsgTo { - shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..." - } - - menuItemStatusMsg.SetTitle("Status: " + shortenedMsg) - } - - // Set SPN status if changed. - if spnStatus != nil && activeSPNStatus != spnStatus.Status { - activeSPNStatus = spnStatus.Status - menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck - } - - // Set SPN switch if changed. - newSPNSwitch := "Enable SPN" - if spnEnabled.IsSet() { - newSPNSwitch = "Disable SPN" - } - if activeSPNSwitch != newSPNSwitch { - activeSPNSwitch = newSPNSwitch - menuItemSPNSwitch.SetTitle(activeSPNSwitch) - } -} - -func clickListener(item *systray.MenuItem, fn func()) { - for range item.ClickedCh { - fn() - } -} - -func launchApp() { - // build path to app - pmStartPath := filepath.Join(dataDir, "portmaster-start") - if runtime.GOOS == "windows" { - pmStartPath += ".exe" - } - - // start app - cmd := exec.Command(pmStartPath, "app", "--data", dataDir) - err := cmd.Start() - if err != nil { - log.Warningf("failed to start app: %s", err) - return - } - - // Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources. - // See https://github.com/golang/go/issues/36534 - go func() { - err := cmd.Wait() - if err != nil { - log.Warningf("failed to wait/release app process: %s", err) - } - }() -} diff --git a/cmds/notifier/wintoast/notification_builder.go b/cmds/notifier/wintoast/notification_builder.go deleted file mode 100644 index 89eca798..00000000 --- a/cmds/notifier/wintoast/notification_builder.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build windows - -package wintoast - -import ( - "unsafe" - - "golang.org/x/sys/windows" -) - -type NotificationBuilder struct { - templatePointer uintptr - lib *WinToast -} - -func newNotification(lib *WinToast, title string, message string) (*NotificationBuilder, error) { - lib.Lock() - defer lib.Unlock() - - titleUTF, _ := windows.UTF16PtrFromString(title) - messageUTF, _ := windows.UTF16PtrFromString(message) - titleP := unsafe.Pointer(titleUTF) - messageP := unsafe.Pointer(messageUTF) - - ptr, _, err := lib.createNotification.Call(uintptr(titleP), uintptr(messageP)) - if ptr == 0 { - return nil, err - } - - return &NotificationBuilder{ptr, lib}, nil -} - -func (n *NotificationBuilder) Delete() { - if n == nil { - return - } - - n.lib.Lock() - defer n.lib.Unlock() - - _, _, _ = n.lib.deleteNotification.Call(n.templatePointer) -} - -func (n *NotificationBuilder) AddButton(text string) error { - n.lib.Lock() - defer n.lib.Unlock() - textUTF, _ := windows.UTF16PtrFromString(text) - textP := unsafe.Pointer(textUTF) - - rc, _, err := n.lib.addButton.Call(n.templatePointer, uintptr(textP)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) SetImage(iconPath string) error { - n.lib.Lock() - defer n.lib.Unlock() - pathUTF, _ := windows.UTF16PtrFromString(iconPath) - pathP := unsafe.Pointer(pathUTF) - - rc, _, err := n.lib.setImage.Call(n.templatePointer, uintptr(pathP)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) SetSound(option int, path int) error { - n.lib.Lock() - defer n.lib.Unlock() - - rc, _, err := n.lib.setSound.Call(n.templatePointer, uintptr(option), uintptr(path)) - if rc != 1 { - return err - } - return nil -} - -func (n *NotificationBuilder) Show() (int64, error) { - n.lib.Lock() - defer n.lib.Unlock() - - id, _, err := n.lib.showNotification.Call(n.templatePointer) - if int64(id) == -1 { - return -1, err - } - return int64(id), nil -} diff --git a/cmds/notifier/wintoast/wintoast.go b/cmds/notifier/wintoast/wintoast.go deleted file mode 100644 index 5d9a3380..00000000 --- a/cmds/notifier/wintoast/wintoast.go +++ /dev/null @@ -1,217 +0,0 @@ -//go:build windows - -package wintoast - -import ( - "fmt" - "sync" - "unsafe" - - "github.com/tevino/abool" - - "golang.org/x/sys/windows" -) - -// WinNotify holds the DLL handle. -type WinToast struct { - sync.RWMutex - - dll *windows.DLL - - initialized *abool.AtomicBool - - initialize *windows.Proc - isInitialized *windows.Proc - createNotification *windows.Proc - deleteNotification *windows.Proc - addButton *windows.Proc - setImage *windows.Proc - setSound *windows.Proc - showNotification *windows.Proc - hideNotification *windows.Proc - setActivatedCallback *windows.Proc - setDismissedCallback *windows.Proc - setFailedCallback *windows.Proc -} - -func New(dllPath string) (*WinToast, error) { - if dllPath == "" { - return nil, fmt.Errorf("winnotifiy: path to dll not specified") - } - - libraryObject := &WinToast{} - libraryObject.initialized = abool.New() - - // load dll - var err error - libraryObject.dll, err = windows.LoadDLL(dllPath) - if err != nil { - return nil, fmt.Errorf("winnotifiy: failed to load notifier dll %w", err) - } - - // load functions - libraryObject.initialize, err = libraryObject.dll.FindProc("PortmasterToastInitialize") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastInitialize not found %w", err) - } - - libraryObject.isInitialized, err = libraryObject.dll.FindProc("PortmasterToastIsInitialized") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastIsInitialized not found %w", err) - } - - libraryObject.createNotification, err = libraryObject.dll.FindProc("PortmasterToastCreateNotification") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastCreateNotification not found %w", err) - } - - libraryObject.deleteNotification, err = libraryObject.dll.FindProc("PortmasterToastDeleteNotification") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastDeleteNotification not found %w", err) - } - - libraryObject.addButton, err = libraryObject.dll.FindProc("PortmasterToastAddButton") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastAddButton not found %w", err) - } - - libraryObject.setImage, err = libraryObject.dll.FindProc("PortmasterToastSetImage") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastSetImage not found %w", err) - } - - libraryObject.setSound, err = libraryObject.dll.FindProc("PortmasterToastSetSound") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastSetSound not found %w", err) - } - - libraryObject.showNotification, err = libraryObject.dll.FindProc("PortmasterToastShow") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastShow not found %w", err) - } - - libraryObject.setActivatedCallback, err = libraryObject.dll.FindProc("PortmasterToastActivatedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterActivatedCallback not found %w", err) - } - - libraryObject.setDismissedCallback, err = libraryObject.dll.FindProc("PortmasterToastDismissedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastDismissedCallback not found %w", err) - } - - libraryObject.setFailedCallback, err = libraryObject.dll.FindProc("PortmasterToastFailedCallback") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastFailedCallback not found %w", err) - } - - libraryObject.hideNotification, err = libraryObject.dll.FindProc("PortmasterToastHide") - if err != nil { - return nil, fmt.Errorf("winnotifiy: PortmasterToastHide not found %w", err) - } - - return libraryObject, nil -} - -func (lib *WinToast) Initialize(appName, aumi, originalShortcutPath string) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - lib.Lock() - defer lib.Unlock() - - // Initialize all necessary string for the notification meta data - appNameUTF, _ := windows.UTF16PtrFromString(appName) - aumiUTF, _ := windows.UTF16PtrFromString(aumi) - linkUTF, _ := windows.UTF16PtrFromString(originalShortcutPath) - - // They are needed as unsafe pointers - appNameP := unsafe.Pointer(appNameUTF) - aumiP := unsafe.Pointer(aumiUTF) - linkP := unsafe.Pointer(linkUTF) - - // Initialize notifications - rc, _, err := lib.initialize.Call(uintptr(appNameP), uintptr(aumiP), uintptr(linkP)) - if rc != 0 { - return fmt.Errorf("wintoast: failed to initialize library rc = %d, %w", rc, err) - } - - // Check if if the initialization was successfully - rc, _, _ = lib.isInitialized.Call() - if rc == 1 { - lib.initialized.Set() - } else { - return fmt.Errorf("wintoast: initialized flag was not set: rc = %d", rc) - } - - return nil -} - -func (lib *WinToast) SetCallbacks(activated func(id int64, actionIndex int32), dismissed func(id int64, reason int32), failed func(id int64, reason int32)) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - if lib.initialized.IsNotSet() { - return fmt.Errorf("winnotifiy: library not initialized") - } - - // Initialize notification activated callback - callback := windows.NewCallback(func(id int64, actionIndex int32) uint64 { - activated(id, actionIndex) - return 0 - }) - rc, _, err := lib.setActivatedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize activated callback %w", err) - } - - // Initialize notification dismissed callback - callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { - dismissed(id, actionIndex) - return 0 - }) - rc, _, err = lib.setDismissedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize dismissed callback %w", err) - } - - // Initialize notification failed callback - callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { - failed(id, actionIndex) - return 0 - }) - rc, _, err = lib.setFailedCallback.Call(callback) - if rc != 1 { - return fmt.Errorf("winnotifiy: failed to initialize failed callback %s", err) - } - - return nil -} - -// NewNotification starts a creation of new notification. NotificationBuilder.Delete should allays be called when done using the object or there will be memory leeks -func (lib *WinToast) NewNotification(title string, content string) (*NotificationBuilder, error) { - if lib == nil { - return nil, fmt.Errorf("wintoast: lib object was nil") - } - return newNotification(lib, title, content) -} - -// HideNotification hides notification -func (lib *WinToast) HideNotification(id int64) error { - if lib == nil { - return fmt.Errorf("wintoast: lib object was nil") - } - - lib.Lock() - defer lib.Unlock() - - rc, _, _ := lib.hideNotification.Call(uintptr(id)) - - if rc != 1 { - return fmt.Errorf("wintoast: failed to hide notification %d", id) - } - - return nil -} diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go index 0a96df6e..1cb9bafd 100644 --- a/cmds/observation-hub/main.go +++ b/cmds/observation-hub/main.go @@ -19,7 +19,6 @@ import ( "github.com/safing/portmaster/base/metrics" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/updates" - "github.com/safing/portmaster/service/updates/helper" "github.com/safing/portmaster/spn" "github.com/safing/portmaster/spn/captain" "github.com/safing/portmaster/spn/conf" @@ -38,7 +37,6 @@ func main() { // Configure user agent and updates. updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH) - helper.IntelOnly() // Configure SPN mode. conf.EnableClient(true) diff --git a/cmds/portmaster-start/.gitignore b/cmds/portmaster-start/.gitignore deleted file mode 100644 index e3db7ed6..00000000 --- a/cmds/portmaster-start/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# binaries -portmaster-start -portmaster-start.exe - -# test dir -test diff --git a/cmds/portmaster-start/build b/cmds/portmaster-start/build deleted file mode 100755 index 38c552f5..00000000 --- a/cmds/portmaster-start/build +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# get build data -if [[ "$BUILD_COMMIT" == "" ]]; then - BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) -fi -if [[ "$BUILD_USER" == "" ]]; then - BUILD_USER=$(id -un) -fi -if [[ "$BUILD_HOST" == "" ]]; then - BUILD_HOST=$(hostname -f) -fi -if [[ "$BUILD_DATE" == "" ]]; then - BUILD_DATE=$(date +%d.%m.%Y) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) -fi -BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /ยง/g") - -# check -if [[ "$BUILD_COMMIT" == "" ]]; then - echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_USER" == "" ]]; then - echo "could not automatically determine BUILD_USER, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_HOST" == "" ]]; then - echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_DATE" == "" ]]; then - echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." - exit 1 -fi - -# set build options -export CGO_ENABLED=0 - -# special handling for Windows -EXTRA_LD_FLAGS="" -if [[ $GOOS == "windows" ]]; then - # checks - if [[ $CC_FOR_windows_amd64 == "" ]]; then - echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64" - exit 1 - fi - if [[ $CXX_FOR_windows_amd64 == "" ]]; then - echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64" - exit 1 - fi - # compilers - export CC=$CC_FOR_windows_amd64 - export CXX=$CXX_FOR_windows_amd64 - # custom - export CGO_ENABLED=1 - EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available) - # generate resource.syso for windows metadata / icon - go generate -fi - -echo "Please notice, that this build script includes metadata into the build." -echo "This information is useful for debugging and license compliance." -echo "Run the compiled binary with the -version flag to see the information included." - -# build -BUILD_PATH="github.com/safing/portmaster/base/info" -go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@" diff --git a/cmds/portmaster-start/console_default.go b/cmds/portmaster-start/console_default.go deleted file mode 100644 index f11a9fae..00000000 --- a/cmds/portmaster-start/console_default.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package main - -import "os/exec" - -func attachToParentConsole() (attached bool, err error) { - return true, nil -} - -func hideWindow(cmd *exec.Cmd) {} diff --git a/cmds/portmaster-start/console_windows.go b/cmds/portmaster-start/console_windows.go deleted file mode 100644 index 1148d90e..00000000 --- a/cmds/portmaster-start/console_windows.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -// Parts of this file are FORKED -// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go -// on 16.07.2019 -// with Apache-2.0 license -// authored by https://github.com/apenwarr - -// docs/sources: -// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld -// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole - -import ( - "log" - "os" - "os/exec" - "syscall" - - "golang.org/x/sys/windows" -) - -const ( - windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1 -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procAttachConsole = kernel32.NewProc("AttachConsole") -) - -// Windows console output is a mess. -// -// If you compile as "-H windows", then if you launch your program without -// a console, Windows forcibly creates one to use as your stdin/stdout, which -// is silly for a GUI app, so we can't do that. -// -// If you compile as "-H windowsgui", then it doesn't create a console for -// your app... but also doesn't provide a working stdin/stdout/stderr even if -// you *did* launch from the console. However, you can use AttachConsole() -// to get a handle to your parent process's console, if any, and then -// os.NewFile() to turn that handle into a fd usable as stdout/stderr. -// -// However, then you have the problem that if you redirect stdout or stderr -// from the shell, you end up ignoring the redirection by forcing it to the -// console. -// -// To fix *that*, we have to detect whether there was a pre-existing stdout -// or not. We can check GetStdHandle(), which returns 0 for "should be -// console" and nonzero for "already pointing at a file." -// -// Be careful though! As soon as you run AttachConsole(), it resets *all* -// the GetStdHandle() handles to point them at the console instead, thus -// throwing away the original file redirects. So we have to GetStdHandle() -// *before* AttachConsole(). -// -// For some reason, powershell redirections provide a valid file handle, but -// writing to that handle doesn't write to the file. I haven't found a way -// to work around that. (Windows 10.0.17763.379) -// -// Net result is as follows. -// Before: -// SHELL NON-REDIRECTED REDIRECTED -// explorer.exe no console n/a -// cmd.exe broken works -// powershell broken broken -// WSL bash broken works -// After -// SHELL NON-REDIRECTED REDIRECTED -// explorer.exe no console n/a -// cmd.exe works works -// powershell works broken -// WSL bash works works -// -// We don't seem to make anything worse, at least. -func attachToParentConsole() (attached bool, err error) { - // get std handles before we attempt to attach to parent console - stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) - stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) - stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) - - // attempt to attach to parent console - err = procAttachConsole.Find() - if err != nil { - return false, err - } - r1, _, _ := procAttachConsole.Call(windowsAttachParentProcess) - if r1 == 0 { - // possible errors: - // ERROR_ACCESS_DENIED: already attached to console - // ERROR_INVALID_HANDLE: process does not have console - // ERROR_INVALID_PARAMETER: process does not exist - return false, nil - } - - // get std handles after we attached to console - var invalid syscall.Handle - con := invalid - - if stdin == invalid { - stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) - } - if stdout == invalid { - stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) - con = stdout - } - if stderr == invalid { - stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) - con = stderr - } - - // correct output mode - if con != invalid { - // Make sure the console is configured to convert - // \n to \r\n, like Go programs expect. - h := windows.Handle(con) - var st uint32 - err := windows.GetConsoleMode(h, &st) - if err != nil { - log.Printf("failed to get console mode: %s\n", err) - } else { - err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN) - if err != nil { - log.Printf("failed to set console mode: %s\n", err) - } - } - } - - // fix std handles to correct values (ie. redirects) - if stdin != invalid { - os.Stdin = os.NewFile(uintptr(stdin), "stdin") - log.Printf("fixed os.Stdin after attaching to parent console\n") - } - if stdout != invalid { - os.Stdout = os.NewFile(uintptr(stdout), "stdout") - log.Printf("fixed os.Stdout after attaching to parent console\n") - } - if stderr != invalid { - os.Stderr = os.NewFile(uintptr(stderr), "stderr") - log.Printf("fixed os.Stderr after attaching to parent console\n") - } - - log.Println("attached to parent console") - return true, nil -} - -func hideWindow(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - } -} diff --git a/cmds/portmaster-start/dirs.go b/cmds/portmaster-start/dirs.go deleted file mode 100644 index e327963f..00000000 --- a/cmds/portmaster-start/dirs.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(cleanStructureCmd) -} - -var cleanStructureCmd = &cobra.Command{ - Use: "clean-structure", - Short: "Create and clean the required directory structure", - RunE: func(cmd *cobra.Command, args []string) error { - if err := ensureLoggingDir(); err != nil { - return err - } - return cleanAndEnsureExecDir() - }, -} - -func cleanAndEnsureExecDir() error { - execDir := dataRoot.ChildDir("exec", 0o777) - - // Clean up and remove exec dir. - err := os.RemoveAll(execDir.Path) - if err != nil { - log.Printf("WARNING: failed to fully remove exec dir (%q) for cleaning: %s", execDir.Path, err) - } - - // Re-create exec dir. - err = execDir.Ensure() - if err != nil { - return fmt.Errorf("failed to initialize exec dir (%q): %w", execDir.Path, err) - } - - return nil -} diff --git a/cmds/portmaster-start/install_windows.go b/cmds/portmaster-start/install_windows.go deleted file mode 100644 index 5f1d5bb2..00000000 --- a/cmds/portmaster-start/install_windows.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -// Based on the official Go examples from -// https://github.com/golang/sys/blob/master/windows/svc/example -// by The Go Authors. -// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - "time" - - "github.com/spf13/cobra" - "golang.org/x/sys/windows" - "golang.org/x/sys/windows/svc" - "golang.org/x/sys/windows/svc/mgr" -) - -func init() { - rootCmd.AddCommand(installCmd) - installCmd.AddCommand(installService) - - rootCmd.AddCommand(uninstallCmd) - uninstallCmd.AddCommand(uninstallService) -} - -var installCmd = &cobra.Command{ - Use: "install", - Short: "Install system integrations", -} - -var uninstallCmd = &cobra.Command{ - Use: "uninstall", - Short: "Uninstall system integrations", -} - -var installService = &cobra.Command{ - Use: "core-service", - Short: "Install Portmaster Core Windows Service", - RunE: installWindowsService, -} - -var uninstallService = &cobra.Command{ - Use: "core-service", - Short: "Uninstall Portmaster Core Windows Service", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // non-nil dummy to override db flag requirement - return nil - }, - RunE: uninstallWindowsService, -} - -func getAbsBinaryPath() (string, error) { - p, err := filepath.Abs(os.Args[0]) - if err != nil { - return "", err - } - - return p, nil -} - -func getServiceExecCommand(exePath string, escape bool) []string { - return []string{ - maybeEscape(exePath, escape), - "core-service", - "--data", - maybeEscape(dataRoot.Path, escape), - "--input-signals", - } -} - -func maybeEscape(s string, escape bool) string { - if escape { - return windows.EscapeArg(s) - } - return s -} - -func getServiceConfig(exePath string) mgr.Config { - return mgr.Config{ - ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, - StartType: mgr.StartAutomatic, - ErrorControl: mgr.ErrorNormal, - BinaryPathName: strings.Join(getServiceExecCommand(exePath, true), " "), - DisplayName: "Portmaster Core", - Description: "Portmaster Application Firewall - Core Service", - } -} - -func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uint32) { - return []mgr.RecoveryAction{ - { - Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand - Delay: 1 * time.Minute, // the time to wait before performing the specified action - }, - }, 86400 -} - -func installWindowsService(cmd *cobra.Command, args []string) error { - // get exe path - exePath, err := getAbsBinaryPath() - if err != nil { - return fmt.Errorf("failed to get exe path: %s", err) - } - - // connect to Windows service manager - m, err := mgr.Connect() - if err != nil { - return fmt.Errorf("failed to connect to service manager: %s", err) - } - defer m.Disconnect() //nolint:errcheck // TODO - - // open service - created := false - s, err := m.OpenService(serviceName) - if err != nil { - // create service - cmd := getServiceExecCommand(exePath, false) - s, err = m.CreateService(serviceName, cmd[0], getServiceConfig(exePath), cmd[1:]...) - if err != nil { - return fmt.Errorf("failed to create service: %s", err) - } - defer s.Close() - created = true - } else { - // update service - err = s.UpdateConfig(getServiceConfig(exePath)) - if err != nil { - return fmt.Errorf("failed to update service: %s", err) - } - defer s.Close() - } - - // update recovery actions - err = s.SetRecoveryActions(getRecoveryActions()) - if err != nil { - return fmt.Errorf("failed to update recovery actions: %s", err) - } - - if created { - log.Printf("created service %s\n", serviceName) - } else { - log.Printf("updated service %s\n", serviceName) - } - - return nil -} - -func uninstallWindowsService(cmd *cobra.Command, args []string) error { - // connect to Windows service manager - m, err := mgr.Connect() - if err != nil { - return err - } - defer m.Disconnect() //nolint:errcheck // we don't care if we failed to disconnect from the service manager, we're quitting anyway. - - // open service - s, err := m.OpenService(serviceName) - if err != nil { - return fmt.Errorf("service %s is not installed", serviceName) - } - defer s.Close() - - _, err = s.Control(svc.Stop) - if err != nil { - log.Printf("failed to stop service: %s\n", err) - } - - // delete service - err = s.Delete() - if err != nil { - return fmt.Errorf("failed to delete service: %s", err) - } - - log.Printf("uninstalled service %s\n", serviceName) - return nil -} diff --git a/cmds/portmaster-start/lock.go b/cmds/portmaster-start/lock.go deleted file mode 100644 index 0526084c..00000000 --- a/cmds/portmaster-start/lock.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/fs" - "log" - "os" - "os/user" - "path/filepath" - "strconv" - "strings" - - processInfo "github.com/shirou/gopsutil/process" -) - -func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err error) { - lockFilePath := getLockFilePath(path, name, perUser) - - // read current pid file - data, err := os.ReadFile(lockFilePath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - // create new lock - return 0, createInstanceLock(lockFilePath) - } - return 0, err - } - - // file exists! - parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) - if err != nil { - log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err) - return 0, createInstanceLock(lockFilePath) - } - - // Check if process exists. - p, err := processInfo.NewProcess(int32(parsedPid)) - switch { - case err == nil: - // Process exists, continue. - case errors.Is(err, processInfo.ErrorProcessNotRunning): - // A process with the locked PID does not exist. - // This is expected, so we can continue normally. - return 0, createInstanceLock(lockFilePath) - default: - // There was an internal error getting the process. - return 0, err - } - - // Get the process paths and evaluate and clean them. - executingBinaryPath, err := p.Exe() - if err != nil { - return 0, fmt.Errorf("failed to get path of existing process: %w", err) - } - cleanedExecutingBinaryPath, err := filepath.EvalSymlinks(executingBinaryPath) - if err != nil { - return 0, fmt.Errorf("failed to evaluate path of existing process: %w", err) - } - - // Check if the binary is portmaster-start with high probability. - if !strings.Contains(filepath.Base(cleanedExecutingBinaryPath), "portmaster-start") { - // The process with the locked PID belongs to another binary. - // As the Portmaster usually starts very early, it will have a low PID, - // which could be assigned to another process on next boot. - return 0, createInstanceLock(lockFilePath) - } - - // Return PID of already running instance. - return p.Pid, nil -} - -func createInstanceLock(lockFilePath string) error { - // check data root dir - err := dataRoot.Ensure() - if err != nil { - log.Printf("failed to check data root dir: %s\n", err) - } - - // create lock file - // TODO: Investigate required permissions. - err = os.WriteFile(lockFilePath, []byte(strconv.Itoa(os.Getpid())), 0o0666) //nolint:gosec - if err != nil { - return err - } - - return nil -} - -func deleteInstanceLock(path, name string, perUser bool) error { - return os.Remove(getLockFilePath(path, name, perUser)) -} - -func getLockFilePath(path, name string, perUser bool) string { - if !perUser { - return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-lock.pid", name)) - } - - // Get user ID for per-user lock file. - var userID string - usr, err := user.Current() - if err != nil { - log.Printf("failed to get current user: %s\n", err) - userID = "no-user" - } else { - userID = usr.Uid - } - return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-%s-lock.pid", name, userID)) -} diff --git a/cmds/portmaster-start/logs.go b/cmds/portmaster-start/logs.go deleted file mode 100644 index f2f514b9..00000000 --- a/cmds/portmaster-start/logs.go +++ /dev/null @@ -1,127 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - "time" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/database/record" - "github.com/safing/portmaster/base/info" - "github.com/safing/structures/container" - "github.com/safing/structures/dsd" -) - -func initializeLogFile(logFilePath string, identifier string, version string) *os.File { - logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0o0440) //nolint:gosec // As desired. - if err != nil { - log.Printf("failed to create log file %s: %s\n", logFilePath, err) - return nil - } - - // create header, so that the portmaster can view log files as a database - meta := record.Meta{} - meta.Update() - meta.SetAbsoluteExpiry(time.Now().Add(720 * time.Hour).Unix()) // one month - - // manually marshal - // version - c := container.New([]byte{1}) - // meta - metaSection, err := dsd.Dump(meta, dsd.JSON) - if err != nil { - log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile) - return nil - } - c.AppendAsBlock(metaSection) - // log file data type (string) and newline for better manual viewing - c.Append([]byte("S\n")) - c.Append([]byte(fmt.Sprintf("executing %s version %s on %s %s\n", identifier, version, runtime.GOOS, runtime.GOARCH))) - - _, err = logFile.Write(c.CompileData()) - if err != nil { - log.Printf("failed to write header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile) - return nil - } - - return logFile -} - -func finalizeLogFile(logFile *os.File) { - logFilePath := logFile.Name() - - err := logFile.Close() - if err != nil { - log.Printf("failed to close log file %s: %s\n", logFilePath, err) - } - - // check file size - stat, err := os.Stat(logFilePath) - if err != nil { - return - } - - // delete if file is smaller than - if stat.Size() >= 200 { // header + info is about 150 bytes - return - } - - if err := os.Remove(logFilePath); err != nil { - log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err) - } -} - -func getLogFile(options *Options, version, ext string) *os.File { - // check logging dir - logFileBasePath := filepath.Join(logsRoot.Path, options.ShortIdentifier) - err := logsRoot.EnsureAbsPath(logFileBasePath) - if err != nil { - log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err) - } - - // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s%s", time.Now().UTC().Format("2006-01-02-15-04-05"), ext)) - return initializeLogFile(logFilePath, options.Identifier, version) -} - -func getPmStartLogFile(ext string) *os.File { - return getLogFile(&Options{ - ShortIdentifier: "start", - Identifier: "start/portmaster-start", - }, info.Version(), ext) -} - -//nolint:unused // false positive on linux, currently used by windows only. TODO: move to a _windows file. -func logControlError(cErr error) { - // check if error present - if cErr == nil { - return - } - - errorFile := getPmStartLogFile(".error.log") - if errorFile == nil { - return - } - defer func() { - _ = errorFile.Close() - }() - - fmt.Fprintln(errorFile, cErr.Error()) -} - -//nolint:deadcode,unused // false positive on linux, currently used by windows only. TODO: move to a _windows file. -func runAndLogControlError(wrappedFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - err := wrappedFunc(cmd, args) - if err != nil { - logControlError(err) - } - return err - } -} diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go deleted file mode 100644 index f764dfbf..00000000 --- a/cmds/portmaster-start/main.go +++ /dev/null @@ -1,257 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "net/url" - "os" - "os/signal" - "path/filepath" - "runtime" - "strings" - "syscall" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/dataroot" - "github.com/safing/portmaster/base/info" - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/base/utils" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - dataDir string - maxRetries int - dataRoot *utils.DirStructure - logsRoot *utils.DirStructure - forceOldUI bool - - updateURLFlag string - userAgentFlag string - - // Create registry. - registry = &updater.ResourceRegistry{ - Name: "updates", - UpdateURLs: []string{ - "https://updates.safing.io", - }, - UserAgent: fmt.Sprintf("Portmaster Start (%s %s)", runtime.GOOS, runtime.GOARCH), - Verification: helper.VerificationConfig, - DevMode: false, - Online: true, // is disabled later based on command - } - - rootCmd = &cobra.Command{ - Use: "portmaster-start", - Short: "Start Portmaster components", - PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { - mustLoadIndex := indexRequired(cmd) - if err := configureRegistry(mustLoadIndex); err != nil { - return err - } - - if err := ensureLoggingDir(); err != nil { - return err - } - - return nil - }, - SilenceUsage: true, - } -) - -func init() { - // Let cobra ignore if we are running as "GUI" or not - cobra.MousetrapHelpText = "" - - flags := rootCmd.PersistentFlags() - { - flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.") - flags.StringVar(&updateURLFlag, "update-server", "", "Set an alternative update server (full URL)") - flags.StringVar(&userAgentFlag, "update-agent", "", "Set an alternative user agent for requests to the update server") - flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component") - flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.") - flags.BoolVar(&forceOldUI, "old-ui", false, "Use the old ui. (Beta)") - _ = rootCmd.MarkPersistentFlagDirname("data") - _ = flags.MarkHidden("input-signals") - } -} - -func main() { - cobra.OnInitialize(initCobra) - - // set meta info - info.Set("Portmaster Start", "", "GPLv3") - - // catch interrupt for clean shutdown - signalCh := make(chan os.Signal, 2) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // start root command - go func() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } - os.Exit(0) - }() - - // wait for signals - for sig := range signalCh { - if childIsRunning.IsSet() { - log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig) - continue - } - - log.Printf("got %s signal, exiting... (not executing anything)\n", sig) - os.Exit(0) - } -} - -func initCobra() { - // check if we are running in a console (try to attach to parent console if available) - var err error - runningInConsole, err = attachToParentConsole() - if err != nil { - log.Fatalf("failed to attach to parent console: %s\n", err) - } - - // check if meta info is ok - err = info.CheckVersion() - if err != nil { - log.Fatalf("compile error: please compile using the provided build script") - } - - // set up logging - log.SetFlags(log.Ldate | log.Ltime | log.LUTC) - log.SetPrefix("[pmstart] ") - log.SetOutput(os.Stdout) - - // not using portbase logger - portlog.SetLogLevel(portlog.CriticalLevel) -} - -func configureRegistry(mustLoadIndex bool) error { - // Check if update server URL supplied via flag is a valid URL. - if updateURLFlag != "" { - u, err := url.Parse(updateURLFlag) - 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") - } - } - - // Override values from flags. - if userAgentFlag != "" { - registry.UserAgent = userAgentFlag - } - if updateURLFlag != "" { - registry.UpdateURLs = []string{updateURLFlag} - } - - // If dataDir is not set, check the environment variable. - if dataDir == "" { - dataDir = os.Getenv("PORTMASTER_DATA") - } - - // If it's still empty, try to auto-detect it. - if dataDir == "" { - dataDir = detectInstallationDir() - } - - // Finally, if it's still empty, the user must provide it. - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // Remove left over quotes. - dataDir = strings.Trim(dataDir, `\"`) - // Initialize data root. - err := dataroot.Initialize(dataDir, 0o0755) - if err != nil { - return fmt.Errorf("failed to initialize data root: %w", err) - } - dataRoot = dataroot.Root() - - // Initialize registry. - err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755)) - if err != nil { - return err - } - - return updateRegistryIndex(mustLoadIndex) -} - -func ensureLoggingDir() error { - // set up logs root - logsRoot = dataRoot.ChildDir("logs", 0o0777) - err := logsRoot.Ensure() - if err != nil { - return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, err) - } - - // warn about CTRL-C on windows - if runningInConsole && onWindows { - log.Println("WARNING: portmaster-start is marked as a GUI application in order to get rid of the console window.") - log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.") - } - return nil -} - -func updateRegistryIndex(mustLoadIndex bool) error { - // Set indexes based on the release channel. - warning := helper.SetIndexes(registry, "", false, false, false) - if warning != nil { - log.Printf("WARNING: %s\n", warning) - } - - // Load indexes from disk or network, if needed and desired. - err := registry.LoadIndexes(context.Background()) - if err != nil { - log.Printf("WARNING: error loading indexes: %s\n", err) - if mustLoadIndex { - return err - } - } - - // Load versions from disk to know which others we have and which are available. - err = registry.ScanStorage("") - if err != nil { - log.Printf("WARNING: error during storage scan: %s\n", err) - } - - registry.SelectVersions() - return nil -} - -func detectInstallationDir() string { - exePath, err := filepath.Abs(os.Args[0]) - if err != nil { - return "" - } - - parent := filepath.Dir(exePath) - stableJSONFile := filepath.Join(parent, "updates", "stable.json") - stat, err := os.Stat(stableJSONFile) - if err != nil { - return "" - } - - if stat.IsDir() { - return "" - } - - return parent -} diff --git a/cmds/portmaster-start/pack b/cmds/portmaster-start/pack deleted file mode 100755 index 1cf6ca2a..00000000 --- a/cmds/portmaster-start/pack +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash - -baseDir="$( cd "$(dirname "$0")" && pwd )" -cd "$baseDir" - -COL_OFF="\033[0m" -COL_BOLD="\033[01;01m" -COL_RED="\033[31m" -COL_GREEN="\033[32m" -COL_YELLOW="\033[33m" - -destDirPart1="../../dist" -destDirPart2="start" - -function prep { - # output - output="portmaster-start" - # get version - version=$(grep "info.Set" main.go | cut -d'"' -f4) - # build versioned file name - filename="portmaster-start_v${version//./-}" - # platform - platform="${GOOS}_${GOARCH}" - if [[ $GOOS == "windows" ]]; then - filename="${filename}.exe" - output="${output}.exe" - fi - # build destination path - destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename -} - -function check { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[start] $platform $version already built" - else - echo -e "${COL_BOLD}[start] $platform v$version${COL_OFF}" - fi -} - -function build { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[start] $platform already built in v$version, skipping..." - return - fi - - # build - ./build - if [[ $? -ne 0 ]]; then - echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}" - exit 1 - fi - mkdir -p $(dirname $destPath) - cp $output $destPath - echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}" -} - -function reset { - prep - - # delete if file exists - if [[ -f $destPath ]]; then - rm $destPath - echo "[start] $platform v$version deleted." - fi -} - -function check_all { - GOOS=linux GOARCH=amd64 check - GOOS=windows GOARCH=amd64 check - GOOS=darwin GOARCH=amd64 check - GOOS=linux GOARCH=arm64 check - GOOS=windows GOARCH=arm64 check - GOOS=darwin GOARCH=arm64 check -} - -function build_all { - GOOS=linux GOARCH=amd64 build - GOOS=windows GOARCH=amd64 build - GOOS=darwin GOARCH=amd64 build - GOOS=linux GOARCH=arm64 build - GOOS=windows GOARCH=arm64 build - GOOS=darwin GOARCH=arm64 build -} - -function reset_all { - GOOS=linux GOARCH=amd64 reset - GOOS=windows GOARCH=amd64 reset - GOOS=darwin GOARCH=amd64 reset - GOOS=linux GOARCH=arm64 reset - GOOS=windows GOARCH=arm64 reset - GOOS=darwin GOARCH=arm64 reset -} - -case $1 in - "check" ) - check_all - ;; - "build" ) - build_all - ;; - "reset" ) - reset_all - ;; - * ) - echo "" - echo "build list:" - echo "" - check_all - echo "" - read -p "press [Enter] to start building" x - echo "" - build_all - echo "" - echo "finished building." - echo "" - ;; -esac diff --git a/cmds/portmaster-start/recover_linux.go b/cmds/portmaster-start/recover_linux.go deleted file mode 100644 index 96719bd8..00000000 --- a/cmds/portmaster-start/recover_linux.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/hashicorp/go-multierror" - "github.com/spf13/cobra" - - "github.com/safing/portmaster/service/firewall/interception" -) - -var recoverIPTablesCmd = &cobra.Command{ - Use: "recover-iptables", - Short: "Removes obsolete IP tables rules in case of an unclean shutdown", - RunE: func(*cobra.Command, []string) error { - // interception.DeactiveNfqueueFirewall uses coreos/go-iptables - // which shells out to the /sbin/iptables binary. As a result, - // we don't get the errno of the actual error and need to parse the - // output instead. Make sure it's always english by setting LC_ALL=C - currentLocale := os.Getenv("LC_ALL") - _ = os.Setenv("LC_ALL", "C") - defer func() { - _ = os.Setenv("LC_ALL", currentLocale) - }() - - err := interception.DeactivateNfqueueFirewall() - if err == nil { - return nil - } - - // we don't want to show ErrNotExists to the user - // as that only means portmaster did the cleanup itself. - var mr *multierror.Error - if !errors.As(err, &mr) { - return err - } - - var filteredErrors *multierror.Error - for _, err := range mr.Errors { - // if we have a permission denied error, all errors will be the same - if strings.Contains(err.Error(), "Permission denied") { - return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission) - } - - if !strings.Contains(err.Error(), "No such file or directory") { - filteredErrors = multierror.Append(filteredErrors, err) - } - } - - if filteredErrors != nil { - filteredErrors.ErrorFormat = formatNfqErrors - return filteredErrors.ErrorOrNil() - } - - return nil - }, - SilenceUsage: true, -} - -func init() { - rootCmd.AddCommand(recoverIPTablesCmd) -} - -func formatNfqErrors(es []error) string { - if len(es) == 1 { - return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) - } - - points := make([]string, len(es)) - for i, err := range es { - // only display the very first line of each error - first := strings.Split(err.Error(), "\n")[0] - points[i] = fmt.Sprintf("* %s", first) - } - - return fmt.Sprintf( - "%d errors occurred:\n\t%s\n\n", - len(es), strings.Join(points, "\n\t")) -} diff --git a/cmds/portmaster-start/run.go b/cmds/portmaster-start/run.go deleted file mode 100644 index f807fd69..00000000 --- a/cmds/portmaster-start/run.go +++ /dev/null @@ -1,486 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "log" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/spf13/cobra" - "github.com/tevino/abool" - - "github.com/safing/portmaster/service/updates/helper" -) - -const ( - // RestartExitCode is the exit code that any service started by portmaster-start - // can return in order to trigger a restart after a clean shutdown. - RestartExitCode = 23 - - // ControlledFailureExitCode is the exit code that any service started by - // portmaster-start can return in order to signify a controlled failure. - // This disables retrying and exits with an error code. - ControlledFailureExitCode = 24 - - // StartOldUIExitCode is an exit code that is returned by the UI when there. This is manfully triaged by the user, if the new UI does not work for them. - StartOldUIExitCode = 77 - MissingDependencyExitCode = 0xc0000135 // Windows STATUS_DLL_NOT_FOUND - - exeSuffix = ".exe" - zipSuffix = ".zip" -) - -var ( - runningInConsole bool - onWindows = runtime.GOOS == "windows" - stdinSignals bool - childIsRunning = abool.NewBool(false) - - fallBackToOldUI bool = false -) - -// Options for starting component. -type Options struct { - Name string - Identifier string // component identifier - ShortIdentifier string // populated automatically - LockPathPrefix string - LockPerUser bool - PIDFile bool - SuppressArgs bool // do not use any args - AllowDownload bool // allow download of component if it is not yet available - AllowHidingWindow bool // allow hiding the window of the subprocess - NoOutput bool // do not use stdout/err if logging to file is available (did not fail to open log file) - RestartOnFail bool // Try restarting automatically, if the started component fails. -} - -// This is a temp value that will be used to test the new UI in beta. -var app2Options = Options{ - Name: "Portmaster App2", - Identifier: "app2/portmaster-app", - AllowDownload: false, - AllowHidingWindow: false, - RestartOnFail: true, -} - -func init() { - // Make sure the new UI has a proper extension. - if onWindows { - app2Options.Identifier += ".zip" - } - - registerComponent([]Options{ - { - Name: "Portmaster Core", - Identifier: "core/portmaster-core", - AllowDownload: true, - AllowHidingWindow: true, - PIDFile: true, - RestartOnFail: true, - }, - { - Name: "Portmaster App", - Identifier: "app/portmaster-app.zip", - AllowDownload: false, - AllowHidingWindow: false, - RestartOnFail: true, - }, - { - Name: "Portmaster Notifier", - Identifier: "notifier/portmaster-notifier", - LockPerUser: true, - AllowDownload: false, - AllowHidingWindow: true, - PIDFile: true, - LockPathPrefix: "exec", - }, - { - Name: "Safing Privacy Network", - Identifier: "hub/spn-hub", - AllowDownload: true, - AllowHidingWindow: true, - PIDFile: true, - RestartOnFail: true, - }, - app2Options, - }) -} - -func registerComponent(opts []Options) { - for idx := range opts { - opt := &opts[idx] // we need a copy - if opt.ShortIdentifier == "" { - opt.ShortIdentifier = path.Dir(opt.Identifier) - } - - rootCmd.AddCommand( - &cobra.Command{ - Use: opt.ShortIdentifier, - Short: "Run the " + opt.Name, - RunE: func(cmd *cobra.Command, args []string) error { - err := run(opt, args) - initiateShutdown(err) - return err - }, - }, - ) - - showCmd.AddCommand( - &cobra.Command{ - Use: opt.ShortIdentifier, - Short: "Show command to execute the " + opt.Name, - RunE: func(cmd *cobra.Command, args []string) error { - return show(opt, args) - }, - }, - ) - } -} - -func getExecArgs(opts *Options, cmdArgs []string) []string { - if opts.SuppressArgs { - return nil - } - - args := []string{"--data", dataDir} - if stdinSignals { - args = append(args, "--input-signals") - } - - if runtime.GOOS == "linux" && opts.ShortIdentifier == "app" { - // see https://www.freedesktop.org/software/systemd/man/pam_systemd.html#type= - if xdgSessionType := os.Getenv("XDG_SESSION_TYPE"); xdgSessionType == "wayland" { - // we're running the Portmaster UI App under Wayland so make sure we add some arguments - // required by Electron. - args = append(args, - []string{ - "--enable-features=UseOzonePlatform,WaylandWindowDecorations", - "--ozone-platform=wayland", - }..., - ) - } - } - - args = append(args, cmdArgs...) - return args -} - -func run(opts *Options, cmdArgs []string) (err error) { - // set download option - registry.Online = opts.AllowDownload - - if isShuttingDown() { - return nil - } - - // check for duplicate instances - if opts.PIDFile { - pid, err := checkAndCreateInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser) - if err != nil { - return fmt.Errorf("failed to exec lock: %w", err) - } - if pid != 0 { - return fmt.Errorf("another instance of %s is already running: PID %d", opts.Name, pid) - } - defer func() { - err := deleteInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser) - if err != nil { - log.Printf("failed to delete instance lock: %s\n", err) - } - }() - } - - // notify service after some time - go func() { - // assume that after 3 seconds service has finished starting - time.Sleep(3 * time.Second) - startupComplete <- struct{}{} - }() - - // adapt identifier - if onWindows && !strings.HasSuffix(opts.Identifier, zipSuffix) { - opts.Identifier += exeSuffix - } - - // setup logging - // init log file - logFile := getPmStartLogFile(".log") - if logFile != nil { - // don't close logFile, will be closed by system - if opts.NoOutput { - log.Println("disabling log output to stdout... bye!") - log.SetOutput(logFile) - } else { - log.SetOutput(io.MultiWriter(os.Stdout, logFile)) - } - } - - return runAndRestart(opts, cmdArgs) -} - -func runAndRestart(opts *Options, args []string) error { - tries := 0 - for { - tryAgain, err := execute(opts, args) - if err != nil { - log.Printf("%s failed with: %s\n", opts.Identifier, err) - tries++ - if tries >= maxRetries { - log.Printf("encountered %d consecutive errors, giving up ...", tries) - return err - } - } else { - tries = 0 - log.Printf("%s exited without error", opts.Identifier) - } - - if !opts.RestartOnFail || !tryAgain { - return err - } - - // if a restart was requested `tries` is set to 0 so - // this becomes a no-op. - time.Sleep(time.Duration(2*tries) * time.Second) - - if tries >= 2 || err == nil { - // if we are constantly failing or a restart was requested - // try to update the resources. - log.Printf("updating registry index") - _ = updateRegistryIndex(false) // will always return nil - } - } -} - -func fixExecPerm(path string) error { - if onWindows { - return nil - } - - info, err := os.Stat(path) - if err != nil { - return fmt.Errorf("failed to stat %s: %w", path, err) - } - - if info.Mode() == 0o0755 { - return nil - } - - if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights. - return fmt.Errorf("failed to chmod %s: %w", path, err) - } - - return nil -} - -func copyLogs(opts *Options, consoleSink io.Writer, version, ext string, logSource io.Reader, notifier chan<- struct{}) { - defer func() { notifier <- struct{}{} }() - - sink := consoleSink - - fileSink := getLogFile(opts, version, ext) - if fileSink != nil { - defer finalizeLogFile(fileSink) - if opts.NoOutput { - sink = fileSink - } else { - sink = io.MultiWriter(consoleSink, fileSink) - } - } - - if bytes, err := io.Copy(sink, logSource); err != nil { - log.Printf("%s: writing logs failed after %d bytes: %s", fileSink.Name(), bytes, err) - } -} - -func persistOutputStreams(opts *Options, version string, cmd *exec.Cmd) (chan struct{}, error) { - var ( - done = make(chan struct{}) - copyNotifier = make(chan struct{}, 2) - ) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stdout: %w", err) - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stderr: %w", err) - } - - go copyLogs(opts, os.Stdout, version, ".log", stdout, copyNotifier) - go copyLogs(opts, os.Stderr, version, ".error.log", stderr, copyNotifier) - - go func() { - <-copyNotifier - <-copyNotifier - close(copyNotifier) - close(done) - }() - - return done, nil -} - -func execute(opts *Options, args []string) (cont bool, err error) { - // Auto-upgrade to new UI if in beta and new UI is not disabled or failed. - if opts.ShortIdentifier == "app" && - registry.UsePreReleases && - !forceOldUI && - !fallBackToOldUI { - log.Println("auto-upgraded to new UI") - opts = &app2Options - } - - // Compile arguments and add additional arguments based on system configuration. - // Extra parameters can be specified using "-- --some-parameter". - args = getExecArgs(opts, args) - - file, err := registry.GetFile( - helper.PlatformIdentifier(opts.Identifier), - ) - if err != nil { - return true, fmt.Errorf("could not get component: %w", err) - } - binPath := file.Path() - - // Adapt path for packaged software. - if strings.HasSuffix(binPath, zipSuffix) { - // Remove suffix from binary path. - binPath = strings.TrimSuffix(binPath, zipSuffix) - // Add binary with the same name to access the unpacked binary. - binPath = filepath.Join(binPath, filepath.Base(binPath)) - - // Adapt binary path on Windows. - if onWindows { - binPath += exeSuffix - } - } - - // check permission - if err := fixExecPerm(binPath); err != nil { - return true, err - } - - log.Printf("starting %s %s\n", binPath, strings.Join(args, " ")) - - // create command - exc := exec.Command(binPath, args...) - - if !runningInConsole && opts.AllowHidingWindow { - // Windows only: - // only hide (all) windows of program if we are not running in console and windows may be hidden - hideWindow(exc) - } - - outputsWritten, err := persistOutputStreams(opts, file.Version(), exc) - if err != nil { - return true, err - } - - interrupt, err := getProcessSignalFunc(exc) - if err != nil { - return true, err - } - - err = exc.Start() - if err != nil { - return true, fmt.Errorf("failed to start %s: %w", opts.Identifier, err) - } - childIsRunning.Set() - - // wait for completion - finished := make(chan error, 1) - go func() { - defer close(finished) - - <-outputsWritten - // wait for process to return - finished <- exc.Wait() - // update status - childIsRunning.UnSet() - }() - - // state change listeners - select { - case <-shuttingDown: - if err := interrupt(); err != nil { - log.Printf("failed to signal %s to shutdown: %s\n", opts.Identifier, err) - err = exc.Process.Kill() - if err != nil { - return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err) - } - return false, fmt.Errorf("killed %s", opts.Identifier) - } - - // wait until shut down - select { - case <-finished: - case <-time.After(3 * time.Minute): // portmaster core prints stack if not able to shutdown in 3 minutes, give it one more ... - err = exc.Process.Kill() - if err != nil { - return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err) - } - return false, fmt.Errorf("killed %s", opts.Identifier) - } - return false, nil - - case err := <-finished: - return parseExitError(err) - } -} - -func getProcessSignalFunc(cmd *exec.Cmd) (func() error, error) { - if stdinSignals { - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, fmt.Errorf("failed to connect stdin: %w", err) - } - - return func() error { - _, err := fmt.Fprintln(stdin, "SIGINT") - return err - }, nil - } - - return func() error { - return cmd.Process.Signal(os.Interrupt) - }, nil -} - -func parseExitError(err error) (restart bool, errWithCtx error) { - if err == nil { - // clean and coordinated exit - return false, nil - } - - var exErr *exec.ExitError - if errors.As(err, &exErr) { - switch exErr.ProcessState.ExitCode() { - case 0: - return false, fmt.Errorf("clean exit with error: %w", err) - case 1: - return true, fmt.Errorf("error during execution: %w", err) - case RestartExitCode: - return true, nil - case ControlledFailureExitCode: - return false, errors.New("controlled failure, check logs") - case StartOldUIExitCode: - fallBackToOldUI = true - return true, errors.New("user requested old UI") - case MissingDependencyExitCode: - fallBackToOldUI = true - return true, errors.New("new UI failed with missing dependency") - default: - return true, fmt.Errorf("unknown exit code %w", exErr) - } - } - - return true, fmt.Errorf("unexpected error type: %w", err) -} diff --git a/cmds/portmaster-start/service_windows.go b/cmds/portmaster-start/service_windows.go deleted file mode 100644 index bd47c5b2..00000000 --- a/cmds/portmaster-start/service_windows.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -// Based on the official Go examples from -// https://github.com/golang/sys/blob/master/windows/svc/example -// by The Go Authors. -// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory. - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/spf13/cobra" - "golang.org/x/sys/windows/svc" - "golang.org/x/sys/windows/svc/debug" -) - -var ( - runCoreService = &cobra.Command{ - Use: "core-service", - Short: "Run the Portmaster Core as a Windows Service", - RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error { - return runService(cmd, &Options{ - Name: "Portmaster Core Service", - Identifier: "core/portmaster-core", - ShortIdentifier: "core", - AllowDownload: true, - AllowHidingWindow: false, - NoOutput: true, - RestartOnFail: true, - }, args) - }), - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, - } - - // wait groups - runWg sync.WaitGroup - finishWg sync.WaitGroup -) - -func init() { - rootCmd.AddCommand(runCoreService) -} - -const serviceName = "PortmasterCore" - -type windowsService struct{} - -func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { - const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown - changes <- svc.Status{State: svc.StartPending} - -service: - for { - select { - case <-startupComplete: - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - case <-shuttingDown: - changes <- svc.Status{State: svc.StopPending} - break service - case c := <-changeRequests: - switch c.Cmd { - case svc.Interrogate: - changes <- c.CurrentStatus - case svc.Stop, svc.Shutdown: - initiateShutdown(nil) - default: - log.Printf("unexpected control request: #%d\n", c) - } - } - } - - // define return values - if getShutdownError() != nil { - ssec = true // this error is specific to this service (ie. custom) - errno = 1 // generic error, check logs / windows events - } - - // wait until everything else is finished - finishWg.Wait() - // send stopped status - changes <- svc.Status{State: svc.Stopped} - // wait a little for the status to reach Windows - time.Sleep(100 * time.Millisecond) - - return ssec, errno -} - -func runService(_ *cobra.Command, opts *Options, cmdArgs []string) error { - // check if we are running interactively - isDebug, err := svc.IsAnInteractiveSession() - if err != nil { - return fmt.Errorf("could not determine if running interactively: %s", err) - } - // select service run type - svcRun := svc.Run - if isDebug { - log.Printf("WARNING: running interactively, switching to debug execution (no real service).\n") - svcRun = debug.Run - } - - runWg.Add(2) - finishWg.Add(1) - - // run service client - go func() { - sErr := svcRun(serviceName, &windowsService{}) - initiateShutdown(sErr) - runWg.Done() - }() - - // run service - go func() { - // run slightly delayed - time.Sleep(250 * time.Millisecond) - err := run(opts, getExecArgs(opts, cmdArgs)) - initiateShutdown(err) - finishWg.Done() - runWg.Done() - }() - - runWg.Wait() - - err = getShutdownError() - if err != nil { - log.Printf("%s service experienced an error: %s\n", serviceName, err) - } - - return err -} diff --git a/cmds/portmaster-start/show.go b/cmds/portmaster-start/show.go deleted file mode 100644 index 7ae6fc85..00000000 --- a/cmds/portmaster-start/show.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/service/updates/helper" -) - -func init() { - rootCmd.AddCommand(showCmd) - // sub-commands of show are registered using registerComponent -} - -var showCmd = &cobra.Command{ - Use: "show", - PersistentPreRunE: func(*cobra.Command, []string) error { - // All show sub-commands need the registry but no logging. - return configureRegistry(false) - }, - Short: "Show the command to run a Portmaster component yourself", -} - -func show(opts *Options, cmdArgs []string) error { - // get original arguments - args := getExecArgs(opts, cmdArgs) - - // adapt identifier - if onWindows { - opts.Identifier += exeSuffix - } - - file, err := registry.GetFile( - helper.PlatformIdentifier(opts.Identifier), - ) - if err != nil { - return fmt.Errorf("could not get component: %w", err) - } - - fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " ")) - - return nil -} diff --git a/cmds/portmaster-start/shutdown.go b/cmds/portmaster-start/shutdown.go deleted file mode 100644 index 31d34f96..00000000 --- a/cmds/portmaster-start/shutdown.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "sync" -) - -var ( - // startupComplete signals that the start procedure completed. - // The channel is not closed, just signaled once. - startupComplete = make(chan struct{}) - - // shuttingDown signals that we are shutting down. - // The channel will be closed, but may not be closed directly - only via initiateShutdown. - shuttingDown = make(chan struct{}) - - // shutdownError is protected by shutdownLock. - shutdownError error //nolint:unused,errname // Not what the linter thinks it is. Currently used on windows only. - shutdownLock sync.Mutex -) - -func initiateShutdown(err error) { - shutdownLock.Lock() - defer shutdownLock.Unlock() - - select { - case <-shuttingDown: - return - default: - shutdownError = err - close(shuttingDown) - } -} - -func isShuttingDown() bool { - select { - case <-shuttingDown: - return true - default: - return false - } -} - -//nolint:deadcode,unused // false positive on linux, currently used by windows only -func getShutdownError() error { - shutdownLock.Lock() - defer shutdownLock.Unlock() - - return shutdownError -} diff --git a/cmds/portmaster-start/update.go b/cmds/portmaster-start/update.go deleted file mode 100644 index 544047e1..00000000 --- a/cmds/portmaster-start/update.go +++ /dev/null @@ -1,158 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/spf13/cobra" - - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - reset bool - intelOnly bool -) - -func init() { - rootCmd.AddCommand(updateCmd) - rootCmd.AddCommand(purgeCmd) - - flags := updateCmd.Flags() - flags.BoolVar(&reset, "reset", false, "Delete all resources and re-download the basic set") - flags.BoolVar(&intelOnly, "intel-only", false, "Only make downloading intel updates mandatory") -} - -var ( - updateCmd = &cobra.Command{ - Use: "update", - Short: "Run a manual update process", - RunE: func(cmd *cobra.Command, args []string) error { - return downloadUpdates() - }, - } - - purgeCmd = &cobra.Command{ - Use: "purge", - Short: "Remove old resource versions that are superseded by at least three versions", - RunE: func(cmd *cobra.Command, args []string) error { - return purge() - }, - } -) - -func indexRequired(cmd *cobra.Command) bool { - switch cmd { - case updateCmd, purgeCmd: - return true - default: - return false - } -} - -func downloadUpdates() error { - // Check if only intel data is mandatory. - if intelOnly { - helper.IntelOnly() - } - - // Set registry state notify callback. - registry.StateNotifyFunc = logProgress - - // Set required updates. - registry.MandatoryUpdates = helper.MandatoryUpdates() - registry.AutoUnpack = helper.AutoUnpackUpdates() - - if reset { - // Delete storage. - err := os.RemoveAll(registry.StorageDir().Path) - if err != nil { - return fmt.Errorf("failed to reset update dir: %w", err) - } - err = registry.StorageDir().Ensure() - if err != nil { - return fmt.Errorf("failed to create update dir: %w", err) - } - - // Reset registry resources. - registry.ResetResources() - } - - // Update all indexes. - err := registry.UpdateIndexes(context.TODO()) - if err != nil { - return err - } - - // Check if updates are available. - if len(registry.GetState().Updates.PendingDownload) == 0 { - log.Println("all resources are up to date") - return nil - } - - // Download all required updates. - err = registry.DownloadUpdates(context.TODO(), true) - if err != nil { - return err - } - - // Select versions and unpack the selected. - registry.SelectVersions() - err = registry.UnpackResources() - if err != nil { - return fmt.Errorf("failed to unpack resources: %w", err) - } - - if !intelOnly { - // Fix chrome-sandbox permissions - if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { - return fmt.Errorf("failed to fix electron permissions: %w", err) - } - } - - return nil -} - -func logProgress(state *updater.RegistryState) { - switch state.ID { - case updater.StateChecking: - if state.Updates.LastCheckAt == nil { - log.Println("checking for new versions") - } - case updater.StateDownloading: - if state.Details == nil { - log.Printf("downloading %d updates\n", len(state.Updates.PendingDownload)) - } else if downloadDetails, ok := state.Details.(*updater.StateDownloadingDetails); ok { - if downloadDetails.FinishedUpTo < len(downloadDetails.Resources) { - log.Printf( - "[%d/%d] downloading %s", - downloadDetails.FinishedUpTo+1, - len(downloadDetails.Resources), - downloadDetails.Resources[downloadDetails.FinishedUpTo], - ) - } else if state.Updates.LastDownloadAt == nil { - log.Println("finalizing downloads") - } - } - } -} - -func purge() error { - portlog.SetLogLevel(portlog.TraceLevel) - - // logging is configured as a persistent pre-run method inherited from - // the root command but since we don't use run.Run() we need to start - // logging ourself. - err := portlog.Start() - if err != nil { - fmt.Printf("failed to start logging: %s\n", err) - } - defer portlog.Shutdown() - - registry.Purge(3) - return nil -} diff --git a/cmds/portmaster-start/verify.go b/cmds/portmaster-start/verify.go deleted file mode 100644 index 9d63c51a..00000000 --- a/cmds/portmaster-start/verify.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io/fs" - "log" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/safing/jess" - "github.com/safing/jess/filesig" - portlog "github.com/safing/portmaster/base/log" - "github.com/safing/portmaster/base/updater" - "github.com/safing/portmaster/service/updates/helper" -) - -var ( - verifyVerbose bool - verifyFix bool - - verifyCmd = &cobra.Command{ - Use: "verify", - Short: "Check integrity of updates / components", - RunE: func(cmd *cobra.Command, args []string) error { - return verifyUpdates(cmd.Context()) - }, - } -) - -func init() { - rootCmd.AddCommand(verifyCmd) - - flags := verifyCmd.Flags() - flags.BoolVarP(&verifyVerbose, "verbose", "v", false, "Enable verbose output") - flags.BoolVar(&verifyFix, "fix", false, "Delete and re-download broken components") -} - -func verifyUpdates(ctx context.Context) error { - // Force registry to require signatures for all enabled scopes. - for _, opts := range registry.Verification { - if opts != nil { - opts.DownloadPolicy = updater.SignaturePolicyRequire - opts.DiskLoadPolicy = updater.SignaturePolicyRequire - } - } - - // Load indexes again to ensure they are correctly signed. - err := registry.LoadIndexes(ctx) - if err != nil { - if verifyFix { - log.Println("[WARN] loading indexes failed, re-downloading...") - err = registry.UpdateIndexes(ctx) - if err != nil { - return fmt.Errorf("failed to download indexes: %w", err) - } - log.Println("[ OK ] indexes re-downloaded and verified") - } else { - return fmt.Errorf("failed to verify indexes: %w", err) - } - } else { - log.Println("[ OK ] indexes verified") - } - - // Verify all resources. - export := registry.Export() - var verified, fails, skipped int - for _, rv := range export { - for _, version := range rv.Versions { - // Don't verify files we don't have. - if !version.Available { - continue - } - - // Verify file signature. - file := version.GetFile() - fileData, err := file.Verify() - switch { - case err == nil: - verified++ - if verifyVerbose { - verifOpts := registry.GetVerificationOptions(file.Identifier()) - if verifOpts != nil { - log.Printf( - "[ OK ] valid signature for %s: signed by %s", - file.Path(), getSignedByMany(fileData, verifOpts.TrustStore), - ) - } else { - log.Printf("[ OK ] valid signature for %s", file.Path()) - } - } - - case errors.Is(err, updater.ErrVerificationNotConfigured): - skipped++ - if verifyVerbose { - log.Printf("[SKIP] no verification configured for %s", file.Path()) - } - - default: - log.Printf("[FAIL] failed to verify %s: %s", file.Path(), err) - fails++ - if verifyFix { - // Delete file. - err = os.Remove(file.Path()) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path(), err) - } else { - // We should not be changing the version, but we are in a cmd-like - // scenario here without goroutines. - version.Available = false - } - // Delete file sig. - err = os.Remove(file.Path() + filesig.Extension) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path()+filesig.Extension, err) - } else { - // We should not be changing the version, but we are in a cmd-like - // scenario here without goroutines. - version.SigAvailable = false - } - } - } - } - } - - if verified > 0 { - log.Printf("[STAT] verified %d files", verified) - } - if skipped > 0 && verifyVerbose { - log.Printf("[STAT] skipped %d files (no verification configured)", skipped) - } - if fails > 0 { - if verifyFix { - log.Printf("[WARN] verification failed on %d files, re-downloading...", fails) - } else { - return fmt.Errorf("failed to verify %d files", fails) - } - } else { - // Everything was verified! - return nil - } - - // Start logging system for update process. - portlog.SetLogLevel(portlog.InfoLevel) - err = portlog.Start() - if err != nil { - log.Printf("[WARN] failed to start logging for monitoring update process: %s\n", err) - } - defer portlog.Shutdown() - - // Re-download broken files. - registry.MandatoryUpdates = helper.MandatoryUpdates() - registry.AutoUnpack = helper.AutoUnpackUpdates() - err = registry.DownloadUpdates(ctx, true) - if err != nil { - return fmt.Errorf("failed to re-download files: %w", err) - } - - return nil -} - -func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string { - signedBy := make([]string, 0, len(fds)) - for _, fd := range fds { - if sig := fd.Signature(); sig != nil { - for _, seal := range sig.Signatures { - if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { - signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) - } else { - signedBy = append(signedBy, seal.ID) - } - } - } - } - return strings.Join(signedBy, " and ") -} diff --git a/cmds/portmaster-start/version.go b/cmds/portmaster-start/version.go deleted file mode 100644 index 6c28362a..00000000 --- a/cmds/portmaster-start/version.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime" - "sort" - "strings" - "text/tabwriter" - - "github.com/spf13/cobra" - - "github.com/safing/portmaster/base/info" -) - -var ( - showShortVersion bool - showAllVersions bool - versionCmd = &cobra.Command{ - Use: "version", - Short: "Display various portmaster versions", - Args: cobra.NoArgs, - PersistentPreRunE: func(*cobra.Command, []string) error { - if showAllVersions { - // If we are going to show all component versions, - // we need the registry to be configured. - if err := configureRegistry(false); err != nil { - return err - } - } - - return nil - }, - RunE: func(*cobra.Command, []string) error { - if !showAllVersions { - if showShortVersion { - fmt.Println(info.Version()) - return nil - } - - fmt.Println(info.FullVersion()) - return nil - } - - fmt.Printf("portmaster-start %s\n\n", info.Version()) - fmt.Printf("Assets:\n") - - all := registry.Export() - keys := make([]string, 0, len(all)) - for identifier := range all { - keys = append(keys, identifier) - } - sort.Strings(keys) - - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - for _, identifier := range keys { - res := all[identifier] - - if showShortVersion { - // in "short" mode, skip all resources that are irrelevant on that platform - if !strings.HasPrefix(identifier, "all") && !strings.HasPrefix(identifier, runtime.GOOS) { - continue - } - } - - fmt.Fprintf(tw, " %s\t%s\n", identifier, res.SelectedVersion.VersionNumber) - } - return tw.Flush() - }, - } -) - -func init() { - flags := versionCmd.Flags() - { - flags.BoolVar(&showShortVersion, "short", false, "Print only the version number.") - flags.BoolVar(&showAllVersions, "all", false, "Dump versions for all assets.") - } - - rootCmd.AddCommand(versionCmd) -} diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go index b6d722dc..b56da4ea 100644 --- a/service/intel/geoip/init_test.go +++ b/service/intel/geoip/init_test.go @@ -21,7 +21,7 @@ type testInstance struct { var _ instance = &testInstance{} -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -54,6 +54,15 @@ func runTest(m *testing.M) error { return fmt.Errorf("failed to initialize dataroot: %w", err) } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "geoip_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } stub := &testInstance{} stub.db, err = dbmodule.New(stub) @@ -68,7 +77,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/netenv/init_test.go b/service/netenv/init_test.go index b747111b..1c026d68 100644 --- a/service/netenv/init_test.go +++ b/service/netenv/init_test.go @@ -21,7 +21,7 @@ type testInstance struct { var _ instance = &testInstance{} -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -54,6 +54,15 @@ func runTest(m *testing.M) error { return fmt.Errorf("failed to initialize dataroot: %w", err) } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "netenv_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } stub := &testInstance{} stub.db, err = dbmodule.New(stub) @@ -68,7 +77,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/profile/endpoints/endpoints_test.go b/service/profile/endpoints/endpoints_test.go index bbc81f6a..93219473 100644 --- a/service/profile/endpoints/endpoints_test.go +++ b/service/profile/endpoints/endpoints_test.go @@ -27,7 +27,7 @@ type testInstance struct { geoip *geoip.GeoIP } -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -61,6 +61,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "endpoints_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} stub.db, err = dbmodule.New(stub) if err != nil { @@ -74,7 +84,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create api: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/resolver/main_test.go b/service/resolver/main_test.go index 4efc8eb8..bc780e40 100644 --- a/service/resolver/main_test.go +++ b/service/resolver/main_test.go @@ -26,9 +26,7 @@ type testInstance struct { netenv *netenv.NetEnv } -// var _ instance = &testInstance{} - -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -70,6 +68,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "resolver_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} stub.db, err = dbmodule.New(stub) if err != nil { @@ -91,7 +99,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create netenv: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/service/updates/assets/portmaster.service b/service/updates/assets/portmaster.service deleted file mode 100644 index c69a9ff5..00000000 --- a/service/updates/assets/portmaster.service +++ /dev/null @@ -1,44 +0,0 @@ -[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 diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go index 9b796880..3f82e367 100644 --- a/service/updates/bundlegeneration.go +++ b/service/updates/bundlegeneration.go @@ -3,6 +3,7 @@ package updates import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "os" "path" @@ -179,3 +180,50 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string, // `dirPath + filename` is guaranteed by path.Split() return dirPath + filename, version, true } + +// GenerateMockFolder generates mock bundle folder for testing. +func GenerateMockFolder(dir, name, version string) error { + // Make sure dir exists + _ = os.MkdirAll(dir, defaultDirMode) + + // Create empty files + file, err := os.Create(filepath.Join(dir, "portmaster")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster-core")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "portmaster.zip")) + if err != nil { + return err + } + _ = file.Close() + file, err = os.Create(filepath.Join(dir, "assets.zip")) + if err != nil { + return err + } + _ = file.Close() + + bundle, err := GenerateBundleFromDir(dir, BundleFileSettings{ + Name: name, + Version: version, + }) + if err != nil { + return err + } + + bundleStr, err := json.MarshalIndent(bundle, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err) + } + + err = os.WriteFile(filepath.Join(dir, "index.json"), bundleStr, defaultFileMode) + if err != nil { + return err + } + return nil +} diff --git a/service/updates/downloader.go b/service/updates/downloader.go index 130265be..4087964b 100644 --- a/service/updates/downloader.go +++ b/service/updates/downloader.go @@ -40,8 +40,10 @@ func CreateDownloader(index UpdateIndex) Downloader { func (d *Downloader) downloadIndexFile(ctx context.Context) error { // Make sure dir exists - _ = os.MkdirAll(d.dir, defaultDirMode) - var err error + err := os.MkdirAll(d.dir, defaultDirMode) + if err != nil { + return fmt.Errorf("failed to create directory for updates: %s", d.dir) + } var content string for _, url := range d.indexURLs { content, err = d.downloadIndexFileFromURL(ctx, url) @@ -50,15 +52,15 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { continue } // Downloading was successful. - - bundle, err := ParseBundle(content) + var bundle *Bundle + bundle, err = ParseBundle(content) if err != nil { log.Warningf("updates: %s", err) continue } // Parsing was successful - - version, err := semver.NewVersion(d.bundle.Version) + var version *semver.Version + version, err = semver.NewVersion(d.bundle.Version) if err != nil { log.Warningf("updates: failed to parse bundle version: %s", err) continue @@ -79,7 +81,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error { indexFilepath := filepath.Join(d.dir, d.indexFile) err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode) if err != nil { - return fmt.Errorf("failed to write index file: %s", err) + return fmt.Errorf("failed to write index file: %w", err) } return nil diff --git a/service/updates/module.go b/service/updates/module.go index 5df50c66..44b1146f 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -6,8 +6,6 @@ import ( "runtime" "time" - "github.com/safing/portmaster/base/api" - "github.com/safing/portmaster/base/config" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/service/mgr" @@ -215,8 +213,6 @@ func (u *Updates) Stop() error { } type instance interface { - API() *api.API - Config() *config.Config Restart() Shutdown() Notifications() *notifications.Notifications diff --git a/service/updates/updates_test.go b/service/updates/updates_test.go new file mode 100644 index 00000000..d2857aa5 --- /dev/null +++ b/service/updates/updates_test.go @@ -0,0 +1,89 @@ +package updates + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/safing/portmaster/base/notifications" +) + +type testInstance struct{} + +func (i *testInstance) Restart() {} +func (i *testInstance) Shutdown() {} + +func (i *testInstance) Notifications() *notifications.Notifications { + return nil +} + +func (i *testInstance) Ready() bool { + return true +} + +func (i *testInstance) SetCmdLineOperation(f func() error) {} + +func TestPreformUpdate(t *testing.T) { + t.Parallel() + + // Initialize mock instance + stub := &testInstance{} + + // Make tmp dirs + installedDir, err := os.MkdirTemp("", "updates_current") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(installedDir) }() + updateDir, err := os.MkdirTemp("", "updates_new") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(updateDir) }() + purgeDir, err := os.MkdirTemp("", "updates_purge") + if err != nil { + panic(err) + } + defer func() { _ = os.RemoveAll(purgeDir) }() + + // Generate mock files + if err := GenerateMockFolder(installedDir, "Test", "1.0.0"); err != nil { + panic(err) + } + if err := GenerateMockFolder(updateDir, "Test", "1.0.1"); err != nil { + panic(err) + } + + // Create updater + updates, err := New(stub, "Test", UpdateIndex{ + Directory: installedDir, + DownloadDirectory: updateDir, + PurgeDirectory: purgeDir, + IndexFile: "index.json", + AutoApply: false, + NeedsRestart: false, + }) + if err != nil { + panic(err) + } + // Read and parse the index file + if err := updates.downloader.Verify(); err != nil { + panic(err) + } + // Try to apply the updates + err = updates.applyUpdates(nil) + if err != nil { + panic(err) + } + + // CHeck if the current version is now the new. + bundle, err := LoadBundle(filepath.Join(installedDir, "index.json")) + if err != nil { + panic(err) + } + + if bundle.Version != "1.0.1" { + panic(fmt.Errorf("expected version 1.0.1 found %s", bundle.Version)) + } +} diff --git a/spn/hub/hub_test.go b/spn/hub/hub_test.go index 391a61e7..d82bd6cc 100644 --- a/spn/hub/hub_test.go +++ b/spn/hub/hub_test.go @@ -24,7 +24,7 @@ type testInstance struct { base *base.Base } -func (stub *testInstance) Updates() *updates.Updates { +func (stub *testInstance) IntelUpdates() *updates.Updates { return stub.updates } @@ -62,6 +62,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "hub_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} // Init stub.db, err = dbmodule.New(stub) @@ -76,7 +86,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) } diff --git a/spn/instance.go b/spn/instance.go index 69114d38..ed942933 100644 --- a/spn/instance.go +++ b/spn/instance.go @@ -48,11 +48,12 @@ type Instance struct { runtime *runtime.Runtime rng *rng.Rng - core *core.Core - updates *updates.Updates - geoip *geoip.GeoIP - netenv *netenv.NetEnv - filterLists *filterlists.FilterLists + core *core.Core + binaryUpdates *updates.Updates + intelUpdates *updates.Updates + geoip *geoip.GeoIP + netenv *netenv.NetEnv + filterLists *filterlists.FilterLists access *access.Access cabin *cabin.Cabin @@ -74,6 +75,14 @@ func New() (*Instance, error) { instance := &Instance{} instance.ctx, instance.cancelCtx = context.WithCancel(context.Background()) + binaryUpdateIndex := updates.UpdateIndex{ + // FIXME: fill + } + + intelUpdateIndex := updates.UpdateIndex{ + // FIXME: fill + } + var err error // Base modules @@ -111,7 +120,11 @@ func New() (*Instance, error) { if err != nil { return instance, fmt.Errorf("create core module: %w", err) } - instance.updates, err = updates.New(instance) + instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex) + if err != nil { + return instance, fmt.Errorf("create updates module: %w", err) + } + instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex) if err != nil { return instance, fmt.Errorf("create updates module: %w", err) } @@ -181,7 +194,8 @@ func New() (*Instance, error) { instance.rng, instance.core, - instance.updates, + instance.binaryUpdates, + instance.intelUpdates, instance.geoip, instance.netenv, @@ -255,9 +269,14 @@ func (i *Instance) Base() *base.Base { return i.base } -// Updates returns the updates module. -func (i *Instance) Updates() *updates.Updates { - return i.updates +// BinaryUpdates returns the updates module. +func (i *Instance) BinaryUpdates() *updates.Updates { + return i.binaryUpdates +} + +// IntelUpdates returns the updates module. +func (i *Instance) IntelUpdates() *updates.Updates { + return i.intelUpdates } // GeoIP returns the geoip module. diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go index d02e1e5d..6ad2ea46 100644 --- a/spn/navigator/module_test.go +++ b/spn/navigator/module_test.go @@ -62,6 +62,16 @@ func runTest(m *testing.M) error { } defer func() { _ = os.RemoveAll(ds) }() + installDir, err := os.MkdirTemp("", "geoip_installdir") + if err != nil { + return fmt.Errorf("failed to create tmp install dir: %w", err) + } + defer func() { _ = os.RemoveAll(installDir) }() + err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0") + if err != nil { + return fmt.Errorf("failed to generate mock installation: %w", err) + } + stub := &testInstance{} log.SetLogLevel(log.DebugLevel) @@ -78,7 +88,10 @@ func runTest(m *testing.M) error { if err != nil { return fmt.Errorf("failed to create config: %w", err) } - stub.updates, err = updates.New(stub, "Intel Test", updates.UpdateIndex{}) + stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{ + Directory: installDir, + IndexFile: "index.json", + }) if err != nil { return fmt.Errorf("failed to create updates: %w", err) }