wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
294
service/core/api.go
Normal file
294
service/core/api.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
"github.com/safing/portmaster/service/status"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "core/shutdown",
|
||||
Write: api.PermitSelf,
|
||||
// Do NOT register as belonging to the module, so that the API is available
|
||||
// when something fails during starting of this module or a dependency.
|
||||
ActionFunc: shutdown,
|
||||
Name: "Shut Down Portmaster",
|
||||
Description: "Shut down the Portmaster Core Service and all UI components.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "core/restart",
|
||||
Write: api.PermitAdmin,
|
||||
// Do NOT register as belonging to the module, so that the API is available
|
||||
// when something fails during starting of this module or a dependency.
|
||||
ActionFunc: restart,
|
||||
Name: "Restart Portmaster",
|
||||
Description: "Restart the Portmaster Core Service.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "debug/core",
|
||||
Read: api.PermitAnyone,
|
||||
BelongsTo: module,
|
||||
DataFunc: debugInfo,
|
||||
Name: "Get Debug Information",
|
||||
Description: "Returns network debugging information, similar to debug/info, but with system status data.",
|
||||
Parameters: []api.Parameter{{
|
||||
Method: http.MethodGet,
|
||||
Field: "style",
|
||||
Value: "github",
|
||||
Description: "Specify the formatting style. The default is simple markdown formatting.",
|
||||
}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "app/auth",
|
||||
Read: api.PermitAnyone,
|
||||
BelongsTo: module,
|
||||
StructFunc: authorizeApp,
|
||||
Name: "Request an authentication token with a given set of permissions. The user will be prompted to either authorize or deny the request. Used for external or third-party tool integrations.",
|
||||
Parameters: []api.Parameter{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "app-name",
|
||||
Description: "The name of the application requesting access",
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "read",
|
||||
Description: "The requested read permission",
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "write",
|
||||
Description: "The requested write permission",
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Field: "ttl",
|
||||
Description: "The time-to-live for the new access token. Defaults to 24h",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "app/profile",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: getMyProfile,
|
||||
Name: "Get the ID of the calling profile",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shutdown shuts the Portmaster down.
|
||||
func shutdown(_ *api.Request) (msg string, err error) {
|
||||
log.Warning("core: user requested shutdown via action")
|
||||
|
||||
// Do not run in worker, as this would block itself here.
|
||||
go modules.Shutdown() //nolint:errcheck
|
||||
|
||||
return "shutdown initiated", nil
|
||||
}
|
||||
|
||||
// restart restarts the Portmaster.
|
||||
func restart(_ *api.Request) (msg string, err error) {
|
||||
log.Info("core: user requested restart via action")
|
||||
|
||||
// Let the updates module handle restarting.
|
||||
updates.RestartNow()
|
||||
|
||||
return "restart initiated", nil
|
||||
}
|
||||
|
||||
// debugInfo returns the debugging information for support requests.
|
||||
func debugInfo(ar *api.Request) (data []byte, err error) {
|
||||
// Create debug information helper.
|
||||
di := new(debug.Info)
|
||||
di.Style = ar.Request.URL.Query().Get("style")
|
||||
|
||||
// Add debug information.
|
||||
|
||||
// Very basic information at the start.
|
||||
di.AddVersionInfo()
|
||||
di.AddPlatformInfo(ar.Context())
|
||||
|
||||
// Errors and unexpected logs.
|
||||
di.AddLastReportedModuleError()
|
||||
di.AddLastUnexpectedLogs()
|
||||
|
||||
// Status Information from various modules.
|
||||
status.AddToDebugInfo(di)
|
||||
captain.AddToDebugInfo(di)
|
||||
resolver.AddToDebugInfo(di)
|
||||
config.AddToDebugInfo(di)
|
||||
|
||||
// Detailed information.
|
||||
updates.AddToDebugInfo(di)
|
||||
compat.AddToDebugInfo(di)
|
||||
di.AddGoroutineStack()
|
||||
|
||||
// Return data.
|
||||
return di.Bytes(), nil
|
||||
}
|
||||
|
||||
// getSavePermission returns the requested api.Permission from p.
|
||||
// It only allows "user" and "admin" as external processes should
|
||||
// never be able to request "self".
|
||||
func getSavePermission(p string) api.Permission {
|
||||
switch p {
|
||||
case "user":
|
||||
return api.PermitUser
|
||||
case "admin":
|
||||
return api.PermitAdmin
|
||||
default:
|
||||
return api.NotSupported
|
||||
}
|
||||
}
|
||||
|
||||
func getMyProfile(ar *api.Request) (interface{}, error) {
|
||||
proc, err := process.GetProcessByRequestOrigin(ar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localProfile := proc.Profile().LocalProfile()
|
||||
|
||||
return map[string]interface{}{
|
||||
"profile": localProfile.ID,
|
||||
"source": localProfile.Source,
|
||||
"name": localProfile.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authorizeApp(ar *api.Request) (interface{}, error) {
|
||||
appName := ar.Request.URL.Query().Get("app-name")
|
||||
readPermStr := ar.Request.URL.Query().Get("read")
|
||||
writePermStr := ar.Request.URL.Query().Get("write")
|
||||
|
||||
ttl := time.Hour * 24
|
||||
if ttlStr := ar.Request.URL.Query().Get("ttl"); ttlStr != "" {
|
||||
var err error
|
||||
ttl, err = time.ParseDuration(ttlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// convert the requested read and write permissions to their api.Permission
|
||||
// value. This ensures only "user" or "admin" permissions can be requested.
|
||||
if getSavePermission(readPermStr) <= api.NotSupported {
|
||||
return nil, fmt.Errorf("invalid read permission")
|
||||
}
|
||||
if getSavePermission(writePermStr) <= api.NotSupported {
|
||||
return nil, fmt.Errorf("invalid read permission")
|
||||
}
|
||||
|
||||
proc, err := process.GetProcessByRequestOrigin(ar)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify requesting process: %w", err)
|
||||
}
|
||||
|
||||
n := notifications.Notification{
|
||||
Type: notifications.Prompt,
|
||||
EventID: "core:authorize-app-" + time.Now().String(),
|
||||
Title: "An app requests access to the Portmaster",
|
||||
Message: "Allow " + appName + " (" + proc.Profile().LocalProfile().Name + ") to query and modify the Portmaster?\n\nBinary: " + proc.Path,
|
||||
ShowOnSystem: true,
|
||||
Expires: time.Now().Add(time.Minute).Unix(),
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
ID: "allow",
|
||||
Text: "Authorize",
|
||||
},
|
||||
{
|
||||
ID: "deny",
|
||||
Text: "Deny",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ch := make(chan string)
|
||||
|
||||
validUntil := time.Now().Add(ttl)
|
||||
|
||||
n.SetActionFunction(func(ctx context.Context, n *notifications.Notification) error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if n.SelectedActionID != "allow" {
|
||||
close(ch)
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := config.Concurrent.GetAsStringArray(api.CfgAPIKeys, []string{})()
|
||||
|
||||
newKeyData, err := rng.Bytes(8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newKeyHex := hex.EncodeToString(newKeyData)
|
||||
|
||||
query := url.Values{
|
||||
"read": []string{readPermStr},
|
||||
"write": []string{writePermStr},
|
||||
"expires": []string{validUntil.Format(time.RFC3339)},
|
||||
}
|
||||
|
||||
keys = append(keys, fmt.Sprintf("%s?%s", newKeyHex, query.Encode()))
|
||||
|
||||
if err := config.SetConfigOption(api.CfgAPIKeys, keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch <- newKeyHex
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
n.Save()
|
||||
|
||||
select {
|
||||
case key := <-ch:
|
||||
if len(key) == 0 {
|
||||
return nil, fmt.Errorf("access denied")
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"key": key,
|
||||
"validUntil": validUntil,
|
||||
}, nil
|
||||
case <-ar.Context().Done():
|
||||
return nil, fmt.Errorf("timeout")
|
||||
}
|
||||
}
|
||||
43
service/core/base/databases.go
Normal file
43
service/core/base/databases.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/database"
|
||||
_ "github.com/safing/portbase/database/dbmodule"
|
||||
_ "github.com/safing/portbase/database/storage/bbolt"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
var (
|
||||
DefaultDatabaseStorageType = "bbolt"
|
||||
)
|
||||
|
||||
func registerDatabases() error {
|
||||
_, err := database.Register(&database.Database{
|
||||
Name: "core",
|
||||
Description: "Holds core data, such as settings and profiles",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = database.Register(&database.Database{
|
||||
Name: "cache",
|
||||
Description: "Cached data, such as Intelligence and DNS Records",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// _, err = database.Register(&database.Database{
|
||||
// Name: "history",
|
||||
// Description: "Historic event data",
|
||||
// StorageType: DefaultDatabaseStorageType,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
69
service/core/base/global.go
Normal file
69
service/core/base/global.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
var (
|
||||
DefaultAPIListenAddress = "127.0.0.1:817"
|
||||
|
||||
dataDir string
|
||||
databaseDir string
|
||||
showVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&dataDir, "data", "", "set data directory")
|
||||
flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
|
||||
flag.BoolVar(&showVersion, "version", false, "show version and exit")
|
||||
|
||||
modules.SetGlobalPrepFn(globalPrep)
|
||||
}
|
||||
|
||||
func globalPrep() error {
|
||||
// check if meta info is ok
|
||||
err := info.CheckVersion()
|
||||
if err != nil {
|
||||
return errors.New("compile error: please compile using the provided build script")
|
||||
}
|
||||
|
||||
// print version
|
||||
if showVersion {
|
||||
fmt.Println(info.FullVersion())
|
||||
return modules.ErrCleanExit
|
||||
}
|
||||
|
||||
// check data root
|
||||
if dataroot.Root() == nil {
|
||||
// initialize data dir
|
||||
|
||||
// backwards compatibility
|
||||
if dataDir == "" {
|
||||
dataDir = databaseDir
|
||||
}
|
||||
|
||||
// check data dir
|
||||
if dataDir == "" {
|
||||
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
||||
}
|
||||
|
||||
// initialize structure
|
||||
err := dataroot.Initialize(dataDir, 0o0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set api listen address
|
||||
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
||||
|
||||
return nil
|
||||
}
|
||||
60
service/core/base/logs.go
Normal file
60
service/core/base/logs.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
const (
|
||||
logTTL = 30 * 24 * time.Hour
|
||||
logFileDir = "logs"
|
||||
logFileSuffix = ".log"
|
||||
)
|
||||
|
||||
func registerLogCleaner() {
|
||||
module.NewTask("log cleaner", logCleaner).
|
||||
Repeat(24 * time.Hour).
|
||||
Schedule(time.Now().Add(15 * time.Minute))
|
||||
}
|
||||
|
||||
func logCleaner(_ context.Context, _ *modules.Task) error {
|
||||
ageThreshold := time.Now().Add(-logTTL)
|
||||
|
||||
return filepath.Walk(
|
||||
filepath.Join(dataroot.Root().Path, logFileDir),
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Warningf("core: failed to access %s while deleting old log files: %s", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case !info.Mode().IsRegular():
|
||||
// Only delete regular files.
|
||||
case !strings.HasSuffix(path, logFileSuffix):
|
||||
// Only delete files that end with the correct suffix.
|
||||
case info.ModTime().After(ageThreshold):
|
||||
// Only delete files that are older that the log TTL.
|
||||
default:
|
||||
// Delete log file.
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
log.Warningf("core: failed to delete old log file %s: %s", path, err)
|
||||
} else {
|
||||
log.Tracef("core: deleted old log file %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
38
service/core/base/module.go
Normal file
38
service/core/base/module.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
_ "github.com/safing/portbase/config"
|
||||
_ "github.com/safing/portbase/metrics"
|
||||
"github.com/safing/portbase/modules"
|
||||
_ "github.com/safing/portbase/rng"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
func init() {
|
||||
module = modules.Register("base", nil, start, nil, "database", "config", "rng", "metrics")
|
||||
|
||||
// For prettier subsystem graph, printed with --print-subsystem-graph
|
||||
/*
|
||||
subsystems.Register(
|
||||
"base",
|
||||
"Base",
|
||||
"THE GROUND.",
|
||||
baseModule,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
func start() error {
|
||||
startProfiling()
|
||||
|
||||
if err := registerDatabases(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerLogCleaner()
|
||||
|
||||
return nil
|
||||
}
|
||||
41
service/core/base/profiling.go
Normal file
41
service/core/base/profiling.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
var cpuProfile string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&cpuProfile, "cpuprofile", "", "write cpu profile to `file`")
|
||||
}
|
||||
|
||||
func startProfiling() {
|
||||
if cpuProfile != "" {
|
||||
module.StartWorker("cpu profiler", cpuProfiler)
|
||||
}
|
||||
}
|
||||
|
||||
func cpuProfiler(ctx context.Context) error {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create CPU profile: %w", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profile: %w", err)
|
||||
}
|
||||
|
||||
// wait for shutdown
|
||||
<-ctx.Done()
|
||||
|
||||
pprof.StopCPUProfile()
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close CPU profile file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
112
service/core/config.go
Normal file
112
service/core/config.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
locale "github.com/Xuanwo/go-locale"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Configuration Keys.
|
||||
var (
|
||||
// CfgDevModeKey was previously defined here.
|
||||
CfgDevModeKey = config.CfgDevModeKey
|
||||
|
||||
CfgNetworkServiceKey = "core/networkService"
|
||||
defaultNetworkServiceMode bool
|
||||
|
||||
CfgLocaleKey = "core/locale"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(
|
||||
&defaultNetworkServiceMode,
|
||||
"network-service",
|
||||
false,
|
||||
"set default network service mode; configuration is stronger",
|
||||
)
|
||||
}
|
||||
|
||||
func registerConfig() error {
|
||||
if err := config.Register(&config.Option{
|
||||
Name: "Network Service",
|
||||
Key: CfgNetworkServiceKey,
|
||||
Description: "Use the Portmaster as a network service, where applicable. You will have to take care of lots of network setup yourself in order to run this properly and securely.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
||||
DefaultValue: defaultNetworkServiceMode,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: 513,
|
||||
config.CategoryAnnotation: "Network Service",
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.Register(&config.Option{
|
||||
Name: "Time and Date Format",
|
||||
Key: CfgLocaleKey,
|
||||
Description: "Configures the time and date format for the user interface. Selection is an example and correct formatting in the UI is a continual work in progress.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
DefaultValue: getDefaultLocale(),
|
||||
PossibleValues: []config.PossibleValue{
|
||||
{
|
||||
Name: "24h DD-MM-YYYY",
|
||||
Value: enGBLocale,
|
||||
},
|
||||
{
|
||||
Name: "12h MM/DD/YYYY",
|
||||
Value: enUSLocale,
|
||||
},
|
||||
},
|
||||
Annotations: config.Annotations{
|
||||
config.CategoryAnnotation: "User Interface",
|
||||
config.DisplayHintAnnotation: config.DisplayHintOneOf,
|
||||
config.RequiresUIReloadAnnotation: true,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDefaultLocale() string {
|
||||
// Get locales from system.
|
||||
detectedLocales, err := locale.DetectAll()
|
||||
if err != nil {
|
||||
log.Warningf("core: failed to detect locale: %s", err)
|
||||
return enGBLocale
|
||||
}
|
||||
|
||||
// log.Debugf("core: detected locales: %s", detectedLocales)
|
||||
|
||||
// Check if there is a locale that corresponds to the en-US locale.
|
||||
for _, detectedLocale := range detectedLocales {
|
||||
if slices.Contains[[]string, string](defaultEnUSLocales, detectedLocale.String()) {
|
||||
return enUSLocale
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, return the en-GB locale as default.
|
||||
return enGBLocale
|
||||
}
|
||||
|
||||
var (
|
||||
enGBLocale = "en-GB"
|
||||
enUSLocale = "en-US"
|
||||
|
||||
defaultEnUSLocales = []string{
|
||||
"en-AS", // English (American Samoa)
|
||||
"en-GU", // English (Guam)
|
||||
"en-UM", // English (U.S. Minor Outlying Islands)
|
||||
"en-US", // English (United States)
|
||||
"en-VI", // English (U.S. Virgin Islands)
|
||||
}
|
||||
)
|
||||
100
service/core/core.go
Normal file
100
service/core/core.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/metrics"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
_ "github.com/safing/portmaster/service/broadcasts"
|
||||
_ "github.com/safing/portmaster/service/netenv"
|
||||
_ "github.com/safing/portmaster/service/netquery"
|
||||
_ "github.com/safing/portmaster/service/status"
|
||||
_ "github.com/safing/portmaster/service/sync"
|
||||
_ "github.com/safing/portmaster/service/ui"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
const (
|
||||
eventShutdown = "shutdown"
|
||||
eventRestart = "restart"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
|
||||
disableShutdownEvent bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "netquery", "interception", "compat", "broadcasts", "sync")
|
||||
subsystems.Register(
|
||||
"core",
|
||||
"Core",
|
||||
"Base Structure and System Integration",
|
||||
module,
|
||||
"config:core/",
|
||||
nil,
|
||||
)
|
||||
|
||||
flag.BoolVar(
|
||||
&disableShutdownEvent,
|
||||
"disable-shutdown-event",
|
||||
false,
|
||||
"disable shutdown event to keep app and notifier open when core shuts down",
|
||||
)
|
||||
|
||||
modules.SetGlobalShutdownFn(shutdownHook)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
registerEvents()
|
||||
|
||||
// init config
|
||||
err := registerConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() error {
|
||||
if err := startPlatformSpecific(); err != nil {
|
||||
return fmt.Errorf("failed to start plattform-specific components: %w", err)
|
||||
}
|
||||
|
||||
// Enable persistent metrics.
|
||||
if err := metrics.EnableMetricPersistence("core:metrics/storage"); err != nil {
|
||||
log.Warningf("core: failed to enable persisted metrics: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerEvents() {
|
||||
module.RegisterEvent(eventShutdown, true)
|
||||
module.RegisterEvent(eventRestart, true)
|
||||
}
|
||||
|
||||
func shutdownHook() {
|
||||
// Notify everyone of the restart/shutdown.
|
||||
if !updates.IsRestarting() {
|
||||
// Only trigger shutdown event if not disabled.
|
||||
if !disableShutdownEvent {
|
||||
module.TriggerEvent(eventShutdown, nil)
|
||||
}
|
||||
} else {
|
||||
module.TriggerEvent(eventRestart, nil)
|
||||
}
|
||||
|
||||
// Wait a bit for the event to propagate.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
8
service/core/os_default.go
Normal file
8
service/core/os_default.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package core
|
||||
|
||||
// only return on Fatal error!
|
||||
func startPlatformSpecific() error {
|
||||
return nil
|
||||
}
|
||||
16
service/core/os_windows.go
Normal file
16
service/core/os_windows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
)
|
||||
|
||||
// only return on Fatal error!
|
||||
func startPlatformSpecific() error {
|
||||
// We can't catch errors when calling WindowsNTVersion() in logging, so we call the function here, just to catch possible errors
|
||||
if _, err := osdetail.WindowsNTVersion(); err != nil {
|
||||
log.Errorf("failed to obtain WindowsNTVersion: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
137
service/core/pmtesting/testing.go
Normal file
137
service/core/pmtesting/testing.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Package pmtesting provides a simple unit test setup routine.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// package name
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
//
|
||||
// "github.com/safing/portmaster/service/core/pmtesting"
|
||||
// )
|
||||
//
|
||||
// func TestMain(m *testing.M) {
|
||||
// pmtesting.TestMain(m, module)
|
||||
// }
|
||||
package pmtesting
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
|
||||
_ "github.com/safing/portbase/database/storage/hashmap"
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/service/core/base"
|
||||
)
|
||||
|
||||
var printStackOnExit bool
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
|
||||
}
|
||||
|
||||
// TestHookFunc describes the functions passed to TestMainWithHooks.
|
||||
type TestHookFunc func() error
|
||||
|
||||
// TestMain provides a simple unit test setup routine.
|
||||
func TestMain(m *testing.M, module *modules.Module) {
|
||||
TestMainWithHooks(m, module, nil, nil)
|
||||
}
|
||||
|
||||
// TestMainWithHooks provides a simple unit test setup routine and calls
|
||||
// afterStartFn after modules have started and beforeStopFn before modules
|
||||
// are shutdown.
|
||||
func TestMainWithHooks(m *testing.M, module *modules.Module, afterStartFn, beforeStopFn TestHookFunc) {
|
||||
// Only enable needed modules.
|
||||
modules.EnableModuleManagement(nil)
|
||||
|
||||
// Enable this module for testing.
|
||||
if module != nil {
|
||||
module.Enable()
|
||||
}
|
||||
|
||||
// switch databases to memory only
|
||||
base.DefaultDatabaseStorageType = "hashmap"
|
||||
|
||||
// switch API to high port
|
||||
base.DefaultAPIListenAddress = "127.0.0.1:10817"
|
||||
|
||||
// set log level
|
||||
log.SetLogLevel(log.TraceLevel)
|
||||
|
||||
// tmp dir for data root (db & config)
|
||||
tmpDir := filepath.Join(os.TempDir(), "portmaster-testing")
|
||||
// initialize data dir
|
||||
err := dataroot.Initialize(tmpDir, 0o0755)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to initialize data root: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// start modules
|
||||
var exitCode int
|
||||
err = modules.Start()
|
||||
if err != nil {
|
||||
// starting failed
|
||||
fmt.Fprintf(os.Stderr, "failed to setup test: %s\n", err)
|
||||
exitCode = 1
|
||||
} else {
|
||||
runTests := true
|
||||
if afterStartFn != nil {
|
||||
if err := afterStartFn(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run test start hook: %s\n", err)
|
||||
runTests = false
|
||||
exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
if runTests {
|
||||
// run tests
|
||||
exitCode = m.Run()
|
||||
}
|
||||
}
|
||||
|
||||
if beforeStopFn != nil {
|
||||
if err := beforeStopFn(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run test shutdown hook: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown
|
||||
_ = modules.Shutdown()
|
||||
if modules.GetExitStatusCode() != 0 {
|
||||
exitCode = modules.GetExitStatusCode()
|
||||
fmt.Fprintf(os.Stderr, "failed to cleanly shutdown test: %s\n", err)
|
||||
}
|
||||
printStack()
|
||||
|
||||
// clean up and exit
|
||||
|
||||
// Important: Do not remove tmpDir, as it is used as a cache for updates.
|
||||
// remove config
|
||||
_ = os.Remove(filepath.Join(tmpDir, "config.json"))
|
||||
// remove databases
|
||||
_ = os.Remove(filepath.Join(tmpDir, "databases.json"))
|
||||
_ = os.RemoveAll(filepath.Join(tmpDir, "databases"))
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func printStack() {
|
||||
if printStackOnExit {
|
||||
fmt.Println("=== PRINTING TRACES ===")
|
||||
fmt.Println("=== GOROUTINES ===")
|
||||
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== BLOCKING ===")
|
||||
_ = pprof.Lookup("block").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== MUTEXES ===")
|
||||
_ = pprof.Lookup("mutex").WriteTo(os.Stdout, 2)
|
||||
fmt.Println("=== END TRACES ===")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user