safing-portmaster/service/profile/get.go
Daniel Hååvi 80664d1a27
Restructure modules ()
* Move portbase into monorepo

* Add new simple module mgr

* [WIP] Switch to new simple module mgr

* Add StateMgr and more worker variants

* [WIP] Switch more modules

* [WIP] Switch more modules

* [WIP] swtich more modules

* [WIP] switch all SPN modules

* [WIP] switch all service modules

* [WIP] Convert all workers to the new module system

* [WIP] add new task system to module manager

* [WIP] Add second take for scheduling workers

* [WIP] Add FIXME for bugs in new scheduler

* [WIP] Add minor improvements to scheduler

* [WIP] Add new worker scheduler

* [WIP] Fix more bug related to new module system

* [WIP] Fix start handing of the new module system

* [WIP] Improve startup process

* [WIP] Fix minor issues

* [WIP] Fix missing subsystem in settings

* [WIP] Initialize managers in constructor

* [WIP] Move module event initialization to constrictors

* [WIP] Fix setting for enabling and disabling the SPN module

* [WIP] Move API registeration into module construction

* [WIP] Update states mgr for all modules

* [WIP] Add CmdLine operation support

* Add state helper methods to module group and instance

* Add notification and module status handling to status package

* Fix starting issues

* Remove pilot widget and update security lock to new status data

* Remove debug logs

* Improve http server shutdown

* Add workaround for cleanly shutting down firewall+netquery

* Improve logging

* Add syncing states with notifications for new module system

* Improve starting, stopping, shutdown; resolve FIXMEs/TODOs

* [WIP] Fix most unit tests

* Review new module system and fix minor issues

* Push shutdown and restart events again via API

* Set sleep mode via interface

* Update example/template module

* [WIP] Fix spn/cabin unit test

* Remove deprecated UI elements

* Make log output more similar for the logging transition phase

* Switch spn hub and observer cmds to new module system

* Fix log sources

* Make worker mgr less error prone

* Fix tests and minor issues

* Fix observation hub

* Improve shutdown and restart handling

* Split up big connection.go source file

* Move varint and dsd packages to structures repo

* Improve expansion test

* Fix linter warnings

* Fix interception module on windows

* Fix linter errors

---------

Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
2024-08-09 18:15:48 +03:00

345 lines
9.3 KiB
Go

package profile
import (
"errors"
"fmt"
"path"
"strings"
"sync"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/mgr"
)
var getProfileLock sync.Mutex
// GetLocalProfile fetches a profile. This function ensures that the loaded profile
// is shared among all callers. Always provide all available data points.
// Passing an ID without MatchingData is valid, but could lead to inconsistent
// data - use with caution.
func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *Profile) ( //nolint:gocognit
profile *Profile,
err error,
) {
// Globally lock getting a profile.
// This does not happen too often, and it ensures we really have integrity
// and no race conditions.
getProfileLock.Lock()
defer getProfileLock.Unlock()
var previousVersion *Profile
// Get active profile based on the ID, if available.
if id != "" {
// Check if there already is an active profile.
profile = getActiveProfile(MakeScopedID(SourceLocal, id))
if profile != nil {
// Mark active and return if not outdated.
if profile.outdated.IsNotSet() {
profile.MarkStillActive()
return profile, nil
}
// If outdated, get from database.
previousVersion = profile
profile = nil
}
}
// In some cases, we might need to get a profile directly, without matching data.
// This could lead to inconsistent data - use with caution.
// Example: Saving prompt results to profile should always be to the same ID!
if md == nil {
if id == "" {
return nil, errors.New("cannot get local profiles without ID and matching data")
}
profile, err = getProfile(MakeScopedID(SourceLocal, id))
if err != nil {
return nil, fmt.Errorf("failed to load profile %s by ID: %w", MakeScopedID(SourceLocal, id), err)
}
}
// Check if we are requesting a special profile.
var created, special bool
if id != "" && isSpecialProfileID(id) {
special = true
// Get special profile from DB.
if profile == nil {
profile, err = getProfile(MakeScopedID(SourceLocal, id))
if err != nil && !errors.Is(err, database.ErrNotFound) {
log.Warningf("profile: failed to get special profile %s: %s", id, err)
}
}
// Create profile if not found or if it needs a reset.
if profile == nil || specialProfileNeedsReset(profile) {
profile = createSpecialProfile(id, md.Path())
created = true
}
}
// If we don't have a profile yet, find profile based on matching data.
if profile == nil {
profile, err = findProfile(SourceLocal, md)
if err != nil {
return nil, fmt.Errorf("failed to search for profile: %w", err)
}
}
// If we still don't have a profile, create a new one.
if profile == nil {
created = true
// Try the profile creation callback, if we have one.
if createProfileCallback != nil {
profile = createProfileCallback()
}
// If that did not work, create a standard profile.
if profile == nil {
fpPath := md.MatchingPath()
if fpPath == "" {
fpPath = md.Path()
}
profile = New(&Profile{
ID: id,
Source: SourceLocal,
PresentationPath: md.Path(),
UsePresentationPath: true,
Fingerprints: []Fingerprint{
{
Type: FingerprintTypePathID,
Operation: FingerprintOperationEqualsID,
Value: fpPath,
},
},
})
}
}
// Initialize and update profile.
// Update metadata.
var changed bool
if md != nil {
if special {
changed = updateSpecialProfileMetadata(profile, md.Path())
} else {
changed = profile.updateMetadata(md.Path())
}
}
// Save if created or changed.
if created || changed {
// Save profile.
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save profile %s after creation: %s", profile.ScopedID(), err)
}
}
// Trigger further metadata fetching from system if profile was created.
if created && profile.UsePresentationPath && !special {
module.mgr.Go("get profile metadata", func(wc *mgr.WorkerCtx) error {
return profile.updateMetadataFromSystem(wc.Ctx(), md)
})
}
// Prepare profile for first use.
// Process profiles are coming directly from the database or are new.
// As we don't use any caching, these will be new objects.
// Add a layeredProfile.
// If we are refetching, assign the layered profile from the previous version.
// The internal references will be updated when the layered profile checks for updates.
if previousVersion != nil && previousVersion.layeredProfile != nil {
profile.layeredProfile = previousVersion.layeredProfile
}
// Profiles must have a layered profile, create a new one if it
// does not yet exist.
if profile.layeredProfile == nil {
profile.layeredProfile = NewLayeredProfile(profile)
}
// Add the profile to the currently active profiles.
addActiveProfile(profile)
return profile, nil
}
// getProfile fetches the profile for the given scoped ID.
func getProfile(scopedID string) (profile *Profile, err error) {
// Get profile from the database.
r, err := profileDB.Get(ProfilesDBPath + scopedID)
if err != nil {
return nil, err
}
// Parse and prepare the profile, return the result.
return loadProfile(r)
}
// findProfile searches for a profile with the given linked path. If it cannot
// find one, it will create a new profile for the given linked path.
func findProfile(source ProfileSource, md MatchingData) (profile *Profile, err error) {
// TODO: Loading every profile from database and parsing it for every new
// process might be quite expensive. Measure impact and possibly improve.
// Get iterator over all profiles.
it, err := profileDB.Query(query.New(ProfilesDBPath + MakeScopedID(source, "")))
if err != nil {
return nil, fmt.Errorf("failed to query for profiles: %w", err)
}
// Find best matching profile.
var (
highestScore int
bestMatch record.Record
)
profileFeed:
for r := range it.Next {
// Parse fingerprints.
prints, err := loadProfileFingerprints(r)
if err != nil {
log.Debugf("profile: failed to load fingerprints of %s: %s", r.Key(), err)
}
// Continue with any returned fingerprints.
if prints == nil {
continue profileFeed
}
// Get matching score and compare.
score := MatchFingerprints(prints, md)
switch {
case score == 0:
// Continue to next.
case score > highestScore:
highestScore = score
bestMatch = r
case score == highestScore:
// Notify user of conflict and abort.
// Use first match - this should be consistent.
notifyConflictingProfiles(bestMatch, r, md)
it.Cancel()
break profileFeed
}
}
// Check if there was an error while iterating.
if it.Err() != nil {
return nil, fmt.Errorf("failed to iterate over profiles: %w", err)
}
// Return nothing if no profile matched.
if bestMatch == nil {
return nil, nil
}
// If we have a match, parse and return the profile.
profile, err = loadProfile(bestMatch)
if err != nil {
return nil, fmt.Errorf("failed to parse selected profile %s: %w", bestMatch.Key(), err)
}
// Check if this profile is already active and return the active version instead.
if activeProfile := getActiveProfile(profile.ScopedID()); activeProfile != nil && !activeProfile.IsOutdated() {
return activeProfile, nil
}
// Return nothing if no profile matched.
return profile, nil
}
func loadProfileFingerprints(r record.Record) (parsed *ParsedFingerprints, err error) {
// Ensure it's a profile.
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}
// Parse and return fingerprints.
return ParseFingerprints(profile.Fingerprints, profile.LinkedPath)
}
func loadProfile(r record.Record) (*Profile, error) {
// ensure its a profile
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}
// prepare profile
profile.prepProfile()
// parse config
err = profile.parseConfig()
if err != nil {
log.Errorf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// 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
}
func notifyConflictingProfiles(a, b record.Record, md MatchingData) {
// Get profile names.
var idA, nameA, idB, nameB string
profileA, err := EnsureProfile(a)
if err == nil {
idA = profileA.ScopedID()
nameA = profileA.Name
} else {
idA = strings.TrimPrefix(a.Key(), ProfilesDBPath)
nameA = path.Base(idA)
}
profileB, err := EnsureProfile(b)
if err == nil {
idB = profileB.ScopedID()
nameB = profileB.Name
} else {
idB = strings.TrimPrefix(b.Key(), ProfilesDBPath)
nameB = path.Base(idB)
}
// Notify user about conflict.
notifications.NotifyWarn(
fmt.Sprintf("profiles:match-conflict:%s:%s", idA, idB),
"App Settings Match Conflict",
fmt.Sprintf(
"Multiple app settings match the app at %q with the same priority, please change on of them: %q or %q",
md.Path(),
nameA,
nameB,
),
notifications.Action{
Text: "Change (1)",
Type: notifications.ActionTypeOpenProfile,
Payload: idA,
},
notifications.Action{
Text: "Change (2)",
Type: notifications.ActionTypeOpenProfile,
Payload: idB,
},
notifications.Action{
ID: "ack",
Text: "OK",
},
)
}