mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
Move from LinkedPath to Fingerprints and PresentationPath
This commit is contained in:
parent
e1e6a40498
commit
59f776ce2f
9 changed files with 699 additions and 253 deletions
|
@ -38,21 +38,6 @@ func getAllActiveProfiles() []*Profile {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// findActiveProfile searched for an active local profile using the linked path.
|
|
||||||
func findActiveProfile(linkedPath string) *Profile {
|
|
||||||
activeProfilesLock.RLock()
|
|
||||||
defer activeProfilesLock.RUnlock()
|
|
||||||
|
|
||||||
for _, activeProfile := range activeProfiles {
|
|
||||||
if activeProfile.LinkedPath == linkedPath {
|
|
||||||
activeProfile.MarkStillActive()
|
|
||||||
return activeProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addActiveProfile registers a active profile.
|
// addActiveProfile registers a active profile.
|
||||||
func addActiveProfile(profile *Profile) {
|
func addActiveProfile(profile *Profile) {
|
||||||
activeProfilesLock.Lock()
|
activeProfilesLock.Lock()
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portmaster/intel/filterlists"
|
"github.com/safing/portmaster/intel/filterlists"
|
||||||
"github.com/safing/portmaster/profile/endpoints"
|
"github.com/safing/portmaster/profile/endpoints"
|
||||||
|
@ -91,11 +90,7 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
// build global profile for reference
|
// Build config.
|
||||||
profile := New(SourceSpecial, "global-config", "", nil)
|
|
||||||
profile.Name = "Global Configuration"
|
|
||||||
profile.Internal = true
|
|
||||||
|
|
||||||
newConfig := make(map[string]interface{})
|
newConfig := make(map[string]interface{})
|
||||||
// fill profile config options
|
// fill profile config options
|
||||||
for key, value := range cfgStringOptions {
|
for key, value := range cfgStringOptions {
|
||||||
|
@ -111,8 +106,14 @@ func updateGlobalConfigProfile(ctx context.Context, task *modules.Task) error {
|
||||||
newConfig[key] = value()
|
newConfig[key] = value()
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand and assign
|
// Build global profile for reference.
|
||||||
profile.Config = config.Expand(newConfig)
|
profile := New(&Profile{
|
||||||
|
ID: "global-config",
|
||||||
|
Source: SourceSpecial,
|
||||||
|
Name: "Global Configuration",
|
||||||
|
Config: newConfig,
|
||||||
|
Internal: true,
|
||||||
|
})
|
||||||
|
|
||||||
// save profile
|
// save profile
|
||||||
err = profile.Save()
|
err = profile.Save()
|
||||||
|
|
|
@ -65,41 +65,19 @@ func startProfileUpdateChecker() error {
|
||||||
continue profileFeed
|
continue profileFeed
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the record is being deleted, reset the profile.
|
|
||||||
// create an empty profile instead.
|
|
||||||
if r.Meta().IsDeleted() {
|
|
||||||
newProfile, err := GetProfile(
|
|
||||||
activeProfile.Source,
|
|
||||||
activeProfile.ID,
|
|
||||||
activeProfile.LinkedPath,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("profile: failed to create new profile after reset: %s", err)
|
|
||||||
} else {
|
|
||||||
// Copy metadata from the old profile.
|
|
||||||
newProfile.copyMetadataFrom(activeProfile)
|
|
||||||
// Save the new profile.
|
|
||||||
err = newProfile.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("profile: failed to save new profile after reset: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the new profile was successfully created, update layered profile.
|
|
||||||
activeProfile.outdated.Set()
|
|
||||||
if err == nil {
|
|
||||||
newProfile.layeredProfile.Update()
|
|
||||||
}
|
|
||||||
module.TriggerEvent(profileConfigChange, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always increase the revision counter of the layer profile.
|
// Always increase the revision counter of the layer profile.
|
||||||
// This marks previous connections in the UI as decided with outdated settings.
|
// This marks previous connections in the UI as decided with outdated settings.
|
||||||
if activeProfile.layeredProfile != nil {
|
if activeProfile.layeredProfile != nil {
|
||||||
activeProfile.layeredProfile.increaseRevisionCounter(true)
|
activeProfile.layeredProfile.increaseRevisionCounter(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always mark as outdated if the record is being deleted.
|
||||||
|
if r.Meta().IsDeleted() {
|
||||||
|
activeProfile.outdated.Set()
|
||||||
|
module.TriggerEvent(profileConfigChange, nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// If the profile is saved externally (eg. via the API), have the
|
// If the profile is saved externally (eg. via the API), have the
|
||||||
// next one to use it reload the profile from the database.
|
// next one to use it reload the profile from the database.
|
||||||
receivedProfile, err := EnsureProfile(r)
|
receivedProfile, err := EnsureProfile(r)
|
||||||
|
|
323
profile/fingerprint.go
Normal file
323
profile/fingerprint.go
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// # Matching and Scores
|
||||||
|
//
|
||||||
|
// There are three levels:
|
||||||
|
//
|
||||||
|
// 1. Type: What matched?
|
||||||
|
// 1. Tag: 40.000 points
|
||||||
|
// 2. Env: 30.000 points
|
||||||
|
// 3. MatchingPath: 20.000 points
|
||||||
|
// 4. Path: 10.000 points
|
||||||
|
// 2. Operation: How was it mached?
|
||||||
|
// 1. Equals: 3.000 points
|
||||||
|
// 2. Prefix: 2.000 points
|
||||||
|
// 3. Regex: 1.000 points
|
||||||
|
// 3. How "strong" was the match?
|
||||||
|
// 1. Equals: Length of path (irrelevant)
|
||||||
|
// 2. Prefix: Length of prefix
|
||||||
|
// 3. Regex: 0 (we are not suicidal)
|
||||||
|
|
||||||
|
// ms-store:Microsoft.One.Note
|
||||||
|
|
||||||
|
// Path Match /path/to/file
|
||||||
|
// Tag MS-Store Match value
|
||||||
|
// Env Regex Key Value
|
||||||
|
|
||||||
|
// Fingerprint Type IDs.
|
||||||
|
const (
|
||||||
|
FingerprintTypeTagID = "tag"
|
||||||
|
FingerprintTypeEnvID = "env"
|
||||||
|
FingerprintTypePathID = "path" // Matches both MatchingPath and Path.
|
||||||
|
|
||||||
|
FingerprintOperationEqualsID = "equals"
|
||||||
|
FingerprintOperationPrefixID = "prefix"
|
||||||
|
FingerprintOperationRegexID = "regex"
|
||||||
|
|
||||||
|
tagMatchBaseScore = 40_000
|
||||||
|
envMatchBaseScore = 30_000
|
||||||
|
matchingPathMatchBaseScore = 20_000
|
||||||
|
pathMatchBaseScore = 10_000
|
||||||
|
|
||||||
|
fingerprintEqualsBaseScore = 3_000
|
||||||
|
fingerprintPrefixBaseScore = 2_000
|
||||||
|
fingerprintRegexBaseScore = 1_000
|
||||||
|
|
||||||
|
maxMatchStrength = 499
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Fingerprint defines a way of matching a process.
|
||||||
|
// The Key is only valid - but required - for some types.
|
||||||
|
Fingerprint struct {
|
||||||
|
Type string
|
||||||
|
Key string // Key must always fully match.
|
||||||
|
Operation string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag represents a simple key/value kind of tag used in process metadata
|
||||||
|
// and fingerprints.
|
||||||
|
Tag struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchingData is an interface to fetching data in the matching process.
|
||||||
|
MatchingData interface {
|
||||||
|
Tags() []Tag
|
||||||
|
Env() map[string]string
|
||||||
|
Path() string
|
||||||
|
MatchingPath() string
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingFingerprint interface {
|
||||||
|
MatchesKey(key string) bool
|
||||||
|
Match(value string) (score int)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatchesKey returns whether the optional fingerprint key (for some types
|
||||||
|
// only) matches the given key.
|
||||||
|
func (fp Fingerprint) MatchesKey(key string) bool {
|
||||||
|
return key == fp.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyInTags checks is the given key is in the tags.
|
||||||
|
func KeyInTags(tags []Tag, key string) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if key == tag.Key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyAndValueInTags checks is the given key/value pair is in the tags.
|
||||||
|
func KeyAndValueInTags(tags []Tag, key, value string) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if key == tag.Key && value == tag.Value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type fingerprintEquals struct {
|
||||||
|
Fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp fingerprintEquals) Match(value string) (score int) {
|
||||||
|
if value == fp.Value {
|
||||||
|
return fingerprintEqualsBaseScore + checkMatchStrength(len(fp.Value))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type fingerprintPrefix struct {
|
||||||
|
Fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp fingerprintPrefix) Match(value string) (score int) {
|
||||||
|
if strings.HasPrefix(value, fp.Value) {
|
||||||
|
return fingerprintPrefixBaseScore + checkMatchStrength(len(fp.Value))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type fingerprintRegex struct {
|
||||||
|
Fingerprint
|
||||||
|
regex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp fingerprintRegex) Match(value string) (score int) {
|
||||||
|
if fp.regex.MatchString(value) {
|
||||||
|
// Do not return any deviation from the base score.
|
||||||
|
// Trying to assign different scores to regex probably won't turn out to
|
||||||
|
// be a good idea.
|
||||||
|
return fingerprintRegexBaseScore
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type parsedFingerprints struct {
|
||||||
|
tagPrints []matchingFingerprint
|
||||||
|
envPrints []matchingFingerprint
|
||||||
|
pathPrints []matchingFingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *parsedFingerprints, firstErr error) {
|
||||||
|
parsed = &parsedFingerprints{}
|
||||||
|
|
||||||
|
// Add deprecated linked path to fingerprints.
|
||||||
|
if deprecatedLinkedPath != "" {
|
||||||
|
parsed.pathPrints = append(parsed.pathPrints, &fingerprintEquals{
|
||||||
|
Fingerprint: Fingerprint{
|
||||||
|
Type: FingerprintTypePathID,
|
||||||
|
Operation: FingerprintOperationEqualsID,
|
||||||
|
Value: deprecatedLinkedPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse all fingerprints.
|
||||||
|
// Do not fail when one fails, instead return the first encountered error.
|
||||||
|
for _, entry := range raw {
|
||||||
|
// Check type and required key.
|
||||||
|
switch entry.Type {
|
||||||
|
case FingerprintTypeTagID, FingerprintTypeEnvID:
|
||||||
|
if entry.Key == "" {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("%s fingerprint is missing key", entry.Type)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case FingerprintTypePathID:
|
||||||
|
// Don't need a key.
|
||||||
|
default:
|
||||||
|
// Unknown type.
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("unknown fingerprint type: %q", entry.Type)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and/or collect operation match functions.
|
||||||
|
switch entry.Operation {
|
||||||
|
case FingerprintOperationEqualsID:
|
||||||
|
parsed.addMatchingFingerprint(entry, fingerprintEquals{entry})
|
||||||
|
|
||||||
|
case FingerprintOperationPrefixID:
|
||||||
|
parsed.addMatchingFingerprint(entry, fingerprintPrefix{entry})
|
||||||
|
|
||||||
|
case FingerprintOperationRegexID:
|
||||||
|
regex, err := regexp.Compile(entry.Value)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("failed to compile regex fingerprint: %s", entry.Value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsed.addMatchingFingerprint(entry, fingerprintRegex{
|
||||||
|
Fingerprint: entry,
|
||||||
|
regex: regex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("unknown fingerprint operation: %q", entry.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed, firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parsed *parsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchingPrint matchingFingerprint) {
|
||||||
|
switch fp.Type {
|
||||||
|
case FingerprintTypeTagID:
|
||||||
|
parsed.tagPrints = append(parsed.tagPrints, matchingPrint)
|
||||||
|
case FingerprintTypeEnvID:
|
||||||
|
parsed.envPrints = append(parsed.envPrints, matchingPrint)
|
||||||
|
case FingerprintTypePathID:
|
||||||
|
parsed.pathPrints = append(parsed.pathPrints, matchingPrint)
|
||||||
|
default:
|
||||||
|
// This should never happen, as the types are checked already.
|
||||||
|
panic(fmt.Sprintf("unknown fingerprint type: %q", fp.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFingerprints returns the highest matching score of the given
|
||||||
|
// fingerprints and matching data.
|
||||||
|
func MatchFingerprints(prints *parsedFingerprints, md MatchingData) (highestScore int) {
|
||||||
|
// Check tags.
|
||||||
|
for _, tagPrint := range prints.tagPrints {
|
||||||
|
for _, tag := range md.Tags() {
|
||||||
|
// Check if tag key matches.
|
||||||
|
if !tagPrint.MatchesKey(tag.Key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try matching the tag value.
|
||||||
|
score := tagPrint.Match(tag.Value)
|
||||||
|
if score > highestScore {
|
||||||
|
highestScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If something matched, add base score and return.
|
||||||
|
if highestScore > 0 {
|
||||||
|
return tagMatchBaseScore + highestScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check env.
|
||||||
|
for _, envPrint := range prints.envPrints {
|
||||||
|
for key, value := range md.Env() {
|
||||||
|
// Check if env key matches.
|
||||||
|
if !envPrint.MatchesKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try matching the env value.
|
||||||
|
score := envPrint.Match(value)
|
||||||
|
if score > highestScore {
|
||||||
|
highestScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If something matched, add base score and return.
|
||||||
|
if highestScore > 0 {
|
||||||
|
return envMatchBaseScore + highestScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check matching path.
|
||||||
|
matchingPath := md.MatchingPath()
|
||||||
|
if matchingPath != "" {
|
||||||
|
for _, pathPrint := range prints.pathPrints {
|
||||||
|
// Try matching the path value.
|
||||||
|
score := pathPrint.Match(matchingPath)
|
||||||
|
if score > highestScore {
|
||||||
|
highestScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If something matched, add base score and return.
|
||||||
|
if highestScore > 0 {
|
||||||
|
return matchingPathMatchBaseScore + highestScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path.
|
||||||
|
path := md.Path()
|
||||||
|
if path != "" {
|
||||||
|
for _, pathPrint := range prints.pathPrints {
|
||||||
|
// Try matching the path value.
|
||||||
|
score := pathPrint.Match(path)
|
||||||
|
if score > highestScore {
|
||||||
|
highestScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If something matched, add base score and return.
|
||||||
|
if highestScore > 0 {
|
||||||
|
return pathMatchBaseScore + highestScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing matched.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMatchStrength(value int) int {
|
||||||
|
if value > maxMatchStrength {
|
||||||
|
return maxMatchStrength
|
||||||
|
}
|
||||||
|
if value < -maxMatchStrength {
|
||||||
|
return -maxMatchStrength
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
297
profile/get.go
297
profile/get.go
|
@ -2,20 +2,24 @@ package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
|
||||||
"github.com/safing/portbase/database/query"
|
"github.com/safing/portbase/database/query"
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
var getProfileLock sync.Mutex
|
var getProfileLock sync.Mutex
|
||||||
|
|
||||||
// GetProfile fetches a profile. This function ensures that the loaded profile
|
// GetLocalProfile fetches a profile. This function ensures that the loaded profile
|
||||||
// is shared among all callers. You must always supply both the scopedID and
|
// is shared among all callers. Always provide all available data points.
|
||||||
// linkedPath parameters whenever available.
|
// Passing an ID without MatchingData is valid, but could lead to inconsistent
|
||||||
func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nolint:gocognit
|
// data - use with caution.
|
||||||
|
func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *Profile) ( //nolint:gocognit
|
||||||
profile *Profile,
|
profile *Profile,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
|
@ -27,99 +31,90 @@ func GetProfile(source profileSource, id, linkedPath string, reset bool) ( //nol
|
||||||
|
|
||||||
var previousVersion *Profile
|
var previousVersion *Profile
|
||||||
|
|
||||||
// Fetch profile depending on the available information.
|
// Get active profile based on the ID, if available.
|
||||||
switch {
|
if id != "" {
|
||||||
case id != "":
|
// Check if there already is an active profile.
|
||||||
scopedID := makeScopedID(source, id)
|
profile = getActiveProfile(makeScopedID(SourceLocal, id))
|
||||||
|
|
||||||
// Get profile via the scoped ID.
|
|
||||||
// Check if there already is an active and not outdated profile.
|
|
||||||
profile = getActiveProfile(scopedID)
|
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
profile.MarkStillActive()
|
// Mark active and return if not outdated.
|
||||||
|
if profile.outdated.IsNotSet() {
|
||||||
if profile.outdated.IsSet() || reset {
|
profile.MarkStillActive()
|
||||||
previousVersion = profile
|
|
||||||
} else {
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get from database.
|
// If outdated, get from database.
|
||||||
if !reset {
|
previousVersion = profile
|
||||||
profile, err = getProfile(scopedID)
|
profile = nil
|
||||||
// Check if the profile is special and needs a reset.
|
|
||||||
if err == nil && specialProfileNeedsReset(profile) {
|
|
||||||
profile = getSpecialProfile(id, linkedPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Simulate missing profile to create new one.
|
|
||||||
err = database.ErrNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case linkedPath != "":
|
|
||||||
// Search for profile via a linked path.
|
|
||||||
// Check if there already is an active and not outdated profile for
|
|
||||||
// the linked path.
|
|
||||||
profile = findActiveProfile(linkedPath)
|
|
||||||
if profile != nil {
|
|
||||||
if profile.outdated.IsSet() || reset {
|
|
||||||
previousVersion = profile
|
|
||||||
} else {
|
|
||||||
return profile, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get from database.
|
|
||||||
if !reset {
|
|
||||||
profile, err = findProfile(linkedPath)
|
|
||||||
// Check if the profile is special and needs a reset.
|
|
||||||
if err == nil && specialProfileNeedsReset(profile) {
|
|
||||||
profile = getSpecialProfile(id, linkedPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Simulate missing profile to create new one.
|
|
||||||
err = database.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errors.New("cannot fetch profile without ID or path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new profile if none was found.
|
// In some cases, we might need to get a profile directly, without matching data.
|
||||||
if errors.Is(err, database.ErrNotFound) {
|
// This could lead to inconsistent data - use with caution.
|
||||||
err = nil
|
if md == nil {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.New("cannot get local profiles without ID and matching data")
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there is a special profile for this ID.
|
profile, err = getProfile(makeScopedID(SourceLocal, id))
|
||||||
profile = getSpecialProfile(id, linkedPath)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load profile %s by ID: %w", makeScopedID(SourceLocal, id), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If not, create a standard profile.
|
// 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 {
|
||||||
|
// 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 {
|
if profile == nil {
|
||||||
profile = New(SourceLocal, id, linkedPath, nil)
|
fpPath := md.MatchingPath()
|
||||||
|
if fpPath == "" {
|
||||||
|
fpPath = md.Path()
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = New(&Profile{
|
||||||
|
ID: id,
|
||||||
|
Source: SourceLocal,
|
||||||
|
PresentationPath: md.Path(),
|
||||||
|
Fingerprints: []Fingerprint{
|
||||||
|
{
|
||||||
|
Type: FingerprintTypePathID,
|
||||||
|
Operation: FingerprintOperationEqualsID,
|
||||||
|
Value: fpPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there was a non-recoverable error, return here.
|
// Prepare profile for first use.
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process profiles are coming directly from the database or are new.
|
// Process profiles are coming directly from the database or are new.
|
||||||
// As we don't use any caching, these will be new objects.
|
// As we don't use any caching, these will be new objects.
|
||||||
|
|
||||||
// Add a layeredProfile to local and network profiles.
|
// Add a layeredProfile.
|
||||||
if profile.Source == SourceLocal || profile.Source == SourceNetwork {
|
|
||||||
// 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 {
|
|
||||||
profile.layeredProfile = previousVersion.layeredProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local profiles must have a layered profile, create a new one if it
|
// If we are refetching, assign the layered profile from the previous version.
|
||||||
// does not yet exist.
|
// The internal references will be updated when the layered profile checks for updates.
|
||||||
if profile.layeredProfile == nil {
|
if previousVersion != nil && previousVersion.layeredProfile != nil {
|
||||||
profile.layeredProfile = NewLayeredProfile(profile)
|
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.
|
// Add the profile to the currently active profiles.
|
||||||
|
@ -137,40 +132,89 @@ func getProfile(scopedID string) (profile *Profile, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and prepare the profile, return the result.
|
// Parse and prepare the profile, return the result.
|
||||||
return prepProfile(r)
|
return loadProfile(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findProfile searches for a profile with the given linked path. If it cannot
|
// 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.
|
// find one, it will create a new profile for the given linked path.
|
||||||
func findProfile(linkedPath string) (profile *Profile, err error) {
|
func findProfile(source profileSource, md MatchingData) (profile *Profile, err error) {
|
||||||
// Search the database for a matching profile.
|
// TODO: Loading every profile from database and parsing it for every new
|
||||||
it, err := profileDB.Query(
|
// process might be quite expensive. Measure impact and possibly improve.
|
||||||
query.New(makeProfileKey(SourceLocal, "")).Where(
|
|
||||||
query.Where("LinkedPath", query.SameAs, linkedPath),
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only wait for the first result, or until the query ends.
|
// Parse and return fingerprints.
|
||||||
r := <-it.Next
|
return parseFingerprints(profile.Fingerprints, profile.LinkedPath)
|
||||||
// Then cancel the query, should it still be running.
|
|
||||||
it.Cancel()
|
|
||||||
|
|
||||||
// Prep and return an existing profile.
|
|
||||||
if r != nil {
|
|
||||||
profile, err = prepProfile(r)
|
|
||||||
return profile, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there was no profile in the database, create a new one, and return it.
|
|
||||||
profile = New(SourceLocal, "", linkedPath, nil)
|
|
||||||
|
|
||||||
return profile, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepProfile(r record.Record) (*Profile, error) {
|
func loadProfile(r record.Record) (*Profile, error) {
|
||||||
// ensure its a profile
|
// ensure its a profile
|
||||||
profile, err := EnsureProfile(r)
|
profile, err := EnsureProfile(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -192,3 +236,50 @@ func prepProfile(r record.Record) (*Profile, error) {
|
||||||
// return parsed profile
|
// return parsed profile
|
||||||
return profile, nil
|
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",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -74,9 +74,11 @@ func getProfileRevision(p *Profile) (*LayeredProfile, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profiles if necessary.
|
// Update profiles if necessary.
|
||||||
if layeredProfile.NeedsUpdate() {
|
// TODO: Cannot update as we have too little information.
|
||||||
layeredProfile.Update()
|
// Just return the current state. Previous code:
|
||||||
}
|
// if layeredProfile.NeedsUpdate() {
|
||||||
|
// layeredProfile.Update()
|
||||||
|
// }
|
||||||
|
|
||||||
return layeredProfile, nil
|
return layeredProfile, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,8 +57,8 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||||
|
|
||||||
lp := &LayeredProfile{
|
lp := &LayeredProfile{
|
||||||
localProfile: localProfile,
|
localProfile: localProfile,
|
||||||
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
|
layers: make([]*Profile, 0, 1),
|
||||||
LayerIDs: make([]string, 0, len(localProfile.LinkedProfiles)+1),
|
LayerIDs: make([]string, 0, 1),
|
||||||
globalValidityFlag: config.NewValidityFlag(),
|
globalValidityFlag: config.NewValidityFlag(),
|
||||||
RevisionCounter: 1,
|
RevisionCounter: 1,
|
||||||
securityLevel: &securityLevelVal,
|
securityLevel: &securityLevelVal,
|
||||||
|
@ -246,17 +246,23 @@ func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update checks for and replaces any outdated profiles.
|
// Update checks for and replaces any outdated profiles.
|
||||||
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
|
func (lp *LayeredProfile) Update(md MatchingData, createProfileCallback func() *Profile) (revisionCounter uint64) {
|
||||||
lp.Lock()
|
lp.Lock()
|
||||||
defer lp.Unlock()
|
defer lp.Unlock()
|
||||||
|
|
||||||
var changed bool
|
var changed bool
|
||||||
for i, layer := range lp.layers {
|
for i, layer := range lp.layers {
|
||||||
if layer.outdated.IsSet() {
|
if layer.outdated.IsSet() {
|
||||||
changed = true
|
// Check for unsupported sources.
|
||||||
|
if layer.Source != SourceLocal {
|
||||||
|
log.Warningf("profile: updating profiles outside of local source is not supported: %s", layer.ScopedID())
|
||||||
|
layer.outdated.UnSet()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Update layer.
|
// Update layer.
|
||||||
newLayer, err := GetProfile(layer.Source, layer.ID, layer.LinkedPath, false)
|
changed = true
|
||||||
|
newLayer, err := GetLocalProfile(layer.ID, md, createProfileCallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
|
log.Errorf("profiles: failed to update profile %s: %s", layer.ScopedID(), err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,11 +26,8 @@ type profileSource string
|
||||||
|
|
||||||
// Profile Sources.
|
// Profile Sources.
|
||||||
const (
|
const (
|
||||||
SourceLocal profileSource = "local" // local, editable
|
SourceLocal profileSource = "local" // local, editable
|
||||||
SourceSpecial profileSource = "special" // specials (read-only)
|
SourceSpecial profileSource = "special" // specials (read-only)
|
||||||
SourceNetwork profileSource = "network"
|
|
||||||
SourceCommunity profileSource = "community"
|
|
||||||
SourceEnterprise profileSource = "enterprise"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Action IDs.
|
// Default Action IDs.
|
||||||
|
@ -83,11 +80,17 @@ type Profile struct { //nolint:maligned // not worth the effort
|
||||||
Icon string
|
Icon string
|
||||||
// IconType describes the type of the Icon property.
|
// IconType describes the type of the Icon property.
|
||||||
IconType iconType
|
IconType iconType
|
||||||
// LinkedPath is a filesystem path to the executable this
|
// Deprecated: LinkedPath used to point to the executableis this
|
||||||
// profile was created for.
|
// profile was created for.
|
||||||
|
// Until removed, it will be added to the Fingerprints as an exact path match.
|
||||||
LinkedPath string // constant
|
LinkedPath string // constant
|
||||||
// LinkedProfiles is a list of other profiles
|
// PresentationPath holds the path of an executable that should be used for
|
||||||
LinkedProfiles []string
|
// get representative information from, like the name of the program or the icon.
|
||||||
|
// Is automatically removed when the path does not exist.
|
||||||
|
// Is automatically populated with the next match when empty.
|
||||||
|
PresentationPath string
|
||||||
|
// Fingerprints holds process matching information.
|
||||||
|
Fingerprints []Fingerprint
|
||||||
// SecurityLevel is the mininum security level to apply to
|
// SecurityLevel is the mininum security level to apply to
|
||||||
// connections made with this profile.
|
// connections made with this profile.
|
||||||
// Note(ppacher): we may deprecate this one as it can easily
|
// Note(ppacher): we may deprecate this one as it can easily
|
||||||
|
@ -143,6 +146,11 @@ func (profile *Profile) prepProfile() {
|
||||||
// prepare configuration
|
// prepare configuration
|
||||||
profile.outdated = abool.New()
|
profile.outdated = abool.New()
|
||||||
profile.lastActive = new(int64)
|
profile.lastActive = new(int64)
|
||||||
|
|
||||||
|
// Migration of LinkedPath to PresentationPath
|
||||||
|
if profile.PresentationPath == "" && profile.LinkedPath != "" {
|
||||||
|
profile.PresentationPath = profile.LinkedPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (profile *Profile) parseConfig() error {
|
func (profile *Profile) parseConfig() error {
|
||||||
|
@ -227,29 +235,25 @@ func (profile *Profile) parseConfig() error {
|
||||||
|
|
||||||
// New returns a new Profile.
|
// New returns a new Profile.
|
||||||
// Optionally, you may supply custom configuration in the flat (key=value) form.
|
// Optionally, you may supply custom configuration in the flat (key=value) form.
|
||||||
func New(
|
func New(profile *Profile) *Profile {
|
||||||
source profileSource,
|
// Create profile if none is given.
|
||||||
id string,
|
if profile == nil {
|
||||||
linkedPath string,
|
profile = &Profile{}
|
||||||
customConfig map[string]interface{},
|
|
||||||
) *Profile {
|
|
||||||
if customConfig != nil {
|
|
||||||
customConfig = config.Expand(customConfig)
|
|
||||||
} else {
|
|
||||||
customConfig = make(map[string]interface{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := &Profile{
|
// Set default and internal values.
|
||||||
ID: id,
|
profile.Created = time.Now().Unix()
|
||||||
Source: source,
|
profile.savedInternally = true
|
||||||
LinkedPath: linkedPath,
|
|
||||||
Created: time.Now().Unix(),
|
// Expand any given configuration.
|
||||||
Config: customConfig,
|
if profile.Config != nil {
|
||||||
savedInternally: true,
|
profile.Config = config.Expand(profile.Config)
|
||||||
|
} else {
|
||||||
|
profile.Config = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate random ID if none is given.
|
// Generate random ID if none is given.
|
||||||
if id == "" {
|
if profile.ID == "" {
|
||||||
profile.ID = utils.RandomUUID("").String()
|
profile.ID = utils.RandomUUID("").String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,13 +441,13 @@ func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
|
||||||
var needsUpdateFromSystem bool
|
var needsUpdateFromSystem bool
|
||||||
|
|
||||||
// Check profile name.
|
// Check profile name.
|
||||||
filename := filepath.Base(profile.LinkedPath)
|
filename := filepath.Base(profile.PresentationPath)
|
||||||
|
|
||||||
// Update profile name if it is empty or equals the filename, which is the
|
// Update profile name if it is empty or equals the filename, which is the
|
||||||
// case for older profiles.
|
// case for older profiles.
|
||||||
if strings.TrimSpace(profile.Name) == "" || profile.Name == filename {
|
if strings.TrimSpace(profile.Name) == "" || profile.Name == filename {
|
||||||
// Generate a default profile name if does not exist.
|
// Generate a default profile name if does not exist.
|
||||||
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.LinkedPath)
|
profile.Name = osdetail.GenerateBinaryNameFromPath(profile.PresentationPath)
|
||||||
if profile.Name == filename {
|
if profile.Name == filename {
|
||||||
// TODO: Theoretically, the generated name could be identical to the
|
// TODO: Theoretically, the generated name could be identical to the
|
||||||
// filename.
|
// filename.
|
||||||
|
@ -462,37 +466,12 @@ func (profile *Profile) UpdateMetadata(binaryPath string) (changed bool) {
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (profile *Profile) copyMetadataFrom(otherProfile *Profile) (changed bool) {
|
|
||||||
if profile.Name != otherProfile.Name {
|
|
||||||
profile.Name = otherProfile.Name
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if profile.Description != otherProfile.Description {
|
|
||||||
profile.Description = otherProfile.Description
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if profile.Homepage != otherProfile.Homepage {
|
|
||||||
profile.Homepage = otherProfile.Homepage
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if profile.Icon != otherProfile.Icon {
|
|
||||||
profile.Icon = otherProfile.Icon
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if profile.IconType != otherProfile.IconType {
|
|
||||||
profile.IconType = otherProfile.IconType
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateMetadataFromSystem updates the profile metadata with data from the
|
// updateMetadataFromSystem updates the profile metadata with data from the
|
||||||
// operating system and saves it afterwards.
|
// operating system and saves it afterwards.
|
||||||
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||||
// This function is only valid for local profiles.
|
// This function is only valid for local profiles.
|
||||||
if profile.Source != SourceLocal || profile.LinkedPath == "" {
|
if profile.Source != SourceLocal || profile.PresentationPath == "" {
|
||||||
return fmt.Errorf("tried to update metadata for non-local / non-linked profile %s", profile.ScopedID())
|
return fmt.Errorf("tried to update metadata for non-local or non-path profile %s", profile.ScopedID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the profile when finished, if needed.
|
// Save the profile when finished, if needed.
|
||||||
|
@ -507,14 +486,14 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Get binary name from linked path.
|
// Get binary name from linked path.
|
||||||
newName, err := osdetail.GetBinaryNameFromSystem(profile.LinkedPath)
|
newName, err := osdetail.GetBinaryNameFromSystem(profile.PresentationPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, osdetail.ErrNotSupported):
|
case errors.Is(err, osdetail.ErrNotSupported):
|
||||||
case errors.Is(err, osdetail.ErrNotFound):
|
case errors.Is(err, osdetail.ErrNotFound):
|
||||||
case errors.Is(err, osdetail.ErrEmptyOutput):
|
case errors.Is(err, osdetail.ErrEmptyOutput):
|
||||||
default:
|
default:
|
||||||
log.Warningf("profile: error while getting binary name for %s: %s", profile.LinkedPath, err)
|
log.Warningf("profile: error while getting binary name for %s: %s", profile.PresentationPath, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -525,7 +504,7 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get filename of linked path for comparison.
|
// Get filename of linked path for comparison.
|
||||||
filename := filepath.Base(profile.LinkedPath)
|
filename := filepath.Base(profile.PresentationPath)
|
||||||
|
|
||||||
// TODO: Theoretically, the generated name from the system could be identical
|
// TODO: Theoretically, the generated name from the system could be identical
|
||||||
// to the filename. This would mean that the worker is triggered every time
|
// to the filename. This would mean that the worker is triggered every time
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/database"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -74,6 +77,73 @@ If you think you might have messed up the settings of the System DNS Client, jus
|
||||||
PortmasterNotifierProfileDescription = `This is the Portmaster UI Tray Notifier.`
|
PortmasterNotifierProfileDescription = `This is the Portmaster UI Tray Notifier.`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetSpecialProfile fetches a special profile. This function ensures that the loaded profile
|
||||||
|
// is shared among all callers. Always provide all available data points.
|
||||||
|
func GetSpecialProfile(id string, path string) ( //nolint:gocognit
|
||||||
|
profile *Profile,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
// Check if we have an ID.
|
||||||
|
if id == "" {
|
||||||
|
return nil, errors.New("cannot get special profile without ID")
|
||||||
|
}
|
||||||
|
scopedID := makeScopedID(SourceLocal, id)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// Check if there already is an active profile.
|
||||||
|
var previousVersion *Profile
|
||||||
|
profile = getActiveProfile(scopedID)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get special profile from DB and check if it needs a reset.
|
||||||
|
profile, err = getProfile(scopedID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, database.ErrNotFound) {
|
||||||
|
log.Warningf("profile: failed to get special profile %s: %s", id, err)
|
||||||
|
}
|
||||||
|
profile = createSpecialProfile(id, path)
|
||||||
|
} else if specialProfileNeedsReset(profile) {
|
||||||
|
log.Debugf("profile: resetting special profile %s", id)
|
||||||
|
profile = createSpecialProfile(id, path)
|
||||||
|
}
|
||||||
|
if profile == nil {
|
||||||
|
return nil, errors.New("given ID is not a special profile ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare profile for first use.
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, changed bool) {
|
func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, changed bool) {
|
||||||
// Get new profile name and check if profile is applicable to special handling.
|
// Get new profile name and check if profile is applicable to special handling.
|
||||||
var newProfileName, newDescription string
|
var newProfileName, newDescription string
|
||||||
|
@ -115,35 +185,45 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update LinkedPath to new value.
|
// Update PresentationPath to new value.
|
||||||
if profile.LinkedPath != binaryPath {
|
if profile.PresentationPath != binaryPath {
|
||||||
profile.LinkedPath = binaryPath
|
profile.PresentationPath = binaryPath
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, changed
|
return true, changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpecialProfile(profileID, linkedPath string) *Profile {
|
func createSpecialProfile(profileID string, path string) *Profile {
|
||||||
switch profileID {
|
switch profileID {
|
||||||
case UnidentifiedProfileID:
|
case UnidentifiedProfileID:
|
||||||
return New(SourceLocal, UnidentifiedProfileID, linkedPath, nil)
|
return New(&Profile{
|
||||||
|
ID: UnidentifiedProfileID,
|
||||||
|
Source: SourceLocal,
|
||||||
|
PresentationPath: path,
|
||||||
|
})
|
||||||
|
|
||||||
case SystemProfileID:
|
case SystemProfileID:
|
||||||
return New(SourceLocal, SystemProfileID, linkedPath, nil)
|
return New(&Profile{
|
||||||
|
ID: SystemProfileID,
|
||||||
|
Source: SourceLocal,
|
||||||
|
PresentationPath: path,
|
||||||
|
})
|
||||||
|
|
||||||
case SystemResolverProfileID:
|
case SystemResolverProfileID:
|
||||||
systemResolverProfile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: SystemResolverProfileID,
|
||||||
SystemResolverProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
// Explicitly setting the default action to "permit" will improve the
|
// Explicitly setting the default action to "permit" will improve the
|
||||||
// user experience for people who set the global default to "prompt".
|
// user experience for people who set the global default to "prompt".
|
||||||
// Resolved domain from the system resolver are checked again when
|
// Resolved domain from the system resolver are checked again when
|
||||||
// attributed to a connection of a regular process. Otherwise, users
|
// attributed to a connection of a regular process. Otherwise, users
|
||||||
// would see two connection prompts for the same domain.
|
// would see two connection prompts for the same domain.
|
||||||
CfgOptionDefaultActionKey: "permit",
|
CfgOptionDefaultActionKey: "permit",
|
||||||
|
// Explicitly allow incoming connections.
|
||||||
|
CfgOptionBlockInboundKey: status.SecurityLevelOff,
|
||||||
// Explicitly allow localhost and answers to multicast protocols that
|
// Explicitly allow localhost and answers to multicast protocols that
|
||||||
// are commonly used by system resolvers.
|
// are commonly used by system resolvers.
|
||||||
// TODO: When the Portmaster gains the ability to attribute multicast
|
// TODO: When the Portmaster gains the ability to attribute multicast
|
||||||
|
@ -154,6 +234,7 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
|
||||||
"+ LAN UDP/5353", // Allow inbound mDNS requests and multicast replies.
|
"+ LAN UDP/5353", // Allow inbound mDNS requests and multicast replies.
|
||||||
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
|
"+ LAN UDP/5355", // Allow inbound LLMNR requests and multicast replies.
|
||||||
"+ LAN UDP/1900", // Allow inbound SSDP requests and multicast replies.
|
"+ LAN UDP/1900", // Allow inbound SSDP requests and multicast replies.
|
||||||
|
"- *", // Deny everything else.
|
||||||
},
|
},
|
||||||
// Explicitly disable all filter lists, as these will be checked later
|
// Explicitly disable all filter lists, as these will be checked later
|
||||||
// with the attributed connection. As this is the system resolver, this
|
// with the attributed connection. As this is the system resolver, this
|
||||||
|
@ -161,44 +242,44 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
|
||||||
// the system resolver is used. Users who want to
|
// the system resolver is used. Users who want to
|
||||||
CfgOptionFilterListsKey: []string{},
|
CfgOptionFilterListsKey: []string{},
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
return systemResolverProfile
|
|
||||||
|
|
||||||
case PortmasterProfileID:
|
case PortmasterProfileID:
|
||||||
profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil)
|
return New(&Profile{
|
||||||
profile.Internal = true
|
ID: PortmasterProfileID,
|
||||||
return profile
|
Source: SourceLocal,
|
||||||
|
PresentationPath: path,
|
||||||
|
Internal: true,
|
||||||
|
})
|
||||||
|
|
||||||
case PortmasterAppProfileID:
|
case PortmasterAppProfileID:
|
||||||
profile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: PortmasterAppProfileID,
|
||||||
PortmasterAppProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
CfgOptionDefaultActionKey: "block",
|
CfgOptionDefaultActionKey: "block",
|
||||||
CfgOptionEndpointsKey: []string{
|
CfgOptionEndpointsKey: []string{
|
||||||
"+ Localhost",
|
"+ Localhost",
|
||||||
"+ .safing.io",
|
"+ .safing.io",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
Internal: true,
|
||||||
profile.Internal = true
|
})
|
||||||
return profile
|
|
||||||
|
|
||||||
case PortmasterNotifierProfileID:
|
case PortmasterNotifierProfileID:
|
||||||
profile := New(
|
return New(&Profile{
|
||||||
SourceLocal,
|
ID: PortmasterNotifierProfileID,
|
||||||
PortmasterNotifierProfileID,
|
Source: SourceLocal,
|
||||||
linkedPath,
|
PresentationPath: path,
|
||||||
map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
CfgOptionDefaultActionKey: "block",
|
CfgOptionDefaultActionKey: "block",
|
||||||
CfgOptionEndpointsKey: []string{
|
CfgOptionEndpointsKey: []string{
|
||||||
"+ Localhost",
|
"+ Localhost",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
Internal: true,
|
||||||
profile.Internal = true
|
})
|
||||||
return profile
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Add table
Reference in a new issue