mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Finalize profile merging, add profile metadata state handling, re-attribute connections after profile deletion
This commit is contained in:
parent
32342ec91a
commit
bed5c72a6b
14 changed files with 622 additions and 96 deletions
|
@ -2,13 +2,18 @@ package firewall
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/modules/subsystems"
|
||||
_ "github.com/safing/portmaster/core"
|
||||
"github.com/safing/portmaster/network"
|
||||
"github.com/safing/portmaster/profile"
|
||||
"github.com/safing/spn/access"
|
||||
"github.com/safing/spn/captain"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
@ -25,12 +30,6 @@ func init() {
|
|||
)
|
||||
}
|
||||
|
||||
const (
|
||||
configChangeEvent = "config change"
|
||||
profileConfigChangeEvent = "profile config change"
|
||||
onSPNConnectEvent = "spn connect"
|
||||
)
|
||||
|
||||
func prep() error {
|
||||
network.SetDefaultFirewallHandler(verdictHandler)
|
||||
|
||||
|
@ -38,8 +37,8 @@ func prep() error {
|
|||
// this will be triggered on spn enable/disable
|
||||
err := module.RegisterEventHook(
|
||||
"config",
|
||||
configChangeEvent,
|
||||
"reset connection verdicts",
|
||||
config.ChangeEvent,
|
||||
"reset connection verdicts after global config change",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
|
@ -52,10 +51,20 @@ func prep() error {
|
|||
// Reset connections every time profile changes
|
||||
err = module.RegisterEventHook(
|
||||
"profiles",
|
||||
profileConfigChangeEvent,
|
||||
"reset connection verdicts",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
profile.ConfigChangeEvent,
|
||||
"reset connection verdicts after profile config change",
|
||||
func(ctx context.Context, eventData interface{}) error {
|
||||
// Expected event data: scoped profile ID.
|
||||
profileID, ok := eventData.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("event data is not a string: %v", eventData)
|
||||
}
|
||||
profileSource, profileID, ok := strings.Cut(profileID, "/")
|
||||
if !ok {
|
||||
return fmt.Errorf("event data does not seem to be a scoped profile ID: %v", eventData)
|
||||
}
|
||||
|
||||
resetProfileConnectionVerdict(profileSource, profileID)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
@ -67,8 +76,8 @@ func prep() error {
|
|||
// connect and disconnecting is triggered on config change event but connecting takеs more time
|
||||
err = module.RegisterEventHook(
|
||||
"captain",
|
||||
onSPNConnectEvent,
|
||||
"reset connection verdicts",
|
||||
captain.SPNConnectedEvent,
|
||||
"reset connection verdicts on SPN connect",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
|
@ -83,7 +92,7 @@ func prep() error {
|
|||
err = module.RegisterEventHook(
|
||||
"access",
|
||||
access.AccountUpdateEvent,
|
||||
"update connection feature flags",
|
||||
"update connection feature flags after account update",
|
||||
func(ctx context.Context, _ interface{}) error {
|
||||
resetAllConnectionVerdicts()
|
||||
return nil
|
||||
|
@ -93,6 +102,24 @@ func prep() error {
|
|||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
|
||||
err = module.RegisterEventHook(
|
||||
"network",
|
||||
network.ConnectionReattributedEvent,
|
||||
"reset verdict of re-attributed connection",
|
||||
func(ctx context.Context, eventData interface{}) error {
|
||||
// Expected event data: connection ID.
|
||||
connID, ok := eventData.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("event data is not a string: %v", eventData)
|
||||
}
|
||||
resetSingleConnectionVerdict(connID)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to register event hook: %s", err)
|
||||
}
|
||||
|
||||
if err := registerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -43,13 +43,55 @@ var (
|
|||
ownPID = os.Getpid()
|
||||
)
|
||||
|
||||
func resetAllConnectionVerdicts() {
|
||||
// Resetting will force all the connection to be evaluated by the firewall again
|
||||
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
|
||||
log.Info("interception: re-evaluating all connections")
|
||||
|
||||
func resetSingleConnectionVerdict(connID string) {
|
||||
// Create tracing context.
|
||||
ctx, tracer := log.AddTracer(context.Background())
|
||||
defer tracer.Submit()
|
||||
|
||||
conn, ok := network.GetConnection(connID)
|
||||
if !ok {
|
||||
conn, ok = network.GetDNSConnection(connID)
|
||||
if !ok {
|
||||
tracer.Debugf("filter: could not find re-attributed connection %s for re-evaluation", connID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resetConnectionVerdict(ctx, conn)
|
||||
}
|
||||
|
||||
func resetProfileConnectionVerdict(profileSource, profileID string) {
|
||||
// Create tracing context.
|
||||
ctx, tracer := log.AddTracer(context.Background())
|
||||
defer tracer.Submit()
|
||||
|
||||
// Resetting will force all the connection to be evaluated by the firewall again
|
||||
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
|
||||
tracer.Infof("filter: re-evaluating connections of %s/%s", profileSource, profileID)
|
||||
|
||||
// Re-evaluate all connections.
|
||||
var changedVerdicts int
|
||||
for _, conn := range network.GetAllConnections() {
|
||||
// Check if connection is complete and attributed to the deleted profile.
|
||||
if conn.DataIsComplete() &&
|
||||
conn.ProcessContext.Profile == profileID &&
|
||||
conn.ProcessContext.Source == profileSource {
|
||||
if resetConnectionVerdict(ctx, conn) {
|
||||
changedVerdicts++
|
||||
}
|
||||
}
|
||||
}
|
||||
tracer.Infof("filter: changed verdict on %d connections", changedVerdicts)
|
||||
}
|
||||
|
||||
func resetAllConnectionVerdicts() {
|
||||
// Create tracing context.
|
||||
ctx, tracer := log.AddTracer(context.Background())
|
||||
defer tracer.Submit()
|
||||
|
||||
// Resetting will force all the connection to be evaluated by the firewall again
|
||||
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
|
||||
tracer.Info("filter: re-evaluating all connections")
|
||||
|
||||
// Re-evaluate all connections.
|
||||
var changedVerdicts int
|
||||
|
@ -59,13 +101,22 @@ func resetAllConnectionVerdicts() {
|
|||
continue
|
||||
}
|
||||
|
||||
func() {
|
||||
if resetConnectionVerdict(ctx, conn) {
|
||||
changedVerdicts++
|
||||
}
|
||||
}
|
||||
tracer.Infof("filter: changed verdict on %d connections", changedVerdicts)
|
||||
}
|
||||
|
||||
func resetConnectionVerdict(ctx context.Context, conn *network.Connection) (verdictChanged bool) {
|
||||
tracer := log.Tracer(ctx)
|
||||
|
||||
conn.Lock()
|
||||
defer conn.Unlock()
|
||||
|
||||
// Update feature flags.
|
||||
if err := conn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
|
||||
tracer.Warningf("network: failed to update connection feature flags: %s", err)
|
||||
tracer.Warningf("filter: failed to update connection feature flags: %s", err)
|
||||
}
|
||||
|
||||
// Skip internal connections:
|
||||
|
@ -73,8 +124,8 @@ func resetAllConnectionVerdicts() {
|
|||
// - Redirected DNS requests
|
||||
// - SPN Uplink to Home Hub
|
||||
if conn.Internal {
|
||||
tracer.Tracef("filter: skipping internal connection %s", conn)
|
||||
return
|
||||
// tracer.Tracef("filter: skipping internal connection %s", conn)
|
||||
return false
|
||||
}
|
||||
|
||||
tracer.Debugf("filter: re-evaluating verdict of %s", conn)
|
||||
|
@ -99,14 +150,11 @@ func resetAllConnectionVerdicts() {
|
|||
}
|
||||
conn.Save()
|
||||
tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb())
|
||||
changedVerdicts++
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
tracer.Tracef("filter: verdict to connection %s unchanged at %s", conn, conn.VerdictVerb())
|
||||
}
|
||||
}()
|
||||
}
|
||||
tracer.Infof("filter: changed verdict on %d connections", changedVerdicts)
|
||||
tracer.Submit()
|
||||
return false
|
||||
}
|
||||
|
||||
// SetNameserverIPMatcher sets a function that is used to match the internal
|
||||
|
|
|
@ -582,6 +582,11 @@ func GetAllConnections() []*Connection {
|
|||
return conns.list()
|
||||
}
|
||||
|
||||
// GetDNSConnection fetches a DNS Connection from the database.
|
||||
func GetDNSConnection(dnsConnID string) (*Connection, bool) {
|
||||
return dnsConns.get(dnsConnID)
|
||||
}
|
||||
|
||||
// SetLocalIP sets the local IP address together with its network scope. The
|
||||
// connection is not locked for this.
|
||||
func (conn *Connection) SetLocalIP(ip net.IP) {
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/netenv"
|
||||
"github.com/safing/portmaster/network/state"
|
||||
"github.com/safing/portmaster/profile"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -12,8 +19,14 @@ var (
|
|||
defaultFirewallHandler FirewallHandler
|
||||
)
|
||||
|
||||
// Events.
|
||||
var (
|
||||
ConnectionReattributedEvent = "connection re-attributed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("network", prep, start, nil, "base", "netenv", "processes")
|
||||
module.RegisterEvent(ConnectionReattributedEvent, false)
|
||||
}
|
||||
|
||||
// SetDefaultFirewallHandler sets the default firewall handler.
|
||||
|
@ -45,5 +58,95 @@ func start() error {
|
|||
module.StartServiceWorker("clean connections", 0, connectionCleaner)
|
||||
module.StartServiceWorker("write open dns requests", 0, openDNSRequestWriter)
|
||||
|
||||
if err := module.RegisterEventHook(
|
||||
"profiles",
|
||||
profile.DeletedEvent,
|
||||
"re-attribute connections from deleted profile",
|
||||
reAttributeConnections,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var reAttributionLock sync.Mutex
|
||||
|
||||
// reAttributeConnections finds all connections of a deleted profile and re-attributes them.
|
||||
// Expected event data: scoped profile ID.
|
||||
func reAttributeConnections(_ context.Context, eventData any) error {
|
||||
profileID, ok := eventData.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("event data is not a string: %v", eventData)
|
||||
}
|
||||
profileSource, profileID, ok := strings.Cut(profileID, "/")
|
||||
if !ok {
|
||||
return fmt.Errorf("event data does not seem to be a scoped profile ID: %v", eventData)
|
||||
}
|
||||
|
||||
// Hold a lock for re-attribution, to prevent simultaneous processing of the
|
||||
// same connections and make logging cleaner.
|
||||
reAttributionLock.Lock()
|
||||
defer reAttributionLock.Unlock()
|
||||
|
||||
// Create tracing context.
|
||||
ctx, tracer := log.AddTracer(context.Background())
|
||||
defer tracer.Submit()
|
||||
tracer.Infof("network: re-attributing connections from deleted profile %s/%s", profileSource, profileID)
|
||||
|
||||
// Count and log how many connections were re-attributed.
|
||||
var reAttributed int
|
||||
|
||||
// Re-attribute connections.
|
||||
for _, conn := range conns.clone() {
|
||||
// Check if connection is complete and attributed to the deleted profile.
|
||||
if conn.DataIsComplete() &&
|
||||
conn.ProcessContext.Profile == profileID &&
|
||||
conn.ProcessContext.Source == profileSource {
|
||||
|
||||
reAttributeConnection(ctx, conn)
|
||||
reAttributed++
|
||||
tracer.Debugf("filter: re-attributed %s to %s", conn, conn.process.PrimaryProfileID)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-attribute dns connections.
|
||||
for _, conn := range dnsConns.clone() {
|
||||
// Check if connection is complete and attributed to the deleted profile.
|
||||
if conn.DataIsComplete() &&
|
||||
conn.ProcessContext.Profile == profileID &&
|
||||
conn.ProcessContext.Source == profileSource {
|
||||
|
||||
reAttributeConnection(ctx, conn)
|
||||
reAttributed++
|
||||
tracer.Debugf("filter: re-attributed %s to %s", conn, conn.process.PrimaryProfileID)
|
||||
}
|
||||
}
|
||||
|
||||
tracer.Infof("filter: re-attributed %d connections", reAttributed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func reAttributeConnection(ctx context.Context, conn *Connection) {
|
||||
// Check if data is complete.
|
||||
if !conn.DataIsComplete() {
|
||||
return
|
||||
}
|
||||
|
||||
conn.Lock()
|
||||
defer conn.Unlock()
|
||||
|
||||
// Attempt to assign new profile.
|
||||
err := conn.process.RefetchProfile(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("network: failed to refetch profile for %s: %s", conn, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the new process context.
|
||||
conn.ProcessContext = getProcessContext(ctx, conn.process)
|
||||
conn.Save()
|
||||
|
||||
// Trigger event for re-attribution.
|
||||
module.TriggerEvent(ConnectionReattributedEvent, conn.ID)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,24 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// RefetchProfile removes the profile and finds and assigns a new profile.
|
||||
func (p *Process) RefetchProfile(ctx context.Context) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// Get special or regular profile.
|
||||
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), p.MatchingData(), p.CreateProfileCallback)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find profile: %w", err)
|
||||
}
|
||||
|
||||
// Assign profile to process.
|
||||
p.PrimaryProfileID = localProfile.ScopedID()
|
||||
p.profile = localProfile.LayeredProfile()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSpecialProfileID returns the special profile ID for the process, if any.
|
||||
func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
// Check if we need a special profile.
|
||||
|
|
66
profile/api.go
Normal file
66
profile/api.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Merge profiles",
|
||||
Description: "Merge multiple profiles into a new one.",
|
||||
Path: "profile/merge",
|
||||
Write: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: handleMergeProfiles,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mergeProfilesRequest struct {
|
||||
Name string `json:"name"` // Name of the new merged profile.
|
||||
To string `json:"to"` // Profile scoped ID.
|
||||
From []string `json:"from"` // Profile scoped IDs.
|
||||
}
|
||||
|
||||
type mergeprofilesResponse struct {
|
||||
New string `json:"new"` // Profile scoped ID.
|
||||
}
|
||||
|
||||
func handleMergeProfiles(ar *api.Request) (i interface{}, err error) {
|
||||
request := &mergeProfilesRequest{}
|
||||
_, err = dsd.MimeLoad(ar.InputData, ar.Header.Get("Content-Type"), request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request: %w", err)
|
||||
}
|
||||
|
||||
// Get all profiles.
|
||||
var (
|
||||
primary *Profile
|
||||
secondaries = make([]*Profile, 0, len(request.From))
|
||||
)
|
||||
if primary, err = getProfile(request.To); err != nil {
|
||||
return nil, fmt.Errorf("failed to get profile %s: %w", request.To, err)
|
||||
}
|
||||
for _, from := range request.From {
|
||||
sp, err := getProfile(from)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get profile %s: %w", request.To, err)
|
||||
}
|
||||
secondaries = append(secondaries, sp)
|
||||
}
|
||||
|
||||
newProfile, err := MergeProfiles(request.Name, primary, secondaries...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to merge profiles: %w", err)
|
||||
}
|
||||
|
||||
return &mergeprofilesResponse{
|
||||
New: newProfile.ScopedID(),
|
||||
}, nil
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
// core:profiles/<scope>/<id>
|
||||
// cache:profiles/index/<identifier>/<value>
|
||||
|
||||
// ProfilesDBPath is the base database path for profiles.
|
||||
const ProfilesDBPath = "core:profiles/"
|
||||
|
||||
var profileDB = database.NewInterface(&database.Options{
|
||||
|
@ -59,8 +60,14 @@ func startProfileUpdateChecker() error {
|
|||
}
|
||||
|
||||
// Get active profile.
|
||||
activeProfile := getActiveProfile(strings.TrimPrefix(r.Key(), ProfilesDBPath))
|
||||
scopedID := strings.TrimPrefix(r.Key(), ProfilesDBPath)
|
||||
activeProfile := getActiveProfile(scopedID)
|
||||
if activeProfile == nil {
|
||||
// Check if profile is being deleted.
|
||||
if r.Meta().IsDeleted() {
|
||||
meta.MarkDeleted(scopedID)
|
||||
}
|
||||
|
||||
// Don't do any additional actions if the profile is not active.
|
||||
continue profileFeed
|
||||
}
|
||||
|
@ -74,7 +81,9 @@ func startProfileUpdateChecker() error {
|
|||
// Always mark as outdated if the record is being deleted.
|
||||
if r.Meta().IsDeleted() {
|
||||
activeProfile.outdated.Set()
|
||||
module.TriggerEvent(profileConfigChange, nil)
|
||||
|
||||
meta.MarkDeleted(scopedID)
|
||||
module.TriggerEvent(DeletedEvent, scopedID)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -83,7 +92,7 @@ func startProfileUpdateChecker() error {
|
|||
receivedProfile, err := EnsureProfile(r)
|
||||
if err != nil || !receivedProfile.savedInternally {
|
||||
activeProfile.outdated.Set()
|
||||
module.TriggerEvent(profileConfigChange, nil)
|
||||
module.TriggerEvent(ConfigChangeEvent, scopedID)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
|
@ -105,6 +114,11 @@ func (h *databaseHook) UsesPrePut() bool {
|
|||
|
||||
// PrePut implements the Hook interface.
|
||||
func (h *databaseHook) PrePut(r record.Record) (record.Record, error) {
|
||||
// Do not intervene with metadata key.
|
||||
if r.Key() == profilesMetadataKey {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// convert
|
||||
profile, err := EnsureProfile(r)
|
||||
if err != nil {
|
||||
|
|
|
@ -202,7 +202,8 @@ func invalidDefinitionError(fields []string, msg string) error {
|
|||
return fmt.Errorf(`invalid endpoint definition: "%s" - %s`, strings.Join(fields, " "), msg)
|
||||
}
|
||||
|
||||
func parseEndpoint(value string) (endpoint Endpoint, err error) { //nolint:gocognit
|
||||
//nolint:gocognit,nakedret
|
||||
func parseEndpoint(value string) (endpoint Endpoint, err error) {
|
||||
fields := strings.Fields(value)
|
||||
if len(fields) < 2 {
|
||||
return nil, fmt.Errorf(`invalid endpoint definition: "%s"`, value)
|
||||
|
|
|
@ -287,6 +287,9 @@ func loadProfile(r record.Record) (*Profile, error) {
|
|||
// Set saved internally to suppress outdating profiles if saving internally.
|
||||
profile.savedInternally = true
|
||||
|
||||
// Mark as recently seen.
|
||||
meta.UpdateLastSeen(profile.ScopedID())
|
||||
|
||||
// return parsed profile
|
||||
return profile, nil
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
)
|
||||
|
@ -11,19 +13,42 @@ import (
|
|||
// The new profile is saved and returned.
|
||||
// Only the icon and fingerprints are inherited from other profiles.
|
||||
// All other information is taken only from the primary profile.
|
||||
func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profile, err error) {
|
||||
func MergeProfiles(name string, primary *Profile, secondaries ...*Profile) (newProfile *Profile, err error) {
|
||||
if primary == nil || len(secondaries) == 0 {
|
||||
return nil, errors.New("must supply both a primary and at least one secondary profile for merging")
|
||||
}
|
||||
|
||||
// Fill info from primary profile.
|
||||
nowUnix := time.Now().Unix()
|
||||
newProfile = &Profile{
|
||||
Base: record.Base{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
ID: "", // Omit ID to derive it from the new fingerprints.
|
||||
Source: primary.Source,
|
||||
Name: primary.Name,
|
||||
Name: name,
|
||||
Description: primary.Description,
|
||||
Homepage: primary.Homepage,
|
||||
UsePresentationPath: false, // Disable presentation path.
|
||||
SecurityLevel: primary.SecurityLevel,
|
||||
Config: primary.Config,
|
||||
Created: nowUnix,
|
||||
}
|
||||
|
||||
// Fall back to name of primary profile, if none is set.
|
||||
if newProfile.Name == "" {
|
||||
newProfile.Name = primary.Name
|
||||
}
|
||||
|
||||
// If any profile was edited, set LastEdited to now.
|
||||
if primary.LastEdited > 0 {
|
||||
newProfile.LastEdited = nowUnix
|
||||
} else {
|
||||
for _, sp := range secondaries {
|
||||
if sp.LastEdited > 0 {
|
||||
newProfile.LastEdited = nowUnix
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all icons.
|
||||
|
@ -35,7 +60,7 @@ func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profi
|
|||
newProfile.Icons = sortAndCompactIcons(newProfile.Icons)
|
||||
|
||||
// Collect all fingerprints.
|
||||
newProfile.Fingerprints = make([]Fingerprint, 0, len(secondaries)+1) // Guess the needed space.
|
||||
newProfile.Fingerprints = make([]Fingerprint, 0, len(primary.Fingerprints)+len(secondaries)) // Guess the needed space.
|
||||
newProfile.Fingerprints = addFingerprints(newProfile.Fingerprints, primary.Fingerprints, primary.ScopedID())
|
||||
for _, sp := range secondaries {
|
||||
newProfile.Fingerprints = addFingerprints(newProfile.Fingerprints, sp.Fingerprints, sp.ScopedID())
|
||||
|
@ -44,26 +69,19 @@ func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profi
|
|||
|
||||
// Save new profile.
|
||||
newProfile = New(newProfile)
|
||||
err = newProfile.Save()
|
||||
if err != nil {
|
||||
if err := newProfile.Save(); err != nil {
|
||||
return nil, fmt.Errorf("failed to save merged profile: %w", err)
|
||||
}
|
||||
// FIXME: Should we ... ?
|
||||
// newProfile.updateMetadata()
|
||||
// newProfile.updateMetadataFromSystem()
|
||||
|
||||
// Delete all previous profiles.
|
||||
// FIXME:
|
||||
/*
|
||||
primary.Meta().Delete()
|
||||
// Set as outdated and remove from active profiles.
|
||||
// Signify that profile was deleted and save for sync.
|
||||
for _, sp := range secondaries {
|
||||
sp.Meta().Delete()
|
||||
// Set as outdated and remove from active profiles.
|
||||
// Signify that profile was deleted and save for sync.
|
||||
if err := primary.delete(); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete primary profile %s: %w", primary.ScopedID(), err)
|
||||
}
|
||||
for _, sp := range secondaries {
|
||||
if err := sp.delete(); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete secondary profile %s: %w", sp.ScopedID(), err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return newProfile, nil
|
||||
}
|
||||
|
|
184
profile/meta.go
Normal file
184
profile/meta.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
)
|
||||
|
||||
// ProfilesMetadata holds metadata about all profiles that are not fit to be
|
||||
// stored with the profiles themselves.
|
||||
type ProfilesMetadata struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
States map[string]*MetaState
|
||||
}
|
||||
|
||||
// MetaState describes the state of a profile.
|
||||
type MetaState struct {
|
||||
State string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Profile metadata states.
|
||||
const (
|
||||
MetaStateSeen = "seen"
|
||||
MetaStateDeleted = "deleted"
|
||||
)
|
||||
|
||||
// EnsureProfilesMetadata ensures that the given record is a *ProfilesMetadata, and returns it.
|
||||
func EnsureProfilesMetadata(r record.Record) (*ProfilesMetadata, error) {
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
newMeta := &ProfilesMetadata{}
|
||||
err := record.Unwrap(r, newMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newMeta, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
newMeta, ok := r.(*ProfilesMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *Profile, but %T", r)
|
||||
}
|
||||
return newMeta, nil
|
||||
}
|
||||
|
||||
var (
|
||||
profilesMetadataKey = ProfilesDBPath + "meta"
|
||||
|
||||
meta *ProfilesMetadata
|
||||
|
||||
removeDeletedEntriesAfter = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// loadProfilesMetadata loads the profile metadata from the database.
|
||||
// It may only be called during module starting, as there is no lock for "meta" itself.
|
||||
func loadProfilesMetadata() error {
|
||||
r, err := profileDB.Get(profilesMetadataKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loadedMeta, err := EnsureProfilesMetadata(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set package variable.
|
||||
meta = loadedMeta
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *ProfilesMetadata) check() {
|
||||
if meta.States == nil {
|
||||
meta.States = make(map[string]*MetaState)
|
||||
}
|
||||
}
|
||||
|
||||
// Save saves the profile metadata to the database.
|
||||
func (meta *ProfilesMetadata) Save() error {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
func() {
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
if !meta.KeyIsSet() {
|
||||
meta.SetKey(profilesMetadataKey)
|
||||
}
|
||||
}()
|
||||
|
||||
meta.Clean()
|
||||
return profileDB.Put(meta)
|
||||
}
|
||||
|
||||
// Clean removes old entries.
|
||||
func (meta *ProfilesMetadata) Clean() {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
for key, state := range meta.States {
|
||||
switch {
|
||||
case state == nil:
|
||||
delete(meta.States, key)
|
||||
case state.State != MetaStateDeleted:
|
||||
continue
|
||||
case time.Since(state.At) > removeDeletedEntriesAfter:
|
||||
delete(meta.States, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLastSeen returns when the profile with the given ID was last seen.
|
||||
func (meta *ProfilesMetadata) GetLastSeen(scopedID string) *time.Time {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
state := meta.States[scopedID]
|
||||
switch {
|
||||
case state == nil:
|
||||
return nil
|
||||
case state.State == MetaStateSeen:
|
||||
return &state.At
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLastSeen sets the profile with the given ID as last seen now.
|
||||
func (meta *ProfilesMetadata) UpdateLastSeen(scopedID string) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
meta.States[scopedID] = &MetaState{
|
||||
State: MetaStateSeen,
|
||||
At: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// MarkDeleted marks the profile with the given ID as deleted.
|
||||
func (meta *ProfilesMetadata) MarkDeleted(scopedID string) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
meta.States[scopedID] = &MetaState{
|
||||
State: MetaStateDeleted,
|
||||
At: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveState removes any state of the profile with the given ID.
|
||||
func (meta *ProfilesMetadata) RemoveState(scopedID string) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meta.Lock()
|
||||
defer meta.Unlock()
|
||||
|
||||
delete(meta.States, scopedID)
|
||||
}
|
|
@ -32,11 +32,11 @@ func registerMigrations() error {
|
|||
Version: "v1.4.7",
|
||||
MigrateFunc: migrateIcons,
|
||||
},
|
||||
// migration.Migration{
|
||||
// Description: "Migrate from random profile IDs to fingerprint-derived IDs",
|
||||
// Version: "v1.5.1",
|
||||
// MigrateFunc: migrateToDerivedIDs,
|
||||
// },
|
||||
migration.Migration{
|
||||
Description: "Migrate from random profile IDs to fingerprint-derived IDs",
|
||||
Version: "v1.5.0",
|
||||
MigrateFunc: migrateToDerivedIDs,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/migration"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
|
@ -16,13 +18,16 @@ var (
|
|||
updatesPath string
|
||||
)
|
||||
|
||||
// Events.
|
||||
const (
|
||||
profileConfigChange = "profile config change"
|
||||
ConfigChangeEvent = "profile config change"
|
||||
DeletedEvent = "profile deleted"
|
||||
)
|
||||
|
||||
func init() {
|
||||
module = modules.Register("profiles", prep, start, nil, "base", "updates")
|
||||
module.RegisterEvent(profileConfigChange, true)
|
||||
module = modules.Register("profiles", prep, start, stop, "base", "updates")
|
||||
module.RegisterEvent(ConfigChangeEvent, true)
|
||||
module.RegisterEvent(DeletedEvent, true)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
|
@ -47,6 +52,14 @@ func start() error {
|
|||
updatesPath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
if err := loadProfilesMetadata(); err != nil {
|
||||
if !errors.Is(err, database.ErrNotFound) {
|
||||
log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err)
|
||||
}
|
||||
meta = &ProfilesMetadata{}
|
||||
}
|
||||
meta.check()
|
||||
|
||||
if err := migrations.Migrate(module.Ctx); err != nil {
|
||||
log.Errorf("profile: migrations failed: %s", err)
|
||||
}
|
||||
|
@ -73,5 +86,13 @@ func start() error {
|
|||
log.Warningf("profile: error during loading global profile from configuration: %s", err)
|
||||
}
|
||||
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
return meta.Save()
|
||||
}
|
||||
|
|
|
@ -304,6 +304,24 @@ func (profile *Profile) Save() error {
|
|||
return profileDB.Put(profile)
|
||||
}
|
||||
|
||||
// delete deletes the profile from the database.
|
||||
func (profile *Profile) delete() error {
|
||||
// Check if a key is set.
|
||||
if !profile.KeyIsSet() {
|
||||
return errors.New("key is not set")
|
||||
}
|
||||
|
||||
// Delete from database.
|
||||
profile.Meta().Delete()
|
||||
err := profileDB.Put(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Post handling is done by the profile update feed.
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkStillActive marks the profile as still active.
|
||||
func (profile *Profile) MarkStillActive() {
|
||||
atomic.StoreInt64(profile.lastActive, time.Now().Unix())
|
||||
|
|
Loading…
Add table
Reference in a new issue