[WIP] Fix unit tests

This commit is contained in:
Vladimir Stoilov 2024-10-08 14:13:08 +03:00
parent a8517cd65f
commit a874ec9412
No known key found for this signature in database
GPG key ID: 2F190B67A43A81AF
48 changed files with 264 additions and 4088 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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(" <INTERRUPT>")
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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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\<user>\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()
}

View file

@ -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))
}
}

View file

@ -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/)

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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)
}
}()
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -1,6 +0,0 @@
# binaries
portmaster-start
portmaster-start.exe
# test dir
test

View file

@ -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}" "$@"

View file

@ -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) {}

View file

@ -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,
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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"))
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 ")
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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))
}
}

View file

@ -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)
}

View file

@ -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.

View file

@ -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)
}