wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

294
service/core/api.go Normal file
View 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")
}
}

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

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

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

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

View File

@@ -0,0 +1,8 @@
//go:build !windows
package core
// only return on Fatal error!
func startPlatformSpecific() error {
return nil
}

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

View 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 ===")
}
}